#
Junjie
昨天 223e200a69aff78811cd93de72f6053532f97d3c
#
2个文件已修改
1068 ■■■■ 已修改文件
src/main/java/com/zy/system/controller/ConfigController.java 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/debugParam/debugParam.html 1040 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/controller/ConfigController.java
@@ -31,12 +31,7 @@
    @RequestMapping(value = "/config/refreshCache")
    @ManagerAuth
    public R refreshCache(){
        HashMap<String, String> systemConfigMap = new HashMap<>();
        List<Config> configList = configService.list(new QueryWrapper<>());
        for (Config config : configList) {
            systemConfigMap.put(config.getCode(), config.getValue());
        }
        redisUtil.set(RedisKeyType.SYSTEM_CONFIG_MAP.key, systemConfigMap);
        refreshCacheData();
        return R.ok();
    }
@@ -85,6 +80,7 @@
    @RequestMapping(value = "/config/updateBatch")
    @ManagerAuth
    public R updateBatch(@RequestBody List<ConfigUpdateBatchParam> params){
        boolean updated = false;
        for (ConfigUpdateBatchParam param : params) {
            String code = param.getCode();
            String value = param.getValue();
@@ -92,9 +88,20 @@
            if (config == null) {
                continue;
            }
            if (Short.valueOf((short) 2).equals(config.getType()) && !checkJson(value)) {
                return R.error(config.getCode() + " json解析失败");
            }
            if (Objects.equals(config.getValue(), value)) {
                continue;
            }
            config.setValue(value);
            configService.updateById(config);
            updated = true;
        }
        if (updated) {
            Parameter.reset();
            refreshCacheData();
        }
        return R.ok();
    }
@@ -229,4 +236,13 @@
        return parse != null;
    }
    private void refreshCacheData() {
        HashMap<String, String> systemConfigMap = new HashMap<>();
        List<Config> configList = configService.list(new QueryWrapper<>());
        for (Config config : configList) {
            systemConfigMap.put(config.getCode(), config.getValue());
        }
        redisUtil.set(RedisKeyType.SYSTEM_CONFIG_MAP.key, systemConfigMap);
    }
}
src/main/webapp/views/debugParam/debugParam.html
@@ -1,299 +1,811 @@
<!DOCTYPE html>
<html lang="en">
<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>
            .show-box {
                width: 20%;
                display: flex;
                justify-content: flex-start;
                align-items: center;
                margin-bottom: 30px;
            }
        </style>
    </head>
