calibration-pc/src/renderer/src/views/calibration/index.vue

2279 lines
65 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="calibration-page">
<el-header class="calibration-header">
<div class="header-left">
<el-select v-model="selectedScheme" placeholder="选择方案" :disabled="playState" style="width: 200px; margin-right: 15px" @change="schemeChange">
<el-option v-for="(item, index) in schemeList" :key="index" :label="item.schemeName" :value="item.id"></el-option>
</el-select>
<el-button icon="CaretRight" type="primary" :disabled="playState" @click="startExecution">开始执行</el-button>
<el-button icon="RemoveFilled" type="danger" @click="stopExecution">停止</el-button>
<span style="margin-left: 10px">操作员编号:</span>
<el-input v-model="operatorCode" placeholder="输入操作员编号" style="width: 120px; margin-left: 10px" />
<div class="statistics-box">
生产总数/合格数/不合格次数: <span class="statistics-number">{{ equipmentStatistics.totalDevice }}/{{ equipmentStatistics.successDevice }}/{{ equipmentStatistics.failCount }}</span>
</div>
</div>
<div class="header-right">
<el-button icon="RefreshRight" type="primary" :disabled="playState" @click="generateDeviceList">刷新列表</el-button>
<el-button icon="Setting" :disabled="playState" style="margin-left: 10px" @click="openSettings">配置</el-button>
</div>
</el-header>
<div v-if="activeScheme && activeScheme.series" class="scheme-list list-one">
<span class="scheme-item"
>系列: <span class="scheme-value">{{ activeScheme.series }}</span></span
>
<span class="scheme-item"
>框架: <span class="scheme-value">{{ activeScheme.framework }}</span></span
>
<span class="scheme-item"
>P位: <span class="scheme-value">{{ activeScheme.schemeType }}</span></span
>
<span class="scheme-item"
>额定电压(V): <span class="scheme-value">{{ activeScheme.configParam.format[1].value }}</span></span
>
<span class="scheme-item"
>额定电流(A): <span class="scheme-value">{{ activeScheme.configParam.format[2].value }}</span></span
>
<span class="scheme-item"
>漏电流(mA): <span class="scheme-value">{{ activeScheme.configParam.format[5].value }}</span></span
>
<span class="scheme-item"></span>
</div>
<div v-if="activeScheme && activeScheme.series" class="scheme-list list-two">
<template v-for="(item, index) in activeScheme.configParam.format">
<span v-if="item.key !== 'reserve' && index !== 1 && index !== 2 && index !== 5" :key="index" class="scheme-item"
>{{ item.unit ? item.name + '(' + item.unit + ')' : item.name }}:
<span v-if="index === 6 || index === 7 || index === 8" class="scheme-value"
>2<sup style="margin-left: 1px; font-size: 10px">{{ item.value }}</sup></span
><span v-else class="scheme-value">{{ item.value }}</span></span
>
</template>
</div>
<el-main class="calibration-main">
<div class="device-grid">
<el-card v-for="(device, index) in devices" :key="device.id" class="device-card" :class="activeDeviceIndex === index ? 'active' : ''" @click="selectDevice(device, index)">
<template #header>
<div class="device-card-header">
<!-- <span>{{ device.code }}</span>-->
<span>设备{{ device.id }}</span>
<el-tag :type="device.connectStatus === 1 ? 'success' : 'info'" size="small">
{{ device.connectStatus === 1 ? '已连接' : '未连接' }}
</el-tag>
</div>
</template>
<div class="device-card-body">
<div v-if="device.activeStep" class="process-status" :class="device.stepStatus === -1 ? 'color-red' : device.stepStatus === 1 ? 'color-green' : 'color-orange'">{{ device.activeStep }}</div>
<div
v-if="device.connectStatus === 1"
:class="[
'iconfont',
'circuit-breaker',
device.switch === 0 ? 'icon-zhahe' : 'icon-zhakai',
device.result === 1 ? 'color-green' : device.result === -1 ? 'color-red' : device.result === 2 ? 'color-orange' : ''
]"
></div>
<div
v-else
:class="['iconfont', 'icon-zhakai', 'circuit-breaker', device.switch === 0 ? 'icon-zhahe' : 'icon-zhakai', device.result === 1 ? 'color-green' : device.result === -1 ? 'color-red' : '']"
></div>
<p>
芯片ID:
<span v-if="device.cpuId"
><el-tooltip class="box-item" effect="dark" :content="device.cpuId" placement="top-start">
{{ device.cpuId || '无' }}
</el-tooltip></span
>
<span v-else>{{ device.cpuId || '无' }}</span>
</p>
<p>设备端口: {{ device.devicePort || '无' }}</p>
<p>
标定结果:
<span :class="['device-result', device.result === 1 ? 'color-green' : device.result === -1 ? 'color-red' : device.result === 2 ? 'color-orange' : '']">{{
device.result === 1 ? '成功' : device.result === -1 ? '失败' : device.result === 2 ? '标定中' : '无' || '无'
}}</span>
</p>
<p>
结果描述:
<span v-if="device.resultTxt" class="color-red"
><el-tooltip class="box-item" effect="dark" :content="device.resultTxt" placement="top-start">
{{ device.resultTxt || '无' }}
</el-tooltip></span
>
<span v-else>{{ device.resultTxt || '无' }}</span>
</p>
</div>
</el-card>
</div>
</el-main>
<el-footer class="calibration-footer" :class="isOpenLog ? 'open-log' : ''">
<div id="log-box-main" class="log-box-main">
<!-- 浮窗搜索栏 -->
<div v-if="isSearchActive" class="floating-search-bar">
<div class="search-input-wrapper">
<el-input ref="searchInputRef" v-model="searchKeyword" placeholder="搜索日志..." size="small" clearable @keyup.enter="navigateSearch('next')" @keyup.esc="closeSearch" @input="performSearch">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div v-if="searchResults.length > 0" class="search-stats">{{ currentSearchIndex + 1 }}/{{ totalSearchCount }}</div>
<div class="search-navigation">
<el-button v-if="searchResults.length > 0" size="mini" :disabled="currentSearchIndex <= 0" @click="navigateSearch('prev')">
<el-icon><ArrowUp /></el-icon>
</el-button>
<el-button v-if="searchResults.length > 0" size="mini" :disabled="currentSearchIndex >= searchResults.length - 1" @click="navigateSearch('next')">
<el-icon><ArrowDown /></el-icon>
</el-button>
<el-button size="mini" @click="closeSearch">
<el-icon><Close /></el-icon>
</el-button>
</div>
</div>
<div class="log-list">
<div
v-for="(item, index) in logs"
:key="index"
class="log-item"
:class="{
'search-highlight': isSearchActive && searchResults.some(result => result.originalIndex === index),
'search-current': isSearchActive && searchResults[currentSearchIndex] && searchResults[currentSearchIndex].originalIndex === index,
'finish-txt': item.type == 'calibrate_finish'
}"
>
<el-tooltip v-if="item.type == 'calibrate_error'" effect="light" content="错误信息" placement="top">
<span class="iconfont icon-bug-fill" style="color: #ff3b2b"></span>
</el-tooltip>
<el-tooltip v-else-if="item.type == 'calibrate_result_error'" effect="light" content="标定错误" placement="top">
<span class="iconfont icon-xwtubiaoku-15" style="color: #ff3b2b"></span>
</el-tooltip>
<el-tooltip v-else-if="item.type == 'calibrate_result_success'" effect="light" content="标定成功" placement="top">
<span class="iconfont icon-xwtubiaoku-13" style="color: #55e800"></span>
</el-tooltip>
<el-tooltip v-else effect="light" content="系统" placement="top">
<span class="iconfont icon-rizhi1"></span>
</el-tooltip>
<span class="log-item-txt" v-html="getHighlightedText(item)"></span>
</div>
</div>
</div>
<div class="calibration-footer-btn">
<div class="btn-box">
<view class="log-box-operate-item">
<div>日志状态:</div>
<div v-if="socketStatus" class="log-socket-state" @mouseover="logSocketStateHover = true" @mouseleave="logSocketStateHover = false" @click="reconnectSocket">
{{ logSocketStateHover ? '重连' : '连接' }} <span v-if="logSocketStateHover" span class="iconfont icon-icon_gengxin"></span>
</div>
<div v-else style="color: #ff3b2b; display: flex; align-items: center" @click="reconnectSocket">
<el-tooltip class="box-item" content="重新连接" effect="dark" placement="bottom">
<div style="display: flex; align-items: center"><span>断连</span><span class="iconfont icon-icon_gengxin"></span></div>
</el-tooltip>
</div>
</view>
<view class="log-box-operate-item" @click="toggleIsScroll">
<el-tooltip v-if="isScroll" class="box-item" content="关闭滚动" effect="dark" placement="bottom">
<span class="iconfont icon-unlock"></span>
</el-tooltip>
<el-tooltip v-else class="box-item" content="开启滚动" effect="dark" placement="bottom">
<span class="iconfont icon-icon_suoding"></span>
</el-tooltip>
</view>
<view class="log-box-operate-item" @click="openSearch">
<el-tooltip class="box-item" content="搜索日志" effect="dark" placement="bottom">
<span class="iconfont icon-sousuo"></span>
</el-tooltip>
</view>
<view class="log-box-operate-item" @click="clearLog">
<el-tooltip class="box-item" content="清空日志" effect="dark" placement="bottom">
<span class="iconfont icon-icon_shanchu"></span>
</el-tooltip>
</view>
<view class="log-box-operate-item" @click="toggleOpenLog">
<el-tooltip v-if="isOpenLog" class="box-item" content="收缩日志" effect="dark" placement="bottom">
<span class="iconfont icon-up-arrow"></span>
</el-tooltip>
<el-tooltip v-else class="box-item" content="展开日志" effect="dark" placement="bottom">
<span class="iconfont icon-shousuoshangjiantou"></span>
</el-tooltip>
</view>
</div>
<div class="calibration-footer-btn-right">
<el-tooltip placement="top">
<template #content>
<el-table :data="shortcutKeyList" style="width: 100%" class="dark-table">
<el-table-column prop="action" label="功能" width="160" />
<el-table-column prop="label" label="快捷键" width="100" />
</el-table>
</template>
<span class="iconfont icon-kuaijiejian"></span>
</el-tooltip>
<div class="version-info">V{{ version }}</div>
</div>
</div>
</el-footer>
<!-- 方案配置抽屉 -->
<el-drawer v-model="schemeDrawerVisible" title="方案配置" size="80%" :destroy-on-close="true">
<div class="scheme-drawer-content">
<div class="scheme-list-header">
<div class="scheme-list-header-left">
<el-button type="primary" @click="addScheme">新增方案</el-button>
<el-button type="success" @click="exportSelectedSchemes">导出方案</el-button>
<el-button type="warning" @click="importSchemes">导入方案</el-button>
<el-button icon="Download" type="success" style="margin-left: 10px" @click="showExportProdDialog = true">导出生产数据</el-button>
<input ref="importInputRef" type="file" accept="application/json" style="display: none" @change="handleImportFile" />
</div>
<el-button type="primary" @click="loadSchemes">刷新</el-button>
</div>
<el-table ref="schemeTableRef" v-loading="loading" :data="schemeList" style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column type="index" width="50" />
<el-table-column prop="schemeName" label="方案名称" />
<el-table-column prop="schemeType" label="断路器类型"></el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button type="success" link @click="copyScheme(scope.row)">复制</el-button>
<el-button type="primary" link @click="viewScheme(scope.row)">修改</el-button>
<el-button type="danger" link @click="deleteScheme(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-drawer>
<!-- 方案详情对话框 -->
<el-dialog v-model="schemeDetailVisible" :title="currentScheme.schemeName" width="95%" :destroy-on-close="true" align-center :close-on-click-modal="false">
<el-form :model="currentScheme" label-width="100px">
<el-form-item label="方案名称">
<el-input v-model="currentScheme.schemeName" :disabled="isEditMode" />
</el-form-item>
<el-row>
<el-col :span="5">
<el-form-item label="系列">
<el-select v-model="currentScheme.series" placeholder="请选择系列">
<el-option label="B7" value="B7" />
<el-option label="B7L" value="B7L" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="框架">
<el-select v-model="currentScheme.framework" placeholder="请选择框架">
<el-option label="100ZS" value="100ZS" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="断路器类型">
<el-select v-model="currentScheme.schemeType" placeholder="请选择断路器类型">
<el-option label="1P" value="1P" />
<el-option label="2P" value="2P" />
<el-option label="3P" value="3P" />
<el-option label="4P" value="4P" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="标定中检测">
<el-switch v-model="currentScheme.middleDetect" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="标定后检测">
<el-switch v-model="currentScheme.laterDetect" />
</el-form-item>
</el-col>
</el-row>
<el-divider>断路器校准参数</el-divider>
<div class="calibrate-params">
<template v-for="(item, index) in defaultConfList.format">
<el-form-item v-if="item.key !== 'reserve'" :key="item.key" :label="item.unit ? item.name + '(' + item.unit + ')' : item.name" :label-width="145">
<el-input v-model="currentScheme.configParam.format[index].value"> </el-input>
</el-form-item>
</template>
</div>
<!-- <el-divider>断路器类型参数</el-divider>
<div class="device-params">
<div v-for="item in currentScheme.propertyParam" :key="item.id" class="device-param-item">
<span class="param-label">{{ item.unit ? item.name + '(' + item.unit + ')' : item.name }}</span>
</div>
</div> -->
<el-divider>校准精度(%)</el-divider>
<div class="accuracy-title">普通电流</div>
<div class="accuracy-params">
<el-form-item v-for="(value, key) in currentScheme.errorRange.range" :key="key" label-width="auto" :label="accuracyType[key]">
<el-input-number v-model="currentScheme.errorRange.range[key]" style="width: 120px" :min="0" :max="100" :precision="2" />
</el-form-item>
</div>
<div class="accuracy-title">小电流</div>
<div class="accuracy-params">
<el-form-item v-for="(value, key) in currentScheme.errorRange.smallRange" :key="key" label-width="auto" :label="accuracyType[key]">
<el-input-number v-model="currentScheme.errorRange.smallRange[key]" style="width: 120px" :min="0" :max="100" :precision="2" />
</el-form-item>
</div>
<el-divider>标定中检测步骤</el-divider>
<div class="calibration-steps">
<div class="steps-header">
<el-button type="primary" @click="addCalibrationStep">添加步骤</el-button>
</div>
<el-table :data="currentScheme.steps" size="small" style="width: 100%">
<!-- <el-table-column type="index" width="50" />-->
<el-table-column prop="id" align="center" label="排序" width="80" />
<!-- <el-table-column prop="id" align="center" label="步骤值">-->
<!-- <template #default="scope">-->
<!-- <el-input-number v-model="scope.row.id" :min="0" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column prop="step" align="center" label="步骤值">
<template #default="scope">
<el-input-number v-model="scope.row.step" :min="1" />
</template>
</el-table-column>
<el-table-column prop="voltage" align="center" label="电压U(%)">
<template #default="scope">
<el-input-number v-model="scope.row.voltage" :min="0" :precision="1" />
</template>
</el-table-column>
<el-table-column prop="current" align="center" label="电流I(%)/漏电流(%)">
<template #default="scope">
<div v-if="!scope.row.isILeak" style="display: flex; align-items: center;">
<!-- <el-tooltip class="box-item" effect="dark" content="电流I(%)" placement="left">
<el-input-number v-model="scope.row.current" :min="0" :precision="1" />
</el-tooltip> -->
<p style="width:50px">电流: </p>
<el-input-number v-model="scope.row.current" :min="0" :precision="1" />
</div>
<!-- <el-input-number v-model="scope.row.current" :min="0" :precision="1" /> -->
<div v-else style="display: flex; align-items: center;">
<!-- <el-tooltip class="box-item" effect="dark" content="漏电流(%)" placement="left">
<el-input-number v-model="scope.row.current" :min="0" :precision="1" />
</el-tooltip> -->
<p style="width:50px">漏电流: </p>
<el-input-number v-model="scope.row.current" :min="0" :precision="1" />
</div>
<!-- <span v-else>{{ currentScheme.configParam.format[5].value + currentScheme.configParam.format[5].unit }}</span> -->
</template>
</el-table-column>
<el-table-column prop="powerFactor" align="center" label="功率因数" width="120">
<template #default="scope">
<el-select v-model="scope.row.powerFactor">
<el-option label="0.5L" value="0.5L" />
<el-option label="0.8L" value="0.8L" />
<el-option label="1" value="1" />
<el-option label="0.5C" value="0.5C" />
<el-option label="0.8C" value="0.8C" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="isILeak" align="center" label="检测漏电" width="120">
<template #default="scope">
<el-switch v-model="scope.row.isILeak" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="scope">
<el-button type="danger" link @click="deleteCalibrationStep(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-divider>标定后检测步骤</el-divider>
<div class="calibration-steps">
<div class="steps-header">
<!-- <el-button type="primary" @click="addCalibrationLaterStep">添加步骤</el-button> -->
</div>
<el-table :data="currentScheme.laterSteps" size="small" style="width: 100%">
<!-- <el-table-column type="index" width="50" />-->
<el-table-column prop="id" align="center" label="排序" width="80" />
<!-- <el-table-column prop="id" align="center" label="步骤值">-->
<!-- <template #default="scope">-->
<!-- <el-input-number v-model="scope.row.id" :min="0" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column prop="step" align="center" label="步骤值">
<template #default="scope">
<el-input-number v-model="scope.row.step" :min="1" />
</template>
</el-table-column>
<el-table-column prop="voltage" align="center" label="电压U(%)">
<template #default="scope">
<el-input-number v-model="scope.row.voltage" :min="0" :precision="1" />
</template>
</el-table-column>
<el-table-column prop="current" align="center" label="电流I(%)/漏电流(%)">
<template #default="scope">
<div v-if="!scope.row.isILeak" style="display: flex; align-items: center;">
<!-- <el-tooltip class="box-item" effect="dark" content="电流I(%)" placement="left">
<el-input-number v-model="scope.row.current" :min="0" :precision="1" />
</el-tooltip> -->
<p style="width:50px">电流: </p>
<el-input-number v-model="scope.row.current" :min="0" :precision="1" />
</div>
<!-- <el-input-number v-model="scope.row.current" :min="0" :precision="1" /> -->
<div v-else style="display: flex; align-items: center;">
<!-- <el-tooltip class="box-item" effect="dark" content="漏电流(%)" placement="left">
<el-input-number v-model="scope.row.current" :min="0" :precision="1" />
</el-tooltip> -->
<p style="width:50px">漏电流: </p>
<el-input-number v-model="scope.row.current" :min="0" :precision="1" />
</div>
</template>
</el-table-column>
<!-- <el-table-column prop="current" align="center" label="电流Ib(%)">
<template #default="scope">
<el-input-number v-model="scope.row.current" :min="0" :precision="1" />
</template>
</el-table-column> -->
<el-table-column prop="powerFactor" align="center" label="功率因数" width="120">
<template #default="scope">
<el-select v-model="scope.row.powerFactor">
<el-option label="0.5L" value="0.5L" />
<el-option label="0.8L" value="0.8L" />
<el-option label="1" value="1" />
<el-option label="0.5C" value="0.5C" />
<el-option label="0.8C" value="0.8C" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="isILeak" align="center" label="检测漏电" width="120">
<template #default="scope">
<el-switch v-model="scope.row.isILeak" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="scope">
<!-- <el-button type="danger" link @click="deleteCalibrationLaterStep(scope.$index)">删除</el-button> -->
</template>
</el-table-column>
</el-table>
</div>
<!-- 新增:漏电检测设置分组 -->
<el-divider>漏电检测设置</el-divider>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="漏电检测">
<el-switch v-model="currentScheme.iLeakDetectObj.iLeakDetect" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="检测电流(mA)">
<el-input-number v-model="currentScheme.iLeakDetectObj.detectInput" :min="0" :step="1" style="width: 120px" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="漏电保护">
<el-switch v-model="currentScheme.iLeakDetectObj.iLeakProtect" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="保护电流(mA)">
<el-input-number v-model="currentScheme.iLeakDetectObj.protectInput" :min="0" :step="1" style="width: 120px" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="schemeDetailVisible = false">取消</el-button>
<el-button type="primary" @click="saveScheme">保存</el-button>
</span>
</template>
</el-dialog>
<!-- 方案配置抽屉 -->
<el-drawer v-model="logVisible" title="设备日志" size="90%" :destroy-on-close="true">
<div class="scheme-drawer-content log-content">
<div class="device-info">
<div class="device-info-item">设备: {{ activeDeviceInfo.id || '-' }}</div>
<div class="device-info-item">芯片ID: {{ activeDeviceInfo.cpuId || '-' }}</div>
<div class="device-info-item">设备端口: {{ activeDeviceInfo.devicePort || '-' }}</div>
<div class="device-info-item">
标定结果:
<span style="font-weight: bold" :class="['device-result', activeDeviceInfo.result === 1 ? 'color-green' : activeDeviceInfo.result === -1 ? 'color-red' : '']">{{
activeDeviceInfo.result === 1 ? '成功' : activeDeviceInfo.result === -1 ? '失败' : '未标定' || '无'
}}</span>
</div>
<div class="device-info-item">结果描述: {{ activeDeviceInfo.resultTxt || '-' }}</div>
</div>
<el-tabs v-model="logTabActive" class="demo-tabs" @tab-click="logTabChange">
<el-tab-pane label="标定日志" name="log">
<el-table :data="stepLogRows" style="width: 100%" size="small" scrollbar-always-on resizable empty-text="暂无日志">
<el-table-column prop="step" label="步骤" fixed align="center" />
<el-table-column v-for="col in logParamColumns" :key="col.key" :prop="'paramMap.' + col.valueKey + '.actualError'" :label="col.valueKey" align="center">
<template #header>
<el-tooltip effect="dark" :content="(col.valueName || col.valueKey) + (col.valueUnit ? ' [' + col.valueUnit + ']' : '')" placement="top">
<span>{{ (col.valueName || col.valueKey) + (col.valueUnit ? ' [' + col.valueUnit + ']' : '') }}</span>
</el-tooltip>
</template>
<template #default="scope">
<el-tooltip
effect="dark"
:content="
'差值百分比: ' +
(scope.row.paramMap[col.valueKey]?.actualError + '%' ?? '') +
'\n' +
'范围百分比: ' +
(scope.row.paramMap[col.valueKey]?.expectedError + '%' ?? '') +
'\n' +
'源输出值: ' +
(scope.row.paramMap[col.valueKey]?.outputValue ?? '') +
'\n' +
'读取值: ' +
(scope.row.paramMap[col.valueKey]?.actualValue ?? '') +
'\n' +
'范围最小值: ' +
(scope.row.paramMap[col.valueKey]?.ExpectedMin ?? '') +
'\n' +
'范围最大值: ' +
(scope.row.paramMap[col.valueKey]?.expectedMax ?? '')
"
placement="top"
>
<span :style="{ color: scope.row.paramMap[col.valueKey]?.result === 1 ? 'green' : 'red' }">
{{
typeof scope.row.paramMap[col.valueKey]?.actualError === 'number'
? Number(scope.row.paramMap[col.valueKey].actualError.toFixed(6)).toString()
: scope.row.paramMap[col.valueKey]?.actualError
}}<span v-if="scope.row.paramMap[col.valueKey]">%</span>
<template v-if="scope.row.paramMap[col.valueKey]">
<el-icon v-if="scope.row.paramMap[col.valueKey].actualValue > scope.row.paramMap[col.valueKey].outputValue"><Top /></el-icon>
<el-icon v-else-if="scope.row.paramMap[col.valueKey].actualValue < scope.row.paramMap[col.valueKey].outputValue"><Bottom /></el-icon>
</template>
</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="result" fixed="right" label="结果" align="center" width="80">
<template #default="scope">
<span :style="{ color: scope.row.result === 1 ? 'green' : 'red' }">
{{ scope.row.result === 1 ? '成功' : '失败' }}
</span>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="错误日志" name="second">
<el-table :data="errorLogList" size="small" style="width: 100%">
<el-table-column type="index" width="50" />
<el-table-column prop="errorCode" label="错误码" width="180">
<template #default="scope">
<span style="color: red">
{{ scope.row.errorCode }}
</span>
</template>
</el-table-column>
<el-table-column prop="logMsg" label="错误日志">
<template #default="scope">
<el-tooltip effect="dark" :content="scope.row.logMsg" placement="top">
<span class="log-msg">{{ scope.row.logMsg }}</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</el-drawer>
<!-- 导出生产数据弹窗 -->
<el-dialog v-model="showExportProdDialog" title="导出生产数据" width="450px" :close-on-click-modal="false">
<el-form>
<el-form-item label="时间范围">
<el-date-picker
v-model="exportProdRange"
type="daterange"
unlink-panels
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:shortcuts="exportProdPickerOptions"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showExportProdDialog = false">取消</el-button>
<el-button type="primary" @click="handleExportProd">导出</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Search, ArrowUp, ArrowDown, Top, Bottom, Close } from '@element-plus/icons-vue';
import { throttle } from 'lodash-es';
import dayjs from 'dayjs';
import axios from 'axios';
// import config from '@renderer/util/config.js';
const config = window.electron.readConfig();
console.log('当前获取config', config);
import { logWebSocketStore } from '@renderer/stores/logWebSocket.js';
const webSocketStore = logWebSocketStore();
const selectedScheme = ref('');
const playState = ref(false);
const devices = ref([]);
const isScroll = ref(true);
const logs = ref([]);
const version = ref('V1.0.0');
const logTabActive = ref('log');
const operatorCode = ref('');
const loading = ref(true);
const isOpenLog = ref(false);
const logSocketStateHover = ref(false);
// 方案配置相关
const schemeDrawerVisible = ref(false);
const schemeDetailVisible = ref(false);
const schemeList = ref([]);
const activeScheme = ref();
const equipmentStatistics = ref({
totalDevice: 0,
successDevice: 0,
failCount: 0
});
const accuracyType = ref({
voltage: '电压',
current: '电流',
powerFactor: '功率因数',
activePower: '有功功率',
reactivePower: '无功功率',
apparentPower: '视在功率'
});
const defaultConfList = ref({
id: 0,
key: 'calibration_conf',
name: '标定参数',
addr: 30018,
len: 4,
dataType: 'uint16_t',
rwType: 'R',
default: '',
precision: '',
unit: '',
format: []
});
const defaultiLeakDetectObj = ref({
iLeakDetect: false,
detectInput: 100,
iLeakProtect: false,
protectInput: 100
});
const logVisible = ref(false);
// 搜索相关变量
const searchKeyword = ref('');
const searchResults = ref([]);
const currentSearchIndex = ref(0);
const totalSearchCount = ref(0);
const isSearchActive = ref(false);
const logLoading = ref(false);
const logBoxRef = ref(null);
const MAX_LOG_LENGTH = 2000;
const activeDeviceIndex = ref(0);
const activeDeviceInfo = ref({
id: 0,
devicePort: '',
cpuId: '',
connectStatus: 0,
switch: 0,
result: 0,
resultTxt: '',
stepStatus: 0,
activeStep: ''
});
const defaultPropList = ref([]);
const shortcutKeyList = ref([
{
label: 'Ctrl+F',
action: '显/隐日志查询框'
},
{
label: '↑',
action: '上一个日志(查询时)'
},
{
label: '↓',
action: '下一个日志(查询时)'
},
{
label: 'Enter',
action: '下一个日志(查询时)'
},
{
label: 'Esc',
action: '退出查询'
}
]);
const currentScheme = ref({
schemeName: '',
schemeType: '1P',
series: 'B7',
framework: '100ZS',
middleDetect: true,
laterDetect: false,
configParam: defaultConfList,
propertyParam: defaultPropList,
errorRange: {
range: {
voltage: 0.5,
current: 0.5,
powerFactor: 0.5,
activePower: 0.5,
reactivePower: 0.5,
apparentPower: 0.5
},
smallRange: {
voltage: 1,
current: 1,
powerFactor: 1,
activePower: 1,
reactivePower: 1,
apparentPower: 1
}
},
steps: [],
laterSteps: [
{
id: 1,
step: 1,
voltage: 110,
current: 110,
powerFactor: '0.5L',
isILeak: false
},
{
id: 2,
step: 2,
voltage: 80,
current: 80,
powerFactor: '0.5L',
isILeak: false
},
{
id: 3,
step: 3,
voltage: 100,
current: 80,
powerFactor: '0.5L',
isILeak: true
}
],
iLeakDetectObj: defaultiLeakDetectObj
});
// 1. 新增设备日志参数集合和step分组数据
const logParamColumns = ref([]); // [{ valueKey, valueName, valueUnit }]
const stepLogRows = ref([]); // [{ step, paramMap: { valueKey: item }, result: 0/1 }]
const errorLogList = ref([]);
const logTabChange = (tab, event) => {
console.log(tab, event);
};
const selectDevice = async (item, index) => {
activeDeviceInfo.value = item;
activeDeviceIndex.value = index;
// 调用日志接口获取设备日志
await fetchDeviceLogs(item.cpuId);
logVisible.value = true;
};
// 获取设备日志数据
const fetchDeviceLogs = async deviceId => {
logLoading.value = true;
try {
const response = await axios.get(config.url + '/master/calibrate/result', {
params: { cpuId: deviceId }
});
if (response.data.code === 0) {
processLogData(response.data.data.result);
errorLogList.value = response.data.data.log || [];
} else {
ElMessage.error(response.data.message || '获取设备日志失败');
}
} catch (error) {
console.error('获取设备日志失败:', error);
ElMessage.error('获取设备日志失败');
} finally {
logLoading.value = false;
}
};
// 重写 processLogData
const processLogData = apiData => {
if (!apiData || !Array.isArray(apiData)) {
logParamColumns.value = [];
stepLogRows.value = [];
return;
}
// 收集所有参数优先从activeScheme.propertyParam获取名称和单位
const paramMap = new Map();
apiData.forEach(item => {
let valueName = item.valueName || item.valueKey;
let valueUnit = item.valueUnit || '';
if (activeScheme.value && Array.isArray(defaultPropList.value)) {
const found = defaultPropList.value.find(param => param.key === item.valueKey);
if (found) {
valueName = found.name || valueName;
valueUnit = found.unit || valueUnit;
}
}
const key = item.valueKey + (valueUnit ? `(${valueUnit})` : '');
if (!paramMap.has(key)) {
paramMap.set(key, {
valueKey: item.valueKey,
valueName,
valueUnit,
key: key
});
}
});
logParamColumns.value = Array.from(paramMap.values());
// 按step分组
const stepGroups = {};
apiData.forEach(item => {
if (!stepGroups[item.step]) stepGroups[item.step] = [];
stepGroups[item.step].push(item);
});
// 生成每行数据
stepLogRows.value = Object.entries(stepGroups).map(([step, arr]) => {
// paramMap: { valueKey: item }
const paramMapRow = {};
arr.forEach(item => {
paramMapRow[item.valueKey] = item;
});
// 结果判定
// 检查是否有任何参数的result为0如果有则直接判定为失败
// const hasFailure = logParamColumns.value.some(col => paramMapRow[col.valueKey]?.result === 0);
// if (hasFailure) return {
// step,
// paramMap: paramMapRow,
// result: 0
// };
// }
// 检查所有有result值的参数是否都为1
const allOk = logParamColumns.value.every(col => {
const param = paramMapRow[col.valueKey];
// 没有result值的参数不参与判断
return param?.result === undefined || param?.result === 1;
});
return {
step,
paramMap: paramMapRow,
result: allOk ? 1 : 0
};
});
};
const schemeChange = e => {
if (e) {
const scheme = schemeList.value.filter(item => item.id === e);
activeScheme.value = scheme[0];
} else {
activeScheme.value = {};
}
};
// 安全解析 JSON
const tryParseJSON = data => {
try {
return JSON.parse(data);
} catch {
return data;
}
};
// 节流滚动
const throttledScroll = throttle(() => {
logBoxRef.value.scrollTop = logBoxRef.value.scrollHeight;
}, 500);
const setDeviceInfo = msg => {
let deviceInfo = tryParseJSON(msg);
let type = false;
if (deviceInfo) {
devices.value.forEach((item, index) => {
if (item.id === deviceInfo.id) {
// debugger
devices.value[index] = deviceInfo;
// item=deviceInfo;
if (deviceInfo.result === -1) {
type = 'calibrate_result_error';
} else if (deviceInfo.result === 1) {
type = 'calibrate_result_success';
} else {
type = 'calibrate_result_hidden';
}
}
});
}
return type;
};
const getSocketMeassage = message => {
const msg = tryParseJSON(message.data);
console.log('msg', msg);
const newLog = {
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
msg: msg.data ?? msg,
type: msg.msgType ?? null
};
if (msg.msgType === 'calibrate_result') {
let resultType = setDeviceInfo(msg.data);
if (resultType !== 'calibrate_result_hidden') {
newLog.type = resultType;
} else {
return;
}
}
if (msg.msgType === 'calibrate_finish') {
playState.value = false;
ElMessage.success('标定完成');
getQquipmentStatistics();
}
// 批量更新日志数组
logs.value = [...logs.value.slice(-MAX_LOG_LENGTH + 1), newLog];
// 按需触发滚动
// if (isScroll.value) throttledScroll();
// let msg = JSON.parse(message.data);
// if (msg.msgType !== undefined) {
// if (logs.value.length > 400) {
// logs.value.shift();
// }
// logs.value.push({
// time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
// msg: msg.data,
// type: msg.msgType
// });
// } else {
// if (logs.value.length > 400) {
// logs.value.shift();
// }
// logs.value.push({
// time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
// msg,
// type: null
// });
// }
// if (isScroll.value) {
// requestAnimationFrame(() => {
// logBoxRef.value.scrollTop = logBoxRef.value.scrollHeight;
// });
// }
};
const initSocket = () => {
webSocketStore.init(getSocketMeassage);
};
const reconnectSocket = () => {
webSocketStore.close();
webSocketStore.init(getSocketMeassage);
};
// socket连接状态
const socketStatus = computed(() => {
return webSocketStore.socket_open;
});
// 键盘快捷键处理
const handleKeydown = event => {
// Ctrl+F: 打开搜索
if (event.ctrlKey && event.key === 'f') {
event.preventDefault();
openSearch();
}
// 在搜索模式中的快捷键
if (isSearchActive.value) {
switch (event.key) {
case 'ArrowUp':
// ↑: 上一个结果
event.preventDefault();
navigateSearch('prev');
break;
case 'ArrowDown':
// ↓: 下一个结果
event.preventDefault();
navigateSearch('next');
break;
case 'Escape':
// Esc: 关闭搜索
event.preventDefault();
closeSearch();
break;
}
}
};
const toggleIsScroll = () => {
isScroll.value = !isScroll.value;
};
const startExecution = () => {
if (selectedScheme.value === '') {
ElMessage.error({ message: '请先选择方案' });
return;
}
if (operatorCode.value.trim() === '') {
ElMessage.error({ message: '请先输入操作员编号', duration: 1000, showClose: true });
return;
}
// 开始标定之前重新连接socket
reconnectSocket();
const scheme = schemeList.value.filter(item => item.id === selectedScheme.value);
console.log(scheme);
ElMessage.success('开始执行方案: ' + scheme[0].schemeName);
playState.value = true;
playCalibration();
getQquipmentStatistics();
// Add actual start logic here
};
const stopExecution = async () => {
try {
const response = await axios.get(config.url + '/master/scheme/stop');
if (response.data.code === 0) {
// devices.value = response.data.data.result || [];
playState.value = false;
ElMessage.success('停止成功');
} else {
ElMessage.error(response.data.message || '停止失败');
}
} catch (error) {
ElMessage.error('停止失败');
}
playState.value = false;
};
const openSettings = () => {
ElMessageBox.prompt('请输入管理员密码', '密码验证', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'password',
inputValue: '',
inputPlaceholder: '请输入密码',
inputValidator: value => {
if (!value.trim()) return '请输入密码';
return true;
}
})
.then(async ({ value }) => {
try {
const response = await axios.post(config.url + '/master/scheme/auth?secret=' + value.trim());
if (response.data.code === 0) {
if (response.data.data.result) {
schemeDrawerVisible.value = true;
loadSchemes();
} else {
ElMessage.error(response.data.message || '密码错误');
}
} else {
ElMessage.error(response.data.message || '密码验证失败');
}
} catch (error) {
ElMessage.error('密码验证失败');
}
})
.catch(() => {
// 用户取消,无需处理
});
};
// 获取默认标定参数
const getSchemeDefaultConf = async () => {
try {
const response = await axios.get(config.url + '/master/scheme/defaultConf');
if (response.data.code === 0) {
defaultConfList.value = response.data.data.config || [];
} else {
ElMessage.error(response.data.message || '获取默认标定参数列表失败');
}
} catch (error) {
console.error('获取默认标定参数列表失败:', error);
ElMessage.error('获取默认标定参数列表失败');
}
};
// 获取默认属性参数
const getSchemeDefaultProp = async () => {
try {
const response = await axios.get(config.url + '/master/scheme/defaultProp');
if (response.data.code === 0) {
defaultPropList.value = response.data.data.config || [];
} else {
ElMessage.error(response.data.message || '获取默认属性参数列表失败');
}
} catch (error) {
console.error('获取默认属性参数列表失败:', error);
ElMessage.error('获取默认属性参数列表失败');
}
};
// 从API加载方案列表
const loadSchemes = async () => {
loading.value = true;
try {
const response = await axios.get(config.url + '/master/scheme/query');
if (response.data.code === 0) {
schemeList.value = response.data.data.schemes || [];
// 新增同步逻辑确保activeScheme与selectedScheme一致
let found = null;
if (selectedScheme.value) {
found = schemeList.value.find(item => item.id === selectedScheme.value);
}
if (found) {
activeScheme.value = found;
}
// else if (schemeList.value.length > 0) {
// selectedScheme.value = schemeList.value[0].id;
// activeScheme.value = schemeList.value[0];
// }
else {
selectedScheme.value = '';
activeScheme.value = {};
}
ElMessage.success({ message: '获取方案列表成功', duration: 1000 });
} else {
ElMessage.error(response.data.message || '获取方案列表失败');
}
loading.value = false;
} catch (error) {
loading.value = false;
console.error('加载方案列表失败:', error);
ElMessage.error('加载方案列表失败');
}
};
// 保存方案
const saveScheme = async () => {
// 新增校验标定中检测和标定后检测必须至少有一个为true
if (!currentScheme.value.middleDetect && !currentScheme.value.laterDetect) {
ElMessage.error('"标定中检测"和"标定后检测"必须至少选择一个');
return;
}
try {
// 处理精度赋值
const scheme = JSON.parse(JSON.stringify(currentScheme.value));
// 遍历propertyParam匹配精度并赋值
scheme.propertyParam.forEach(param => {
// 遍历accuracyType查找匹配的精度类型
Object.entries(accuracyType.value).forEach(([key, value]) => {
// 使用正则匹配参数名称
if (param.name.includes(value)) {
// 为每个参数设置bigRange和smallRange
param.errorRange = {
bigRange: scheme.errorRange.range[key],
smallRange: scheme.errorRange.smallRange[key]
};
}
});
});
console.log('提交方案', scheme.propertyParam);
const isEdit = scheme.id !== undefined;
const url = isEdit ? '/master/scheme/update' : '/master/scheme/add';
let response;
if (isEdit) {
response = await axios.put(config.url + url, scheme);
} else {
response = await axios.post(config.url + url, scheme);
}
if (response.data.code === 0) {
ElMessage.success(isEdit ? '修改成功' : '添加成功');
schemeDetailVisible.value = false;
loadSchemes(); // 重新加载方案列表
} else {
ElMessage.error(response.data.message || (isEdit ? '修改失败' : '添加失败'));
}
} catch (error) {
console.error('保存方案失败:', error);
ElMessage.error('保存方案失败');
}
};
// 删除方案
const deleteScheme = scheme => {
ElMessageBox.confirm('确定要删除该方案吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const response = await axios.delete(config.url + '/master/scheme/delete?id=' + scheme.id);
if (response.data.code === 0) {
ElMessage.success('删除成功');
loadSchemes(); // 重新加载方案列表
} else {
ElMessage.error(response.data.message || '删除失败');
}
} catch (error) {
console.error('删除方案失败:', error);
ElMessage.error('删除方案失败');
}
});
};
// 添加新方案
const addScheme = () => {
currentScheme.value = {
schemeName: `方案${schemeList.value.length + 1}`,
schemeType: '1P',
series: 'B7',
framework: '100ZS',
middleDetect: true,
laterDetect: false,
configParam: defaultConfList,
propertyParam: defaultPropList,
errorRange: {
range: {
voltage: 0.5,
current: 0.5,
powerFactor: 0.5,
activePower: 0.5,
reactivePower: 0.5,
apparentPower: 0.5
},
smallRange: {
voltage: 1,
current: 1,
powerFactor: 1,
activePower: 1,
reactivePower: 1,
apparentPower: 1
}
},
steps: [],
laterSteps: [
{
id: 1,
step: 1,
voltage: 110,
current: 110,
powerFactor: '0.5L',
isILeak: false
},
{
id: 2,
step: 2,
voltage: 80,
current: 80,
powerFactor: '0.5L',
isILeak: false
},
{
id: 3,
step: 3,
voltage: 100,
current: 80,
powerFactor: '0.5L',
isILeak: true
}
],
iLeakDetectObj: defaultiLeakDetectObj
};
isEditMode.value = false;
schemeDetailVisible.value = true;
};
// 修改方案
const viewScheme = scheme => {
currentScheme.value = JSON.parse(JSON.stringify(scheme));
isEditMode.value = true;
schemeDetailVisible.value = true;
};
// // 监听断路器类型变化
// const handleTypeChange = type => {
// currentScheme.value.propertyParam = type === '1p2p' ? JSON.parse(JSON.stringify(swich1p2pAttrList)) : JSON.parse(JSON.stringify(swich3p4pAttrList));
// };
// 添加标定步骤
const addCalibrationStep = () => {
currentScheme.value.steps.push({
id: currentScheme.value.steps.length + 1,
step: currentScheme.value.steps.length + 1,
voltage: 100,
current: 100,
powerFactor: '0.5L',
isILeak: false
});
};
// 添加标定步骤
const addCalibrationLaterStep = () => {
currentScheme.value.laterSteps.push({
id: currentScheme.value.laterSteps.length + 1,
step: currentScheme.value.laterSteps.length + 1,
voltage: 100,
current: 100,
powerFactor: '0.5L',
isILeak: false
});
};
const toggleOpenLog = () => {
isOpenLog.value = !isOpenLog.value;
};
const clearLog = () => {
logs.value = [];
};
// 搜索相关函数
const searchInputRef = ref(null);
const openSearch = () => {
if (isSearchActive.value) {
closeSearch();
} else {
isSearchActive.value = true;
searchKeyword.value = '';
searchResults.value = [];
currentSearchIndex.value = 0;
totalSearchCount.value = 0;
// 聚焦到搜索输入框
setTimeout(() => {
if (searchInputRef.value) {
searchInputRef.value.focus();
}
}, 100);
}
};
const closeSearch = () => {
isSearchActive.value = false;
searchKeyword.value = '';
searchResults.value = [];
currentSearchIndex.value = 0;
totalSearchCount.value = 0;
};
const performSearch = () => {
if (!searchKeyword.value.trim()) {
searchResults.value = [];
currentSearchIndex.value = 0;
totalSearchCount.value = 0;
return;
}
const keyword = searchKeyword.value.toLowerCase();
const results = [];
logs.value.forEach((log, index) => {
const logText = `${log.time} ${log.msg}`.toLowerCase();
if (logText.includes(keyword)) {
results.push({
originalIndex: index,
time: log.time,
msg: log.msg,
type: log.type
});
}
});
searchResults.value = results;
currentSearchIndex.value = results.length > 0 ? 0 : -1;
totalSearchCount.value = results.length;
// 如果有搜索结果,滚动到第一个结果
if (results.length > 0) {
scrollToSearchResult(results[0].originalIndex);
}
};
const getHighlightedText = item => {
if (!searchKeyword.value.trim()) {
return `${item.time} ${item.msg}`;
}
const text = `${item.time} ${item.msg}`;
const keyword = searchKeyword.value;
const regex = new RegExp(`(${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return text.replace(regex, '<span class="search-highlight-text">$1</span>');
};
const navigateSearch = direction => {
if (searchResults.value.length === 0) return;
if (direction === 'next') {
currentSearchIndex.value = (currentSearchIndex.value + 1) % searchResults.value.length;
} else if (direction === 'prev') {
currentSearchIndex.value = currentSearchIndex.value === 0 ? searchResults.value.length - 1 : currentSearchIndex.value - 1;
}
// 滚动到当前搜索结果
const currentResult = searchResults.value[currentSearchIndex.value];
if (currentResult) {
scrollToSearchResult(currentResult.originalIndex);
}
};
const scrollToSearchResult = index => {
setTimeout(() => {
const logElements = document.querySelectorAll('.log-item');
if (logElements[index]) {
logElements[index].scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}, 100);
};
// 删除标定步骤
const deleteCalibrationStep = index => {
currentScheme.value.steps.splice(index, 1);
// 重新编号
currentScheme.value.steps.forEach((step, idx) => {
step.id = idx + 1;
});
};
// 删除标定步骤
const deleteCalibrationLaterStep = index => {
currentScheme.value.laterSteps.splice(index, 1);
// 重新编号
currentScheme.value.laterSteps.forEach((step, idx) => {
step.id = idx + 1;
});
};
const generateDeviceList = async () => {
try {
const response = await axios.get(config.url + '/master/device/list');
if (response.data.code === 0) {
devices.value = response.data.data.result || [];
//测试输出结果
// setDeviceInfo("{\"id\":1,\"devicePort\":\"com3\",\"cpuId\":\"SN\",\"connectStatus\":1,\"switch\":0,\"result\":-1,\"resultTxt\":\"\"}")
} else {
ElMessage.error(response.data.message || '刷新列表失败');
}
} catch (error) {
console.error('刷新列表失败:', error);
ElMessage.error('刷新列表失败');
}
};
const playCalibration = async () => {
try {
const response = await axios.get(config.url + '/master/scheme/start?schemeId=' + selectedScheme.value + '&operatorCode=' + operatorCode.value);
if (response.data.code === 0) {
// devices.value = response.data.data.result || [];
} else {
ElMessageBox.alert(response.data.message, '告警', {
// if you want to disable its autofocus
// autofocus: false,
dangerouslyUseHTMLString: true,
confirmButtonText: '确定',
confirmButtonClass: 'custom-message-box',
callback: action => {}
});
playState.value = false;
// ElMessage.error(response.data.message || '开始执行失败');
}
} catch (error) {
console.error('获取设备列表失败:', error);
// ElMessage.error('开始执行失败');
}
};
const copyScheme = scheme => {
const newScheme = JSON.parse(JSON.stringify(scheme));
delete newScheme.id;
if (typeof newScheme.schemeName === 'string') {
newScheme.schemeName = newScheme.schemeName + '_1';
}
currentScheme.value = newScheme;
isEditMode.value = false;
schemeDetailVisible.value = true;
};
const importInputRef = ref(null);
const exportAllSchemes = () => {
if (!schemeList.value.length) {
ElMessage.warning('暂无可导出的方案');
return;
}
const exportArr = schemeList.value.map(sch => {
const obj = JSON.parse(JSON.stringify(sch));
obj.id = '';
return obj;
});
const blob = new Blob([JSON.stringify(exportArr, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '谷云校表方案' + dayjs().format('YYYY-MM-DD HH:mm:ss') + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
ElMessage.success('导出成功');
};
const importSchemes = () => {
if (importInputRef.value) {
importInputRef.value.value = '';
importInputRef.value.click();
}
};
const handleImportFile = async e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async evt => {
try {
const arr = JSON.parse(evt.target.result);
if (!Array.isArray(arr)) {
ElMessage.error('导入文件格式错误,应为方案数组');
return;
}
let successCount = 0,
failCount = 0;
const duplicateNames = [];
for (const sch of arr) {
const newSch = JSON.parse(JSON.stringify(sch));
delete newSch.id;
try {
const res = await axios.post(config.url + '/master/scheme/add', newSch);
if (res.data.code === 0) successCount++;
else {
failCount++;
if (res.data.code === 50 && res.data.message && res.data.message.includes('方案名称重复')) {
duplicateNames.push(newSch.schemeName);
}
}
} catch (err) {
failCount++;
}
}
let msg = `导入完成,成功${successCount}条,失败${failCount}条`;
if (duplicateNames.length) {
msg += `\n名称重复${duplicateNames.join('')}`;
}
ElMessage({ message: msg, type: duplicateNames.length ? 'error' : 'success', duration: 8000 });
loadSchemes();
} catch {
ElMessage.error('导入文件解析失败');
}
};
reader.readAsText(file);
};
const schemeTableRef = ref(null);
const selectedSchemes = ref([]);
const isEditMode = ref(false);
const handleSelectionChange = val => {
selectedSchemes.value = val;
};
const exportSelectedSchemes = () => {
const exportArr = selectedSchemes.value.length ? selectedSchemes.value : [];
if (!exportArr.length) {
ElMessage.warning('请先选择要导出的方案');
return;
}
const arr = exportArr.map(sch => {
const obj = JSON.parse(JSON.stringify(sch));
obj.id = '';
return obj;
});
const blob = new Blob([JSON.stringify(arr, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '谷云校表方案' + dayjs().format('YYYY-MM-DD HH:mm:ss') + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
ElMessage.success('导出成功');
};
const showExportProdDialog = ref(false);
const exportProdRange = ref([]);
const exportProdPickerOptions = [
{
text: '本日',
value: () => {
const end = new Date();
const start = new Date();
// start.setTime(start.getTime() - 3600 * 1000 * 24);
return [start, end];
}
},
{
text: '最近一周',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
}
},
{
text: '最近一个月',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
}
},
{
text: '最近三个月',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
}
}
];
const handleExportProd = async () => {
console.log(window.electron);
if (!exportProdRange.value || exportProdRange.value.length !== 2) {
ElMessage.warning('请选择导出时间范围');
return;
}
const [start, end] = exportProdRange.value;
// 补全时间
const beginTime = new Date(dayjs(start).format('YYYY-MM-DD') + ' 00:00:00').getTime();
const endTime = new Date(dayjs(end).format('YYYY-MM-DD') + ' 23:59:59').getTime();
try {
const response = await axios.get(config.url + `/master/calibrate/resultExport?beginTime=${beginTime}&endTime=${endTime}`);
if (response.data.code === 0) {
ElMessage.success('生成成功');
// 打开exePath/export目录
if (window.electron && window.electron.shell && window.electron.exePath) {
window.electron.shell.openPath(window.electron.exePath + '/export');
} else {
ElMessage.info('请手动前往导出目录"软件安装目录/resources/service/export"');
}
} else {
ElMessage.error(response.data.message || '导出失败');
}
} catch (error) {
ElMessage.error('导出失败');
}
showExportProdDialog.value = false;
};
const getQquipmentStatistics = () => {
axios
.get(config.url + '/master/calibrate/stat')
.then(response => {
if (response.data.code === 0 && response.data.data.state) {
equipmentStatistics.value = response.data.data.state;
} else {
ElMessage.error(response.data.message);
}
})
.catch(error => {
ElMessage.error(error);
});
};
// 监听日志变化滚到到底部
watch(
logs,
() => {
if (isScroll.value) {
nextTick(() => {
throttledScroll();
});
}
},
{
deep: true
}
);
onMounted(() => {
logBoxRef.value = document.querySelector('#log-box-main');
initSocket();
if (window.electron && typeof window.electron.getAppVersion === 'function') {
window.electron.getAppVersion().then(v => {
version.value = v;
});
}
// generateMockDevices();
getSchemeDefaultConf();
getSchemeDefaultProp();
generateDeviceList();
loadSchemes();
getQquipmentStatistics();
// 添加键盘快捷键监听
document.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
if (webSocketStore) {
webSocketStore.close();
}
// 移除键盘快捷键监听
document.removeEventListener('keydown', handleKeydown);
});
</script>
<style scoped lang="scss">
.calibration-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f0f2f5;
}
:deep(.asterisk-left.el-form-item) {
margin-bottom: 10px;
}
.calibration-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
height: 60px;
background-color: #404040; /* Dark grey similar to image */
color: white;
flex-shrink: 0;
}
.scheme-list {
background: #ddd;
padding: 6px;
font-size: 13px;
display: flex;
align-items: center;
user-select: text;
&.list-two {
// justify-content: space-between;
}
.scheme-item {
margin-left: 10px;
width: 14%;
&:first-child {
margin-left: 0;
}
.scheme-value {
font-weight: bold;
}
}
&.list-one {
padding-bottom: 0;
.scheme-item {
// margin-right: 55px;
}
}
}
.header-left,
.header-center,
.header-right {
display: flex;
align-items: center;
}
.header-center .el-button {
margin-left: 10px;
}
.statistics-box {
margin-left: 10px;
font-size: 13px;
.statistics-number {
font-size: 16px;
font-weight: bold;
}
}
.calibration-main {
flex-grow: 1;
padding: 20px 10px;
overflow-y: auto;
}
.device-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 15px;
}
.device-card {
cursor: pointer;
/* background-color: #ffffff; */ /* Default card color is fine */
border: 1px solid #e6e6e6;
&.active {
border: 1px solid #67c23a;
}
}
.device-card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
@keyframes dot-animation {
0% {
content: '.';
}
33% {
content: '..';
}
66% {
content: '...';
}
100% {
content: '.';
}
}
.device-card-body {
font-size: 12px;
text-align: center;
position: relative;
.process-status {
position: absolute;
top: -6px;
left: 0;
width: 100%;
&.color-orange {
&::after {
content: '...';
animation: dot-animation 1.5s infinite;
}
}
}
.device-result {
font-size: 14px;
font-weight: bold;
}
}
.circuit-breaker {
font-size: 70px;
color: #999;
}
.upsideDown {
transform: scaleY(-1);
}
.color-green {
color: #67c23a;
/* Green for connected */
}
.color-orange {
color: #e6a23c;
/* Green for connected */
}
.calibration-footer {
height: 200px; /* Fixed height for logs */
background-color: #303030; /* Darker grey for logs */
color: #a7a7a7; /* Light grey text for logs */
padding: 10px 20px;
display: flex;
flex-direction: column;
flex-shrink: 0;
position: relative;
&.open-log {
height: 350px;
}
}
.log-output {
flex-grow: 1;
overflow-y: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
white-space: pre-wrap;
/* Ensures logs wrap and preserve formatting */
border: 1px solid #555;
padding: 5px;
background-color: #202020;
}
.log-output pre {
margin: 2px 0;
color: #dcdcdc;
}
.log-box-main {
font-family: 'Courier New', Courier, monospace;
font-size: 10px;
white-space: pre-wrap;
flex-grow: 1;
overflow-y: auto;
border: 1px solid #555;
background-color: #202020;
color: #dcdcdc;
scroll-behavior: smooth;
.log-list {
// padding: 5px;
.log-item {
padding: 0 5px;
word-wrap: break-word;
word-break: break-all;
font-size: 12px;
line-height: 18px;
font-weight: 500;
.iconfont {
font-size: 12px;
line-height: 18px;
}
.log-item-txt {
user-select: text;
margin-left: 5px;
}
}
}
}
.calibration-footer-btn {
font-size: 12px;
padding-top: 5px;
display: flex;
align-items: center;
justify-content: space-between;
.btn-box {
display: flex;
align-items: center;
.log-box-operate-item {
display: flex;
align-items: center;
margin-right: 10px;
cursor: pointer;
.icon-icon_gengxin {
margin-left: 2px;
}
.log-socket-state {
color: #00a73c;
display: flex;
align-items: center;
cursor: pointer;
}
}
}
.calibration-footer-btn-right {
display: flex;
align-items: center;
/* 表格暗黑样式 */
}
}
.version-info {
margin-left: 10px;
color: #888;
}
/* Element Plus component overrides if needed */
.el-header {
--el-header-padding: 0 20px;
/* Adjust if necessary */
--el-header-height: 60px;
/* Ensure this matches fixed height */
}
.el-footer {
--el-footer-padding: 0px;
/* Adjust if necessary */
--el-footer-height: 200px;
/* Ensure this matches fixed height */
}
.el-main {
--el-main-padding: 20px;
/* Adjust if necessary */
}
.el-select .el-input__inner {
background-color: #555;
/* Darker input fields */
color: white;
border-color: #666;
}
:deep(.log-content .el-table .cell) {
padding: 0 !important;
}
:deep(.device-grid .el-card__header) {
padding: 10px 5px;
}
:deep(.device-grid .el-card__body) {
padding: 10px 5px;
}
/* Ensure select dropdowns are also styled if needed */
.scheme-drawer-content {
padding: 20px;
user-select: text;
.device-info {
display: flex;
flex-wrap: wrap;
background: #ddd;
padding: 10px;
font-size: 13px;
align-items: center;
.device-info-item {
margin-right: 10px;
}
}
}
.scheme-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.calibrate-params {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.accuracy-title {
font-weight: bold;
margin-bottom: 10px;
font-size: 14px;
color: #000;
}
.device-params,
.accuracy-params {
display: grid;
gap: 10px;
grid-template-columns: repeat(6, 1fr);
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.calibration-steps {
margin-top: 20px;
.steps-header {
margin-bottom: 5px;
}
}
.unit {
margin-left: 5px;
}
:deep(.el-drawer__body) {
padding: 0;
}
:deep(.el-dialog__body) {
padding: 20px;
max-height: calc(100vh - 152px);
overflow-y: auto;
}
:deep(.el-divider--horizontal) {
margin: 20px 0;
}
.device-params {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 15px;
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
.device-param-item {
padding: 8px 12px;
background-color: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
align-items: center;
.param-label {
font-size: 13px;
color: #606266;
}
}
}
// 浮窗搜索样式
.floating-search-bar {
position: absolute;
top: 20px;
right: 30px;
z-index: 1000;
display: flex;
align-items: center;
// padding: 8px;
// background-color: #2a2a2a;
// border: 1px solid #444;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
gap: 8px;
// min-width: 280px;
.search-input-wrapper {
flex: 1;
width: 150px;
}
.search-stats {
color: #ccc;
font-size: 12px;
// width: 40px;
text-align: center;
white-space: nowrap;
}
.search-navigation {
display: flex;
gap: 8px;
.el-button {
margin: 0;
width: 24px;
height: 24px;
}
}
}
// 搜索高亮样式
.log-item {
&.search-highlight {
background-color: rgba(255, 235, 59, 0.3) !important;
}
&.search-current {
background-color: rgba(255, 235, 59, 0.6) !important;
border-left: 3px solid #ffeb3b;
}
:deep(.search-highlight-text) {
background-color: #ffeb3b !important;
color: #000;
padding: 1px 2px;
border-radius: 2px;
font-weight: bold;
}
&.finish-txt {
margin-bottom: 40px;
}
}
.dark-table {
background: #303030 !important;
color: #dcdcdc !important;
border: none !important;
font-size: 13px;
:deep(.el-table__header) {
background: #303030 !important;
color: #e0e0e0 !important;
font-weight: bold;
border-bottom: 1px solid #555555 !important;
}
:deep(tr) {
color: #e0e0e0 !important;
}
:deep(.el-table__body) {
background: #303030 !important;
}
:deep(.el-table__row) {
background: #303030 !important;
transition: background 0.2s;
}
:deep(.el-table__row:hover) {
background: #2a2a2a !important;
}
:deep(.el-table__cell) {
background: #303030 !important;
border-color: #555555 !important;
color: #dcdcdc !important;
padding: 6px 10px;
}
:deep(.el-table__empty-block) {
background: #303030 !important;
color: #888 !important;
}
:deep(.el-table__border) {
border-color: #555555 !important;
}
:deep(.el-table__inner-wrapper) {
box-shadow: none !important;
}
:deep(.el-table__inner-wrapper:before) {
background: #303030 !important;
}
}
.color-red {
color: #f56c6c !important;
/* Red for disconnected */
}
</style>
<style>
.custom-message-box.el-button--primary {
background-color: #f56c6c !important;
border-color: #f56c6c !important;
}
.custom-message-box.el-button--primary:hover {
background-color: #cc0000 !important;
border-color: #cc0000 !important;
}
</style>