feat(renderer): 新增方案管理功能并优化界面

- 新增方案复制、导入、导出功能
- 添加错误日志展示
- 优化方案配置界面布局
- 调整图标样式
- 修复部分功能逻辑
This commit is contained in:
fhysy 2025-07-15 16:56:19 +08:00
parent 1b9ff88922
commit 95f8374a13
8 changed files with 309 additions and 51 deletions

View File

@ -1,6 +1,6 @@
{
"name": "calibration-pc",
"version": "1.0.6",
"name": "gy-calibration",
"version": "1.0.7",
"description": "谷云开发部开发的断路器标定软件",
"main": "./out/main/index.js",
"author": "example.com",

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<title>谷云标定工具</title>
<title>谷云校表产测</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<link rel="stylesheet" href="/src/assets/iconfont/iconfont.css">
<!-- <meta-->

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4622943 */
src: url('iconfont.woff2?t=1752127229460') format('woff2'),
url('iconfont.woff?t=1752127229460') format('woff'),
url('iconfont.ttf?t=1752127229460') format('truetype');
src: url('iconfont.woff2?t=1752478848997') format('woff2'),
url('iconfont.woff?t=1752478848997') format('woff'),
url('iconfont.ttf?t=1752478848997') format('truetype');
}
.iconfont {
@ -145,6 +145,10 @@
content: "\e772";
}
.icon-icon_duanluqi2:before {
content: "\e9c2";
}
.icon-icon_caid:before {
content: "\e75f";
}

View File

@ -5,6 +5,7 @@ import { createApp } from 'vue'
import dayjs from 'dayjs'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import App from '@renderer/App.vue'
@ -18,7 +19,9 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
}
app.use(createPinia())
app.use(ElementPlus)
app.use(ElementPlus, {
locale: zhCn,
})
app.use(router)
app.mount('#app')

View File