<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;
        }
    <body>
        <div id="app">
            <div>
                <el-card class="box-card">
                    <div slot="header" class="clearfix">
                        <span>调度参数</span>
                    </div>
        * {
            box-sizing: border-box;
        }
                    <div style="display: flex;flex-wrap: wrap;">
                        <div class="show-box">
                            <div>调度小车同层最大数量</div>
                            <el-input v-model="codeMap.dispatchShuttleMaxNum" style="width: 60%;"></el-input>
                        </div>
        .page-shell {
            max-width: 1600px;
            margin: 0 auto;
        }
                        <div class="show-box">
                            <div>入库预留小车</div>
                            <el-input v-model="codeMap.shuttleWrkInObligateCount" style="width: 60%;"></el-input>
                        </div>
        .hero-card,
        .filter-card,
        .group-card {
            margin-bottom: 16px;
        }
                        <div class="show-box">
                            <div>避障内圈半径</div>
                            <el-input v-model="codeMap.avoidInnerCircle" style="width: 60%;"></el-input>
                        </div>
        .hero-header,
        .group-header,
        .filter-bar {
            display: flex;
            align-items: flex-start;
            justify-content: space-between;
            gap: 16px;
            flex-wrap: wrap;
        }
                        <div class="show-box">
                            <div>避障外圈半径</div>
                            <el-input v-model="codeMap.avoidOuterCircle" style="width: 60%;"></el-input>
                        </div>
        .hero-title {
            font-size: 20px;
            font-weight: 600;
            line-height: 1.2;
            margin: 0 0 4px;
        }
                        <div class="show-box">
                            <div>地图母轨方向(x,y)</div>
                            <el-input v-model="codeMap.direction_map" style="width: 60%;"></el-input>
                        </div>
        .hero-desc,
        .group-desc {
            margin: 0;
            color: #606266;
            line-height: 1.5;
        }
                        <div class="show-box">
                            <div>小车(x,y)命令运行方向颠倒</div>
                            <el-radio border v-model="codeMap.shuttleDirectionReverse" label="Y">开</el-radio>
                            <el-radio border v-model="codeMap.shuttleDirectionReverse" label="N">关</el-radio>
                        </div>
        .hero-actions,
        .group-actions,
        .row-actions,
        .group-tags {
            display: flex;
            align-items: center;
            gap: 8px;
            flex-wrap: wrap;
        }
                        <div class="show-box">
                            <div>小车出提升机近点距离</div>
                            <el-input v-model="codeMap.shuttleOutLiftLocationDistance" style="width: 60%;"></el-input>
                        </div>
        .summary-strip {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-top: 10px;
        }
                        <div class="show-box">
                            <div>小车移动连续下发指令</div>
                            <el-radio border v-model="codeMap.shuttleMoveCommandsContinuously" label="Y">开</el-radio>
                            <el-radio border v-model="codeMap.shuttleMoveCommandsContinuously" label="N">关</el-radio>
                        </div>
        .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;
        }
                        <div class="show-box">
                            <div>允许交管重新规划路径</div>
                            <el-radio border v-model="codeMap.trafficControlRestartCalcPath" label="Y">开</el-radio>
                            <el-radio border v-model="codeMap.trafficControlRestartCalcPath" label="N">关</el-radio>
                        </div>
        .summary-pill strong {
            font-size: 14px;
            color: #303133;
        }
                        <div class="show-box">
                            <div>输出RCS调试日志</div>
                            <el-radio border v-model="codeMap.wcsDebugShowLog" label="true">开</el-radio>
                            <el-radio border v-model="codeMap.wcsDebugShowLog" label="false">关</el-radio>
                        </div>
                    </div>
        .filter-item {
            min-width: 220px;
            flex: 1 1 260px;
        }
                    <div style="margin-top: 20px;">
                        <el-button type="primary" @click="saveParam('shuttle')">保存</el-button>
                    </div>
                </el-card>
            </div>
        .group-title {
            font-size: 18px;
            font-weight: 600;
            margin-bottom: 6px;
        }
            <div style="margin-top: 20px;">
                <el-card class="box-card">
                    <div slot="header" class="clearfix">
                        <span>充电参数</span>
                    </div>
        .code-cell {
            font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
            color: #606266;
            font-size: 12px;
            line-height: 1.6;
            word-break: break-all;
        }
                    <div style="display: flex;flex-wrap: wrap;">
                        <div class="show-box">
                            <div>小车充电最大阈值</div>
                            <el-input v-model="codeMap.chargeMaxValue" style="width: 60%;"></el-input>
                        </div>
        .config-name {
            display: flex;
            flex-direction: column;
            gap: 4px;
            line-height: 1.5;
        }
                        <div class="show-box">
                            <div>小车电量预警阈值</div>
                            <el-input v-model="codeMap.shuttlePowerEarlyValue" style="width: 60%;"></el-input>
                        </div>
        .config-name-main {
            font-weight: 600;
            color: #303133;
        }
                        <div class="show-box">
                            <div>小车满电校准</div>
                            <el-radio border v-model="codeMap.shuttleMaxPowerVerify" label="true">开</el-radio>
                            <el-radio border v-model="codeMap.shuttleMaxPowerVerify" label="false">关</el-radio>
                        </div>
        .config-name-sub {
            font-size: 12px;
            color: #909399;
        }
                        <div class="show-box">
                            <div>定时充电开关</div>
                            <el-radio border v-model="codeMap.timedCharge" label="Y">开</el-radio>
                            <el-radio border v-model="codeMap.timedCharge" label="N">关</el-radio>
                        </div>
        .editor-cell {
            min-width: 320px;
        }
                        <div class="show-box">
                            <div>定时充电时间段</div>
                            <el-input v-model="codeMap.timedChargeRange" style="width: 60%;"></el-input>
                        </div>
        .status-cell {
            display: flex;
            align-items: center;
            gap: 8px;
            flex-wrap: wrap;
        }
                        <div class="show-box">
                            <div>小车默认充电线</div>
                            <el-input v-model="codeMap.shuttleDefaultChargePowerLine" style="width: 60%;"></el-input>
                        </div>
        .empty-wrap {
            padding: 32px 0 8px;
            background: #fff;
            border-radius: 8px;
        }
                        <div class="show-box">
                            <div>小车定时充电线</div>
                            <el-input v-model="codeMap.timedChargePowerLine" style="width: 60%;"></el-input>
                        </div>
                    </div>
        .el-card__header {
            padding: 16px 20px;
        }
                    <div style="margin-top: 20px;">
                        <el-button type="primary" @click="saveParam('charge')">保存</el-button>
                    </div>
                </el-card>
            </div>
        .el-card__body {
            padding: 18px 20px;
        }
            <div style="margin-top: 20px;">
                <el-card class="box-card">
                    <div slot="header" class="clearfix">
                        <span>演示模式参数</span>
                    </div>
                    <div style="display: flex;flex-wrap: wrap;">
                        <div class="show-box">
                            <div>移动演示模式-楼层</div>
                            <el-input v-model="codeMap.demoRunLev" style="width: 60%;"></el-input>
                        </div>
        .hero-card .el-card__body {
            padding: 14px 18px;
        }
                        <div class="show-box">
                            <div>移动演示模式-是否换层</div>
                            <el-radio border v-model="codeMap.demoSwitchLev" label="Y">开</el-radio>
                            <el-radio border v-model="codeMap.demoSwitchLev" label="N">关</el-radio>
                        </div>
        @media (max-width: 900px) {
            body {
                padding: 12px;
            }
                        <div class="show-box">
                            <div>演示模式-货物搬运</div>
                            <el-radio border v-model="codeMap.demoCargoMove" label="Y">开</el-radio>
                            <el-radio border v-model="codeMap.demoCargoMove" label="N">关</el-radio>
                        </div>
            .editor-cell {
                min-width: 0;
            }
        }
    </style>
