fastbee/yack-web/src/views/dataCenter/dataRepair.vue

1208 lines
44 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="data-repair-page">
<!-- 密码验证遮罩 -->
<div v-if="!isAuthorized" class="auth-overlay">
<div class="auth-card">
<div class="auth-header">
<i class="el-icon-lock"></i>
<h2>数据修复工具</h2>
<p class="auth-desc">此工具仅限内部使用,请输入访问密码</p>
</div>
<el-form :model="authForm" :rules="authRules" ref="authForm" @submit.native.prevent>
<el-form-item prop="password">
<el-input
v-model="authForm.password"
type="password"
placeholder="请输入访问密码"
prefix-icon="el-icon-key"
@keyup.enter.native="handleAuth"
size="large"
></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleAuth" style="width: 100%">验证进入</el-button>
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button type="text" @click="goBack" style="width: 100%">返回上一页</el-button>
</el-form-item>
</el-form>
</div>
</div>
<!-- 主内容区 -->
<div v-else class="main-content">
<el-card class="box-card">
<div slot="header" class="card-header">
<div class="header-left">
<span class="title">数据修复工具</span>
<el-tag type="danger" size="mini" effect="dark">内部工具</el-tag>
</div>
<div class="header-right">
<el-button type="text" icon="el-icon-back" @click="goBack">返回</el-button>
</div>
</div>
<!-- 筛选条件 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" size="small" class="query-form">
<el-form-item label="设备">
<el-select
v-model="queryParams.deviceId"
placeholder="请选择设备"
filterable
clearable
@change="handleDeviceChange"
style="width: 220px"
>
<el-option
v-for="item in deviceList"
:key="item.deviceId"
:label="`${item.deviceName} (${item.serialNumber})`"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="物模型">
<el-select
v-model="queryParams.identifier"
placeholder="请选择物模型"
filterable
clearable
style="width: 180px"
:disabled="!queryParams.deviceId"
>
<el-option
v-for="item in identifierList"
:key="item.identifier"
:label="item.modelName"
:value="item.identifier"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="queryParams.dateRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
:picker-options="pickerOptions"
style="width: 360px"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<div class="action-bar">
<div class="action-left">
<el-button type="warning" icon="el-icon-magic-stick" @click="handleAutoRepair" :loading="repairing" :disabled="tableData.length === 0">
一键修复
</el-button>
<el-button type="success" icon="el-icon-plus" @click="handleAdd" :disabled="!queryParams.deviceId || !queryParams.identifier">
添加数据
</el-button>
<!-- 自动校准配置按钮 -->
<el-button type="primary" icon="el-icon-setting" @click="handleAutoCalibrationConfig">
自动校准配置
</el-button>
</div>
<div class="action-right">
<el-alert
type="info"
:closable="false"
show-icon
>
<template slot="title">
修复逻辑:当相邻三点 A、B、C 满足 <b>((A + C) / 2) - B > 30%</b> 时,修复 B = (A + C) / 2
</template>
</el-alert>
</div>
</div>
<!-- 自动校准配置弹窗 -->
<el-dialog title="自动校准配置" :visible.sync="calibrationConfigVisible" width="600px" append-to-body v-dialogDrag>
<el-alert
type="info"
:closable="false"
show-icon
style="margin-bottom: 15px"
>
<template slot="title">
配置后,系统将在设备上报数据时自动进行数据校准。校准逻辑:当相邻三点 A、B、C 满足 B 与 (A+C)/2 的差值超过 30% 时,自动修复 B = (A+C)/2
</template>
</el-alert>
<el-table
:data="calibrationConfigList"
border
style="width: 100%"
max-height="400"
>
<el-table-column label="设备" prop="deviceName" width="200">
<template slot-scope="scope">
{{ scope.row.deviceName }} ({{ scope.row.serialNumber }})
</template>
</el-table-column>
<el-table-column label="物模型" prop="modelName" width="150" />
<el-table-column label="标识符" prop="identifier" width="120" />
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" style="color: #F56C6C" @click="removeCalibrationConfig(scope.$index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 15px">
<el-form :inline="true" size="small">
<el-form-item label="设备">
<el-select
v-model="newConfig.deviceId"
placeholder="请选择设备"
filterable
clearable
@change="handleConfigDeviceChange"
style="width: 220px"
>
<el-option
v-for="item in deviceList"
:key="item.deviceId"
:label="`${item.deviceName} (${item.serialNumber})`"
:value="item.deviceId"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="物模型">
<el-select
v-model="newConfig.identifier"
placeholder="请选择物模型"
filterable
clearable
style="width: 180px"
:disabled="!newConfig.deviceId"
>
<el-option
v-for="item in configIdentifierList"
:key="item.identifier"
:label="item.modelName"
:value="item.identifier"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="addCalibrationConfig" :disabled="!newConfig.deviceId || !newConfig.identifier">
添加
</el-button>
</el-form-item>
</el-form>
</div>
<div slot="footer">
<el-button @click="calibrationConfigVisible = false">关 闭</el-button>
</div>
</el-dialog>
<!-- 数据表格 -->
<el-table
ref="dataTable"
v-loading="loading"
:data="tableData"
border
stripe
style="width: 100%"
@selection-change="handleSelectionChange"
:row-class-name="tableRowClassName"
max-height="500"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="序号" type="index" width="60" align="center" :index="indexMethod" />
<el-table-column label="记录时间" prop="createTime" width="180" align="center" sortable>
<template slot-scope="scope">
{{ formatTime(scope.row.createTime || scope.row.ts) }}
</template>
</el-table-column>
<el-table-column label="物模型" prop="modelName" width="120" align="center" />
<el-table-column label="标识符" prop="identity" width="120" align="center" />
<el-table-column label="数值" min-width="160" align="center">
<template slot-scope="scope">
<template v-if="scope.row.editing">
<el-input
v-model="scope.row.tempValue"
size="mini"
style="width: 100px"
@keyup.enter.native="saveEdit(scope.row)"
@keyup.esc.native="cancelEdit(scope.row)"
ref="editInput"
></el-input>
<el-button type="text" icon="el-icon-check" @click="saveEdit(scope.row)" style="color: #67C23A; margin-left: 5px"></el-button>
<el-button type="text" icon="el-icon-close" @click="cancelEdit(scope.row)" style="color: #F56C6C"></el-button>
</template>
<template v-else>
<span
class="editable-value"
:class="{ 'need-repair': scope.row.needRepair, 'repaired': scope.row.repaired, 'missing': scope.row.isMissing }"
@dblclick="startEdit(scope.row)"
>
{{ scope.row.logValue || '缺失' }}
</span>
<el-tooltip content="双击编辑" placement="top">
<i class="el-icon-edit edit-icon" @click="startEdit(scope.row)"></i>
</el-tooltip>
</template>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.isMissing" type="danger" size="mini" effect="plain">
<i class="el-icon-warning"></i> 缺失
</el-tag>
<el-tag v-else-if="scope.row.repaired" type="success" size="mini" effect="plain">
<i class="el-icon-check"></i> 已修复
</el-tag>
<el-tag v-else-if="scope.row.needRepair" type="warning" size="mini" effect="plain">
<i class="el-icon-warning"></i> 需修复
</el-tag>
<el-tag v-else type="info" size="mini" effect="plain">正常</el-tag>
</template>
</el-table-column>
<el-table-column label="建议值" width="120" align="center">
<template slot-scope="scope">
<span v-if="scope.row.needRepair && scope.row.suggestValue" class="suggest-value">
{{ scope.row.suggestValue }}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="scope">
<el-button
v-if="scope.row.needRepair && !scope.row.repaired"
type="text"
size="mini"
icon="el-icon-magic-stick"
@click="repairSingle(scope.row)"
>
修复
</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" style="color: #F56C6C" @click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="fetchData"
style="margin-top: 15px"
/>
</el-card>
</div>
<!-- 添加数据弹窗 -->
<el-dialog title="添加数据" :visible.sync="addDialogVisible" width="480px" append-to-body>
<el-form :model="addForm" :rules="addRules" ref="addForm" label-width="90px">
<el-form-item label="设备">
<el-input :value="currentDeviceName" disabled></el-input>
</el-form-item>
<el-form-item label="物模型">
<el-input :value="currentModelName" disabled></el-input>
</el-form-item>
<el-form-item label="数值" prop="logValue">
<el-input v-model="addForm.logValue" placeholder="请输入数值" type="number"></el-input>
</el-form-item>
<el-form-item label="记录时间" prop="createTime">
<el-date-picker
v-model="addForm.createTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择时间"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitAdd" :loading="submitting">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import moment from 'moment';
import { listDeviceShort, listThingsModel } from '@/api/iot/device.js';
import {
getRepairDataList,
addDeviceLog,
updateDeviceLog,
batchUpdateDeviceLog,
getAutoCalibrationConfig,
addAutoCalibrationConfig,
removeAutoCalibrationConfig, deleteDeviceLogsByCreateTime
} from '@/api/data/repair.js';
// 访问密码(前端写死)
const ACCESS_PASSWORD = 'Yan@ancekongdata';
export default {
name: 'DataRepair',
data() {
return {
// 认证相关
isAuthorized: false,
authForm: { password: '' },
authRules: {
password: [{ required: true, message: '请输入访问密码', trigger: 'blur' }]
},
// 自动校准配置
calibrationConfigVisible: false,
calibrationConfigList: [],
newConfig: {
deviceId: null,
identifier: null
},
configIdentifierList: [],
// 查询参数
queryParams: {
deviceId: null,
identifier: null,
dateRange: [
moment().subtract(24, 'hours').format('YYYY-MM-DD HH:mm:ss'),
moment().format('YYYY-MM-DD HH:mm:ss')
],
pageNum: 1,
pageSize: 50
},
// 数据
deviceList: [],
identifierList: [],
tableData: [],
total: 0,
selectedRows: [],
// 状态
loading: false,
repairing: false,
submitting: false,
// 添加弹窗
addDialogVisible: false,
addForm: {
logValue: '',
createTime: ''
},
addRules: {
logValue: [{ required: true, message: '请输入数值', trigger: 'blur' }],
createTime: [{ required: true, message: '请选择时间', trigger: 'change' }]
},
// 时间选择器配置
pickerOptions: {
shortcuts: [
{
text: '最近2小时',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 2);
picker.$emit('pick', [start, end]);
}
},
{
text: '最近24小时',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', [start, end]);
}
},
{
text: '最近3天',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 3);
picker.$emit('pick', [start, end]);
}
},
{
text: '最近7天',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}
]
}
};
},
computed: {
currentDeviceName() {
const device = this.deviceList.find(d => d.deviceId === this.queryParams.deviceId);
return device ? `${device.deviceName} (${device.serialNumber})` : '';
},
currentModelName() {
const model = this.identifierList.find(m => m.identifier === this.queryParams.identifier);
return model ? model.modelName : '';
},
currentDevice() {
return this.deviceList.find(d => d.deviceId === this.queryParams.deviceId);
},
currentModel() {
return this.identifierList.find(m => m.identifier === this.queryParams.identifier);
}
},
mounted() {
this.loadDeviceList();
},
methods: {
// 密码验证
handleAuth() {
this.$refs.authForm.validate(valid => {
if (valid) {
if (this.authForm.password === ACCESS_PASSWORD) {
this.isAuthorized = true;
this.$message.success('验证成功');
} else {
this.$message.error('密码错误,请重试');
}
}
});
},
// 返回
goBack() {
this.$router.go(-1);
},
// 打开自动校准配置
async handleAutoCalibrationConfig() {
this.calibrationConfigVisible = true;
await this.loadCalibrationConfig();
},
// 加载自动校准配置
async loadCalibrationConfig() {
try {
const res = await getAutoCalibrationConfig();
if (res.code === 200) {
this.calibrationConfigList = res.data || [];
}
} catch (e) {
console.error('加载自动校准配置失败', e);
}
},
// 配置设备变化
async handleConfigDeviceChange(deviceId) {
this.newConfig.identifier = null;
this.configIdentifierList = [];
if (!deviceId) return;
try {
const res = await listThingsModel({ deviceId, pageNum: 1, pageSize: 9999 });
if (res.code === 200) {
this.configIdentifierList = res.rows || [];
}
} catch (e) {
console.error('加载物模型失败', e);
}
},
// 添加自动校准配置
async addCalibrationConfig() {
if (!this.newConfig.deviceId || !this.newConfig.identifier) {
return this.$message.warning('请选择设备和物模型');
}
const device = this.deviceList.find(d => d.deviceId === this.newConfig.deviceId);
const model = this.configIdentifierList.find(m => m.identifier === this.newConfig.identifier);
if (!device || !model) {
return this.$message.error('设备或物模型不存在');
}
// 检查是否已存在
const exists = this.calibrationConfigList.some(
item => item.deviceId === this.newConfig.deviceId && item.identifier === this.newConfig.identifier
);
if (exists) {
return this.$message.warning('该配置已存在');
}
try {
const res = await addAutoCalibrationConfig({
deviceId: this.newConfig.deviceId,
serialNumber: device.serialNumber,
deviceName: device.deviceName,
identifier: this.newConfig.identifier,
modelName: model.modelName
});
if (res.code === 200) {
this.$message.success('添加成功');
await this.loadCalibrationConfig();
this.newConfig = {
deviceId: null,
identifier: null
};
}
} catch (e) {
this.$message.error('添加失败');
}
},
// 删除自动校准配置
async removeCalibrationConfig(index) {
const config = this.calibrationConfigList[index];
try {
await this.$confirm('确定要删除该配置吗?', '确认删除', {
type: 'warning'
});
const res = await removeAutoCalibrationConfig({
serialNumber: config.serialNumber,
identifier: config.identifier
});
if (res.code === 200) {
this.$message.success('删除成功');
await this.loadCalibrationConfig();
}
} catch (e) {
if (e !== 'cancel') {
this.$message.error('删除失败');
}
}
},
// 加载设备列表
async loadDeviceList() {
try {
const res = await listDeviceShort({ showChild: true, pageNum: 1, pageSize: 9999 });
if (res.code === 200) {
this.deviceList = res.rows || [];
}
} catch (e) {
console.error('加载设备列表失败', e);
}
},
// 设备变化
async handleDeviceChange(deviceId) {
this.queryParams.identifier = null;
this.identifierList = [];
this.tableData = [];
this.total = 0;
if (!deviceId) return;
try {
const res = await listThingsModel({ deviceId, pageNum: 1, pageSize: 9999 });
if (res.code === 200) {
// 显示所有物模型(属性type=1、功能type=2、事件type=3)
this.identifierList = res.rows || [];
}
} catch (e) {
console.error('加载物模型失败', e);
}
},
// 查询
handleSearch() {
if (!this.queryParams.deviceId) {
return this.$message.warning('请选择设备');
}
if (!this.queryParams.identifier) {
return this.$message.warning('请选择物模型');
}
if (!this.queryParams.dateRange || this.queryParams.dateRange.length !== 2) {
return this.$message.warning('请选择时间范围');
}
this.queryParams.pageNum = 1;
this.fetchData();
},
// 重置
handleReset() {
this.queryParams = {
deviceId: null,
identifier: null,
dateRange: [
moment().subtract(24, 'hours').format('YYYY-MM-DD HH:mm:ss'),
moment().format('YYYY-MM-DD HH:mm:ss')
],
pageNum: 1,
pageSize: 50
};
this.identifierList = [];
this.tableData = [];
this.total = 0;
},
// 获取数据
async fetchData() {
this.loading = true;
try {
const device = this.currentDevice;
const params = {
serialNumber: device?.serialNumber,
identity: this.queryParams.identifier,
productId: device?.productId,
beginTime: this.queryParams.dateRange[0],
endTime: this.queryParams.dateRange[1],
pageNum: this.queryParams.pageNum,
pageSize: this.queryParams.pageSize
};
const res = await getRepairDataList(params);
if (res.code === 200) {
// 处理数据,添加编辑状态
// 后端返回的是List,数据在res.data中,不是res.rows
const dataList = Array.isArray(res.data) ? res.data : (res.rows || []);
this.tableData = dataList.map(item => ({
...item,
editing: false,
tempValue: item.logValue,
needRepair: false,
repaired: false,
suggestValue: null,
isMissing: !item.logValue || item.logValue === '' || item.logValue === null
}));
// 如果后端返回了total就用total,否则用数组长度
this.total = res.total !== undefined ? res.total : dataList.length;
// 分析需要修复的数据
this.analyzeData();
}
} catch (e) {
this.$message.error('获取数据失败');
console.error(e);
} finally {
this.loading = false;
}
},
// 分析数据,标记需要修复的项
analyzeData() {
const data = this.tableData;
// 先重置所有数据点的修复状态
data.forEach(item => {
item.needRepair = false;
item.suggestValue = null;
// 更新缺失状态
item.isMissing = !item.logValue || item.logValue === '' || item.logValue === null;
// 注意:不重置 repaired 状态,因为这是历史修复记录
});
for (let i = data.length - 2; i >= 1; i--) {
const A = parseFloat(data[i + 1].logValue);
const B = parseFloat(data[i].logValue);
const C = parseFloat(data[i - 1].logValue);
if (!isNaN(A) && !isNaN(B) && !isNaN(C)) {
const avg = (A + C) / 2;
// 修复条件:B与(A+C)/2的差值超过30%,无论偏大还是偏小
if (Math.abs(B - avg) > Math.abs(avg) * 0.3) {
data[i].needRepair = true;
data[i].suggestValue = avg.toFixed(3);
// 如果之前已修复但现在又需要修复,清除已修复状态,允许再次修复
if (data[i].repaired) {
data[i].repaired = false;
}
}
}
}
},
// 开始编辑
startEdit(row) {
// 取消其他行的编辑状态
this.tableData.forEach(item => {
if (item !== row) {
item.editing = false;
item.tempValue = item.logValue;
}
});
row.editing = true;
row.tempValue = row.logValue || '';
this.$nextTick(() => {
const input = this.$refs.editInput;
if (input) {
if (Array.isArray(input)) {
input[0]?.focus();
} else {
input.focus();
}
}
});
},
// 取消编辑
cancelEdit(row) {
row.editing = false;
row.tempValue = row.logValue;
},
// 保存编辑
async saveEdit(row) {
const newValue = row.tempValue;
const oldValue = row.logValue;
// 如果值没有变化,直接退出编辑
if (newValue === oldValue) {
row.editing = false;
return;
}
// 判断是新增还是更新:如果数据缺失(没有logId或logValue为空),使用新增接口
const isMissing = !row.logId || !row.logValue || row.logValue === '' || row.logValue === null;
try {
const device = this.currentDevice;
const model = this.currentModel;
if (isMissing) {
// 使用新增接口
const data = {
serialNumber: device?.serialNumber,
identity: this.queryParams.identifier,
logValue: newValue,
logType: 1,
modelName: model?.modelName,
createTime: row.createTime || row.ts || moment().format('YYYY-MM-DD HH:mm:ss')
};
const res = await addDeviceLog(data);
if (res.code === 200) {
// 更新本地数据
row.logValue = newValue;
row.logId = res.data?.logId || row.logId; // 如果有返回logId,更新它
row.editing = false;
row.isMissing = false;
row.needRepair = false;
row.repaired = true;
this.$message.success('添加成功');
// 重新分析数据
this.analyzeData();
}
} else {
// 使用更新接口
const res = await updateDeviceLog({
logId: row.logId,
logValue: newValue
});
if (res.code === 200) {
row.logValue = newValue;
row.editing = false;
row.isMissing = false;
row.needRepair = false;
row.repaired = true;
this.$message.success('修改成功');
// 重新分析数据
this.analyzeData();
}
}
} catch (e) {
this.$message.error(isMissing ? '添加失败' : '修改失败');
}
},
// 单条修复
async repairSingle(row) {
if (!row.suggestValue) return;
try {
const res = await updateDeviceLog({
logId: row.logId,
logValue: row.suggestValue
});
if (res.code === 200) {
row.logValue = row.suggestValue;
row.needRepair = false;
row.repaired = true;
this.$message.success('修复成功');
this.analyzeData();
}
} catch (e) {
this.$message.error('修复失败');
}
},
// 一键修复
async handleAutoRepair() {
const needRepairList = this.tableData.filter(item => item.needRepair && !item.repaired && item.suggestValue);
if (needRepairList.length === 0) {
return this.$message.info('没有需要修复的数据');
}
try {
await this.$confirm(`检测到 ${needRepairList.length} 条异常数据,确定要一键修复吗?`, '确认修复', {
type: 'warning',
confirmButtonText: '确定修复',
cancelButtonText: '取消'
});
this.repairing = true;
const updateList = needRepairList.map(item => ({
logId: item.logId,
logValue: item.suggestValue
}));
const res = await batchUpdateDeviceLog(updateList);
if (res.code === 200) {
// 更新本地数据
needRepairList.forEach(item => {
item.logValue = item.suggestValue;
item.needRepair = false;
item.repaired = true;
});
this.$message.success(`成功修复 ${needRepairList.length} 条数据`);
this.analyzeData();
}
} catch (e) {
if (e !== 'cancel') {
this.$message.error('修复失败');
}
} finally {
this.repairing = false;
}
},
// 添加数据
handleAdd() {
this.addForm = {
logValue: '',
createTime: moment().format('YYYY-MM-DD HH:mm:ss')
};
this.addDialogVisible = true;
},
// 提交添加
async submitAdd() {
this.$refs.addForm.validate(async valid => {
if (!valid) return;
this.submitting = true;
try {
const device = this.currentDevice;
const model = this.currentModel;
const data = {
serialNumber: device?.serialNumber,
identity: this.queryParams.identifier,
logValue: this.addForm.logValue,
logType: 1,
modelName: model?.modelName,
createTime: this.addForm.createTime
};
const res = await addDeviceLog(data);
if (res.code === 200) {
this.$message.success('添加成功');
this.addDialogVisible = false;
this.fetchData();
}
} catch (e) {
this.$message.error('添加失败');
} finally {
this.submitting = false;
}
});
},
// 删除单条
handleDelete(row) {
const deviceName = this.currentDeviceName || row.serialNumber || '未知设备';
const timeStr = this.formatTime(row.createTime || row.ts);
const message = `确定要删除设备 <strong>${deviceName}</strong> 在时间点 <strong>${timeStr}</strong> 的所有数据吗?<br/><br/>该时间点上的所有数据将被全部删除,删除后不可恢复!`;
this.$confirm(message, '确认删除', {
type: 'warning',
confirmButtonClass: 'el-button--danger',
dangerouslyUseHTMLString: true
}).then(async () => {
try {
const res = await deleteDeviceLogsByCreateTime(row.serialNumber, row.createTime);
if (res.code === 200) {
this.$message.success('删除成功');
this.fetchData();
}
} catch (e) {
this.$message.error('删除失败');
}
}).catch(() => {});
},
// 选择变化
handleSelectionChange(selection) {
this.selectedRows = selection;
},
// 表格行样式
tableRowClassName({ row }) {
if (row.isMissing) return 'missing-row';
if (row.repaired) return 'repaired-row';
if (row.needRepair) return 'warning-row';
return '';
},
// 序号
indexMethod(index) {
return (this.queryParams.pageNum - 1) * this.queryParams.pageSize + index + 1;
},
// 格式化时间
formatTime(time) {
if (!time) return '-';
return moment(time).format('YYYY-MM-DD HH:mm:ss');
}
}
};
</script>
<style lang="scss" scoped>
.data-repair-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
// 认证遮罩
.auth-overlay {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
.auth-card {
background: #fff;
border-radius: 12px;
padding: 40px;
width: 100%;
max-width: 400px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
.auth-header {
text-align: center;
margin-bottom: 30px;
.el-icon-lock {
font-size: 48px;
color: #667eea;
margin-bottom: 15px;
}
h2 {
margin: 0 0 10px;
color: #333;
font-size: 24px;
}
.auth-desc {
margin: 0;
color: #999;
font-size: 14px;
}
}
}
}
// 主内容
.main-content {
padding: 20px;
min-height: 100vh;
background: #f5f7fa;
.box-card {
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.header-left {
display: flex;
align-items: center;
.title {
font-size: 18px;
font-weight: bold;
margin-right: 10px;
}
}
}
}
.query-form {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.action-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
flex-wrap: wrap;
gap: 10px;
.action-left {
display: flex;
gap: 10px;
}
.action-right {
flex: 1;
min-width: 300px;
max-width: 560px;
}
}
// 可编辑值样式
.editable-value {
cursor: pointer;
padding: 2px 8px;
border-radius: 4px;
transition: all 0.3s;
&:hover {
background: #f0f0f0;
}
&.need-repair {
color: #E6A23C;
font-weight: bold;
}
&.repaired {
color: #67C23A;
}
&.missing {
color: #F56C6C;
font-weight: bold;
}
}
.edit-icon {
margin-left: 5px;
color: #409EFF;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s;
}
.el-table__row:hover .edit-icon {
opacity: 1;
}
.suggest-value {
color: #67C23A;
font-weight: bold;
}
// 表格行样式
::v-deep .warning-row {
background: #fdf6ec !important;
}
::v-deep .repaired-row {
background: #f0f9eb !important;
}
::v-deep .missing-row {
background: #fef0f0 !important;
}
// 弹窗拖动样式
::v-deep .el-dialog__header {
cursor: move;
user-select: none;
}
::v-deep .el-dialog__headerbtn {
cursor: pointer;
}
// 动画效果
@keyframes borderBlink {
0%, 100% {
border-color: #F56C6C;
box-shadow: 0 0 20px rgba(245, 108, 108, 0.5), 0 0 40px rgba(245, 108, 108, 0.3);
}
50% {
border-color: #FF7875;
box-shadow: 0 0 30px rgba(245, 108, 108, 0.8), 0 0 60px rgba(245, 108, 108, 0.5);
}
}
@keyframes bannerShine {
0%, 100% {
background: linear-gradient(90deg, #F56C6C, #FF7875, #F56C6C);
}
50% {
background: linear-gradient(90deg, #FF7875, #F56C6C, #FF7875);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes iconPulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(1.3);
opacity: 0;
}
}
@keyframes iconShake {
0%, 100% {
transform: rotate(0deg);
}
25% {
transform: rotate(-5deg);
}
75% {
transform: rotate(5deg);
}
}
@keyframes dangerPulse {
0%, 100% {
border-color: #FEF0F0;
box-shadow: 0 0 0 rgba(245, 108, 108, 0);
}
50% {
border-color: #FDE2E2;
box-shadow: 0 0 10px rgba(245, 108, 108, 0.3);
}
}
}
}
</style>