Files
doorcontrol/base/RuoYi-Vue2-master/src/views/iot/device/index.vue
T
2026-11-03 18:31:01 +08:00

791 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="门锁SN" prop="deviceSn">
<el-input
v-model="queryParams.deviceSn"
placeholder="请输入门锁SN"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="门锁名称" prop="deviceName">
<el-input
v-model="queryParams.deviceName"
placeholder="请输入门锁名称"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="房间号" prop="roomNo">
<el-input
v-model="queryParams.roomNo"
placeholder="请输入房间号"
clearable
style="width: 160px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="在线状态" prop="onlineStatus">
<el-select v-model="queryParams.onlineStatus" placeholder="在线状态" clearable style="width: 160px">
<el-option
v-for="dict in dict.type.lock_online_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="电量" prop="batteryMin">
<el-input-number
v-model="queryParams.batteryMin"
:min="0"
:max="100"
:controls="false"
placeholder="最低"
style="width: 80px"
/>
<span class="range-sep">-</span>
<el-input-number
v-model="queryParams.batteryMax"
:min="0"
:max="100"
:controls="false"
placeholder="最高"
style="width: 80px"
/>
</el-form-item>
<el-form-item label="信号" prop="signalMin">
<el-input-number
v-model="queryParams.signalMin"
:min="-150"
:max="0"
:controls="false"
placeholder="最低"
style="width: 80px"
/>
<span class="range-sep">-</span>
<el-input-number
v-model="queryParams.signalMax"
:min="-150"
:max="0"
:controls="false"
placeholder="最高"
style="width: 80px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['lock:device:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['lock:device:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['lock:device:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-time"
size="mini"
:disabled="multiple"
@click="handleBatchSync"
v-hasPermi="['lock:device:sync']"
>时间同步</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-circle-close"
size="mini"
:disabled="multiple"
@click="handleBatchClearFace"
v-hasPermi="['lock:auth:clear']"
>清空人脸授权</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['lock:device:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table
ref="deviceTable"
v-loading="loading"
:data="deviceList"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="门锁ID" align="center" prop="deviceId" width="80" />
<el-table-column label="门锁SN" align="center" prop="deviceSn" :show-overflow-tooltip="true" />
<el-table-column label="门锁名称" align="center" prop="deviceName" :show-overflow-tooltip="true" />
<el-table-column label="房间号" align="center" prop="roomNo" />
<el-table-column label="算法版本" align="center" prop="faceAlgoVersion" :show-overflow-tooltip="true" />
<el-table-column label="网关ID" align="center" prop="gatewayId" width="80" />
<el-table-column label="在线状态" align="center" prop="onlineStatus">
<template slot-scope="scope">
<dict-tag :options="dict.type.lock_online_status" :value="scope.row.onlineStatus"/>
</template>
</el-table-column>
<el-table-column
label="电量"
align="center"
prop="batteryLevel"
width="90"
sortable="custom"
:sort-orders="['descending', 'ascending']"
>
<template slot-scope="scope">
<span v-if="scope.row.batteryLevel != null" :class="{ 'text-danger': scope.row.batteryLevel <= 20 }">
{{ scope.row.batteryLevel }}%
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column
label="信号强度"
align="center"
prop="signalStrength"
width="110"
sortable="custom"
:sort-orders="['ascending', 'descending']"
>
<template slot-scope="scope">
<span v-if="scope.row.signalStrength" :class="signalClass(scope.row.signalStrength)">
{{ scope.row.signalStrength }} dBm
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="最近上报" align="center" prop="lastStatusTime" width="160" />
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="340" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-unlock"
@click="handleOpen(scope.row)"
v-hasPermi="['lock:device:open']"
>开锁</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-time"
@click="handleSync(scope.row)"
v-hasPermi="['lock:device:sync']"
>同步</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-key"
@click="handleDeviceAuth(scope.row)"
v-hasPermi="['lock:auth:list']"
>锁内授权</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['lock:device:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['lock:device:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改智能门锁对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="90px">
<el-form-item label="门锁SN" prop="deviceSn">
<el-input v-model="form.deviceSn" placeholder="请输入门锁SN(协议 devId)" />
</el-form-item>
<el-form-item label="门锁名称" prop="deviceName">
<el-input v-model="form.deviceName" placeholder="请输入门锁名称" />
</el-form-item>
<el-form-item label="门锁密码" prop="devicePassword">
<el-input v-model="form.devicePassword" placeholder="请输入门锁密码(协议 devPsw)" />
</el-form-item>
<el-form-item label="房间号" prop="roomNo">
<el-input v-model="form.roomNo" placeholder="请输入分配房间号" />
</el-form-item>
<el-form-item label="算法版本" prop="faceAlgoVersion">
<el-input v-model="form.faceAlgoVersion" placeholder="人脸算法版本,如 2.14.5_2.7.1_M3" />
</el-form-item>
<el-form-item label="所属网关" prop="gatewayId">
<el-input v-model.number="form.gatewayId" placeholder="请输入所属网关ID" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
<!-- 锁内授权对账对话框 -->
<el-dialog title="锁内授权对账" :visible.sync="deviceAuthOpen" width="1100px" append-to-body @closed="closeDeviceAuth">
<div class="auth-toolbar">
<div class="auth-device">
<span class="auth-device-name">{{ deviceAuthDevice.deviceSn }}</span>
<span class="auth-device-extra">{{ deviceAuthDevice.deviceName }} / 房间 {{ deviceAuthDevice.roomNo || '-' }}</span>
</div>
<div>
<el-button
type="primary"
size="mini"
icon="el-icon-refresh"
:loading="deviceAuthQuerying"
@click="handleReadDeviceAuth"
>读取锁内授权</el-button>
<el-button
size="mini"
icon="el-icon-refresh-right"
:loading="deviceAuthLoading"
@click="loadDeviceAuthSnapshot"
>刷新结果</el-button>
</div>
</div>
<el-alert :title="deviceAuthSnapshot.notice" type="info" :closable="false" show-icon />
<div class="auth-summary">
<el-tag size="mini" :type="queryStatusTag(deviceAuthSnapshot.queryStatus)">读取:{{ deviceAuthSnapshot.queryStatusName || '未读取' }}</el-tag>
<span v-if="deviceAuthSnapshot.queryTime" class="auth-summary-item">时间:{{ deviceAuthSnapshot.queryTime }}</span>
<span v-if="deviceAuthSnapshot.queryMsg" class="auth-summary-item">说明:{{ deviceAuthSnapshot.queryMsg }}</span>
<span class="auth-summary-item">设备侧 {{ deviceAuthSnapshot.deviceCount || 0 }}</span>
<span class="auth-summary-item">平台 {{ deviceAuthSnapshot.platformCount || 0 }}</span>
<span class="auth-summary-item">一致 {{ deviceAuthSnapshot.matchedCount || 0 }}</span>
<span class="auth-summary-item text-danger">仅设备 {{ deviceAuthSnapshot.deviceOnlyCount || 0 }}</span>
<span class="auth-summary-item text-warning">仅平台 {{ deviceAuthSnapshot.platformOnlyCount || 0 }}</span>
<span class="auth-summary-item">不支持查询 {{ deviceAuthSnapshot.notQueryableCount || 0 }}</span>
</div>
<el-table v-loading="deviceAuthLoading" :data="deviceAuthSnapshot.rows" size="mini" max-height="440" border>
<el-table-column label="来源" width="80" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.source === 'DEVICE' ? '' : 'info'">{{ scope.row.source === 'DEVICE' ? '设备侧' : '平台' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="索引" width="60" align="center">
<template slot-scope="scope">{{ scope.row.rowIndex == null ? '-' : scope.row.rowIndex }}</template>
</el-table-column>
<el-table-column label="凭据(脱敏)" prop="credential" min-width="140" show-overflow-tooltip />
<el-table-column label="类型" prop="authTypeName" width="80" align="center" />
<el-table-column label="人员" width="170" show-overflow-tooltip>
<template slot-scope="scope">
<span v-if="scope.row.personNo">{{ scope.row.personName }}({{ scope.row.personNo }})</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="对账" width="130" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="matchStatusTag(scope.row.matchStatus)">{{ scope.row.matchStatusName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="生效时间" prop="beginTime" width="150" align="center" />
<el-table-column label="失效时间" prop="endTime" width="150" align="center" />
<el-table-column label="清除" width="90" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="clearStatusTag(scope.row.clearStatus)">{{ scope.row.clearStatusName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="说明" prop="remark" min-width="180" show-overflow-tooltip />
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button
v-if="scope.row.clearable"
size="mini"
type="text"
icon="el-icon-delete"
style="color: #f56c6c"
@click="handleClearDeviceAuth(scope.row)"
>清除</el-button>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<el-dialog title="选择凭据类型" :visible.sync="deviceAuthClearOpen" width="400px" append-to-body>
<div class="auth-clear-tip">
设备返回的凭据「{{ deviceAuthClearRow.credential }}」在平台没有匹配授权,无法自动判断类型。
</div>
<el-radio-group v-model="deviceAuthClearType">
<el-radio label="02">密码</el-radio>
<el-radio label="04">IC卡</el-radio>
</el-radio-group>
<div slot="footer" class="dialog-footer">
<el-button size="mini" @click="deviceAuthClearOpen = false">取 消</el-button>
<el-button size="mini" type="danger" :loading="deviceAuthClearing" @click="confirmClearDeviceAuth">确认清除</el-button>
</div>
</el-dialog>
</el-dialog>
</div>
</template>
<script>
import {
listDevice,
getDevice,
delDevice,
addDevice,
updateDevice,
syncTime,
batchSyncTime,
remoteSwitch,
clearFaceAuthByDevices,
queryDeviceAuths,
getDeviceAuthSnapshot,
clearDeviceAuth
} from "@/api/iot/device";
export default {
name: "Device",
dicts: ['lock_online_status', 'sys_normal_disable'],
data() {
return {
loading: true,
ids: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
deviceList: [],
title: "",
open: false,
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
deviceSn: undefined,
deviceName: undefined,
roomNo: undefined,
onlineStatus: undefined,
batteryMin: undefined,
batteryMax: undefined,
signalMin: undefined,
signalMax: undefined,
sortField: undefined,
sortOrder: undefined
},
rules: {
deviceSn: [
{ required: true, message: "门锁SN不能为空", trigger: "blur" }
]
},
deviceAuthOpen: false,
deviceAuthLoading: false,
deviceAuthQuerying: false,
deviceAuthDevice: {},
deviceAuthSnapshot: { rows: [] },
deviceAuthClearOpen: false,
deviceAuthClearRow: {},
deviceAuthClearType: "02",
deviceAuthClearing: false,
deviceAuthTimer: null,
deviceAuthRefreshTimer: null
};
},
created() {
this.getList();
},
methods: {
getList() {
this.loading = true;
listDevice(this.queryParams).then(response => {
this.deviceList = response.rows;
this.total = response.total;
this.loading = false;
});
},
cancel() {
this.open = false;
this.reset();
},
reset() {
this.form = {
deviceId: undefined,
deviceSn: undefined,
deviceName: undefined,
devicePassword: undefined,
roomNo: undefined,
faceAlgoVersion: undefined,
gatewayId: undefined,
status: "0",
remark: undefined
};
this.resetForm("form");
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
resetQuery() {
this.resetForm("queryForm");
// 排序不在表单里,单独清空并复位表头箭头
this.queryParams.sortField = undefined;
this.queryParams.sortOrder = undefined;
if (this.$refs.deviceTable) {
this.$refs.deviceTable.clearSort();
}
this.handleQuery();
},
handleSortChange({ prop, order }) {
// 只有电量和信号两列开启自定义排序,其它列不触发
const sortField = order ? (prop === "batteryLevel" ? "battery" : "signal") : undefined;
const sortOrder = order ? (order === "ascending" ? "asc" : "desc") : undefined;
if (sortField === this.queryParams.sortField && sortOrder === this.queryParams.sortOrder) {
return;
}
this.queryParams.sortField = sortField;
this.queryParams.sortOrder = sortOrder;
this.queryParams.pageNum = 1;
this.getList();
},
signalClass(val) {
const rssi = Number(val);
if (isNaN(rssi)) {
return "";
}
if (rssi <= -85) {
return "text-danger";
}
return rssi <= -70 ? "text-warning" : "";
},
handleSelectionChange(selection) {
this.ids = selection.map(item => item.deviceId);
this.single = selection.length != 1;
this.multiple = !selection.length;
},
handleAdd() {
this.reset();
this.open = true;
this.title = "添加智能门锁";
},
handleUpdate(row) {
this.reset();
const deviceId = row.deviceId || this.ids;
getDevice(deviceId).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改智能门锁";
});
},
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.deviceId != undefined) {
updateDevice(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
addDevice(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
}
}
});
},
handleSync(row) {
syncTime(row.deviceId).then(response => {
this.$modal.msgSuccess("时间同步指令已下发");
});
},
handleBatchSync() {
batchSyncTime(this.ids).then(response => {
this.$modal.msgSuccess("批量时间同步指令已下发");
});
},
handleBatchClearFace() {
const deviceIds = this.ids;
this.$modal.confirm('确认清空选中 ' + deviceIds.length + ' 把门锁上的所有人脸授权?将下发删除人脸指令,平台侧该锁的人脸授权同时标记为已清除,之后不再自动下发。').then(function() {
return clearFaceAuthByDevices(deviceIds);
}).then(response => {
this.$modal.msgSuccess("已向 " + (response.data || 0) + " 把门锁下发清空指令,平台状态已置为已清除");
}).catch(() => {});
},
handleOpen(row) {
this.$modal.confirm('确认对门锁"' + row.deviceSn + '"下发远程开锁指令?').then(function() {
return remoteSwitch(row.deviceId, true);
}).then(() => {
this.$modal.msgSuccess("开锁指令已下发");
}).catch(() => {});
},
handleDeviceAuth(row) {
this.deviceAuthDevice = row;
this.deviceAuthSnapshot = { rows: [] };
this.deviceAuthOpen = true;
this.loadDeviceAuthSnapshot();
},
loadDeviceAuthSnapshot() {
if (!this.deviceAuthDevice.deviceId) {
return Promise.resolve();
}
this.deviceAuthLoading = true;
return getDeviceAuthSnapshot(this.deviceAuthDevice.deviceId).then(response => {
this.deviceAuthSnapshot = response.data || { rows: [] };
if (!this.deviceAuthSnapshot.rows) {
this.deviceAuthSnapshot.rows = [];
}
this.deviceAuthLoading = false;
}, () => {
this.deviceAuthLoading = false;
});
},
handleReadDeviceAuth() {
if (this.deviceAuthQuerying) {
return;
}
this.deviceAuthQuerying = true;
queryDeviceAuths(this.deviceAuthDevice.deviceId).then(() => {
this.$modal.msgSuccess("读取指令已下发,正在等待设备返回");
this.loadDeviceAuthSnapshot();
this.startDeviceAuthPolling();
}, () => {}).then(() => {
this.deviceAuthQuerying = false;
});
},
startDeviceAuthPolling() {
this.stopDeviceAuthPolling();
let tries = 0;
this.deviceAuthTimer = setInterval(() => {
tries++;
this.loadDeviceAuthSnapshot().then(() => {
const status = this.deviceAuthSnapshot.queryStatus;
if ((status && status !== "0" && status !== "1") || tries >= 15) {
this.stopDeviceAuthPolling();
}
});
}, 2000);
},
stopDeviceAuthPolling() {
if (this.deviceAuthTimer) {
clearInterval(this.deviceAuthTimer);
this.deviceAuthTimer = null;
}
},
closeDeviceAuth() {
this.stopDeviceAuthPolling();
if (this.deviceAuthRefreshTimer) {
clearTimeout(this.deviceAuthRefreshTimer);
this.deviceAuthRefreshTimer = null;
}
this.deviceAuthClearOpen = false;
},
handleClearDeviceAuth(row) {
if (!row || !row.deviceAuthId) {
return;
}
if (row.authType) {
this.$modal.confirm('确认清除设备侧' + (row.authTypeName || '') + '授权「' + row.credential + '」?平台对应授权状态将同步更新。').then(() => {
this.submitClearDeviceAuth(row, row.authType);
}).catch(() => {});
return;
}
this.deviceAuthClearRow = row;
this.deviceAuthClearType = "02";
this.deviceAuthClearOpen = true;
},
confirmClearDeviceAuth() {
this.submitClearDeviceAuth(this.deviceAuthClearRow, this.deviceAuthClearType);
},
submitClearDeviceAuth(row, authType) {
const target = row || this.deviceAuthClearRow;
const type = authType || this.deviceAuthClearType;
if (!target || !target.deviceAuthId || this.deviceAuthClearing) {
return;
}
this.deviceAuthClearing = true;
clearDeviceAuth(this.deviceAuthDevice.deviceId, {
deviceAuthId: target.deviceAuthId,
authType: type
}).then(() => {
this.deviceAuthClearOpen = false;
this.$modal.msgSuccess("清除指令已下发");
this.loadDeviceAuthSnapshot();
this.scheduleDeviceAuthRefresh();
}, () => {}).then(() => {
this.deviceAuthClearing = false;
});
},
scheduleDeviceAuthRefresh() {
if (this.deviceAuthRefreshTimer) {
clearTimeout(this.deviceAuthRefreshTimer);
}
this.deviceAuthRefreshTimer = setTimeout(() => {
this.deviceAuthRefreshTimer = null;
this.loadDeviceAuthSnapshot();
}, 4000);
},
queryStatusTag(status) {
if (status === "2") {
return "success";
}
if (status === "3" || status === "4") {
return "danger";
}
if (status === "0" || status === "1") {
return "warning";
}
return "info";
},
matchStatusTag(status) {
if (status === "MATCHED") {
return "success";
}
if (status === "DEVICE_ONLY") {
return "danger";
}
if (status === "PLATFORM_ONLY") {
return "warning";
}
return "info";
},
clearStatusTag(status) {
if (status === "2") {
return "success";
}
if (status === "1") {
return "warning";
}
if (status === "3") {
return "danger";
}
return "info";
},
handleDelete(row) {
const deviceIds = row.deviceId || this.ids;
this.$modal.confirm('是否确认删除门锁编号为"' + deviceIds + '"的数据项?').then(function() {
return delDevice(deviceIds);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
handleExport() {
this.download('lock/device/export', {
...this.queryParams
}, `device_${new Date().getTime()}.xlsx`);
}
}
};
</script>
<style scoped>
.range-sep {
margin: 0 4px;
color: #909399;
}
.text-danger {
color: #f56c6c;
}
.text-warning {
color: #e6a23c;
}
.auth-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.auth-device-name {
font-weight: 600;
margin-right: 8px;
}
.auth-device-extra {
color: #909399;
font-size: 12px;
}
.auth-summary {
display: flex;
flex-wrap: wrap;
align-items: center;
margin: 10px 0;
font-size: 12px;
color: #606266;
}
.auth-summary-item {
margin-left: 14px;
}
.auth-clear-tip {
margin-bottom: 16px;
color: #606266;
line-height: 1.6;
}
</style>