<!DOCTYPE html>
|
<html lang="zh-CN">
|
|
<head>
|
<meta charset="UTF-8">
|
<title>调试参数</title>
|
<link rel="stylesheet" href="../../static/vue/element/element.css">
|
<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
|
<script type="text/javascript" src="../../static/js/common.js"></script>
|
<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
|
<script type="text/javascript" src="../../static/vue/element/element.js"></script>
|
<style>
|
body {
|
margin: 0;
|
padding: 16px;
|
background: #f5f7fa;
|
color: #303133;
|
box-sizing: border-box;
|
}
|
|
* {
|
box-sizing: border-box;
|
}
|
|
.page-shell {
|
max-width: 1600px;
|
margin: 0 auto;
|
}
|
|
.hero-card,
|
.filter-card,
|
.group-card {
|
margin-bottom: 16px;
|
}
|
|
.hero-header,
|
.group-header,
|
.filter-bar {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 16px;
|
flex-wrap: wrap;
|
}
|
|
.hero-title {
|
font-size: 20px;
|
font-weight: 600;
|
line-height: 1.2;
|
margin: 0 0 4px;
|
}
|
|
.hero-desc,
|
.group-desc {
|
margin: 0;
|
color: #606266;
|
line-height: 1.5;
|
}
|
|
.hero-actions,
|
.group-actions,
|
.row-actions,
|
.group-tags {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
|
.summary-strip {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
margin-top: 10px;
|
}
|
|
.summary-pill {
|
display: inline-flex;
|
align-items: center;
|
gap: 8px;
|
padding: 6px 10px;
|
border-radius: 999px;
|
background: #f7f9fc;
|
border: 1px solid #ebeef5;
|
font-size: 12px;
|
color: #606266;
|
line-height: 1.4;
|
}
|
|
.summary-pill strong {
|
font-size: 14px;
|
color: #303133;
|
}
|
|
.filter-item {
|
min-width: 220px;
|
flex: 1 1 260px;
|
}
|
|
.group-title {
|
font-size: 18px;
|
font-weight: 600;
|
margin-bottom: 6px;
|
}
|
|
.code-cell {
|
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
color: #606266;
|
font-size: 12px;
|
line-height: 1.6;
|
word-break: break-all;
|
}
|
|
.config-name {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
line-height: 1.5;
|
}
|
|
.config-name-main {
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.config-name-sub {
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.editor-cell {
|
min-width: 320px;
|
}
|
|
.status-cell {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
|
.empty-wrap {
|
padding: 32px 0 8px;
|
background: #fff;
|
border-radius: 8px;
|
}
|
|
.el-card__header {
|
padding: 16px 20px;
|
}
|
|
.el-card__body {
|
padding: 18px 20px;
|
}
|
|
.hero-card .el-card__body {
|
padding: 14px 18px;
|
}
|
|
@media (max-width: 900px) {
|
body {
|
padding: 12px;
|
}
|
|
.editor-cell {
|
min-width: 0;
|
}
|
}
|
</style>
|
</head>
|
|
<body>
|
<div id="app" class="page-shell" v-loading="loading">
|
<el-card shadow="never" class="hero-card">
|
<div class="hero-header">
|
<div>
|
<h1 class="hero-title">调试参数</h1>
|
<p class="hero-desc">
|
基于当前设备拓扑和 <code>sys_config</code> 实时展示运行参数。
|
</p>
|
</div>
|
<div class="hero-actions">
|
<el-button icon="el-icon-refresh" @click="reloadData">刷新数据</el-button>
|
<el-button type="primary"
|
icon="el-icon-check"
|
:loading="saveAllLoading"
|
:disabled="!hasChanges"
|
@click="saveAll">
|
保存全部变更
|
</el-button>
|
</div>
|
</div>
|
|
<div class="summary-strip">
|
<div class="summary-pill" v-for="item in summaryCards" :key="item.key">
|
<span>{{ item.label }}</span>
|
<strong>{{ item.value }}</strong>
|
</div>
|
</div>
|
</el-card>
|
|
<el-card shadow="never" class="filter-card">
|
<div class="filter-bar">
|
<div class="filter-item">
|
<el-input v-model.trim="searchText"
|
clearable
|
prefix-icon="el-icon-search"
|
placeholder="按配置名称或编码筛选">
|
</el-input>
|
</div>
|
<div style="width: 240px;">
|
<el-select v-model="selectedType" filterable style="width: 100%;">
|
<el-option label="全部分组" value="all"></el-option>
|
<el-option v-for="type in selectTypeOptions"
|
:key="type"
|
:label="getGroupLabel(type)"
|
:value="type">
|
</el-option>
|
</el-select>
|
</div>
|
<div>
|
<el-switch v-model="changedOnly" active-text="仅看已修改"></el-switch>
|
</div>
|
<div class="group-tags">
|
<el-tag size="small">参数 {{ configList.length }} 项</el-tag>
|
<el-tag size="small">分组 {{ selectTypeOptions.length }} 个</el-tag>
|
<el-tag v-if="hasChanges" type="warning" size="small">待保存 {{ changedCount }} 项</el-tag>
|
</div>
|
</div>
|
</el-card>
|
|
<div v-if="filteredGroups.length === 0" class="empty-wrap">
|
<el-empty description="当前筛选条件下没有可显示的参数"></el-empty>
|
</div>
|
|
<el-card v-for="group in filteredGroups" :key="group.type" shadow="never" class="group-card">
|
<div slot="header" class="group-header">
|
<div>
|
<div class="group-title">{{ group.label }}</div>
|
<p class="group-desc">{{ group.desc }}</p>
|
<div class="group-tags" style="margin-top: 10px;">
|
<el-tag size="mini">{{ group.items.length }} 项</el-tag>
|
<el-tag v-if="group.deviceText" type="success" size="mini">{{ group.deviceText }}</el-tag>
|
<el-tag v-if="group.changedCount > 0" type="warning" size="mini">已修改 {{ group.changedCount }} 项</el-tag>
|
</div>
|
</div>
|
<div class="group-actions">
|
<el-button size="mini"
|
:disabled="group.changedCount === 0"
|
:loading="saveGroupLoading[group.type]"
|
type="primary"
|
@click="saveGroup(group)">
|
保存本组
|
</el-button>
|
</div>
|
</div>
|
|
<el-table :data="group.items" border stripe size="mini">
|
<el-table-column label="参数" min-width="260">
|
<template slot-scope="scope">
|
<div class="config-name">
|
<span class="config-name-main">{{ scope.row.name || scope.row.code }}</span>
|
<span class="config-name-sub">
|
{{ getValueTypeText(scope.row) }}
|
<span v-if="scope.row.status === 0"> / 已禁用</span>
|
</span>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="编码" min-width="210">
|
<template slot-scope="scope">
|
<div class="code-cell">{{ scope.row.code }}</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="当前值" min-width="420">
|
<template slot-scope="scope">
|
<div class="editor-cell">
|
<el-radio-group v-if="resolveEditor(scope.row) === 'boolean'"
|
v-model="codeMap[scope.row.code]"
|
size="mini">
|
<el-radio-button v-for="option in getBooleanOptions(scope.row)"
|
:key="option.value"
|
:label="option.value">
|
{{ option.label }}
|
</el-radio-button>
|
</el-radio-group>
|
|
<el-input v-else-if="resolveEditor(scope.row) === 'textarea'"
|
v-model="codeMap[scope.row.code]"
|
type="textarea"
|
resize="vertical"
|
:autosize="{ minRows: 2, maxRows: 5 }">
|
</el-input>
|
|
<el-input v-else
|
v-model="codeMap[scope.row.code]"
|
clearable>
|
</el-input>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="状态" width="170">
|
<template slot-scope="scope">
|
<div class="status-cell">
|
<el-tag v-if="isChanged(scope.row.code)" type="warning" size="mini">待保存</el-tag>
|
<el-tag v-else type="success" size="mini">已同步</el-tag>
|
<el-button v-if="isChanged(scope.row.code)"
|
type="text"
|
size="mini"
|
@click="resetConfig(scope.row.code)">
|
还原
|
</el-button>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-card>
|
</div>
|
|
<script>
|
new Vue({
|
el: '#app',
|
data: {
|
loading: false,
|
saveAllLoading: false,
|
saveGroupLoading: {},
|
configList: [],
|
deviceList: [],
|
codeMap: {},
|
originalCodeMap: {},
|
searchText: '',
|
selectedType: 'all',
|
changedOnly: false,
|
groupOrder: ['system', 'solver', 'crn', 'fake', 'notify', 'String', 'other']
|
},
|
computed: {
|
selectTypeOptions: function () {
|
var typeMap = {};
|
(this.configList || []).forEach(function (item) {
|
var type = item && item.selectType ? item.selectType : 'other';
|
typeMap[type] = true;
|
});
|
return this.sortTypes(Object.keys(typeMap));
|
},
|
deviceTypeCountMap: function () {
|
var result = {};
|
(this.deviceList || []).forEach(function (item) {
|
if (!item || !item.deviceType) {
|
return;
|
}
|
result[item.deviceType] = (result[item.deviceType] || 0) + 1;
|
});
|
return result;
|
},
|
fakeDeviceCount: function () {
|
var total = 0;
|
(this.deviceList || []).forEach(function (item) {
|
if (item && String(item.fake) === '1') {
|
total += 1;
|
}
|
});
|
return total;
|
},
|
changedCount: function () {
|
var self = this;
|
var total = 0;
|
Object.keys(this.codeMap || {}).forEach(function (code) {
|
if (self.isChanged(code)) {
|
total += 1;
|
}
|
});
|
return total;
|
},
|
hasChanges: function () {
|
return this.changedCount > 0;
|
},
|
summaryCards: function () {
|
return [
|
{
|
key: 'crn',
|
label: '堆垛机',
|
value: this.deviceTypeCountMap.Crn || 0,
|
hint: '当前已配置的单工位堆垛机数量'
|
},
|
{
|
key: 'dualCrn',
|
label: '双工位堆垛机',
|
value: this.deviceTypeCountMap.DualCrn || 0,
|
hint: '当前已配置的双工位堆垛机数量'
|
},
|
{
|
key: 'devp',
|
label: '输送站控制器',
|
value: this.deviceTypeCountMap.Devp || 0,
|
hint: '当前站台/输送线控制器数量'
|
},
|
{
|
key: 'rgv',
|
label: 'RGV',
|
value: this.deviceTypeCountMap.Rgv || 0,
|
hint: '当前 RGV 控制器数量'
|
},
|
{
|
key: 'fake',
|
label: '仿真设备',
|
value: this.fakeDeviceCount,
|
hint: 'device_config 中 fake=1 的设备数'
|
},
|
{
|
key: 'groups',
|
label: '配置分组',
|
value: this.selectTypeOptions.length,
|
hint: '来自 sys_config.select_type'
|
},
|
{
|
key: 'configs',
|
label: '参数总数',
|
value: this.configList.length,
|
hint: '当前页面纳入管理的 sys_config 数量'
|
},
|
{
|
key: 'changed',
|
label: '待保存变更',
|
value: this.changedCount,
|
hint: this.changedCount > 0 ? '存在尚未提交的参数改动' : '当前没有未保存的改动'
|
}
|
];
|
},
|
filteredGroups: function () {
|
var self = this;
|
var groups = {};
|
var keyword = (this.searchText || '').toLowerCase();
|
var list = Array.isArray(this.configList) ? this.configList.slice() : [];
|
list.sort(function (a, b) {
|
return (a.id || 0) - (b.id || 0);
|
});
|
|
list.forEach(function (item) {
|
if (!item || !item.code) {
|
return;
|
}
|
var type = item.selectType || 'other';
|
if (self.selectedType !== 'all' && self.selectedType !== type) {
|
return;
|
}
|
if (keyword) {
|
var text = ((item.name || '') + ' ' + item.code).toLowerCase();
|
if (text.indexOf(keyword) === -1) {
|
return;
|
}
|
}
|
if (self.changedOnly && !self.isChanged(item.code)) {
|
return;
|
}
|
if (!groups[type]) {
|
groups[type] = {
|
type: type,
|
label: self.getGroupLabel(type),
|
desc: self.getGroupDesc(type),
|
deviceText: self.getGroupDeviceText(type),
|
items: [],
|
changedCount: 0
|
};
|
}
|
groups[type].items.push(item);
|
if (self.isChanged(item.code)) {
|
groups[type].changedCount += 1;
|
}
|
});
|
|
return self.sortTypes(Object.keys(groups)).map(function (type) {
|
return groups[type];
|
});
|
}
|
},
|
created: function () {
|
this.reloadData();
|
},
|
methods: {
|
reloadData: function () {
|
var self = this;
|
var pending = 2;
|
var failed = false;
|
self.loading = true;
|
|
function finish() {
|
pending -= 1;
|
if (pending <= 0) {
|
self.loading = false;
|
if (failed) {
|
self.$message.error('部分数据加载失败');
|
}
|
}
|
}
|
|
$.ajax({
|
url: baseUrl + "/config/listAll/auth",
|
headers: self.authHeaders(),
|
dataType: 'json',
|
method: 'GET',
|
success: function (res) {
|
if (self.handleForbidden(res)) {
|
return;
|
}
|
if (!res || res.code !== 200 || !Array.isArray(res.data)) {
|
failed = true;
|
finish();
|
return;
|
}
|
self.configList = res.data.slice();
|
self.rebuildCodeMaps();
|
finish();
|
},
|
error: function () {
|
failed = true;
|
finish();
|
}
|
});
|
|
$.ajax({
|
url: baseUrl + "/deviceConfig/list/auth",
|
headers: self.authHeaders(),
|
dataType: 'json',
|
method: 'GET',
|
data: {
|
curr: 1,
|
limit: 200
|
},
|
success: function (res) {
|
if (self.handleForbidden(res)) {
|
return;
|
}
|
if (!res || res.code !== 200 || !res.data) {
|
failed = true;
|
finish();
|
return;
|
}
|
self.deviceList = Array.isArray(res.data.records) ? res.data.records : [];
|
finish();
|
},
|
error: function () {
|
failed = true;
|
finish();
|
}
|
});
|
},
|
rebuildCodeMaps: function () {
|
var current = {};
|
(this.configList || []).forEach(function (item) {
|
if (item && item.code) {
|
current[item.code] = item.value == null ? '' : String(item.value);
|
}
|
});
|
this.codeMap = $.extend({}, current);
|
this.originalCodeMap = $.extend({}, current);
|
},
|
authHeaders: function () {
|
return {
|
token: localStorage.getItem('token')
|
};
|
},
|
handleForbidden: function (res) {
|
if (res && res.code === 403) {
|
top.location.href = baseUrl + "/";
|
return true;
|
}
|
return false;
|
},
|
sortTypes: function (types) {
|
var self = this;
|
return (types || []).slice().sort(function (a, b) {
|
var aIndex = self.groupOrder.indexOf(a);
|
var bIndex = self.groupOrder.indexOf(b);
|
if (aIndex === -1) {
|
aIndex = self.groupOrder.length + 100;
|
}
|
if (bIndex === -1) {
|
bIndex = self.groupOrder.length + 100;
|
}
|
if (aIndex !== bIndex) {
|
return aIndex - bIndex;
|
}
|
return String(a).localeCompare(String(b));
|
});
|
},
|
getGroupLabel: function (type) {
|
var labels = {
|
system: '系统运行参数',
|
solver: '求解器与调度参数',
|
crn: '堆垛机控制参数',
|
fake: '仿真模式参数',
|
notify: '通知上报参数',
|
String: '站点路径参数',
|
other: '其他参数'
|
};
|
return labels[type] || type;
|
},
|
getGroupDesc: function (type) {
|
var descMap = {
|
system: '覆盖 WMS 联动、设备日志、输送线任务和监控地图等系统级参数。',
|
solver: '用于堆垛机调度与求解器执行,包括速度、权重、最大求解时长等。',
|
crn: '针对堆垛机控制链路的调试参数,优先用于异常回滚和执行保护。',
|
fake: '仿真开关与任务生成策略,适合联调或演示环境。',
|
notify: '外部消息通知、钉钉告警和 WMS 回调重试策略。',
|
String: '站点路径评分与默认模板选择,影响输送站点寻径结果。',
|
other: '当前未归类的配置项。'
|
};
|
return descMap[type] || '当前分组未配置说明。';
|
},
|
getGroupDeviceText: function (type) {
|
var crnCount = this.deviceTypeCountMap.Crn || 0;
|
var dualCrnCount = this.deviceTypeCountMap.DualCrn || 0;
|
var devpCount = this.deviceTypeCountMap.Devp || 0;
|
var rgvCount = this.deviceTypeCountMap.Rgv || 0;
|
if (type === 'system') {
|
return '设备范围:堆垛机 ' + crnCount + ' 台 / 双工位 ' + dualCrnCount + ' 台 / 输送站控制器 ' + devpCount + ' 台 / RGV ' + rgvCount + ' 台';
|
}
|
if (type === 'solver' || type === 'crn') {
|
return '关联堆垛机:单工位 ' + crnCount + ' 台 / 双工位 ' + dualCrnCount + ' 台';
|
}
|
if (type === 'fake') {
|
return '仿真设备:' + this.fakeDeviceCount + ' 台';
|
}
|
if (type === 'notify') {
|
return '外部链路:WMS / 钉钉 / 通知重试';
|
}
|
if (type === 'String') {
|
return '路径配置:输送站点评分与模板';
|
}
|
return '';
|
},
|
getValueTypeText: function (config) {
|
if (config && config.type === 2) {
|
return 'JSON';
|
}
|
if (this.resolveEditor(config) === 'boolean') {
|
return '布尔开关';
|
}
|
if (this.resolveEditor(config) === 'textarea') {
|
return '长文本';
|
}
|
return '字符串';
|
},
|
resolveEditor: function (config) {
|
var value = this.getCodeValue(config.code);
|
if (config && config.type === 2) {
|
return 'textarea';
|
}
|
if (this.isBooleanValue(value)) {
|
return 'boolean';
|
}
|
if ((config && /url|uri|path/i.test(config.code || '')) || String(value || '').length > 60) {
|
return 'textarea';
|
}
|
return 'input';
|
},
|
isBooleanValue: function (value) {
|
var text = value == null ? '' : String(value);
|
return text === 'Y' || text === 'N' || text === 'true' || text === 'false';
|
},
|
getBooleanOptions: function (config) {
|
var value = this.getCodeValue(config.code);
|
if (String(value) === 'Y' || String(value) === 'N') {
|
return [
|
{ label: '开', value: 'Y' },
|
{ label: '关', value: 'N' }
|
];
|
}
|
return [
|
{ label: '开', value: 'true' },
|
{ label: '关', value: 'false' }
|
];
|
},
|
getCodeValue: function (code) {
|
if (!code) {
|
return '';
|
}
|
return this.codeMap[code] == null ? '' : String(this.codeMap[code]);
|
},
|
isChanged: function (code) {
|
return this.getCodeValue(code) !== (this.originalCodeMap[code] == null ? '' : String(this.originalCodeMap[code]));
|
},
|
resetConfig: function (code) {
|
this.$set(this.codeMap, code, this.originalCodeMap[code] == null ? '' : String(this.originalCodeMap[code]));
|
},
|
collectChangedItems: function (items) {
|
var self = this;
|
return (items || []).filter(function (item) {
|
return item && item.code && self.isChanged(item.code);
|
});
|
},
|
validateItems: function (items) {
|
var error = '';
|
(items || []).some(function (item) {
|
if (!item) {
|
return false;
|
}
|
if (item.type === 2) {
|
try {
|
JSON.parse(item.value);
|
} catch (e) {
|
error = (item.name || item.code) + ' JSON 格式不正确';
|
return true;
|
}
|
}
|
return false;
|
});
|
return error;
|
},
|
doSave: function (items, saveKey) {
|
var self = this;
|
var changedItems = self.collectChangedItems(items);
|
if (changedItems.length === 0) {
|
self.$message({
|
message: '当前没有需要保存的变更',
|
type: 'info'
|
});
|
return;
|
}
|
|
var payload = changedItems.map(function (item) {
|
return {
|
id: item.id,
|
name: item.name,
|
code: item.code,
|
value: self.getCodeValue(item.code),
|
type: item.type,
|
status: item.status,
|
selectType: item.selectType
|
};
|
});
|
|
var validateMsg = self.validateItems(payload);
|
if (validateMsg) {
|
self.$message({
|
message: validateMsg,
|
type: 'error'
|
});
|
return;
|
}
|
|
if (saveKey === 'all') {
|
self.saveAllLoading = true;
|
} else {
|
self.$set(self.saveGroupLoading, saveKey, true);
|
}
|
|
$.ajax({
|
url: baseUrl + "/config/updateBatch",
|
headers: self.authHeaders(),
|
data: JSON.stringify(payload.map(function (item) {
|
return {
|
code: item.code,
|
value: item.value
|
};
|
})),
|
dataType: 'json',
|
contentType: 'application/json;charset=UTF-8',
|
method: 'POST',
|
success: function (res) {
|
if (saveKey === 'all') {
|
self.saveAllLoading = false;
|
} else {
|
self.$set(self.saveGroupLoading, saveKey, false);
|
}
|
if (self.handleForbidden(res)) {
|
return;
|
}
|
if (!res || res.code !== 200) {
|
self.$message({
|
message: res && res.msg ? res.msg : '保存失败',
|
type: 'error'
|
});
|
return;
|
}
|
payload.forEach(function (item) {
|
self.originalCodeMap[item.code] = item.value == null ? '' : String(item.value);
|
});
|
self.$message({
|
message: '保存成功,运行缓存已同步刷新',
|
type: 'success'
|
});
|
},
|
error: function () {
|
if (saveKey === 'all') {
|
self.saveAllLoading = false;
|
} else {
|
self.$set(self.saveGroupLoading, saveKey, false);
|
}
|
self.$message({
|
message: '保存失败',
|
type: 'error'
|
});
|
}
|
});
|
},
|
saveGroup: function (group) {
|
this.doSave(group.items, group.type);
|
},
|
saveAll: function () {
|
this.doSave(this.configList, 'all');
|
}
|
}
|
});
|
</script>
|
</body>
|
|
</html>
|