权限系统完善,实现维修日志信息自动填写功能,同时优化设备分配页面设计

This commit is contained in:
YueL1331
2026-01-11 17:39:33 +08:00
parent ca03816668
commit 94ff1ddf57
5 changed files with 641 additions and 246 deletions

View File

@ -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>