</head>
                        <div class="show-box">
                            <div>演示模式-跑库</div>
                            <el-radio border v-model="codeMap.demoModeRunLoc" label="Y">开</el-radio>
                            <el-radio border v-model="codeMap.demoModeRunLoc" label="N">关</el-radio>
                        </div>
<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>
        <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>
                    <div style="margin-top: 20px;">
                        <el-button type="primary" @click="saveParam('demo')">保存</el-button>
                    </div>
                </el-card>
            </div>
        </div>
        <script>
            var app = new Vue({
                el: '#app',
                data: {
                    codeMap: {},
                },
                created() {
                    this.init()
                },
                methods: {
                    init() {
                        this.getConfigData()
                    },
                    getConfigData() {
                        let that = this;
                        $.ajax({
                            url: baseUrl + "/config/listAll/auth",
                            headers: {
                                'token': localStorage.getItem('token')
                            },
                            data: {},
                            dataType: 'json',
                            contentType: 'application/json;charset=UTF-8',
                            method: 'GET',
                            success: function(res) {
                                if (res.code == 200) {
                                    let codeMap = {}
                                    res.data.forEach((item) => {
                                        codeMap[item.code] = item.value
                                    })
                                    that.codeMap = codeMap;
                                } else if (res.code === 403) {
                                    top.location.href = baseUrl + "/";
                                } else {
                                    that.$message({
                                        message: res.msg,
                                        type: 'error'
                                    });
                                }
                            }
                        });
                    },
                    saveParam(type) {
                        let that = this;
                        let codeList = this.getParamData(type)
                        let updateCodeList = [];
    <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>
                        codeList.forEach((key) => {
                            let value = this.codeMap[key]
                            updateCodeList.push({
                                code: key,
                                value: value
                            })
                        })
    <div v-if="filteredGroups.length === 0" class="empty-wrap">
        <el-empty description="当前筛选条件下没有可显示的参数"></el-empty>
    </div>
                        console.log(updateCodeList)
    <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>
                        $.ajax({
                            url: baseUrl + "/config/updateBatch",
                            headers: {
                                'token': localStorage.getItem('token')
                            },
                            data: JSON.stringify(updateCodeList),
                            dataType: 'json',
                            contentType: 'application/json;charset=UTF-8',
                            method: 'POST',
                            success: function(res) {
                                if (res.code == 200) {
                                    that.$message({
                                        message: '保存成功',
                                        type: 'success'
                                    });
                                } else if (res.code === 403) {
                                    top.location.href = baseUrl + "/";
                                } else {
                                    that.$message({
                                        message: res.msg,
                                        type: 'error'
                                    });
                                }
                            }
                        });
                    },
                    getParamData(type) {
                        let data = []
                        if(type == "shuttle") {
                            data.push('dispatchShuttleMaxNum');
                            data.push('shuttleWrkInObligateCount');
                            data.push('avoidInnerCircle');
                            data.push('avoidOuterCircle');
                            data.push('direction_map');
                            data.push('shuttleDirectionReverse');
                            data.push('shuttleOutLiftLocationDistance');
                            data.push('shuttleMoveCommandsContinuously');
                            data.push('trafficControlRestartCalcPath');
                            data.push('wcsDebugShowLog');
                        }else if (type == "charge") {
                            data.push('chargeMaxValue');
                            data.push('shuttlePowerEarlyValue');
                            data.push('shuttleMaxPowerVerify');
                            data.push('timedCharge');
                            data.push('timedChargeRange');
                            data.push('shuttleDefaultChargePowerLine');
                            data.push('timedChargePowerLine');
                        }else if (type == "demo") {
                            data.push('demoRunLev');
                            data.push('demoSwitchLev');
                            data.push('demoCargoMove');
                            data.push('demoModeRunLoc');
                        }
        <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>
                        return data;
                    },
                },
            })
        </script>
    </body>
            <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>