<template>
|
<main class="visitor-page visitor-device-page">
|
<section class="visitor-card">
|
<header class="visitor-header">
|
<h1>{{ pageTitle }}管理</h1>
|
</header>
|
|
<div class="visitor-side-layout">
|
<aside class="visitor-side">
|
<h2>区域</h2>
|
<el-input v-model="areaFilterText" placeholder="搜索区域" clearable prefix-icon="el-icon-search" />
|
<el-tree
|
ref="areaTree"
|
:data="areaTree"
|
:props="areaProps"
|
:expand-on-click-node="false"
|
:filter-node-method="filterAreaNode"
|
node-key="areaId"
|
default-expand-all
|
highlight-current
|
@node-click="handleAreaClick"
|
>
|
<span slot-scope="{ node, data }" class="area-tree-node">
|
<i :class="areaNodeIconClass(data)" />
|
<span class="area-tree-label">{{ node.label }}</span>
|
</span>
|
</el-tree>
|
</aside>
|
|
<section class="device-panel">
|
<h2>设备列表</h2>
|
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true" class="visitor-query">
|
<el-form-item label="设备名称:" prop="deviceName">
|
<el-input v-model="queryParams.deviceName" placeholder="请输入" clearable @keyup.enter.native="handleQuery" />
|
</el-form-item>
|
<el-form-item label="ID:" prop="deviceId">
|
<el-input v-model="queryParams.deviceId" placeholder="请输入" clearable @keyup.enter.native="handleQuery" />
|
</el-form-item>
|
<el-form-item label="状态:" prop="enabled">
|
<el-select v-model="queryParams.enabled" placeholder="请选择" clearable>
|
<el-option label="启用" :value="true" />
|
<el-option label="禁用" :value="false" />
|
</el-select>
|
</el-form-item>
|
<el-form-item>
|
<el-button @click="resetQuery">重置</el-button>
|
<el-button type="primary" @click="handleQuery">搜索</el-button>
|
</el-form-item>
|
</el-form>
|
|
<div v-if="!isMiddlePlatformReadonly && hasDevicePerm('add')" class="visitor-toolbar">
|
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增设备</el-button>
|
</div>
|
|
<el-table
|
v-loading="loading"
|
class="visitor-table"
|
:data="deviceList"
|
>
|
<el-table-column label="序号" width="64" align="center">
|
<template slot-scope="scope">{{ scope.$index + 1 }}</template>
|
</el-table-column>
|
<el-table-column label="设备名称" prop="deviceName" min-width="120" show-overflow-tooltip />
|
<el-table-column label="品牌" prop="brand" min-width="100" show-overflow-tooltip />
|
<el-table-column label="设备ID" prop="deviceId" min-width="150" show-overflow-tooltip />
|
<el-table-column v-if="deviceType === 'machine'" label="百度SDK指纹" prop="fingerprint" min-width="160" show-overflow-tooltip />
|
<el-table-column label="所属区域" prop="areaName" min-width="130" show-overflow-tooltip />
|
<el-table-column label="启用状态" align="center" width="100">
|
<template slot-scope="scope">
|
<span class="status-pill" :class="scope.row.enabled ? 'enabled' : 'disabled'">{{ scope.row.enabled ? '启用' : '禁用' }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="在线状态" align="center" width="100">
|
<template slot-scope="scope">
|
<span class="status-pill" :class="onlineClass(scope.row.onlineStatus)">{{ onlineLabel(scope.row.onlineStatus) }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
|
<el-table-column v-if="!isMiddlePlatformReadonly && hasAnyDevicePerm(['edit', 'status', 'logout', 'remove'])" label="操作" align="center" width="150" fixed="right">
|
<template slot-scope="scope">
|
<el-button v-if="hasDevicePerm('edit')" type="text" @click="handleUpdate(scope.row)">编辑</el-button>
|
<el-dropdown v-if="hasAnyDevicePerm(['status', 'logout', 'remove'])" trigger="click" @command="command => handleMoreCommand(command, scope.row)">
|
<el-button type="text">更多<i class="el-icon-arrow-down el-icon--right" /></el-button>
|
<el-dropdown-menu slot="dropdown" class="visitor-device-more-menu">
|
<el-dropdown-item v-if="hasDevicePerm('logout')" command="logout">
|
<i class="el-icon-refresh-left" />注销
|
</el-dropdown-item>
|
<el-dropdown-item v-if="hasDevicePerm('status')" command="status">
|
<i :class="scope.row.enabled ? 'el-icon-circle-close' : 'el-icon-view'" />{{ scope.row.enabled ? '禁用' : '启用' }}
|
</el-dropdown-item>
|
<el-dropdown-item v-if="hasDevicePerm('remove')" command="delete" class="danger-item">
|
<i class="el-icon-delete" />删除
|
</el-dropdown-item>
|
</el-dropdown-menu>
|
</el-dropdown>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div v-if="!loading && !deviceList.length" class="visitor-empty">暂无设备数据</div>
|
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
</section>
|
</div>
|
</section>
|
|
<el-dialog :title="title" :visible.sync="open" width="654px" append-to-body custom-class="visitor-device-dialog">
|
<el-form ref="form" :model="form" :rules="rules" label-position="top">
|
<el-form-item label="区域" prop="areaId">
|
<el-cascader
|
v-model="form.areaId"
|
:options="modalAreaOptions"
|
:props="areaCascaderProps"
|
clearable
|
filterable
|
placeholder="请选择"
|
/>
|
</el-form-item>
|
<el-form-item label="设备名称" prop="deviceName">
|
<el-input v-model="form.deviceName" placeholder="请输入" />
|
</el-form-item>
|
<el-form-item label="品牌" prop="brand">
|
<el-input v-model="form.brand" placeholder="请输入" />
|
</el-form-item>
|
<el-form-item label="设备ID" prop="deviceId">
|
<el-input v-model="form.deviceId" :placeholder="deviceType === 'door' ? '请输入设备IP或IP:端口' : '请输入'" />
|
</el-form-item>
|
<template v-if="deviceType === 'door'">
|
<el-form-item label="NetSDK端口" prop="devicePort">
|
<el-input-number v-model="form.devicePort" :min="1" :max="65535" controls-position="right" />
|
</el-form-item>
|
<el-form-item label="设备登录账号" prop="loginUsername">
|
<el-input v-model="form.loginUsername" placeholder="请输入设备登录账号" autocomplete="off" />
|
</el-form-item>
|
<el-form-item label="设备登录密码" prop="loginPassword">
|
<el-input
|
v-model="form.loginPassword"
|
type="password"
|
show-password
|
autocomplete="new-password"
|
:placeholder="form.doorId && form.passwordConfigured ? '留空表示保留原密码' : '请输入设备登录密码'"
|
/>
|
</el-form-item>
|
<el-form-item label="门通道序号" prop="doorNo">
|
<el-input-number v-model="form.doorNo" :min="0" controls-position="right" />
|
</el-form-item>
|
<el-form-item label="时间模板序号" prop="timeSectionNo">
|
<el-input-number v-model="form.timeSectionNo" :min="0" :max="255" controls-position="right" />
|
</el-form-item>
|
<el-form-item label="姓名编码" prop="charset">
|
<el-select v-model="form.charset" placeholder="请选择">
|
<el-option label="GBK" value="GBK" />
|
<el-option label="UTF-8" value="UTF-8" />
|
</el-select>
|
</el-form-item>
|
</template>
|
<el-form-item label="备注" prop="remark">
|
<el-input v-model="form.remark" placeholder="请输入" />
|
</el-form-item>
|
<el-form-item label="启用状态" prop="enabled">
|
<el-switch v-model="form.enabled" active-color="#13ce66" inactive-color="#dcdfe6" />
|
</el-form-item>
|
</el-form>
|
<div slot="footer" class="dialog-footer">
|
<el-button @click="open = false">取消</el-button>
|
<el-button type="primary" @click="submitForm">保存</el-button>
|
</div>
|
</el-dialog>
|
</main>
|
</template>
|
|
<script>
|
import { treeArea } from '@/api/visitor-admin/area'
|
import { listDevice, getDevice, addDevice, updateDevice, changeDeviceStatus, logoutDevice, delDevice } from '@/api/visitor-admin/device'
|
import { getResponseData, getPageRows, getPageTotal, normalizeBoolean, onlineStatusClass, onlineText, toIdArray } from '@/utils/visitor-admin'
|
|
export default {
|
name: 'VisitorDevice',
|
// 目的:初始化设备列表、区域筛选、查询条件和编辑表单状态。
|
data() {
|
return {
|
loading: false,
|
open: false,
|
title: '',
|
areaFilterText: '',
|
areaTree: [],
|
areaProps: {
|
children: 'children',
|
label: 'areaName'
|
},
|
areaCascaderProps: {
|
value: 'areaId',
|
label: 'areaName',
|
children: 'children',
|
checkStrictly: true,
|
emitPath: false
|
},
|
ids: [],
|
total: 0,
|
deviceList: [],
|
queryParams: {
|
pageNum: 1,
|
pageSize: 10,
|
areaId: undefined,
|
deviceName: '',
|
deviceId: '',
|
enabled: undefined,
|
onlineStatus: ''
|
},
|
form: {},
|
rules: {
|
areaId: [{ required: true, message: '请选择区域', trigger: 'change' }],
|
deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
|
brand: [{ required: true, message: '请输入品牌', trigger: 'blur' }],
|
deviceId: [{ required: true, message: '请输入设备ID', trigger: 'blur' }],
|
devicePort: [{ validator: (rule, value, callback) => this.validateDoorRequired(value, '请输入NetSDK端口', callback), trigger: 'blur' }],
|
loginUsername: [{ validator: (rule, value, callback) => this.validateDoorRequired(value, '请输入设备登录账号', callback), trigger: 'blur' }],
|
loginPassword: [{ validator: (rule, value, callback) => this.validateDoorPassword(value, callback), trigger: 'blur' }],
|
doorNo: [{ validator: (rule, value, callback) => this.validateDoorRequired(value, '请输入门通道序号', callback), trigger: 'blur' }],
|
timeSectionNo: [{ validator: (rule, value, callback) => this.validateDoorRequired(value, '请输入时间模板序号', callback), trigger: 'blur' }],
|
charset: [{ validator: (rule, value, callback) => this.validateDoorRequired(value, '请选择姓名编码', callback), trigger: 'change' }]
|
}
|
}
|
},
|
computed: {
|
// 目的:根据路由判断当前页面管理访客机还是门禁设备。
|
deviceType() {
|
const type = this.$route.query.type || this.$route.meta.deviceType
|
return type === 'door' ? 'door' : 'machine'
|
},
|
// 目的:返回当前设备类型对应的主键字段名。
|
idKey() {
|
return this.deviceType === 'door' ? 'doorId' : 'machineId'
|
},
|
// 目的:返回当前设备类型的页面标题前缀。
|
pageTitle() {
|
return this.deviceType === 'door' ? '门禁' : '访客机'
|
},
|
// 目的:弹窗区域选择只保留一级和二级,避免二级继续展开空面板。
|
modalAreaOptions() {
|
return this.normalizeModalAreaOptions(this.areaTree)
|
}
|
},
|
watch: {
|
// 目的:区域树筛选关键字变化时实时过滤区域节点。
|
areaFilterText(val) {
|
this.$refs.areaTree && this.$refs.areaTree.filter(val)
|
},
|
// 目的:路由设备类型切换时重置查询并刷新列表。
|
'$route.query.type'() {
|
this.resetQuery()
|
}
|
},
|
// 目的:页面初始化时加载区域树和设备分页列表。
|
created() {
|
this.getAreaTree()
|
this.getList()
|
},
|
methods: {
|
// 目的:仅在门禁表单中校验连接参数必填,访客机表单保持原有规则。
|
validateDoorRequired(value, message, callback) {
|
if (this.deviceType === 'door' && (value === undefined || value === null || value === '')) {
|
callback(new Error(message))
|
return
|
}
|
callback()
|
},
|
// 目的:新增门禁必须输入密码;编辑留空时由后端保留已有密文。
|
validateDoorPassword(value, callback) {
|
if (this.deviceType === 'door' && !value && !(this.form.doorId && this.form.passwordConfigured)) {
|
callback(new Error('请输入设备登录密码'))
|
return
|
}
|
callback()
|
},
|
// 目的:判断当前设备类型是否具备指定操作权限。
|
hasDevicePerm(action) {
|
return this.$auth.hasPermi('visitor:' + this.deviceType + ':' + action)
|
},
|
// 目的:判断当前设备类型是否具备任一操作权限。
|
hasAnyDevicePerm(actions) {
|
return actions.some(action => this.hasDevicePerm(action))
|
},
|
// 目的:加载区域树,供设备筛选和表单选择使用。
|
getAreaTree() {
|
treeArea({}).then(res => {
|
this.areaTree = getResponseData(res) || []
|
}).catch(() => {
|
this.areaTree = []
|
})
|
},
|
// 目的:按弹窗交互要求移除二级以下区域和叶子节点空 children。
|
normalizeModalAreaOptions(list, level = 1) {
|
return (list || []).map(item => {
|
const option = {
|
...item
|
}
|
const children = Array.isArray(item.children) ? item.children : []
|
if (level < 2 && children.length) {
|
option.children = this.normalizeModalAreaOptions(children, level + 1)
|
} else {
|
delete option.children
|
}
|
return option
|
})
|
},
|
// 目的:按当前设备类型和查询条件加载分页列表。
|
getList() {
|
this.loading = true
|
listDevice(this.deviceType, this.queryParams).then(res => {
|
this.deviceList = getPageRows(res).map(item => ({
|
...item,
|
enabled: normalizeBoolean(item.enabled)
|
}))
|
this.total = getPageTotal(res)
|
}).catch(() => {
|
this.deviceList = []
|
this.total = 0
|
}).finally(() => {
|
this.loading = false
|
})
|
},
|
// 目的:把在线状态值转换为页面展示文本。
|
onlineLabel(value) {
|
return onlineText(value)
|
},
|
// 目的:根据数据库在线状态枚举返回对应的标签样式。
|
onlineClass(value) {
|
return onlineStatusClass(value)
|
},
|
// 目的:根据区域是否有子级展示目录或文档图标,匹配区域树层级语义。
|
areaNodeIconClass(data) {
|
const hasChildren = Array.isArray(data.children) && data.children.length > 0
|
return hasChildren ? 'el-icon-folder area-tree-node-icon is-folder' : 'el-icon-document area-tree-node-icon is-file'
|
},
|
// 目的:按区域名称关键字过滤区域树节点。
|
filterAreaNode(value, data) {
|
return !value || (data.areaName || '').indexOf(value) !== -1
|
},
|
// 目的:点击区域节点后按区域筛选设备列表。
|
handleAreaClick(data) {
|
this.queryParams.areaId = data.areaId
|
this.handleQuery()
|
},
|
// 目的:从第一页重新执行设备查询。
|
handleQuery() {
|
this.queryParams.pageNum = 1
|
this.getList()
|
},
|
// 目的:清空设备筛选条件、区域搜索和树选中状态后刷新列表。
|
resetQuery() {
|
this.resetForm('queryForm')
|
this.queryParams.areaId = undefined
|
this.areaFilterText = ''
|
this.$refs.areaTree && this.$refs.areaTree.setCurrentKey(null)
|
this.handleQuery()
|
},
|
// 目的:同步表格勾选的设备主键集合。
|
handleSelectionChange(selection) {
|
this.ids = selection.map(item => item[this.idKey])
|
},
|
// 目的:打开新增设备弹窗;中台只读模式下禁止本地新增。
|
handleAdd() {
|
if (this.isMiddlePlatformReadonly) {
|
this.denyMiddlePlatformWrite()
|
return
|
}
|
this.title = '新增' + this.pageTitle
|
this.form = {
|
areaId: this.queryParams.areaId,
|
enabled: true,
|
devicePort: this.deviceType === 'door' ? 37777 : undefined,
|
loginUsername: this.deviceType === 'door' ? 'admin' : undefined,
|
doorNo: this.deviceType === 'door' ? 0 : undefined,
|
timeSectionNo: this.deviceType === 'door' ? 255 : undefined,
|
charset: this.deviceType === 'door' ? 'GBK' : undefined
|
}
|
this.open = true
|
},
|
// 目的:加载设备详情并打开编辑弹窗;中台只读模式下禁止本地编辑。
|
handleUpdate(row) {
|
if (this.isMiddlePlatformReadonly) {
|
this.denyMiddlePlatformWrite()
|
return
|
}
|
const id = row ? row[this.idKey] : this.ids[0]
|
getDevice(this.deviceType, id).then(res => {
|
const data = getResponseData(res) || {}
|
this.title = '编辑' + this.pageTitle
|
this.form = {
|
...data,
|
loginPassword: '',
|
enabled: normalizeBoolean(data.enabled)
|
}
|
this.open = true
|
})
|
},
|
// 目的:校验并提交设备新增或编辑表单;中台只读模式下禁止提交。
|
submitForm() {
|
if (this.isMiddlePlatformReadonly) {
|
this.denyMiddlePlatformWrite()
|
return
|
}
|
this.$refs.form.validate(valid => {
|
if (!valid) {
|
return
|
}
|
const request = this.form[this.idKey] ? updateDevice : addDevice
|
request(this.deviceType, this.form).then(() => {
|
this.$modal.msgSuccess('保存成功')
|
this.open = false
|
this.getList()
|
})
|
})
|
},
|
// 目的:处理更多菜单命令,并在中台只读模式下统一拦截写操作。
|
handleMoreCommand(command, row) {
|
if (this.isMiddlePlatformReadonly) {
|
this.denyMiddlePlatformWrite()
|
return
|
}
|
if (command === 'status') {
|
this.handleStatusChange(row)
|
} else if (command === 'logout') {
|
this.handleLogout(row)
|
} else if (command === 'delete') {
|
this.handleDelete(row)
|
}
|
},
|
// 目的:切换设备启停状态;中台只读模式下禁止本地变更。
|
handleStatusChange(row) {
|
if (this.isMiddlePlatformReadonly) {
|
this.denyMiddlePlatformWrite()
|
return
|
}
|
const nextEnabled = !row.enabled
|
changeDeviceStatus(this.deviceType, [row[this.idKey]], nextEnabled).then(() => {
|
row.enabled = nextEnabled
|
this.$modal.msgSuccess('状态已更新')
|
})
|
},
|
// 目的:注销设备;中台只读模式下禁止本地注销。
|
handleLogout(row) {
|
if (this.isMiddlePlatformReadonly) {
|
this.denyMiddlePlatformWrite()
|
return
|
}
|
const ids = row ? [row[this.idKey]] : this.ids
|
this.$modal.confirm('是否确认注销选中的' + this.pageTitle + '?').then(() => logoutDevice(this.deviceType, ids)).then(() => {
|
this.$modal.msgSuccess('注销成功')
|
this.getList()
|
})
|
},
|
// 目的:删除设备;中台只读模式下禁止本地删除。
|
handleDelete(row) {
|
if (this.isMiddlePlatformReadonly) {
|
this.denyMiddlePlatformWrite()
|
return
|
}
|
const ids = row ? [row[this.idKey]] : toIdArray(this.ids)
|
this.$modal.confirm('是否确认删除选中的' + this.pageTitle + '?').then(() => delDevice(this.deviceType, ids)).then(() => {
|
this.$modal.msgSuccess('删除成功')
|
this.getList()
|
})
|
}
|
}
|
}
|
</script>
|
|
<style lang="scss">
|
.visitor-device-dialog {
|
.el-dialog__body {
|
padding: 18px 24px 24px;
|
}
|
|
.el-form {
|
padding: 0 8px;
|
}
|
|
.el-form-item {
|
margin-bottom: 18px;
|
}
|
|
.el-form-item__label {
|
padding: 0 0 8px;
|
color: #303133;
|
font-weight: 600;
|
line-height: 20px;
|
text-align: left;
|
}
|
|
.el-form-item__content {
|
line-height: 1;
|
}
|
|
.el-cascader,
|
.el-input,
|
.el-select,
|
.el-input-number {
|
width: 100%;
|
}
|
|
.el-switch {
|
display: inline-flex;
|
align-items: center;
|
}
|
}
|
</style>
|