Files
xiaozhi-esp32-server/main/manager-web/src/views/DeviceManagement.vue
T

550 lines
18 KiB
Vue
Raw Normal View History

2025-03-18 21:49:57 +08:00
<template>
<div class="welcome">
2025-09-16 00:06:57 +08:00
<HeaderBar />
2025-04-14 07:52:00 +08:00
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
2025-04-15 17:59:29 +08:00
<el-card class="device-card" shadow="never">
2026-06-24 17:37:37 +08:00
<div class="operation-header">
<h2 class="page-title">{{ $t('device.management') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('device.searchPlaceholder')" v-model="searchKeyword" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<CustomButton icon="el-icon-search" type="confirm" @click="handleSearch">{{ $t('device.search') }}</CustomButton>
2025-04-14 07:52:00 +08:00
</div>
</div>
2026-06-24 17:37:37 +08:00
<CustomTable
ref="deviceTable"
:data="paginatedDeviceList"
:columns="tableColumns"
:loading="loading"
:loading-text="$t('deviceManagement.loading')"
:show-selection="true"
:show-operations="true"
:operations-label="$t('device.operation')"
:total="filteredDeviceList.length"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@size-change="handlePageSizeChange"
@page-change="goToPage"
>
<template slot="model" slot-scope="scope">
{{ getFirmwareTypeName(scope.row.model) }}
</template>
<template slot="macAddress" slot-scope="scope">
<MacAddressMask :macAddress="scope.row.macAddress" />
</template>
<template slot="deviceStatus" slot-scope="scope">
<el-tag v-if="scope.row.deviceStatus === 'online'" type="success">{{ $t('device.online') }}</el-tag>
<el-tag v-else type="danger">{{ $t('device.offline') }}</el-tag>
</template>
<template slot="remark" slot-scope="scope">
<el-input v-show="scope.row.isEdit" v-model="scope.row.remark" size="mini" maxlength="64" show-word-limit
@blur="onRemarkBlur(scope.row)" @keyup.enter.native="onRemarkEnter(scope.row)" />
<span v-show="!scope.row.isEdit" class="remark-view">
<i class="el-icon-edit" @click="scope.row.isEdit = true" style="cursor: pointer;"></i>
<span @click="scope.row.isEdit = true">
{{ scope.row.remark || '-' }}
</span>
</span>
</template>
<template slot="otaSwitch" slot-scope="scope">
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
@change="handleOtaSwitchChange(scope.row)"></el-switch>
</template>
<template slot="operations" slot-scope="scope">
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
{{ $t('device.unbind') }}
</el-button>
<el-button v-if="isGenerate(scope.row)" size="mini" type="text" @click="handleGenertor(scope.row)">
{{ $t('device.deviceThemeGeneration') }}
</el-button>
</template>
<template slot="footer-btns">
<div class="ctrl_btn">
<CustomButton :icon="isCurrentPageAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="small" @click="handleSelectAll">
{{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
</CustomButton>
<CustomButton icon="el-icon-plus" type="add" size="small" @click="handleAddDevice">
{{ $t('device.bindWithCode') }}
</CustomButton>
<CustomButton icon="el-icon-plus" type="add" size="small" @click="handleManualAddDevice">
{{ $t('device.manualAdd') }}
</CustomButton>
<CustomButton size="small" type="delete" icon="el-icon-delete" @click="deleteSelected">
{{ $t('device.unbind') }}
</CustomButton>
</div>
</template>
</CustomTable>
2025-04-14 07:52:00 +08:00
</el-card>
2025-04-12 08:27:03 +08:00
</div>
2025-03-18 21:49:57 +08:00
</div>
2025-04-14 07:52:00 +08:00
</div>
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
2025-09-16 00:06:57 +08:00
@refresh="fetchBindDevices(currentAgentId)" />
2025-06-26 11:09:17 +08:00
<ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId"
2025-09-16 00:06:57 +08:00
@refresh="fetchBindDevices(currentAgentId)" />
<el-footer>
<version-footer />
</el-footer>
2025-03-18 21:49:57 +08:00
</div>
</template>
<script>
2025-04-05 21:03:46 +08:00
import Api from '@/apis/api';
2025-03-18 21:49:57 +08:00
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
2025-04-05 20:16:02 +08:00
import HeaderBar from "@/components/HeaderBar.vue";
2025-09-16 00:06:57 +08:00
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue";
2026-06-03 14:40:01 +08:00
import MacAddressMask from "@/components/MacAddressMask.vue";
2026-06-24 17:37:37 +08:00
import CustomButton from "@/components/CustomButton.vue";
import CustomTable from "@/components/CustomTable.vue";
2025-03-18 21:49:57 +08:00
export default {
2025-06-26 11:09:17 +08:00
components: {
2025-09-16 00:06:57 +08:00
HeaderBar,
2025-06-26 11:09:17 +08:00
AddDeviceDialog,
ManualAddDeviceDialog,
2026-06-03 14:40:01 +08:00
VersionFooter,
MacAddressMask,
2026-06-24 17:37:37 +08:00
CustomButton,
CustomTable,
2025-06-26 11:09:17 +08:00
},
2025-03-18 21:49:57 +08:00
data() {
return {
addDeviceDialogVisible: false,
2025-06-26 11:09:17 +08:00
manualAddDeviceDialogVisible: false,
2025-09-16 00:06:57 +08:00
selectedDeviceId: '',
searchKeyword: "",
activeSearchKeyword: "",
2025-03-27 17:54:05 +08:00
currentAgentId: this.$route.query.agentId || '',
2025-03-25 16:23:28 +08:00
currentPage: 1,
pageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
2025-03-25 16:23:28 +08:00
deviceList: [],
loading: false,
2025-03-27 11:24:49 +08:00
userApi: null,
2025-05-01 16:57:58 +08:00
firmwareTypes: [],
2026-06-24 17:37:37 +08:00
mqttServiceAvailable: false,
2025-03-18 21:49:57 +08:00
};
},
2025-03-25 16:23:28 +08:00
computed: {
filteredDeviceList() {
const keyword = this.activeSearchKeyword.toLowerCase();
if (!keyword) return this.deviceList;
return this.deviceList.filter(device =>
2025-09-16 00:06:57 +08:00
(device.model && device.model.toLowerCase().includes(keyword)) ||
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
);
},
2025-03-25 16:23:28 +08:00
paginatedDeviceList() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.filteredDeviceList.slice(start, end);
2025-04-12 08:27:03 +08:00
},
isCurrentPageAllSelected() {
2025-09-16 00:06:57 +08:00
return this.paginatedDeviceList.length > 0 &&
this.paginatedDeviceList.every(device => device.selected);
},
2026-06-24 17:37:37 +08:00
tableColumns() {
const columns = [
{ prop: 'model', label: this.$t('device.model'), align: 'center' },
{ prop: 'firmwareVersion', label: this.$t('device.firmwareVersion'), align: 'center' },
{ prop: 'macAddress', label: this.$t('device.macAddress'), align: 'center' },
{ prop: 'bindTime', label: this.$t('device.bindTime'), align: 'center' },
{ prop: 'lastConversation', label: this.$t('device.lastConversation'), align: 'center' },
];
if (this.mqttServiceAvailable) {
columns.push({ prop: 'deviceStatus', label: this.$t('device.deviceStatus'), align: 'center' });
2025-04-12 08:27:03 +08:00
}
2026-06-24 17:37:37 +08:00
columns.push({ prop: 'remark', label: this.$t('device.remark'), align: 'center' });
columns.push({ prop: 'otaSwitch', label: this.$t('device.autoUpdate'), align: 'center' });
return columns;
2025-04-15 17:59:29 +08:00
},
2025-03-25 16:23:28 +08:00
},
mounted() {
const agentId = this.$route.query.agentId;
2025-04-05 21:03:46 +08:00
if (agentId) {
this.fetchBindDevices(agentId);
}
2025-03-25 16:23:28 +08:00
},
2025-05-01 16:57:58 +08:00
created() {
this.getFirmwareTypes()
},
2025-03-18 21:49:57 +08:00
methods: {
2025-05-01 16:57:58 +08:00
async getFirmwareTypes() {
try {
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
this.firmwareTypes = res.data
} catch (error) {
console.error(this.$t('device.getFirmwareTypeFailed') + ':', error)
this.$message.error(error.message || this.$t('device.getFirmwareTypeFailed'))
2025-05-01 16:57:58 +08:00
}
},
2025-04-15 17:59:29 +08:00
handlePageSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
},
handleSearch() {
this.activeSearchKeyword = this.searchKeyword;
this.currentPage = 1;
},
2025-04-12 09:28:19 +08:00
handleSelectAll() {
const shouldSelectAll = !this.isCurrentPageAllSelected;
this.paginatedDeviceList.forEach(row => {
row.selected = shouldSelectAll;
});
2025-04-12 09:28:19 +08:00
},
2025-04-14 07:52:00 +08:00
deleteSelected() {
const selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
if (selectedDevices.length === 0) {
this.$message.warning({
message: this.$t('device.selectAtLeastOne'),
showClose: true
});
return;
}
this.$confirm(this.$t('device.confirmBatchUnbind').replace('{count}', selectedDevices.length), this.$t('message.warning'), {
confirmButtonText: this.$t('button.ok'),
cancelButtonText: this.$t('button.cancel'),
type: 'warning'
}).then(() => {
const deviceIds = selectedDevices.map(device => device.device_id);
this.batchUnbindDevices(deviceIds);
});
},
batchUnbindDevices(deviceIds) {
const promises = deviceIds.map(id => {
return new Promise((resolve, reject) => {
2025-09-16 00:06:57 +08:00
Api.device.unbindDevice(id, ({ data }) => {
if (data.code === 0) {
resolve();
} else {
reject(data.msg || this.$t('device.bindFailed'));
}
});
});
});
Promise.all(promises)
2025-09-16 00:06:57 +08:00
.then(() => {
this.$message.success({
message: this.$t('device.batchUnbindSuccess').replace('{count}', deviceIds.length),
showClose: true
});
2025-09-16 00:06:57 +08:00
this.fetchBindDevices(this.currentAgentId);
})
.catch(error => {
this.$message.error({
message: error || this.$t('device.batchUnbindError'),
showClose: true
});
});
2025-04-14 07:52:00 +08:00
},
2025-03-18 21:49:57 +08:00
handleAddDevice() {
this.addDeviceDialogVisible = true;
},
2025-06-26 11:09:17 +08:00
handleManualAddDevice() {
this.manualAddDeviceDialogVisible = true;
},
submitRemark(row) {
if (row._submitting) return;
const text = (row.remark || '').trim();
if (text.length > 64) {
this.$message.warning(this.$t('device.remarkTooLong'));
return;
}
if (text === row._originalRemark) {
return;
}
row._submitting = true;
this.updateDeviceInfo(row.device_id, { alias: text }, (ok, resp) => {
if (ok) {
row._originalRemark = text;
this.$message.success(this.$t('device.remarkSaved'));
} else {
row.remark = row._originalRemark;
this.$message.error(resp.msg || this.$t('device.remarkSaveFailed'));
}
row._submitting = false;
});
2025-03-18 21:49:57 +08:00
},
onRemarkBlur(row) {
row.isEdit = false;
setTimeout(() => {
this.submitRemark(row);
2026-06-24 17:37:37 +08:00
}, 100);
},
onRemarkEnter(row) {
row.isEdit = false;
this.submitRemark(row);
2025-03-18 21:49:57 +08:00
},
2025-03-27 11:24:49 +08:00
handleUnbind(device_id) {
this.$confirm(this.$t('device.confirmUnbind'), this.$t('message.warning'), {
confirmButtonText: this.$t('button.ok'),
cancelButtonText: this.$t('button.cancel'),
2025-03-27 11:24:49 +08:00
type: 'warning'
}).then(() => {
2025-09-16 00:06:57 +08:00
Api.device.unbindDevice(device_id, ({ data }) => {
2025-03-27 11:24:49 +08:00
if (data.code === 0) {
2025-09-16 00:06:57 +08:00
this.$message.success({
message: this.$t('device.unbindSuccess'),
showClose: true
});
2025-03-27 11:24:49 +08:00
this.fetchBindDevices(this.$route.query.agentId);
} else {
2025-09-16 00:06:57 +08:00
this.$message.error({
message: data.msg || this.$t('device.unbindFailed'),
showClose: true
});
2025-03-27 11:24:49 +08:00
}
});
});
2025-03-18 21:49:57 +08:00
},
2026-01-23 15:29:43 +08:00
handleGenertor(row) {
const pathname = window.location.pathname;
const basePath = pathname.split('/').slice(0, -1).join('/');
2026-01-23 15:29:43 +08:00
const url = `${window.location.origin}${basePath}/generator/?deviceId=${row.device_id}`;
sessionStorage.setItem('devicePath', window.location.href);
window.location.href = url;
},
2025-04-12 08:27:03 +08:00
goToPage(page) {
this.currentPage = page;
},
fetchBindDevices(agentId) {
this.loading = true;
2025-09-16 00:06:57 +08:00
Api.device.getAgentBindDevices(agentId, ({ data }) => {
2025-04-05 21:03:46 +08:00
this.loading = false;
if (data.code === 0) {
this.deviceList = data.data.map(device => {
return {
device_id: device.id,
2026-05-29 09:50:09 +08:00
model: device.board,
2025-04-05 21:03:46 +08:00
firmwareVersion: device.appVersion,
macAddress: device.macAddress,
2025-05-03 23:30:39 +08:00
bindTime: device.createDate,
lastConversation: device.lastConnectedAtTimestamp
? this.formatRelativeTime(device.lastConnectedAtTimestamp)
: '-',
2025-04-05 21:03:46 +08:00
remark: device.alias,
_originalRemark: device.alias,
2025-04-05 21:03:46 +08:00
isEdit: false,
_submitting: false,
2025-04-05 21:03:46 +08:00
otaSwitch: device.autoUpdate === 1,
rawBindTime: new Date(device.createDate).getTime(),
selected: false,
deviceStatus: 'offline'
2025-04-05 21:03:46 +08:00
};
})
2025-09-16 00:06:57 +08:00
.sort((a, b) => a.rawBindTime - b.rawBindTime);
this.activeSearchKeyword = "";
this.searchKeyword = "";
this.fetchDeviceStatus(agentId);
2025-04-05 21:03:46 +08:00
} else {
this.$message.error(data.msg || this.$t('device.getListFailed'));
2025-04-05 21:03:46 +08:00
}
});
},
fetchDeviceStatus(agentId) {
2026-01-20 15:35:41 +08:00
this.loading = true;
Api.device.getDeviceStatus(agentId, ({ data }) => {
2026-01-20 15:35:41 +08:00
this.loading = false;
if (data.code === 0) {
try {
const statusData = JSON.parse(data.data);
if (statusData && typeof statusData === 'object') {
this.mqttServiceAvailable = true;
this.updateDeviceStatusFromResponse(statusData);
} else {
this.mqttServiceAvailable = false;
}
} catch (error) {
this.mqttServiceAvailable = false;
}
} else {
this.mqttServiceAvailable = false;
}
});
},
updateDeviceStatusFromResponse(deviceStatusMap) {
this.deviceList.forEach(device => {
const macAddress = device.macAddress ? device.macAddress.replace(/:/g, '_') : 'unknown';
const groupId = device.model ? device.model.replace(/:/g, '_') : 'GID_default';
const mqttClientId = `${groupId}@@@${macAddress}@@@${macAddress}`;
if (deviceStatusMap[mqttClientId]) {
const statusInfo = deviceStatusMap[mqttClientId];
let isOnline = false;
if (statusInfo.isAlive === true) {
isOnline = true;
} else if (statusInfo.isAlive === false) {
isOnline = false;
} else if (statusInfo.isAlive === null && statusInfo.exists === true) {
isOnline = true;
} else {
isOnline = false;
}
device.deviceStatus = isOnline ? 'online' : 'offline';
} else {
device.deviceStatus = 'offline';
}
});
},
getFirmwareTypeName(type) {
2025-05-01 16:57:58 +08:00
const firmwareType = this.firmwareTypes.find(item => item.key === type)
return firmwareType ? firmwareType.name : type
},
updateDeviceInfo(device_id, payload, callback) {
2025-09-16 00:06:57 +08:00
return Api.device.updateDeviceInfo(device_id, payload, ({ data }) => {
callback(data.code === 0, data);
})
},
handleOtaSwitchChange(row) {
2025-09-16 00:06:57 +08:00
this.updateDeviceInfo(row.device_id, { autoUpdate: row.otaSwitch ? 1 : 0 }, (result, { msg }) => {
if (result) {
this.$message.success(row.otaSwitch ? this.$t('device.autoUpdateEnabled') : this.$t('device.autoUpdateDisabled'));
return;
}
row.otaSwitch = !row.otaSwitch
this.$message.error(msg || this.$t('message.error'))
})
},
isGenerate(row) {
const version = row.firmwareVersion.replace(/\./g, '');
return Number(version) >= 200;
},
formatRelativeTime(timestamp) {
if (!timestamp) return '-';
const ts = Number(timestamp);
if (isNaN(ts)) return '-';
const date = new Date(ts);
if (isNaN(date.getTime())) return '-';
2026-06-24 17:37:37 +08:00
return date.toLocaleString();
},
2025-03-18 21:49:57 +08:00
}
};
</script>
2026-06-24 17:37:37 +08:00
<style lang="scss" scoped>
2025-03-18 21:49:57 +08:00
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
2025-04-14 07:52:00 +08:00
position: relative;
2025-03-18 21:49:57 +08:00
flex-direction: column;
2026-06-24 17:37:37 +08:00
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
2025-03-18 21:49:57 +08:00
background-size: cover;
-webkit-background-size: cover;
-o-background-size: cover;
2026-06-24 17:37:37 +08:00
overflow: hidden;
2025-03-18 21:49:57 +08:00
}
2025-04-14 07:52:00 +08:00
.main-wrapper {
2026-06-24 17:37:37 +08:00
height: calc(100vh - 63px - 35px);
padding: 20px 22px 0;
2025-04-14 07:52:00 +08:00
position: relative;
2025-04-15 17:59:29 +08:00
display: flex;
flex-direction: column;
2026-06-24 17:37:37 +08:00
box-sizing: border-box;
2025-03-18 21:49:57 +08:00
}
2026-06-24 17:37:37 +08:00
.operation-header {
2025-04-14 07:52:00 +08:00
display: flex;
align-items: center;
2026-06-24 17:37:37 +08:00
justify-content: space-between;
padding: 0 0 16px 0;
2025-04-14 07:52:00 +08:00
}
.page-title {
2026-06-24 17:37:37 +08:00
font-weight: 500;
2025-04-14 07:52:00 +08:00
font-size: 24px;
margin: 0;
}
.right-operations {
display: flex;
gap: 10px;
margin-left: auto;
}
.search-input {
2026-06-24 17:37:37 +08:00
width: 240px;
2025-04-15 17:59:29 +08:00
}
2025-04-14 07:52:00 +08:00
.content-panel {
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.content-area {
flex: 1;
height: 100%;
min-width: 600px;
2025-04-15 17:59:29 +08:00
overflow: auto;
2025-04-14 07:52:00 +08:00
background-color: white;
2025-04-15 17:59:29 +08:00
display: flex;
flex-direction: column;
2025-04-14 07:52:00 +08:00
}
2025-04-15 17:59:29 +08:00
.device-card {
2025-04-14 07:52:00 +08:00
background: white;
2026-06-24 17:37:37 +08:00
flex: 1;
display: flex;
flex-direction: column;
2025-04-14 07:52:00 +08:00
border: none;
box-shadow: none;
2025-04-15 17:59:29 +08:00
overflow: hidden;
2026-06-24 17:37:37 +08:00
::v-deep .el-card__body {
padding: 14px 20px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
2025-04-22 11:33:17 +08:00
}
2025-04-14 07:52:00 +08:00
.ctrl_btn {
display: flex;
gap: 8px;
}
2026-06-24 17:37:37 +08:00
:deep(.el-table .el-button--text) {
color: #7079aa;
2025-03-18 21:49:57 +08:00
}
2026-06-24 17:37:37 +08:00
:deep(.el-table .el-button--text:hover) {
color: #5a64b5;
}
2025-04-14 07:52:00 +08:00
:deep(.el-icon-edit) {
color: #7079aa;
cursor: pointer;
}
2025-04-14 07:52:00 +08:00
:deep(.el-icon-edit:hover) {
color: #5a64b5;
}
2025-03-25 16:23:28 +08:00
</style>