@ -7,11 +7,12 @@
</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>
<span style="margin-left: 10px">操作员编号:</span>
<el-input v-model="operatorCode" placeholder="请输入操作员编号" style="width: 150px; margin-left: 10px" />
</div>
<div class="header-right">
<el-button icon="RefreshRight" type="primary" :disabled="playState" @click="generateDeviceList">刷新列表</el-button>
<el-button icon="Download" type="success" style="margin-left: 10px" @click="showExportProdDialog = true">导出生产数据</el-button>
<el-button icon="Setting" :disabled="playState" @click="openSettings">配置方案</el-button>
</div>
</el-header>
@ -25,17 +26,26 @@
<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'" :key="index" class="scheme-item"
>{{ item.unit ? item.name + '(' + item.unit + ')' : item.name }}: <span class="scheme-value">{{ item.value }}</span></span
<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">
@ -56,30 +66,35 @@
v-if="device.connectStatus === 1"
:class="[
'iconfont',
'icon-icon_duanluqi',
'icon-icon_duanluqi2',
'circuit-breaker',
device.switch === 0 ? 'upsideDown' : '',
device.result === 1 ? 'color-green' : device.result === -1 ? 'color-red' : device.result === 2 ? 'color-orange' : ''
]"
></div>
<div v-else :class="['iconfont', 'icon-icon_duanluqi', 'circuit-breaker', device.result === 1 ? 'color-green' : device.result === -1 ? 'color-red' : '']"></div>
<div
v-else
:class="['iconfont', 'icon-icon_duanluqi2', 'circuit-breaker', device.switch === 0 ? 'upsideDown' : '', device.result === 1 ? 'color-green' : device.result === -1 ? 'color-red' : '']"
></div>
<p>
芯片ID:
<span
<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 :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"
<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
@ -201,15 +216,22 @@
<el-drawer v-model="schemeDrawerVisible" title="方案配置" size="80%" :destroy-on-close="true">
<div class="scheme-drawer-content">
<div class="scheme-list-header">
<el-button type="primary" @click="addScheme">新增方案</el-button>
<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>
<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 v-loading="loading" :data="schemeList" style="width: 100%">
<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>
@ -222,7 +244,7 @@
<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" />
<el-input v-model="currentScheme.schemeName" :disabled="isEditMode" />
</el-form-item>
<el-row>
<el-col :span="5">
@ -243,10 +265,10 @@
<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-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>
@ -264,7 +286,7 @@
<el-divider>断路器校准参数</el-divider>
<div class="calibrate-params">
<template v-for="(item, index) in defaultConfList.format">
<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>
@ -450,10 +472,49 @@
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="错误日志" name="second"> 错误日志 </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>
@ -571,7 +632,7 @@ const shortcutKeyList = ref([
const currentScheme = ref({
schemeName: '',
schemeType: '1p',
schemeType: '1P',
series: 'B7',
framework: '100ZS',
middleDetect: true,
@ -603,6 +664,7 @@ const currentScheme = ref({
// 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);
@ -626,6 +688,7 @@ const fetchDeviceLogs = async deviceId => {
if (response.data.code === 0) {
processLogData(response.data.data.result);
errorLogList.value = response.data.data.log || [];
} else {
ElMessage.error(response.data.message || '获取设备日志失败');
}
@ -801,7 +864,6 @@ const socketStatus = computed(() => {
return webSocketStore.socket_open;
});
onMounted(() => {
logBoxRef.value = document.querySelector('#log-box-main');
initSocket();
@ -899,8 +961,39 @@ const stopExecution = async () => {
};
const openSettings = () => {
schemeDrawerVisible.value = true;
loadSchemes();
ElMessageBox.prompt('请输入管理员密码', '密码验证', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'password',
inputValue: 'gy999',
inputPlaceholder: '请输入密码',
closeOnClickModal: false
// inputValidator: (value) => {
// if (!value) return '';
// if (value !== 'gy999') return '';
// return true;
// }
})
.then(async ({ value }) => {
try {
const response = await axios.post(config.url + '/master/scheme/auth?secret=' + value);
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(() => {
//
});
};
//
@ -1042,7 +1135,7 @@ const deleteScheme = scheme => {
const addScheme = () => {
currentScheme.value = {
schemeName: `方案${schemeList.value.length + 1}`,
schemeType: '1p',
schemeType: '1P',
series: 'B7',
framework: '100ZS',
middleDetect: true,
@ -1070,12 +1163,14 @@ const addScheme = () => {
steps: [],
iLeakDetectObj: defaultiLeakDetectObj
};
isEditMode.value = false;
schemeDetailVisible.value = true;
};
//
//
const viewScheme = scheme => {
currentScheme.value = JSON.parse(JSON.stringify(scheme));
isEditMode.value = true;
schemeDetailVisible.value = true;
};
@ -1243,6 +1338,165 @@ const playCalibration = async () => {
// 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 * 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 = () => {
if (!exportProdRange.value || exportProdRange.value.length !== 2) {
ElMessage.warning('请选择导出时间范围');
return;
}
// TODO:
// exportProdRange.value[0]exportProdRange.value[1]
showExportProdDialog.value = false;
ElMessage.success('导出请求已发起(请补充实际导出逻辑)');
};
</script>
<style scoped lang="scss">
@ -1252,7 +1506,7 @@ const playCalibration = async () => {
height: 100vh;
background-color: #f0f2f5;
}
:deep(.asterisk-left.el-form-item){
:deep(.asterisk-left.el-form-item) {
margin-bottom: 10px;
}
@ -1273,10 +1527,11 @@ const playCalibration = async () => {
display: flex;
align-items: center;
&.list-two {
justify-content: space-between;
// justify-content: space-between;
}
.scheme-item {
margin-left: 10px;
width: 14%;
&:first-child {
margin-left: 0;
}
@ -1287,7 +1542,7 @@ const playCalibration = async () => {
&.list-one {
padding-bottom: 0;
.scheme-item {
margin-right: 55px;
// margin-right: 55px;
}
}
}
@ -1373,6 +1628,10 @@ const playCalibration = async () => {
color: #999;
}
.upsideDown {
transform: scaleY(-1);
}
.color-green {
color: #67c23a;
/* Green for connected */
@ -1383,11 +1642,6 @@ const playCalibration = async () => {
/* Green for connected */
}
.color-red {
color: #f56c6c;
/* Red for disconnected */
}
.calibration-footer {
height: 200px; /* Fixed height for logs */
background-color: #303030; /* Darker grey for logs */
@ -1440,19 +1694,12 @@ const playCalibration = async () => {
font-weight: 500;
.iconfont {
font-size: 14px;
font-size: 12px;
line-height: 18px;
&.icon-icon_fuzhi {
line-height: 26px;
margin-left: 8px;
font-size: 16px;
color: #0066cc;
cursor: pointer;
}
}
.log-item-txt {
user-select: auto;
user-select: text;
margin-left: 5px;
}
}
@ -1566,7 +1813,7 @@ const playCalibration = async () => {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
:deep(.el-form-item){
:deep(.el-form-item) {
margin-bottom: 0;
}
}
@ -1583,7 +1830,7 @@ const playCalibration = async () => {
display: grid;
gap: 10px;
grid-template-columns: repeat(6, 1fr);
:deep(.el-form-item){
:deep(.el-form-item) {
margin-bottom: 0;
}
}
@ -1741,4 +1988,8 @@ const playCalibration = async () => {
background: #303030 !important;
}
}
.color-red {
color: #f56c6c !important;
/* Red for disconnected */
}
</style>