|
|
|
|
@ -36,6 +36,11 @@ const selectedStatus = ref('')
|
|
|
|
|
const selectedSearchField = ref('device_name') // 新增:搜索字段选择
|
|
|
|
|
const showCustomModal = ref(false)
|
|
|
|
|
const customModalMessage = ref('')
|
|
|
|
|
const showConfirmModal = ref(false)
|
|
|
|
|
const confirmModalMessage = ref('')
|
|
|
|
|
const confirmModalCallback = ref(null)
|
|
|
|
|
const showToast = ref(false)
|
|
|
|
|
const toastMessage = ref('')
|
|
|
|
|
|
|
|
|
|
const statusOptions = [
|
|
|
|
|
{value: '', label: '全部状态', icon: '📊'},
|
|
|
|
|
@ -211,12 +216,43 @@ const showErrorModal = (message) => {
|
|
|
|
|
showCustomModal.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示成功Toast提示
|
|
|
|
|
const showSuccessToast = (message) => {
|
|
|
|
|
toastMessage.value = message
|
|
|
|
|
showToast.value = true
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
showToast.value = false
|
|
|
|
|
}, 2000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 关闭错误提示模态框
|
|
|
|
|
const closeCustomModal = () => {
|
|
|
|
|
showCustomModal.value = false
|
|
|
|
|
customModalMessage.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示确认模态框
|
|
|
|
|
const showConfirm = (message, callback) => {
|
|
|
|
|
confirmModalMessage.value = message
|
|
|
|
|
confirmModalCallback.value = callback
|
|
|
|
|
showConfirmModal.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确认操作
|
|
|
|
|
const handleConfirm = () => {
|
|
|
|
|
if (confirmModalCallback.value) {
|
|
|
|
|
confirmModalCallback.value()
|
|
|
|
|
}
|
|
|
|
|
closeConfirmModal()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 关闭确认模态框
|
|
|
|
|
const closeConfirmModal = () => {
|
|
|
|
|
showConfirmModal.value = false
|
|
|
|
|
confirmModalMessage.value = ''
|
|
|
|
|
confirmModalCallback.value = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleReset = () => {
|
|
|
|
|
searchQuery.value = ''
|
|
|
|
|
selectedStatus.value = ''
|
|
|
|
|
@ -278,50 +314,64 @@ const visiblePages = computed(() => {
|
|
|
|
|
// --- 批量操作逻辑 ---
|
|
|
|
|
const handleBatchExecute = async () => {
|
|
|
|
|
if (selectedIds.value.length === 0) {
|
|
|
|
|
alert('请先在表格左侧勾选需要操作的设备!')
|
|
|
|
|
showErrorModal('请先在表格左侧勾选需要操作的设备!')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
if (selectedBatchAction.value === 'unbind') {
|
|
|
|
|
if (!confirm(`确定要批量解除这 ${selectedIds.value.length} 台设备的绑定吗?`)) return
|
|
|
|
|
await batchUnbindDevices(selectedIds.value)
|
|
|
|
|
alert(`批量解绑成功!`)
|
|
|
|
|
showConfirm(`确定要批量解除这 ${selectedIds.value.length} 台设备的绑定吗?`, async () => {
|
|
|
|
|
try {
|
|
|
|
|
await batchUnbindDevices(selectedIds.value)
|
|
|
|
|
showSuccessToast('批量解绑成功!')
|
|
|
|
|
selectedIds.value = []
|
|
|
|
|
fetchDashboardData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("批量解绑失败", error)
|
|
|
|
|
showErrorModal('批量操作失败,请确保后端接口已启动')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} else if (selectedBatchAction.value === 'delete') {
|
|
|
|
|
if (!confirm(`⚠️ 警告:确定要永久删除这 ${selectedIds.value.length} 台设备吗?数据不可恢复!`)) return
|
|
|
|
|
await batchDeleteDevices(selectedIds.value)
|
|
|
|
|
alert(`批量删除成功!`)
|
|
|
|
|
showConfirm(`⚠️ 警告:确定要永久删除这 ${selectedIds.value.length} 台设备吗?数据不可恢复!`, async () => {
|
|
|
|
|
try {
|
|
|
|
|
await batchDeleteDevices(selectedIds.value)
|
|
|
|
|
showSuccessToast('批量删除成功!')
|
|
|
|
|
selectedIds.value = []
|
|
|
|
|
fetchDashboardData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("批量删除失败", error)
|
|
|
|
|
showErrorModal('批量操作失败,请确保后端接口已启动')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
selectedIds.value = []
|
|
|
|
|
fetchDashboardData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("批量操作失败", error)
|
|
|
|
|
alert('批量操作失败,请确保后端接口已启动')
|
|
|
|
|
showErrorModal('批量操作失败,请确保后端接口已启动')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- 单个操作 ---
|
|
|
|
|
const unbindDevice = async (id, name) => {
|
|
|
|
|
if (confirm(`确定要解除设备 ${name} 的绑定吗?`)) {
|
|
|
|
|
showConfirm(`确定要解除设备 ${name} 的绑定吗?`, async () => {
|
|
|
|
|
try {
|
|
|
|
|
await adminUnbindDevice(id)
|
|
|
|
|
alert('解绑成功');
|
|
|
|
|
showSuccessToast('解绑成功')
|
|
|
|
|
fetchDashboardData()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
alert('操作异常')
|
|
|
|
|
showErrorModal('操作异常')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deleteDevice = async (id, name) => {
|
|
|
|
|
if (confirm(`确定要删除设备 ${name} 吗?`)) {
|
|
|
|
|
showConfirm(`确定要删除设备 ${name} 吗?`, async () => {
|
|
|
|
|
try {
|
|
|
|
|
await adminDeleteDevice(id)
|
|
|
|
|
alert('删除成功');
|
|
|
|
|
showSuccessToast('删除成功')
|
|
|
|
|
fetchDashboardData()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
alert('操作异常')
|
|
|
|
|
showErrorModal('操作异常')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- 数据弹窗相关 ---
|
|
|
|
|
@ -340,7 +390,7 @@ const currentDeviceData = ref({
|
|
|
|
|
const viewData = async (deviceId, deviceName) => {
|
|
|
|
|
console.log("正在查看设备ID:", deviceId);
|
|
|
|
|
if (!deviceId) {
|
|
|
|
|
alert("无法获取设备ID,请检查数据");
|
|
|
|
|
showErrorModal("无法获取设备ID,请检查数据");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -353,7 +403,7 @@ const viewData = async (deviceId, deviceName) => {
|
|
|
|
|
const userId = deviceDetail?.device?.user_id;
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
alert("该设备未绑定用户,无法查看数据");
|
|
|
|
|
showErrorModal("该设备未绑定用户,无法查看数据");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -373,7 +423,7 @@ const viewData = async (deviceId, deviceName) => {
|
|
|
|
|
await loadDeviceHealthData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("获取设备详情失败:", error)
|
|
|
|
|
alert("获取设备详情失败: " + error.message)
|
|
|
|
|
showErrorModal("获取设备详情失败: " + error.message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -391,7 +441,7 @@ const loadDeviceHealthData = async () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
alert("设备未绑定用户,无法获取数据")
|
|
|
|
|
showErrorModal("设备未绑定用户,无法获取数据")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -415,7 +465,7 @@ const loadDeviceHealthData = async () => {
|
|
|
|
|
currentDeviceData.value.healthData = res || []
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('加载设备数据失败:', err)
|
|
|
|
|
alert('加载数据失败: ' + err.message)
|
|
|
|
|
showErrorModal('加载数据失败: ' + err.message)
|
|
|
|
|
} finally {
|
|
|
|
|
currentDeviceData.value.loading = false
|
|
|
|
|
}
|
|
|
|
|
@ -428,7 +478,7 @@ const changeDateRange = async (range) => {
|
|
|
|
|
|
|
|
|
|
const applyCustomDateRange = async () => {
|
|
|
|
|
if (!currentDeviceData.value.customStartDate || !currentDeviceData.value.customEndDate) {
|
|
|
|
|
alert('请输入完整日期');
|
|
|
|
|
showErrorModal('请输入完整日期');
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
await loadDeviceHealthData()
|
|
|
|
|
@ -484,6 +534,11 @@ const formatTime = (time) => {
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<LayoutFrame title="设备管理">
|
|
|
|
|
<!-- Toast提示 -->
|
|
|
|
|
<div v-if="showToast" class="toast-message text-body">
|
|
|
|
|
{{ toastMessage }}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 统计概览 (保持不变) -->
|
|
|
|
|
<div class="cards-grid">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
@ -840,6 +895,22 @@ const formatTime = (time) => {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 确认操作弹窗 -->
|
|
|
|
|
<div v-if="showConfirmModal" class="dialog-overlay" @click="closeConfirmModal">
|
|
|
|
|
<div class="dialog" @click.stop>
|
|
|
|
|
<div class="dialog-header">
|
|
|
|
|
<h3>确认操作</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="dialog-body">
|
|
|
|
|
<p>{{ confirmModalMessage }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="dialog-actions">
|
|
|
|
|
<button @click="closeConfirmModal" class="btn secondary">取消</button>
|
|
|
|
|
<button @click="handleConfirm" class="btn primary">确定</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</LayoutFrame>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
@ -1875,6 +1946,102 @@ const formatTime = (time) => {
|
|
|
|
|
background: #3a8ee6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-cancel-btn-notification {
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
color: #606266;
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
padding: 8px 24px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
margin-right: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-cancel-btn-notification:hover {
|
|
|
|
|
background: #e6e6e6;
|
|
|
|
|
border-color: #c0c4cc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-cancel-btn-notification:active {
|
|
|
|
|
background: #d9d9d9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 确认弹窗样式(与糖友圈发布管理页面一致) */
|
|
|
|
|
.dialog-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
backdrop-filter: blur(4px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog {
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: var(--radius-xl);
|
|
|
|
|
padding: var(--space-6);
|
|
|
|
|
min-width: 400px;
|
|
|
|
|
max-width: 500px;
|
|
|
|
|
box-shadow: var(--shadow-xl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-header {
|
|
|
|
|
margin-bottom: var(--space-4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-header h3 {
|
|
|
|
|
font-size: var(--text-lg);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-gray-900);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-body {
|
|
|
|
|
margin-bottom: var(--space-6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-body p {
|
|
|
|
|
color: var(--color-gray-700);
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: var(--space-3);
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Toast提示样式(与糖友圈发布管理页面一致) */
|
|
|
|
|
.toast-message {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 20px;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
background: #67c23a;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
z-index: 9999;
|
|
|
|
|
animation: slideDown 0.3s ease-out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes slideDown {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(-50%) translateY(-20px);
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateX(-50%) translateY(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
@ -1908,5 +2075,10 @@ const formatTime = (time) => {
|
|
|
|
|
padding-left: 20px;
|
|
|
|
|
padding-right: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog {
|
|
|
|
|
min-width: 300px;
|
|
|
|
|
margin: var(--space-4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|