| New file |
| | |
| | | package com.zy.asrs.controller; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.core.annotations.ManagerAuth; |
| | | import com.core.common.R; |
| | | import com.zy.common.utils.RedisUtil; |
| | | import com.zy.core.enums.RedisKeyType; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.util.*; |
| | | |
| | | @RestController |
| | | @RequestMapping("/watch/stationColor") |
| | | public class WatchStationColorController { |
| | | |
| | | @Autowired |
| | | private RedisUtil redisUtil; |
| | | |
| | | @GetMapping("/config/auth") |
| | | @ManagerAuth |
| | | public R getConfig() { |
| | | return R.ok(buildResponseData(loadStoredColorMap())); |
| | | } |
| | | |
| | | @PostMapping("/config/save/auth") |
| | | @ManagerAuth |
| | | public R saveConfig(@RequestBody Map<String, Object> payload) { |
| | | Map<String, String> storedMap = loadStoredColorMap(); |
| | | Map<String, String> mergedMap = new LinkedHashMap<>(storedMap); |
| | | |
| | | Object itemsObj = payload == null ? null : payload.get("items"); |
| | | if (!(itemsObj instanceof List)) { |
| | | return R.error("请传入颜色配置列表"); |
| | | } |
| | | |
| | | List<?> items = (List<?>) itemsObj; |
| | | Set<String> allowedStatuses = getDefaultColorMap().keySet(); |
| | | for (Object obj : items) { |
| | | if (!(obj instanceof Map)) { |
| | | continue; |
| | | } |
| | | Map<?, ?> item = (Map<?, ?>) obj; |
| | | String status = item.get("status") == null ? null : String.valueOf(item.get("status")).trim(); |
| | | String color = item.get("color") == null ? null : String.valueOf(item.get("color")).trim(); |
| | | if (status == null || status.isEmpty() || !allowedStatuses.contains(status)) { |
| | | continue; |
| | | } |
| | | mergedMap.put(status, normalizeColor(color, getDefaultColorMap().get(status))); |
| | | } |
| | | |
| | | redisUtil.set(RedisKeyType.WATCH_STATION_COLOR_CONFIG.key, JSON.toJSONString(mergedMap)); |
| | | return R.ok(buildResponseData(mergedMap)); |
| | | } |
| | | |
| | | private Map<String, Object> buildResponseData(Map<String, String> storedMap) { |
| | | Map<String, String> defaults = getDefaultColorMap(); |
| | | List<Map<String, String>> items = new ArrayList<>(); |
| | | for (Map<String, String> meta : getColorMetaList()) { |
| | | String status = meta.get("status"); |
| | | Map<String, String> item = new LinkedHashMap<>(meta); |
| | | item.put("defaultColor", defaults.get(status)); |
| | | item.put("color", normalizeColor(storedMap.get(status), defaults.get(status))); |
| | | items.add(item); |
| | | } |
| | | Map<String, Object> data = new LinkedHashMap<>(); |
| | | data.put("items", items); |
| | | data.put("defaults", defaults); |
| | | return data; |
| | | } |
| | | |
| | | private Map<String, String> loadStoredColorMap() { |
| | | Map<String, String> result = new LinkedHashMap<>(getDefaultColorMap()); |
| | | Object object = redisUtil.get(RedisKeyType.WATCH_STATION_COLOR_CONFIG.key); |
| | | if (object == null) { |
| | | return result; |
| | | } |
| | | try { |
| | | JSONObject jsonObject; |
| | | if (object instanceof JSONObject) { |
| | | jsonObject = (JSONObject) object; |
| | | } else { |
| | | jsonObject = JSON.parseObject(String.valueOf(object)); |
| | | } |
| | | if (jsonObject == null) { |
| | | return result; |
| | | } |
| | | for (String status : getDefaultColorMap().keySet()) { |
| | | String color = jsonObject.getString(status); |
| | | if (color != null) { |
| | | result.put(status, normalizeColor(color, getDefaultColorMap().get(status))); |
| | | } |
| | | } |
| | | } catch (Exception ignore) { |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, String> getDefaultColorMap() { |
| | | Map<String, String> defaults = new LinkedHashMap<>(); |
| | | defaults.put("site-auto", "#78FF81"); |
| | | defaults.put("site-auto-run", "#FA51F6"); |
| | | defaults.put("site-auto-id", "#C4C400"); |
| | | defaults.put("site-auto-run-id", "#30BFFC"); |
| | | defaults.put("site-enable-in", "#18C7B8"); |
| | | defaults.put("site-unauto", "#B8B8B8"); |
| | | defaults.put("machine-pakin", "#30BFFC"); |
| | | defaults.put("machine-pakout", "#97B400"); |
| | | defaults.put("site-run-block", "#E69138"); |
| | | return defaults; |
| | | } |
| | | |
| | | private List<Map<String, String>> getColorMetaList() { |
| | | List<Map<String, String>> list = new ArrayList<>(); |
| | | list.add(buildMeta("site-auto", "自动", "站点自动待命时的颜色。")); |
| | | list.add(buildMeta("site-auto-run", "自动 + 有物", "站点自动运行且有物,但还没有工作号时的颜色。")); |
| | | list.add(buildMeta("site-auto-id", "自动 + 工作号", "站点自动且存在工作号,但当前无物时的颜色。")); |
| | | list.add(buildMeta("site-auto-run-id", "自动 + 有物 + 工作号", "普通运行中的站点颜色,未命中入库/出库范围时使用。")); |
| | | list.add(buildMeta("site-enable-in", "启动入库", "工作号为 9998 或站点带启动入库标记时的颜色。")); |
| | | list.add(buildMeta("machine-pakin", "入库任务", "工作号命中入库范围时的颜色。")); |
| | | list.add(buildMeta("machine-pakout", "出库任务", "工作号命中出库范围时的颜色。")); |
| | | list.add(buildMeta("site-run-block", "运行堵塞", "站点处于运行堵塞时的颜色。")); |
| | | list.add(buildMeta("site-unauto", "非自动", "站点非自动时的颜色。")); |
| | | return list; |
| | | } |
| | | |
| | | private Map<String, String> buildMeta(String status, String name, String desc) { |
| | | Map<String, String> map = new LinkedHashMap<>(); |
| | | map.put("status", status); |
| | | map.put("name", name); |
| | | map.put("desc", desc); |
| | | return map; |
| | | } |
| | | |
| | | private String normalizeColor(String color, String fallback) { |
| | | if (color == null) { |
| | | return fallback; |
| | | } |
| | | String value = color.trim(); |
| | | if (value.matches("^#[0-9a-fA-F]{6}$")) { |
| | | return value.toUpperCase(); |
| | | } |
| | | if (value.matches("^#[0-9a-fA-F]{3}$")) { |
| | | return ("#" + value.charAt(1) + value.charAt(1) + value.charAt(2) + value.charAt(2) + value.charAt(3) + value.charAt(3)).toUpperCase(); |
| | | } |
| | | if (value.matches("^0x[0-9a-fA-F]{6}$")) { |
| | | return ("#" + value.substring(2)).toUpperCase(); |
| | | } |
| | | return fallback; |
| | | } |
| | | } |
| | |
| | | CRN_OUT_TASK_COMPLETE_STATION_INFO("crn_out_task_complete_station_info_"), |
| | | |
| | | WATCH_CIRCLE_STATION_("watch_circle_station_"), |
| | | WATCH_STATION_COLOR_CONFIG("watch_station_color_config"), |
| | | STATION_CYCLE_LOAD_RESERVE("station_cycle_load_reserve"), |
| | | |
| | | CURRENT_CIRCLE_TASK_CRN_NO("current_circle_task_crn_no_"), |
| | |
| | | // 1. 首先查询是否有已完成的异步响应 |
| | | String response = wmsOperateUtils.queryAsyncInTaskResponse(barcode, stationIdVal); |
| | | |
| | | if (response != null) { |
| | | if (!Cools.isEmpty(response)) { |
| | | // 2. 有响应结果,处理响应 |
| | | if (response.equals("FAILED") || response.startsWith("ERROR:")) { |
| | | // 请求失败,重新发起异步请求 |
| | |
| | | // 1. 首先查询是否有已完成的异步响应 |
| | | String response = wmsOperateUtils.queryAsyncInTaskResponse(barcode, stationIdVal); |
| | | |
| | | if (response != null) { |
| | | if (!Cools.isEmpty(response)) { |
| | | // 2. 有响应结果,处理响应 |
| | | if (response.equals("FAILED") || response.startsWith("ERROR:")) { |
| | | // 请求失败,重新发起异步请求 |
| | |
| | | // 1. 首先查询是否有已完成的异步响应 |
| | | String response = wmsOperateUtils.queryAsyncInTaskResponse(barcode, stationIdVal); |
| | | |
| | | if (response != null) { |
| | | if (!Cools.isEmpty(response)) { |
| | | // 2. 有响应结果,处理响应 |
| | | if (response.equals("FAILED") || response.startsWith("ERROR:")) { |
| | | // 请求失败,重新发起异步请求 |
| | |
| | | .setTimeout(30, TimeUnit.SECONDS) |
| | | .build() |
| | | .doPost(); |
| | | if (response != null) { |
| | | if (!Cools.isEmpty(response)) { |
| | | JSONObject jsonObject = JSON.parseObject(response); |
| | | if (jsonObject.getInteger("code") == 200) { |
| | | result = 1; |
| | |
| | | # 系统版本信息 |
| | | app: |
| | | version: 1.0.4.7 |
| | | version: 1.0.4.8 |
| | | version-type: dev # prd 或 dev |
| | | |
| | | server: |
| | |
| | | Vue.component("devp-card", { |
| | | template: ` |
| | | <div> |
| | | <div style="display: flex;margin-bottom: 10px;"> |
| | | <div style="width: 100%;">输送监控</div> |
| | | <div style="width: 100%;text-align: right;display: flex;"><el-input size="mini" v-model="searchStationId" placeholder="请输入站号"></el-input><el-button @click="getDevpStateInfo" size="mini">查询</el-button></div> |
| | | <div class="mc-root"> |
| | | <div class="mc-toolbar"> |
| | | <div class="mc-title">输送监控</div> |
| | | <div class="mc-search"> |
| | | <input class="mc-input" v-model="searchStationId" placeholder="请输入站号" /> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="getDevpStateInfo">查询</button> |
| | | </div> |
| | | <div style="margin-bottom: 10px;" v-if="!readOnly"> |
| | | <div style="margin-bottom: 5px;"> |
| | | <el-button v-if="showControl" @click="openControl" size="mini">关闭控制中心</el-button> |
| | | <el-button v-else @click="openControl" size="mini">打开控制中心</el-button> |
| | | </div> |
| | | <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;"> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.stationId" placeholder="站号"></el-input></div> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.taskNo" placeholder="工作号"></el-input></div> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetStationId" placeholder="目标站"></el-input></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommand()" size="mini">下发</el-button></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="resetCommand()" size="mini">复位</el-button></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="!readOnly" class="mc-control-toggle"> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="openControl"> |
| | | {{ showControl ? '收起控制中心' : '打开控制中心' }} |
| | | </button> |
| | | </div> |
| | | |
| | | <div v-if="showControl" class="mc-control"> |
| | | <div class="mc-control-grid"> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">站号</span> |
| | | <input class="mc-input" v-model="controlParam.stationId" placeholder="例如 101" /> |
| | | </label> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">工作号</span> |
| | | <input class="mc-input" v-model="controlParam.taskNo" placeholder="输入工作号" /> |
| | | </label> |
| | | <label class="mc-field mc-span-2"> |
| | | <span class="mc-field-label">目标站</span> |
| | | <input class="mc-input" v-model="controlParam.targetStationId" placeholder="输入目标站号" /> |
| | | </label> |
| | | <div class="mc-action-row"> |
| | | <button type="button" class="mc-btn" @click="controlCommand">下发</button> |
| | | <button type="button" class="mc-btn mc-btn-soft" @click="resetCommand">复位</button> |
| | | </div> |
| | | </div> |
| | | <div style="max-height: 55vh; overflow:auto;"> |
| | | <el-collapse v-model="activeNames" accordion> |
| | | <el-collapse-item v-for="(item) in displayStationList" :name="item.stationId"> |
| | | <template slot="title"> |
| | | <div style="width: 100%;display: flex;"> |
| | | <div style="width: 50%;">{{ item.stationId }}站</div> |
| | | <div style="width: 50%;text-align: right;"> |
| | | <el-tag v-if="item.autoing" type="success" size="small">自动</el-tag> |
| | | <el-tag v-else type="warning" size="small">手动</el-tag> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <el-descriptions border direction="vertical"> |
| | | <el-descriptions-item label="编号">{{ item.stationId }}</el-descriptions-item> |
| | | <el-descriptions-item label="工作号">{{ item.taskNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="目标站">{{ item.targetStaNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="模式">{{ item.autoing ? '自动' : '手动' }}</el-descriptions-item> |
| | | <el-descriptions-item label="有物">{{ item.loading ? '有' : '无' }}</el-descriptions-item> |
| | | <el-descriptions-item label="可入">{{ item.inEnable ? 'Y' : 'N' }}</el-descriptions-item> |
| | | <el-descriptions-item label="可出">{{ item.outEnable ? 'Y' : 'N' }}</el-descriptions-item> |
| | | <el-descriptions-item label="空板信号">{{ item.emptyMk ? 'Y' : 'N' }}</el-descriptions-item> |
| | | <el-descriptions-item label="满板信号">{{ item.fullPlt ? 'Y' : 'N' }}</el-descriptions-item> |
| | | <el-descriptions-item label="运行阻塞">{{ item.runBlock ? 'Y' : 'N' }}</el-descriptions-item> |
| | | <el-descriptions-item label="启动入库">{{ item.enableIn ? 'Y' : 'N' }}</el-descriptions-item> |
| | | <el-descriptions-item label="托盘高度">{{ item.palletHeight }}</el-descriptions-item> |
| | | <el-descriptions-item label="条码"> |
| | | </div> |
| | | |
| | | <div class="mc-collapse"> |
| | | <div |
| | | v-for="item in displayStationList" |
| | | :key="item.stationId" |
| | | :class="['mc-item', { 'is-open': isActive(item.stationId) }]" |
| | | > |
| | | <button type="button" class="mc-head" @click="toggleItem(item)"> |
| | | <div class="mc-head-main"> |
| | | <div class="mc-head-title">{{ item.stationId }}站</div> |
| | | <div class="mc-head-subtitle">任务 {{ orDash(item.taskNo) }} | 目标站 {{ orDash(item.targetStaNo) }}</div> |
| | | </div> |
| | | <div class="mc-head-right"> |
| | | <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span> |
| | | <span class="mc-chevron">{{ isActive(item.stationId) ? '▾' : '▸' }}</span> |
| | | </div> |
| | | </button> |
| | | |
| | | <div v-if="isActive(item.stationId)" class="mc-body"> |
| | | <div class="mc-detail-grid"> |
| | | <div v-for="entry in buildDetailEntries(item)" :key="entry.label" class="mc-detail-cell"> |
| | | <div class="mc-detail-label">{{ entry.label }}</div> |
| | | <div v-if="entry.type === 'barcode'" class="mc-detail-value mc-code"> |
| | | <el-popover v-if="item.barcode" placement="top" width="460" trigger="hover"> |
| | | <div style="text-align: center;"> |
| | | <img |
| | |
| | | /> |
| | | <div style="margin-top: 4px; font-size: 12px; word-break: break-all;">{{ item.barcode }}</div> |
| | | </div> |
| | | <span slot="reference" @click.stop="handleBarcodeClick(item)" style="cursor: pointer; color: #409EFF;">{{ item.barcode }}</span> |
| | | <span |
| | | slot="reference" |
| | | @click.stop="handleBarcodeClick(item)" |
| | | style="cursor: pointer; color: #4677a4; font-weight: 600;" |
| | | >{{ entry.value }}</span> |
| | | </el-popover> |
| | | <span v-else @click.stop="handleBarcodeClick(item)" style="cursor: pointer; color: #409EFF;">-</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="重量">{{ item.weight }}</el-descriptions-item> |
| | | <el-descriptions-item label="任务可写区">{{ item.taskWriteIdx }}</el-descriptions-item> |
| | | <el-descriptions-item label="故障代码">{{ item.error }}</el-descriptions-item> |
| | | <el-descriptions-item label="故障信息">{{ item.errorMsg }}</el-descriptions-item> |
| | | <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-collapse-item> |
| | | </el-collapse> |
| | | <span |
| | | v-else |
| | | @click.stop="handleBarcodeClick(item)" |
| | | style="cursor: pointer; color: #4677a4; font-weight: 600;" |
| | | >{{ entry.value }}</span> |
| | | </div> |
| | | <div v-else class="mc-detail-value" :class="{ 'mc-code': entry.code }">{{ entry.value }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div style="display:flex; justify-content:flex-end; margin-top:8px;"> |
| | | <el-pagination |
| | | small |
| | | @current-change="handlePageChange" |
| | | @size-change="handleSizeChange" |
| | | :current-page="currentPage" |
| | | :page-size="pageSize" |
| | | :page-sizes="[10,20,50,100]" |
| | | layout="total, prev, pager, next" |
| | | :total="stationList.length"> |
| | | </el-pagination> |
| | | </div> |
| | | |
| | | <div v-if="displayStationList.length === 0" class="mc-empty">当前没有可展示的站点数据</div> |
| | | </div> |
| | | |
| | | <div class="mc-footer"> |
| | | <button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">上一页</button> |
| | | <span>{{ currentPage }} / {{ totalPages }}</span> |
| | | <button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">下一页</button> |
| | | </div> |
| | | </div> |
| | | `, |
| | | `, |
| | | props: { |
| | | param: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | }, |
| | | autoRefresh: { |
| | | type: Boolean, |
| | | default: true |
| | | }, |
| | | readOnly: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | param: { type: Object, default: function () { return {}; } }, |
| | | items: { type: Array, default: null }, |
| | | autoRefresh: { type: Boolean, default: true }, |
| | | readOnly: { type: Boolean, default: false } |
| | | }, |
| | | data() { |
| | | data: function () { |
| | | return { |
| | | stationList: [], |
| | | fullStationList: [], |
| | | activeNames: "", |
| | | searchStationId: "", |
| | | showControl: false, |
| | | controlParam: { |
| | | stationId: "", |
| | | taskNo: "", |
| | | targetStationId: "", |
| | | targetStationId: "" |
| | | }, |
| | | barcodePreviewCache: {}, |
| | | pageSize: 25, |
| | | pageSize: 12, |
| | | currentPage: 1, |
| | | timer: null |
| | | }; |
| | | }, |
| | | created() { |
| | | if (this.autoRefresh) { |
| | | this.timer = setInterval(() => { |
| | | this.getDevpStateInfo(); |
| | | }, 1000); |
| | | } |
| | | }, |
| | | beforeDestroy() { |
| | | if (this.timer) { |
| | | clearInterval(this.timer); |
| | | } |
| | | }, |
| | | computed: { |
| | | displayStationList() { |
| | | const start = (this.currentPage - 1) * this.pageSize; |
| | | const end = start + this.pageSize; |
| | | return this.stationList.slice(start, end); |
| | | sourceList: function () { |
| | | return Array.isArray(this.items) ? this.items : this.stationList; |
| | | }, |
| | | filteredStationList: function () { |
| | | var keyword = String(this.searchStationId || "").trim(); |
| | | if (!keyword) { |
| | | return this.sourceList; |
| | | } |
| | | return this.sourceList.filter(function (item) { |
| | | return String(item.stationId) === keyword; |
| | | }); |
| | | }, |
| | | displayStationList: function () { |
| | | var start = (this.currentPage - 1) * this.pageSize; |
| | | return this.filteredStationList.slice(start, start + this.pageSize); |
| | | }, |
| | | totalPages: function () { |
| | | return Math.max(1, Math.ceil(this.filteredStationList.length / this.pageSize) || 1); |
| | | } |
| | | }, |
| | | watch: { |
| | | param: { |
| | | handler(newVal, oldVal) { |
| | | if (newVal && newVal.stationId && newVal.stationId != 0) { |
| | | this.activeNames = newVal.stationId; |
| | | this.searchStationId = newVal.stationId; |
| | | } |
| | | }, |
| | | deep: true, // 深度监听嵌套属性 |
| | | immediate: true, // 立即触发一次(可选) |
| | | items: function () { |
| | | this.afterDataRefresh(); |
| | | }, |
| | | param: { |
| | | deep: true, |
| | | immediate: true, |
| | | handler: function (newVal) { |
| | | if (newVal && newVal.stationId && newVal.stationId !== 0) { |
| | | this.focusStation(newVal.stationId); |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | created: function () { |
| | | MonitorCardKit.ensureStyles(); |
| | | if (this.autoRefresh) { |
| | | this.timer = setInterval(this.getDevpStateInfo, 1000); |
| | | } |
| | | }, |
| | | beforeDestroy: function () { |
| | | if (this.timer) { |
| | | clearInterval(this.timer); |
| | | this.timer = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | handlePageChange(page) { |
| | | orDash: function (value) { |
| | | return MonitorCardKit.orDash(value); |
| | | }, |
| | | getStatusLabel: function (item) { |
| | | return item && item.autoing ? "自动" : "手动"; |
| | | }, |
| | | getStatusTone: function (item) { |
| | | return MonitorCardKit.statusTone(this.getStatusLabel(item)); |
| | | }, |
| | | isActive: function (stationId) { |
| | | return String(this.activeNames) === String(stationId); |
| | | }, |
| | | toggleItem: function (item) { |
| | | var next = String(item.stationId); |
| | | this.activeNames = this.activeNames === next ? "" : next; |
| | | }, |
| | | focusStation: function (stationId) { |
| | | this.searchStationId = String(stationId); |
| | | var index = this.filteredStationList.findIndex(function (item) { |
| | | return String(item.stationId) === String(stationId); |
| | | }); |
| | | this.currentPage = index >= 0 ? Math.floor(index / this.pageSize) + 1 : 1; |
| | | this.activeNames = String(stationId); |
| | | }, |
| | | afterDataRefresh: function () { |
| | | if (this.currentPage > this.totalPages) { |
| | | this.currentPage = this.totalPages; |
| | | } |
| | | if (this.activeNames) { |
| | | var exists = this.filteredStationList.some(function (item) { |
| | | return String(item.stationId) === String(this.activeNames); |
| | | }, this); |
| | | if (!exists) { |
| | | this.activeNames = ""; |
| | | } |
| | | } |
| | | }, |
| | | handlePageChange: function (page) { |
| | | if (page < 1 || page > this.totalPages) { |
| | | return; |
| | | } |
| | | this.currentPage = page; |
| | | }, |
| | | handleSizeChange(size) { |
| | | this.pageSize = size; |
| | | this.currentPage = 1; |
| | | getDevpStateInfo: function () { |
| | | if (this.$root && this.$root.sendWs) { |
| | | this.$root.sendWs(JSON.stringify({ |
| | | url: "/console/latest/data/station", |
| | | data: {} |
| | | })); |
| | | } |
| | | }, |
| | | getBarcodePreview(barcode) { |
| | | const value = String(barcode || "").trim(); |
| | | setStationList: function (res) { |
| | | if (res && res.code === 200) { |
| | | this.stationList = res.data || []; |
| | | this.afterDataRefresh(); |
| | | } |
| | | }, |
| | | openControl: function () { |
| | | this.showControl = !this.showControl; |
| | | }, |
| | | buildDetailEntries: function (item) { |
| | | return [ |
| | | { label: "编号", value: this.orDash(item.stationId) }, |
| | | { label: "工作号", value: this.orDash(item.taskNo) }, |
| | | { label: "目标站", value: this.orDash(item.targetStaNo) }, |
| | | { label: "模式", value: item.autoing ? "自动" : "手动" }, |
| | | { label: "有物", value: MonitorCardKit.yesNo(item.loading) }, |
| | | { label: "可入", value: MonitorCardKit.yesNo(item.inEnable) }, |
| | | { label: "可出", value: MonitorCardKit.yesNo(item.outEnable) }, |
| | | { label: "空板信号", value: MonitorCardKit.yesNo(item.emptyMk) }, |
| | | { label: "满板信号", value: MonitorCardKit.yesNo(item.fullPlt) }, |
| | | { label: "运行阻塞", value: MonitorCardKit.yesNo(item.runBlock) }, |
| | | { label: "启动入库", value: MonitorCardKit.yesNo(item.enableIn) }, |
| | | { label: "托盘高度", value: this.orDash(item.palletHeight) }, |
| | | { label: "条码", value: this.orDash(item.barcode), code: true, type: "barcode" }, |
| | | { label: "重量", value: this.orDash(item.weight) }, |
| | | { label: "任务可写区", value: this.orDash(item.taskWriteIdx) }, |
| | | { label: "故障代码", value: this.orDash(item.error) }, |
| | | { label: "故障信息", value: this.orDash(item.errorMsg) }, |
| | | { label: "扩展数据", value: this.orDash(item.extend) } |
| | | ]; |
| | | }, |
| | | postControl: function (url, payload) { |
| | | $.ajax({ |
| | | url: baseUrl + url, |
| | | headers: { |
| | | token: localStorage.getItem("token") |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(payload), |
| | | success: function (res) { |
| | | if (res && res.code === 200) { |
| | | MonitorCardKit.showMessage(this, res.msg || "操作成功", "success"); |
| | | } else { |
| | | MonitorCardKit.showMessage(this, (res && res.msg) || "操作失败", "warning"); |
| | | } |
| | | }.bind(this) |
| | | }); |
| | | }, |
| | | handleBarcodeClick: function (item) { |
| | | if (this.readOnly || !item || item.stationId == null) { |
| | | return; |
| | | } |
| | | this.$prompt("请输入新的条码值(可留空清空)", "修改条码", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | inputValue: item.barcode || "", |
| | | inputPlaceholder: "请输入条码" |
| | | }).then(function (result) { |
| | | this.updateStationBarcode(item.stationId, result && result.value == null ? "" : String(result.value).trim()); |
| | | }.bind(this)).catch(function () {}); |
| | | }, |
| | | updateStationBarcode: function (stationId, barcode) { |
| | | $.ajax({ |
| | | url: baseUrl + "/station/command/barcode", |
| | | headers: { |
| | | token: localStorage.getItem("token") |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify({ |
| | | stationId: stationId, |
| | | barcode: barcode |
| | | }), |
| | | success: function (res) { |
| | | if (res && res.code === 200) { |
| | | this.syncLocalBarcode(stationId, barcode); |
| | | MonitorCardKit.showMessage(this, "条码修改成功", "success"); |
| | | } else { |
| | | MonitorCardKit.showMessage(this, (res && res.msg) || "条码修改失败", "warning"); |
| | | } |
| | | }.bind(this) |
| | | }); |
| | | }, |
| | | syncLocalBarcode: function (stationId, barcode) { |
| | | var update = function (list) { |
| | | if (!list || !list.length) { |
| | | return; |
| | | } |
| | | list.forEach(function (item) { |
| | | if (item.stationId == stationId) { |
| | | item.barcode = barcode; |
| | | } |
| | | }); |
| | | }; |
| | | update(this.stationList); |
| | | if (Array.isArray(this.items)) { |
| | | update(this.items); |
| | | } |
| | | }, |
| | | getBarcodePreview: function (barcode) { |
| | | var value = String(barcode || "").trim(); |
| | | if (!value) { |
| | | return ""; |
| | | } |
| | | if (this.barcodePreviewCache[value]) { |
| | | return this.barcodePreviewCache[value]; |
| | | } |
| | | const encodeResult = this.encodeCode128(value); |
| | | var encodeResult = this.encodeCode128(value); |
| | | if (!encodeResult) { |
| | | return ""; |
| | | } |
| | | const svg = this.buildCode128Svg(encodeResult, value); |
| | | const dataUrl = "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg); |
| | | var svg = this.buildCode128Svg(encodeResult, value); |
| | | var dataUrl = "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg); |
| | | this.$set(this.barcodePreviewCache, value, dataUrl); |
| | | return dataUrl; |
| | | }, |
| | | encodeCode128(value) { |
| | | encodeCode128: function (value) { |
| | | if (!value) { |
| | | return null; |
| | | } |
| | | const isNumeric = /^\d+$/.test(value); |
| | | var isNumeric = /^\d+$/.test(value); |
| | | if (isNumeric && value.length % 2 === 0) { |
| | | return this.encodeCode128C(value); |
| | | } |
| | | return this.encodeCode128B(value); |
| | | }, |
| | | encodeCode128B(value) { |
| | | const codes = [104]; |
| | | for (let i = 0; i < value.length; i++) { |
| | | const code = value.charCodeAt(i) - 32; |
| | | encodeCode128B: function (value) { |
| | | var codes = [104]; |
| | | for (var i = 0; i < value.length; i++) { |
| | | var code = value.charCodeAt(i) - 32; |
| | | if (code < 0 || code > 94) { |
| | | return null; |
| | | } |
| | |
| | | } |
| | | return this.buildCode128Pattern(codes); |
| | | }, |
| | | encodeCode128C(value) { |
| | | encodeCode128C: function (value) { |
| | | if (value.length % 2 !== 0) { |
| | | return null; |
| | | } |
| | | const codes = [105]; |
| | | for (let i = 0; i < value.length; i += 2) { |
| | | var codes = [105]; |
| | | for (var i = 0; i < value.length; i += 2) { |
| | | codes.push(parseInt(value.substring(i, i + 2), 10)); |
| | | } |
| | | return this.buildCode128Pattern(codes); |
| | | }, |
| | | buildCode128Pattern(codes) { |
| | | const patterns = this.getCode128Patterns(); |
| | | let checksum = codes[0]; |
| | | for (let i = 1; i < codes.length; i++) { |
| | | buildCode128Pattern: function (codes) { |
| | | var patterns = this.getCode128Patterns(); |
| | | var checksum = codes[0]; |
| | | for (var i = 1; i < codes.length; i++) { |
| | | checksum += codes[i] * i; |
| | | } |
| | | const checkCode = checksum % 103; |
| | | const fullCodes = codes.concat([checkCode, 106]); |
| | | let bars = ""; |
| | | for (let i = 0; i < fullCodes.length; i++) { |
| | | const code = fullCodes[i]; |
| | | if (patterns[code] == null) { |
| | | var checkCode = checksum % 103; |
| | | var fullCodes = codes.concat([checkCode, 106]); |
| | | var bars = ""; |
| | | for (var j = 0; j < fullCodes.length; j++) { |
| | | if (patterns[fullCodes[j]] == null) { |
| | | return null; |
| | | } |
| | | bars += patterns[code]; |
| | | bars += patterns[fullCodes[j]]; |
| | | } |
| | | bars += "11"; |
| | | return bars; |
| | | }, |
| | | buildCode128Svg(bars, text) { |
| | | const quietModules = 20; |
| | | const modules = quietModules * 2 + bars.split("").reduce((sum, n) => sum + parseInt(n, 10), 0); |
| | | const moduleWidth = modules > 300 ? 1 : 2; |
| | | const width = modules * moduleWidth; |
| | | const barTop = 10; |
| | | const barHeight = 110; |
| | | let x = quietModules * moduleWidth; |
| | | let black = true; |
| | | let rects = ""; |
| | | for (let i = 0; i < bars.length; i++) { |
| | | const w = parseInt(bars[i], 10) * moduleWidth; |
| | | buildCode128Svg: function (bars, text) { |
| | | var quietModules = 20; |
| | | var modules = quietModules * 2 + bars.split("").reduce(function (sum, n) { |
| | | return sum + parseInt(n, 10); |
| | | }, 0); |
| | | var moduleWidth = modules > 300 ? 1 : 2; |
| | | var width = modules * moduleWidth; |
| | | var barTop = 10; |
| | | var barHeight = 110; |
| | | var x = quietModules * moduleWidth; |
| | | var black = true; |
| | | var rects = ""; |
| | | for (var i = 0; i < bars.length; i++) { |
| | | var w = parseInt(bars[i], 10) * moduleWidth; |
| | | if (black) { |
| | | rects += '<rect x="' + x + '" y="' + barTop + '" width="' + w + '" height="' + barHeight + '" fill="#000" shape-rendering="crispEdges" />'; |
| | | } |
| | | x += w; |
| | | black = !black; |
| | | } |
| | | return ( |
| | | '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="145" viewBox="0 0 ' + width + ' 145">' + |
| | | return '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="145" viewBox="0 0 ' + width + ' 145">' + |
| | | '<rect width="100%" height="100%" fill="#fff" />' + |
| | | rects + |
| | | '<text x="' + (width / 2) + '" y="136" text-anchor="middle" font-family="monospace" font-size="14" fill="#111">' + |
| | | this.escapeXml(text) + |
| | | "</text>" + |
| | | "</svg>" |
| | | ); |
| | | "</text></svg>"; |
| | | }, |
| | | getCode128Patterns() { |
| | | getCode128Patterns: function () { |
| | | return [ |
| | | "212222", "222122", "222221", "121223", "121322", "131222", "122213", "122312", "132212", "221213", |
| | | "221312", "231212", "112232", "122132", "122231", "113222", "123122", "123221", "223211", "221132", |
| | |
| | | "114131", "311141", "411131", "211412", "211214", "211232", "2331112" |
| | | ]; |
| | | }, |
| | | escapeXml(text) { |
| | | escapeXml: function (text) { |
| | | return String(text) |
| | | .replace(/&/g, "&") |
| | | .replace(/</g, "<") |
| | |
| | | .replace(/"/g, """) |
| | | .replace(/'/g, "'"); |
| | | }, |
| | | getDevpStateInfo() { |
| | | if (this.readOnly) { |
| | | // Frontend filtering for readOnly mode |
| | | if (this.searchStationId == "") { |
| | | this.stationList = this.fullStationList; |
| | | } else { |
| | | this.stationList = this.fullStationList.filter(item => item.stationId == this.searchStationId); |
| | | this.currentPage = 1; |
| | | } |
| | | } else if (this.$root.sendWs) { |
| | | this.$root.sendWs(JSON.stringify({ |
| | | "url": "/console/latest/data/station", |
| | | "data": {} |
| | | })); |
| | | } |
| | | controlCommand: function () { |
| | | this.postControl("/station/command/move", this.controlParam); |
| | | }, |
| | | setStationList(res) { |
| | | let that = this; |
| | | if (res.code == 200) { |
| | | let list = res.data; |
| | | that.fullStationList = list; |
| | | if (that.searchStationId == "") { |
| | | that.stationList = list; |
| | | } else { |
| | | let tmp = []; |
| | | list.forEach((item) => { |
| | | if (item.stationId == that.searchStationId) { |
| | | tmp.push(item); |
| | | } |
| | | }); |
| | | that.stationList = tmp; |
| | | that.currentPage = 1; |
| | | } |
| | | } |
| | | }, |
| | | handleBarcodeClick(item) { |
| | | if (this.readOnly || !item || item.stationId == null) { |
| | | return; |
| | | } |
| | | |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/openapi/getFakeSystemRunStatus", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | method: "get", |
| | | success: (res) => { |
| | | if (res.code !== 200 || !res.data || !res.data.isFake || !res.data.running) { |
| | | that.$message({ |
| | | message: "仅仿真模式运行中可修改条码", |
| | | type: "warning", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | that.$prompt("请输入新的条码值(可留空清空)", "修改条码", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | inputValue: item.barcode || "", |
| | | inputPlaceholder: "请输入条码", |
| | | }).then(({ value }) => { |
| | | that.updateStationBarcode(item.stationId, value == null ? "" : String(value).trim()); |
| | | }).catch(() => {}); |
| | | }, |
| | | }); |
| | | }, |
| | | updateStationBarcode(stationId, barcode) { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/station/command/barcode", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify({ |
| | | stationId: stationId, |
| | | barcode: barcode, |
| | | }), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.syncLocalBarcode(stationId, barcode); |
| | | that.$message({ |
| | | message: "条码修改成功", |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg || "条码修改失败", |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }, |
| | | syncLocalBarcode(stationId, barcode) { |
| | | let updateFn = (list) => { |
| | | if (!list || list.length === 0) { |
| | | return; |
| | | } |
| | | list.forEach((row) => { |
| | | if (row.stationId == stationId) { |
| | | row.barcode = barcode; |
| | | } |
| | | }); |
| | | }; |
| | | updateFn(this.stationList); |
| | | updateFn(this.fullStationList); |
| | | }, |
| | | openControl() { |
| | | this.showControl = !this.showControl; |
| | | }, |
| | | controlCommand() { |
| | | let that = this; |
| | | //下发命令 |
| | | $.ajax({ |
| | | url: baseUrl + "/station/command/move", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }, |
| | | resetCommand() { |
| | | let that = this; |
| | | //下发命令 |
| | | $.ajax({ |
| | | url: baseUrl + "/station/command/reset", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }, |
| | | }, |
| | | resetCommand: function () { |
| | | this.postControl("/station/command/reset", this.controlParam); |
| | | } |
| | | } |
| | | }); |
| | |
| | | template: ` |
| | | <div style="width: 100%; height: 100%; position: relative;"> |
| | | <div ref="pixiView" style="position: absolute; inset: 0;"></div> |
| | | <div style="position: absolute; top: 12px; left: 14px; z-index: 30; pointer-events: none; max-width: 52%;"> |
| | | <div :style="cycleCapacityPanelStyle()"> |
| | | <div style="display: flex; flex-direction: column; gap: 6px; align-items: flex-start;"> |
| | | <div v-for="item in cycleCapacity.loopList" |
| | | :key="'loop-' + item.loopNo" |
| | |
| | | <div :style="mapToolFpsStyle()">FPS {{ mapFps }}</div> |
| | | <button type="button" @click="toggleMapToolPanel" :style="mapToolToggleStyle(showMapToolPanel)">{{ showMapToolPanel ? '收起操作' : '地图操作' }}</button> |
| | | <div v-show="showMapToolPanel" :style="mapToolBarStyle()"> |
| | | <button type="button" @click="toggleStationDirection" :style="mapToolButtonStyle(showStationDirection)">{{ showStationDirection ? '隐藏站点方向' : '显示站点方向' }}</button> |
| | | <button type="button" @click="rotateMap" :style="mapToolButtonStyle(false)">旋转</button> |
| | | <button type="button" @click="toggleMirror" :style="mapToolButtonStyle(mapMirrorX)">{{ mapMirrorX ? '取消镜像' : '镜像' }}</button> |
| | | <div :style="mapToolRowStyle()"> |
| | | <button type="button" @click="toggleStationDirection" :style="mapToolButtonStyle(showStationDirection)">{{ showStationDirection ? '隐藏站点方向' : '显示站点方向' }}</button> |
| | | <button type="button" @click="rotateMap" :style="mapToolButtonStyle(false)">旋转</button> |
| | | <button type="button" @click="toggleMirror" :style="mapToolButtonStyle(mapMirrorX)">{{ mapMirrorX ? '取消镜像' : '镜像' }}</button> |
| | | </div> |
| | | <div :style="mapToolRowStyle()"> |
| | | <button type="button" @click="openStationColorConfigPage" :style="mapToolButtonStyle(false)">站点颜色</button> |
| | | </div> |
| | | <div v-if="levList && levList.length > 1" :style="mapToolFloorSectionStyle()"> |
| | | <div :style="mapToolSectionLabelStyle()">楼层</div> |
| | | <div :style="mapToolFloorListStyle()"> |
| | | <button |
| | | v-for="floor in levList" |
| | | :key="'tool-floor-' + floor" |
| | | type="button" |
| | | @click="selectFloorFromTool(floor)" |
| | | :style="mapToolFloorButtonStyle(currentLev == floor)" |
| | | >{{ floor }}F</button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | `, |
| | | props: ['lev', 'crnParam', 'rgvParam', 'devpParam', 'highlightOnParamChange'], |
| | | props: ['lev', 'levList', 'crnParam', 'rgvParam', 'devpParam', 'stationTaskRange', 'highlightOnParamChange', 'viewportPadding', 'hudPadding'], |
| | | data() { |
| | | return { |
| | | map: [], |
| | |
| | | pixiDevpTextureMap: new Map(), |
| | | pixiCrnColorTextureMap: new Map(), |
| | | pixiRgvColorTextureMap: new Map(), |
| | | shelfChunkList: [], |
| | | shelfChunkSize: 2048, |
| | | shelfCullPadding: 160, |
| | | shelfCullRaf: null, |
| | | crnList: [], |
| | | dualCrnList: [], |
| | | rgvList: [], |
| | |
| | | hoverLoopNo: null, |
| | | hoverLoopStationIdSet: new Set(), |
| | | loopHighlightColor: 0xfff34d, |
| | | stationDirectionColor: 0xff5a36 |
| | | stationDirectionColor: 0xff5a36, |
| | | stationStatusColors: { |
| | | 'site-auto': 0x78ff81, |
| | | 'site-auto-run': 0xfa51f6, |
| | | 'site-auto-id': 0xc4c400, |
| | | 'site-auto-run-id': 0x30bffc, |
| | | 'site-enable-in': 0x18c7b8, |
| | | 'site-unauto': 0xb8b8b8, |
| | | 'machine-pakin': 0x30bffc, |
| | | 'machine-pakout': 0x97b400, |
| | | 'site-run-block': 0xe69138 |
| | | } |
| | | } |
| | | }, |
| | | mounted() { |
| | |
| | | this.createMap(); |
| | | this.startContainerResizeObserve(); |
| | | this.loadMapTransformConfig(); |
| | | this.loadStationColorConfig(); |
| | | this.loadLocList(); |
| | | this.connectWs(); |
| | | |
| | |
| | | if (this.timer) { clearInterval(this.timer); } |
| | | |
| | | if (this.hoverRaf) { cancelAnimationFrame(this.hoverRaf); this.hoverRaf = null; } |
| | | if (this.shelfCullRaf) { cancelAnimationFrame(this.shelfCullRaf); this.shelfCullRaf = null; } |
| | | if (window.gsap && this.pixiApp && this.pixiApp.stage) { window.gsap.killTweensOf(this.pixiApp.stage.position); } |
| | | if (this.pixiApp) { this.pixiApp.destroy(true, { children: true }); } |
| | | if (this.containerResizeObserver) { this.containerResizeObserver.disconnect(); this.containerResizeObserver = null; } |
| | | window.removeEventListener('resize', this.resizeToContainer); |
| | |
| | | watch: { |
| | | lev(newLev) { |
| | | if (newLev != null) { this.changeFloor(newLev); } |
| | | }, |
| | | viewportPadding: { |
| | | deep: true, |
| | | handler(newVal, oldVal) { |
| | | if (this.mapContentSize && this.mapContentSize.width > 0 && this.mapContentSize.height > 0) { |
| | | this.adjustStageForViewportPadding(oldVal, newVal); |
| | | } |
| | | } |
| | | }, |
| | | crnParam: { |
| | | deep: true, |
| | |
| | | } |
| | | }, |
| | | methods: { |
| | | cycleCapacityPanelStyle() { |
| | | const hud = this.hudPadding || {}; |
| | | const left = Math.max(14, Number(hud.left) || 0); |
| | | const rightReserve = 220; |
| | | return { |
| | | position: 'absolute', |
| | | top: '12px', |
| | | left: left + 'px', |
| | | zIndex: 30, |
| | | pointerEvents: 'none', |
| | | maxWidth: 'calc(100% - ' + (left + rightReserve) + 'px)' |
| | | }; |
| | | }, |
| | | mapToolBarStyle() { |
| | | return { |
| | | display: 'flex', |
| | | flexDirection: 'column', |
| | | gap: '8px', |
| | | alignItems: 'center', |
| | | alignItems: 'stretch', |
| | | padding: '7px', |
| | | borderRadius: '14px', |
| | | background: 'rgba(255, 255, 255, 0.72)', |
| | | border: '1px solid rgba(160, 180, 205, 0.3)', |
| | | boxShadow: '0 8px 20px rgba(37, 64, 97, 0.08)', |
| | | backdropFilter: 'blur(4px)' |
| | | }; |
| | | }, |
| | | mapToolRowStyle() { |
| | | return { |
| | | display: 'flex', |
| | | gap: '8px', |
| | | alignItems: 'center', |
| | | justifyContent: 'flex-end', |
| | | flexWrap: 'wrap' |
| | | }; |
| | | }, |
| | | mapToolFloorSectionStyle() { |
| | | return { |
| | | display: 'flex', |
| | | flexDirection: 'column', |
| | | gap: '4px', |
| | | paddingTop: '6px', |
| | | borderTop: '1px solid rgba(160, 180, 205, 0.22)' |
| | | }; |
| | | }, |
| | | mapToolSectionLabelStyle() { |
| | | return { |
| | | color: '#6a7f95', |
| | | fontSize: '10px', |
| | | lineHeight: '14px', |
| | | textAlign: 'right' |
| | | }; |
| | | }, |
| | | mapToolFloorListStyle() { |
| | | return { |
| | | display: 'flex', |
| | | flexDirection: 'column', |
| | | gap: '4px', |
| | | alignItems: 'stretch' |
| | | }; |
| | | }, |
| | | mapToolFpsStyle() { |
| | |
| | | whiteSpace: 'nowrap' |
| | | }; |
| | | }, |
| | | mapToolFloorButtonStyle(active) { |
| | | return { |
| | | appearance: 'none', |
| | | border: '1px solid ' + (active ? 'rgba(96, 132, 170, 0.36)' : 'rgba(160, 180, 205, 0.3)'), |
| | | background: active ? 'rgba(235, 243, 251, 0.96)' : 'rgba(255, 255, 255, 0.88)', |
| | | color: active ? '#27425c' : '#4d647d', |
| | | minWidth: '44px', |
| | | height: '26px', |
| | | padding: '0 10px', |
| | | borderRadius: '8px', |
| | | fontSize: '11px', |
| | | lineHeight: '26px', |
| | | cursor: 'pointer', |
| | | fontWeight: '700', |
| | | boxShadow: active ? '0 4px 12px rgba(37, 64, 97, 0.08)' : 'none', |
| | | whiteSpace: 'nowrap' |
| | | }; |
| | | }, |
| | | toggleMapToolPanel() { |
| | | this.showMapToolPanel = !this.showMapToolPanel; |
| | | }, |
| | | selectFloorFromTool(lev) { |
| | | if (lev == null || lev === this.currentLev) { return; } |
| | | this.$emit('switch-lev', lev); |
| | | }, |
| | | createMap() { |
| | | this.pixiApp = new PIXI.Application({ backgroundColor: 0xF5F7F9, antialias: false, powerPreference: 'high-performance', autoDensity: true, resolution: Math.min(window.devicePixelRatio || 1, 2) }); |
| | |
| | | this.objectsContainer2 = new PIXI.Container(); |
| | | this.tracksContainer = new PIXI.ParticleContainer(10000, { scale: true, position: true, rotation: false, uvs: false, alpha: false }); |
| | | this.tracksGraphics = new PIXI.Graphics(); |
| | | this.shelvesContainer = new PIXI.ParticleContainer(10000, { scale: true, position: true, rotation: false, uvs: false, alpha: false }); |
| | | this.shelvesContainer = new PIXI.Container(); |
| | | this.tracksContainer.autoResize = true; |
| | | this.shelvesContainer.autoResize = true; |
| | | this.mapRoot = new PIXI.Container(); |
| | | this.pixiApp.stage.addChild(this.mapRoot); |
| | | this.mapRoot.addChild(this.tracksGraphics); |
| | |
| | | const dx = globalPos.x - mouseDownPoint[0]; |
| | | const dy = globalPos.y - mouseDownPoint[1]; |
| | | this.pixiApp.stage.position.set(stageOriginalPos[0] + dx, stageOriginalPos[1] + dy); |
| | | this.scheduleShelfChunkCulling(); |
| | | } |
| | | }); |
| | | this.pixiApp.renderer.plugins.interaction.on('pointerup', () => { touchBlank = false; }); |
| | |
| | | const newPosX = sx - worldX * newZoomX; |
| | | const newPosY = sy - worldY * newZoomY; |
| | | this.pixiApp.stage.setTransform(newPosX, newPosY, newZoomX, newZoomY, 0, 0, 0, 0, 0); |
| | | this.scheduleAdjustLabels(); |
| | | this.scheduleAdjustLabels(); |
| | | this.scheduleShelfChunkCulling(); |
| | | }); |
| | | //*******************缩放画布******************* |
| | | |
| | |
| | | const rect = this.pixiApp.view ? this.pixiApp.view.getBoundingClientRect() : null; |
| | | return { width: rect ? rect.width : 0, height: rect ? rect.height : 0 }; |
| | | }, |
| | | getViewportPadding() { |
| | | return this.normalizeViewportPadding(this.viewportPadding); |
| | | }, |
| | | normalizeViewportPadding(padding) { |
| | | const source = padding || {}; |
| | | const normalize = (value) => { |
| | | const num = Number(value); |
| | | return isFinite(num) && num > 0 ? num : 0; |
| | | }; |
| | | return { |
| | | top: normalize(source.top), |
| | | right: normalize(source.right), |
| | | bottom: normalize(source.bottom), |
| | | left: normalize(source.left) |
| | | }; |
| | | }, |
| | | getViewportCenter(viewport, padding) { |
| | | const normalized = this.normalizeViewportPadding(padding); |
| | | const availableW = Math.max(1, viewport.width - normalized.left - normalized.right); |
| | | const availableH = Math.max(1, viewport.height - normalized.top - normalized.bottom); |
| | | return { |
| | | x: normalized.left + availableW / 2, |
| | | y: normalized.top + availableH / 2 |
| | | }; |
| | | }, |
| | | adjustStageForViewportPadding(oldPadding, newPadding) { |
| | | if (!this.pixiApp || !this.pixiApp.stage) { return; } |
| | | const viewport = this.getViewportSize(); |
| | | if (viewport.width <= 0 || viewport.height <= 0) { return; } |
| | | const prevCenter = this.getViewportCenter(viewport, oldPadding); |
| | | const nextCenter = this.getViewportCenter(viewport, newPadding); |
| | | const deltaX = nextCenter.x - prevCenter.x; |
| | | const deltaY = nextCenter.y - prevCenter.y; |
| | | if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) { |
| | | return; |
| | | } |
| | | const targetX = this.pixiApp.stage.position.x + deltaX; |
| | | const targetY = this.pixiApp.stage.position.y + deltaY; |
| | | if (window.gsap) { |
| | | window.gsap.killTweensOf(this.pixiApp.stage.position); |
| | | window.gsap.to(this.pixiApp.stage.position, { |
| | | x: targetX, |
| | | y: targetY, |
| | | duration: 0.18, |
| | | ease: 'power1.out', |
| | | onUpdate: () => { |
| | | this.scheduleAdjustLabels(); |
| | | this.scheduleShelfChunkCulling(); |
| | | }, |
| | | onComplete: () => { |
| | | this.scheduleAdjustLabels(); |
| | | this.scheduleShelfChunkCulling(); |
| | | } |
| | | }); |
| | | return; |
| | | } |
| | | this.pixiApp.stage.position.x = targetX; |
| | | this.pixiApp.stage.position.y = targetY; |
| | | this.scheduleAdjustLabels(); |
| | | this.scheduleShelfChunkCulling(); |
| | | }, |
| | | resizeToContainer() { |
| | | const w = this.$el.clientWidth || 0; |
| | | const h = this.$el.clientHeight || 0; |
| | |
| | | this.objectsContainer2.removeChildren(); |
| | | if (this.tracksContainer) { this.tracksContainer.removeChildren(); } |
| | | if (this.tracksGraphics) { this.tracksGraphics.clear(); } |
| | | if (this.shelvesContainer) { this.shelvesContainer.removeChildren(); } |
| | | this.clearShelfChunks(); |
| | | this.crnList = []; |
| | | this.dualCrnList = []; |
| | | this.rgvList = []; |
| | |
| | | this.objectsContainer2.removeChildren(); |
| | | if (this.tracksContainer) { this.tracksContainer.removeChildren(); } |
| | | if (this.tracksGraphics) { this.tracksGraphics.clear(); } |
| | | if (this.shelvesContainer) { this.shelvesContainer.removeChildren(); } |
| | | this.clearShelfChunks(); |
| | | this.crnList = []; |
| | | this.dualCrnList = []; |
| | | this.rgvList = []; |
| | |
| | | this.collectTrackItem(val); |
| | | continue; |
| | | } |
| | | if (val.type === 'shelf') { continue; } |
| | | let sprite = this.getSprite(val, (e) => { |
| | | //回调 |
| | | }); |
| | | if (sprite == null) { continue; } |
| | | if (sprite._kind === 'shelf') { |
| | | this.shelvesContainer.addChild(sprite); |
| | | } else { |
| | | this.objectsContainer.addChild(sprite); |
| | | } |
| | | this.objectsContainer.addChild(sprite); |
| | | this.pixiStageList[index][idx] = sprite; |
| | | } |
| | | }); |
| | |
| | | } |
| | | } |
| | | this.mapContentSize = { width: contentW, height: contentH }; |
| | | this.buildShelfChunks(map, contentW, contentH); |
| | | this.applyMapTransform(true); |
| | | this.map = map; |
| | | this.isSwitchingFloor = false; |
| | |
| | | if (!sites) { return; } |
| | | sites.forEach((item) => { |
| | | let id = item.siteId != null ? item.siteId : item.stationId; |
| | | let status = item.siteStatus != null ? item.siteStatus : item.stationStatus; |
| | | let workNo = item.workNo != null ? item.workNo : item.taskNo; |
| | | if (id == null) { return; } |
| | | let sta = this.pixiStaMap.get(parseInt(id)); |
| | |
| | | sta.statusObj = null; |
| | | if (sta.textObj.parent !== sta) { sta.addChild(sta.textObj); sta.textObj.position.set(sta.width / 2, sta.height / 2); } |
| | | } |
| | | this.setStationBaseColor(sta, this.getStationStatusColor(status)); |
| | | this.setStationBaseColor(sta, this.getStationStatusColor(this.resolveStationStatus(item))); |
| | | }); |
| | | }, |
| | | getCrnInfo() { |
| | |
| | | return brightness > 150 ? '#000000' : '#ffffff'; |
| | | }, |
| | | getStationStatusColor(status) { |
| | | if (status === "site-auto") { return 0x78ff81; } |
| | | if (status === "site-auto-run") { return 0xfa51f6; } |
| | | if (status === "site-auto-id") { return 0xc4c400; } |
| | | if (status === "site-auto-run-id") { return 0x30bffc; } |
| | | if (status === "site-unauto") { return 0xb8b8b8; } |
| | | if (status === "machine-pakin") { return 0x30bffc; } |
| | | if (status === "machine-pakout") { return 0x97b400; } |
| | | if (status === "site-run-block") { return 0xe69138; } |
| | | return 0xb8b8b8; |
| | | const colorMap = this.stationStatusColors || this.getDefaultStationStatusColors(); |
| | | if (status && colorMap[status] != null) { return colorMap[status]; } |
| | | return colorMap['site-unauto'] != null ? colorMap['site-unauto'] : 0xb8b8b8; |
| | | }, |
| | | resolveStationStatus(item) { |
| | | const status = item && (item.siteStatus != null ? item.siteStatus : item.stationStatus); |
| | | const taskNo = this.parseStationTaskNo(item && (item.workNo != null ? item.workNo : item.taskNo)); |
| | | const autoing = !!(item && item.autoing); |
| | | const loading = !!(item && item.loading); |
| | | const runBlock = !!(item && item.runBlock); |
| | | const enableIn = !!(item && item.enableIn); |
| | | if (taskNo === 9998 || enableIn) { return 'site-enable-in'; } |
| | | if (autoing && loading && taskNo > 0 && !runBlock) { |
| | | const taskClass = this.getStationTaskClass(taskNo); |
| | | if (taskClass) { return taskClass; } |
| | | } |
| | | if (status) { return status; } |
| | | if (autoing && loading && taskNo > 0 && runBlock) { return 'site-run-block'; } |
| | | if (autoing && loading && taskNo > 0) { return 'site-auto-run-id'; } |
| | | if (autoing && loading) { return 'site-auto-run'; } |
| | | if (autoing && taskNo > 0) { return 'site-auto-id'; } |
| | | if (autoing) { return 'site-auto'; } |
| | | return 'site-unauto'; |
| | | }, |
| | | parseStationTaskNo(value) { |
| | | const taskNo = parseInt(value, 10); |
| | | return isNaN(taskNo) ? 0 : taskNo; |
| | | }, |
| | | getStationTaskClass(taskNo) { |
| | | if (!(taskNo > 0)) { return null; } |
| | | const range = this.stationTaskRange || {}; |
| | | if (this.isTaskNoInRange(taskNo, range.inbound)) { return 'machine-pakin'; } |
| | | if (this.isTaskNoInRange(taskNo, range.outbound)) { return 'machine-pakout'; } |
| | | return null; |
| | | }, |
| | | isTaskNoInRange(taskNo, range) { |
| | | if (!range) { return false; } |
| | | const start = parseInt(range.start, 10); |
| | | const end = parseInt(range.end, 10); |
| | | if (isNaN(start) || isNaN(end)) { return false; } |
| | | return taskNo >= start && taskNo <= end; |
| | | }, |
| | | getCrnStatusColor(status) { |
| | | if (status === "machine-auto") { return 0x21BA45; } |
| | |
| | | } |
| | | } |
| | | }, |
| | | clearShelfChunks() { |
| | | if (this.shelfCullRaf) { |
| | | cancelAnimationFrame(this.shelfCullRaf); |
| | | this.shelfCullRaf = null; |
| | | } |
| | | this.shelfChunkList = []; |
| | | if (!this.shelvesContainer) { return; } |
| | | const children = this.shelvesContainer.removeChildren(); |
| | | children.forEach((child) => { |
| | | if (child && typeof child.destroy === 'function') { |
| | | child.destroy({ children: true, texture: true, baseTexture: true }); |
| | | } |
| | | }); |
| | | }, |
| | | buildShelfChunks(map, contentW, contentH) { |
| | | this.clearShelfChunks(); |
| | | if (!this.pixiApp || !this.pixiApp.renderer || !this.shelvesContainer || !Array.isArray(map)) { return; } |
| | | const chunkSize = Math.max(256, parseInt(this.shelfChunkSize, 10) || 2048); |
| | | const chunkMap = new Map(); |
| | | for (let r = 0; r < map.length; r++) { |
| | | const row = map[r]; |
| | | if (!row) { continue; } |
| | | for (let c = 0; c < row.length; c++) { |
| | | const cell = row[c]; |
| | | if (!cell || cell.type !== 'shelf' || cell.type === 'merge') { continue; } |
| | | const startChunkX = Math.floor(cell.posX / chunkSize); |
| | | const endChunkX = Math.floor((cell.posX + Math.max(1, cell.width) - 0.01) / chunkSize); |
| | | const startChunkY = Math.floor(cell.posY / chunkSize); |
| | | const endChunkY = Math.floor((cell.posY + Math.max(1, cell.height) - 0.01) / chunkSize); |
| | | for (let chunkY = startChunkY; chunkY <= endChunkY; chunkY++) { |
| | | for (let chunkX = startChunkX; chunkX <= endChunkX; chunkX++) { |
| | | const key = chunkX + ',' + chunkY; |
| | | let list = chunkMap.get(key); |
| | | if (!list) { |
| | | list = []; |
| | | chunkMap.set(key, list); |
| | | } |
| | | list.push(cell); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | const chunkList = []; |
| | | chunkMap.forEach((cells, key) => { |
| | | const keyParts = key.split(','); |
| | | const chunkX = parseInt(keyParts[0], 10) || 0; |
| | | const chunkY = parseInt(keyParts[1], 10) || 0; |
| | | const chunkLeft = chunkX * chunkSize; |
| | | const chunkTop = chunkY * chunkSize; |
| | | const chunkWidth = Math.max(1, Math.min(chunkSize, contentW - chunkLeft)); |
| | | const chunkHeight = Math.max(1, Math.min(chunkSize, contentH - chunkTop)); |
| | | const graphics = new PIXI.Graphics(); |
| | | graphics.beginFill(0xb6e2e2); |
| | | graphics.lineStyle(1, 0xffffff, 1); |
| | | for (let i = 0; i < cells.length; i++) { |
| | | const cell = cells[i]; |
| | | graphics.drawRect(cell.posX - chunkLeft, cell.posY - chunkTop, cell.width, cell.height); |
| | | } |
| | | graphics.endFill(); |
| | | const texture = this.pixiApp.renderer.generateTexture( |
| | | graphics, |
| | | PIXI.SCALE_MODES.LINEAR, |
| | | 1, |
| | | new PIXI.Rectangle(0, 0, chunkWidth, chunkHeight) |
| | | ); |
| | | graphics.destroy(true); |
| | | const sprite = new PIXI.Sprite(texture); |
| | | sprite.position.set(chunkLeft, chunkTop); |
| | | sprite._chunkBounds = { |
| | | x: chunkLeft, |
| | | y: chunkTop, |
| | | width: chunkWidth, |
| | | height: chunkHeight |
| | | }; |
| | | this.shelvesContainer.addChild(sprite); |
| | | chunkList.push(sprite); |
| | | }); |
| | | this.shelfChunkList = chunkList; |
| | | this.updateVisibleShelfChunks(); |
| | | }, |
| | | getViewportLocalBounds(padding) { |
| | | if (!this.mapRoot || !this.pixiApp) { return null; } |
| | | const viewport = this.getViewportSize(); |
| | | const pad = Math.max(0, Number(padding) || 0); |
| | | const points = [ |
| | | new PIXI.Point(-pad, -pad), |
| | | new PIXI.Point(viewport.width + pad, -pad), |
| | | new PIXI.Point(-pad, viewport.height + pad), |
| | | new PIXI.Point(viewport.width + pad, viewport.height + pad) |
| | | ]; |
| | | let minX = Infinity; |
| | | let minY = Infinity; |
| | | let maxX = -Infinity; |
| | | let maxY = -Infinity; |
| | | points.forEach((point) => { |
| | | const local = this.mapRoot.toLocal(point); |
| | | if (local.x < minX) { minX = local.x; } |
| | | if (local.y < minY) { minY = local.y; } |
| | | if (local.x > maxX) { maxX = local.x; } |
| | | if (local.y > maxY) { maxY = local.y; } |
| | | }); |
| | | if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) { return null; } |
| | | return { minX: minX, minY: minY, maxX: maxX, maxY: maxY }; |
| | | }, |
| | | updateVisibleShelfChunks() { |
| | | if (!this.shelfChunkList || this.shelfChunkList.length === 0) { return; } |
| | | const localBounds = this.getViewportLocalBounds(this.shelfCullPadding); |
| | | if (!localBounds) { return; } |
| | | for (let i = 0; i < this.shelfChunkList.length; i++) { |
| | | const sprite = this.shelfChunkList[i]; |
| | | const bounds = sprite && sprite._chunkBounds; |
| | | if (!bounds) { continue; } |
| | | const visible = bounds.x < localBounds.maxX && |
| | | bounds.x + bounds.width > localBounds.minX && |
| | | bounds.y < localBounds.maxY && |
| | | bounds.y + bounds.height > localBounds.minY; |
| | | if (sprite.visible !== visible) { |
| | | sprite.visible = visible; |
| | | } |
| | | } |
| | | }, |
| | | scheduleShelfChunkCulling() { |
| | | if (this.shelfCullRaf) { return; } |
| | | this.shelfCullRaf = requestAnimationFrame(() => { |
| | | this.shelfCullRaf = null; |
| | | this.updateVisibleShelfChunks(); |
| | | }); |
| | | }, |
| | | findIndexByOffsets(offsets, sizes, value) { |
| | | if (!offsets || !sizes || offsets.length === 0) { return -1; } |
| | | for (let i = 0; i < offsets.length; i++) { |
| | |
| | | this.applyMapTransform(true); |
| | | this.saveMapTransformConfig(); |
| | | }, |
| | | openStationColorConfigPage() { |
| | | if (typeof window === 'undefined') { return; } |
| | | const url = (typeof baseUrl !== 'undefined' ? baseUrl : '') + '/views/watch/stationColorConfig.html'; |
| | | const layerInstance = (window.top && window.top.layer) || window.layer; |
| | | if (layerInstance && typeof layerInstance.open === 'function') { |
| | | layerInstance.open({ |
| | | type: 2, |
| | | title: '站点颜色配置', |
| | | maxmin: true, |
| | | area: ['980px', '760px'], |
| | | shadeClose: false, |
| | | content: url |
| | | }); |
| | | return; |
| | | } |
| | | window.open(url, '_blank'); |
| | | }, |
| | | parseRotation(value) { |
| | | const num = parseInt(value, 10); |
| | | if (!isFinite(num)) { return 0; } |
| | |
| | | if (value == null) { return false; } |
| | | const str = String(value).toLowerCase(); |
| | | return str === '1' || str === 'true' || str === 'y'; |
| | | }, |
| | | getDefaultStationStatusColors() { |
| | | return { |
| | | 'site-auto': 0x78ff81, |
| | | 'site-auto-run': 0xfa51f6, |
| | | 'site-auto-id': 0xc4c400, |
| | | 'site-auto-run-id': 0x30bffc, |
| | | 'site-enable-in': 0x18c7b8, |
| | | 'site-unauto': 0xb8b8b8, |
| | | 'machine-pakin': 0x30bffc, |
| | | 'machine-pakout': 0x97b400, |
| | | 'site-run-block': 0xe69138 |
| | | }; |
| | | }, |
| | | parseColorConfigValue(value, fallback) { |
| | | if (typeof value === 'number' && isFinite(value)) { |
| | | return value; |
| | | } |
| | | const str = String(value == null ? '' : value).trim(); |
| | | if (!str) { return fallback; } |
| | | if (/^#[0-9a-fA-F]{6}$/.test(str)) { return parseInt(str.slice(1), 16); } |
| | | if (/^#[0-9a-fA-F]{3}$/.test(str)) { |
| | | const expanded = str.charAt(1) + str.charAt(1) + str.charAt(2) + str.charAt(2) + str.charAt(3) + str.charAt(3); |
| | | return parseInt(expanded, 16); |
| | | } |
| | | if (/^0x[0-9a-fA-F]{6}$/i.test(str)) { return parseInt(str.slice(2), 16); } |
| | | if (/^[0-9]+$/.test(str)) { |
| | | const num = parseInt(str, 10); |
| | | return isNaN(num) ? fallback : num; |
| | | } |
| | | return fallback; |
| | | }, |
| | | loadStationColorConfig() { |
| | | if (!window.$ || typeof baseUrl === 'undefined') { return; } |
| | | $.ajax({ |
| | | url: baseUrl + "/watch/stationColor/config/auth", |
| | | headers: { 'token': localStorage.getItem('token') }, |
| | | dataType: 'json', |
| | | method: 'GET', |
| | | success: (res) => { |
| | | if (!res || res.code !== 200 || !res.data) { |
| | | if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; } |
| | | return; |
| | | } |
| | | this.applyStationColorConfigPayload(res.data); |
| | | } |
| | | }); |
| | | }, |
| | | applyStationColorConfigPayload(data) { |
| | | const defaults = this.getDefaultStationStatusColors(); |
| | | const nextColors = Object.assign({}, defaults); |
| | | const items = Array.isArray(data.items) ? data.items : []; |
| | | items.forEach((item) => { |
| | | if (!item || !item.status || defaults[item.status] == null) { return; } |
| | | nextColors[item.status] = this.parseColorConfigValue(item.color, defaults[item.status]); |
| | | }); |
| | | this.stationStatusColors = nextColors; |
| | | }, |
| | | buildMissingMapConfigList(byCode) { |
| | | const createList = []; |
| | | if (!byCode[this.mapConfigCodes.rotate]) { |
| | | createList.push({ |
| | | name: '地图旋转', |
| | | code: this.mapConfigCodes.rotate, |
| | | value: String(this.mapRotation || 0), |
| | | type: 1, |
| | | status: 1, |
| | | selectType: 'map' |
| | | }); |
| | | } |
| | | if (!byCode[this.mapConfigCodes.mirror]) { |
| | | createList.push({ |
| | | name: '地图镜像', |
| | | code: this.mapConfigCodes.mirror, |
| | | value: this.mapMirrorX ? '1' : '0', |
| | | type: 1, |
| | | status: 1, |
| | | selectType: 'map' |
| | | }); |
| | | } |
| | | return createList; |
| | | }, |
| | | createMapConfigs(createList) { |
| | | if (!window.$ || typeof baseUrl === 'undefined' || !Array.isArray(createList) || createList.length === 0) { return; } |
| | | createList.forEach((cfg) => { |
| | | $.ajax({ |
| | | url: baseUrl + "/config/add/auth", |
| | | headers: { 'token': localStorage.getItem('token') }, |
| | | method: 'POST', |
| | | data: cfg |
| | | }); |
| | | }); |
| | | }, |
| | | loadMapTransformConfig() { |
| | | if (!window.$ || typeof baseUrl === 'undefined') { return; } |
| | |
| | | if (mirrorCfg && mirrorCfg.value != null) { |
| | | this.mapMirrorX = this.parseMirror(mirrorCfg.value); |
| | | } |
| | | if (rotateCfg == null || mirrorCfg == null) { |
| | | this.createMapTransformConfigIfMissing(rotateCfg, mirrorCfg); |
| | | } |
| | | this.createMapConfigs(this.buildMissingMapConfigList(byCode)); |
| | | if (this.mapContentSize && this.mapContentSize.width > 0) { |
| | | this.applyMapTransform(true); |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | createMapTransformConfigIfMissing(rotateCfg, mirrorCfg) { |
| | | if (!window.$ || typeof baseUrl === 'undefined') { return; } |
| | | const createList = []; |
| | | if (!rotateCfg) { |
| | | createList.push({ |
| | | name: '地图旋转', |
| | | code: this.mapConfigCodes.rotate, |
| | | value: String(this.mapRotation || 0), |
| | | type: 1, |
| | | status: 1, |
| | | selectType: 'map' |
| | | }); |
| | | } |
| | | if (!mirrorCfg) { |
| | | createList.push({ |
| | | name: '地图镜像', |
| | | code: this.mapConfigCodes.mirror, |
| | | value: this.mapMirrorX ? '1' : '0', |
| | | type: 1, |
| | | status: 1, |
| | | selectType: 'map' |
| | | }); |
| | | } |
| | | createList.forEach((cfg) => { |
| | | $.ajax({ |
| | | url: baseUrl + "/config/add/auth", |
| | | headers: { 'token': localStorage.getItem('token') }, |
| | | method: 'POST', |
| | | data: cfg |
| | | }); |
| | | }); |
| | | }, |
| | | saveMapTransformConfig() { |
| | |
| | | const viewport = this.getViewportSize(); |
| | | const vw = viewport.width; |
| | | const vh = viewport.height; |
| | | let scale = Math.min(vw / contentW, vh / contentH) * 0.95; |
| | | const padding = this.getViewportPadding(); |
| | | const availableW = Math.max(1, vw - padding.left - padding.right); |
| | | const availableH = Math.max(1, vh - padding.top - padding.bottom); |
| | | let scale = Math.min(availableW / contentW, availableH / contentH) * 0.95; |
| | | if (!isFinite(scale) || scale <= 0) { scale = 1; } |
| | | const baseW = this.mapContentSize.width || contentW; |
| | | const baseH = this.mapContentSize.height || contentH; |
| | | const mirrorX = this.mapMirrorX ? -1 : 1; |
| | | const scaleX = scale * mirrorX; |
| | | const scaleY = scale; |
| | | const posX = (vw / 2) - (baseW / 2) * scaleX; |
| | | const posY = (vh / 2) - (baseH / 2) * scaleY; |
| | | const centerX = padding.left + availableW / 2; |
| | | const centerY = padding.top + availableH / 2; |
| | | const posX = centerX - (baseW / 2) * scaleX; |
| | | const posY = centerY - (baseH / 2) * scaleY; |
| | | this.pixiApp.stage.setTransform(posX, posY, scaleX, scaleY, 0, 0, 0, 0, 0); |
| | | }, |
| | | applyMapTransform(fitToView) { |
| | |
| | | this.mapRoot.scale.set(1, 1); |
| | | if (fitToView) { this.fitStageToContent(); } |
| | | this.scheduleAdjustLabels(); |
| | | this.scheduleShelfChunkCulling(); |
| | | }, |
| | | scheduleAdjustLabels() { |
| | | if (this.adjustLabelTimer) { clearTimeout(this.adjustLabelTimer); } |
| | |
| | | } |
| | | } |
| | | }); |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| New file |
| | |
| | | (function (global) { |
| | | if (global.MonitorCardKit) { |
| | | return; |
| | | } |
| | | |
| | | function ensureStyles() { |
| | | if (document.getElementById("monitor-card-kit-style")) { |
| | | return; |
| | | } |
| | | var style = document.createElement("style"); |
| | | style.id = "monitor-card-kit-style"; |
| | | style.textContent = [ |
| | | "watch-crn-card,watch-dual-crn-card,devp-card,watch-rgv-card{display:block;width:100%;min-width:0;min-height:0;flex:1 1 auto;}", |
| | | ".mc-root{display:flex;flex-direction:column;width:100%;height:100%;min-width:0;min-height:0;color:#375067;font-size:12px;}", |
| | | ".mc-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:10px;}", |
| | | ".mc-title{flex:1;font-size:14px;font-weight:700;color:#22384f;}", |
| | | ".mc-search{display:flex;gap:8px;flex:1;}", |
| | | ".mc-input,.mc-select{width:100%;height:34px;padding:0 11px;border-radius:10px;border:1px solid rgba(219,228,236,.96);background:rgba(255,255,255,.84);box-sizing:border-box;color:#334155;outline:none;}", |
| | | ".mc-input:focus,.mc-select:focus{border-color:rgba(112,148,190,.62);box-shadow:0 0 0 3px rgba(112,148,190,.1);}", |
| | | ".mc-btn{height:34px;padding:0 12px;border-radius:10px;border:none;background:#6f95bd;color:#fff;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;}", |
| | | ".mc-btn.mc-btn-ghost{background:rgba(255,255,255,.88);color:#46607a;border:1px solid rgba(219,228,236,.96);}", |
| | | ".mc-btn.mc-btn-soft{background:rgba(233,239,245,.92);color:#46607a;border:1px solid rgba(210,220,231,.98);}", |
| | | ".mc-control-toggle{margin-bottom:8px;}", |
| | | ".mc-control{margin-bottom:10px;padding:12px;border:1px solid rgba(223,232,240,.94);border-radius:14px;background:rgba(248,251,253,.88);}", |
| | | ".mc-control-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;}", |
| | | ".mc-field{display:flex;flex-direction:column;gap:5px;}", |
| | | ".mc-field.mc-span-2{grid-column:1 / -1;}", |
| | | ".mc-field-label{font-size:11px;font-weight:700;color:#72859a;}", |
| | | ".mc-action-row{display:flex;flex-wrap:wrap;gap:8px;grid-column:1 / -1;padding-top:8px;margin-top:2px;border-top:1px dashed rgba(216,226,235,.92);}", |
| | | ".mc-collapse{flex:1;min-height:0;overflow:auto;padding-right:2px;}", |
| | | ".mc-item{border:1px solid rgba(223,232,240,.92);border-radius:14px;background:rgba(255,255,255,.72);margin-bottom:10px;overflow:hidden;}", |
| | | ".mc-item.is-open{border-color:rgba(132,166,201,.48);box-shadow:0 10px 18px rgba(148,163,184,.08);}", |
| | | ".mc-head{width:100%;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 14px;border:none;background:transparent;cursor:pointer;text-align:left;color:inherit;}", |
| | | ".mc-head-main{min-width:0;flex:1;}", |
| | | ".mc-head-title{font-size:13px;font-weight:700;color:#27425c;line-height:1.35;}", |
| | | ".mc-head-subtitle{margin-top:3px;font-size:11px;color:#7c8d9f;line-height:1.35;}", |
| | | ".mc-head-right{display:flex;align-items:center;gap:8px;flex-shrink:0;}", |
| | | ".mc-badge{padding:4px 8px;border-radius:999px;font-size:10px;font-weight:700;}", |
| | | ".mc-badge.is-success{background:rgba(82,177,126,.12);color:#2d7650;}", |
| | | ".mc-badge.is-working{background:rgba(111,149,189,.12);color:#3f6286;}", |
| | | ".mc-badge.is-warning{background:rgba(214,162,94,.14);color:#9b6a24;}", |
| | | ".mc-badge.is-danger{background:rgba(207,126,120,.14);color:#a14e4a;}", |
| | | ".mc-badge.is-muted{background:rgba(148,163,184,.14);color:#748397;}", |
| | | ".mc-chevron{font-size:14px;color:#6f8194;line-height:1;}", |
| | | ".mc-body{padding:0 14px 14px;border-top:1px solid rgba(232,238,244,.92);}", |
| | | ".mc-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px;}", |
| | | ".mc-detail-cell{padding:10px 12px;border-radius:12px;background:rgba(247,250,252,.9);border:1px solid rgba(232,238,244,.96);}", |
| | | ".mc-detail-label{font-size:11px;color:#7c8d9f;}", |
| | | ".mc-detail-value{margin-top:4px;font-size:12px;color:#31485f;line-height:1.45;word-break:break-all;}", |
| | | ".mc-inline-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;}", |
| | | ".mc-link{padding:0;border:none;background:transparent;color:#4677a4;font-size:12px;font-weight:600;cursor:pointer;}", |
| | | ".mc-empty{padding:24px 10px;text-align:center;color:#8b9aad;}", |
| | | ".mc-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:10px;color:#708396;font-size:11px;}", |
| | | ".mc-page-btn{height:30px;padding:0 10px;border-radius:8px;border:1px solid rgba(219,228,236,.96);background:rgba(255,255,255,.86);color:#46607a;cursor:pointer;}", |
| | | ".mc-page-btn[disabled]{opacity:.45;cursor:not-allowed;}", |
| | | ".mc-code{font-family:monospace;}", |
| | | "@media (max-width: 1100px){.mc-control-grid{grid-template-columns:1fr;}.mc-field.mc-span-2{grid-column:auto;}.mc-toolbar{flex-direction:column;align-items:stretch;}.mc-search{width:100%;}.mc-detail-grid{grid-template-columns:1fr;}}" |
| | | ].join(""); |
| | | document.head.appendChild(style); |
| | | } |
| | | |
| | | function showMessage(vm, message, type) { |
| | | if (!message) { |
| | | return; |
| | | } |
| | | if (vm && typeof vm.$message === "function") { |
| | | vm.$message({ |
| | | message: message, |
| | | type: type === "danger" ? "error" : (type || "info") |
| | | }); |
| | | return; |
| | | } |
| | | if (vm && vm.$root && typeof vm.$root.showPageMessage === "function") { |
| | | vm.$root.showPageMessage(message, type === "danger" ? "error" : type); |
| | | return; |
| | | } |
| | | if (global.ELEMENT && typeof global.ELEMENT.Message === "function") { |
| | | global.ELEMENT.Message({ |
| | | message: message, |
| | | type: type === "danger" ? "error" : (type || "info") |
| | | }); |
| | | return; |
| | | } |
| | | if (global.layer && typeof global.layer.msg === "function") { |
| | | var iconMap = { success: 1, danger: 2, warning: 0 }; |
| | | global.layer.msg(message, { |
| | | icon: iconMap[type] != null ? iconMap[type] : 0, |
| | | time: 1800 |
| | | }); |
| | | return; |
| | | } |
| | | console[type === "danger" ? "error" : "log"](message); |
| | | } |
| | | |
| | | function orDash(value) { |
| | | return value == null || value === "" ? "-" : String(value); |
| | | } |
| | | |
| | | function yesNo(value) { |
| | | if (value === true || value === "Y" || value === "y" || value === 1 || value === "1") { |
| | | return "Y"; |
| | | } |
| | | if (value === false || value === "N" || value === "n" || value === 0 || value === "0") { |
| | | return "N"; |
| | | } |
| | | return value ? "Y" : "N"; |
| | | } |
| | | |
| | | function deviceStatusLabel(status, fallbackManual) { |
| | | var normalized = String(status || "").toUpperCase(); |
| | | if (normalized === "AUTO") { |
| | | return "自动"; |
| | | } |
| | | if (normalized === "WORKING") { |
| | | return "作业中"; |
| | | } |
| | | if (normalized === "ERROR") { |
| | | return "故障"; |
| | | } |
| | | return fallbackManual || "离线"; |
| | | } |
| | | |
| | | function statusTone(label) { |
| | | if (label === "自动") { |
| | | return "success"; |
| | | } |
| | | if (label === "作业中") { |
| | | return "working"; |
| | | } |
| | | if (label === "手动") { |
| | | return "warning"; |
| | | } |
| | | if (label === "故障" || label === "报警") { |
| | | return "danger"; |
| | | } |
| | | return "muted"; |
| | | } |
| | | |
| | | global.MonitorCardKit = { |
| | | ensureStyles: ensureStyles, |
| | | showMessage: showMessage, |
| | | orDash: orDash, |
| | | yesNo: yesNo, |
| | | deviceStatusLabel: deviceStatusLabel, |
| | | statusTone: statusTone |
| | | }; |
| | | })(window); |
| New file |
| | |
| | | Vue.component("monitor-workbench", { |
| | | template: ` |
| | | <div class="wb-root"> |
| | | <div class="wb-tabs" role="tablist"> |
| | | <button |
| | | v-for="tab in tabs" |
| | | :key="tab.key" |
| | | type="button" |
| | | :class="['wb-tab', { 'is-active': activeCard === tab.key }]" |
| | | @click="changeTab(tab.key)" |
| | | >{{ tab.label }}</button> |
| | | </div> |
| | | |
| | | <div class="wb-toolbar"> |
| | | <input |
| | | class="wb-input" |
| | | :value="currentSearch" |
| | | :placeholder="currentSearchPlaceholder()" |
| | | @input="updateSearch($event.target.value)" |
| | | /> |
| | | <div class="wb-toolbar-actions"> |
| | | <button type="button" class="wb-btn wb-btn-ghost" @click="refreshCurrent">刷新</button> |
| | | <button type="button" class="wb-btn" @click="toggleControl"> |
| | | {{ currentShowControl ? '收起操作' : '展开操作' }} |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="wb-main"> |
| | | <div class="wb-side"> |
| | | <div class="wb-list-card"> |
| | | <div class="wb-side-title">设备选择</div> |
| | | <div class="wb-list"> |
| | | <button |
| | | v-for="item in filteredList" |
| | | :key="activeCard + '-' + getItemId(activeCard, item)" |
| | | type="button" |
| | | :class="['wb-list-item', { 'is-active': isSelected(activeCard, item) }]" |
| | | @click="selectItem(activeCard, item)" |
| | | > |
| | | <span :class="['wb-badge', 'is-' + getStatusTone(activeCard, item)]">{{ getStatusLabel(activeCard, item) }}</span> |
| | | <div class="wb-list-main"> |
| | | <div class="wb-list-title">{{ getItemTitle(activeCard, item) }}</div> |
| | | <div class="wb-list-meta">{{ getItemMeta(activeCard, item) }}</div> |
| | | </div> |
| | | </button> |
| | | <div v-if="filteredList.length === 0" class="wb-empty">当前没有可展示的数据</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="currentShowControl" class="wb-control-card"> |
| | | <div class="wb-side-title">快捷操作</div> |
| | | <div class="wb-control-target">{{ controlTargetText }}</div> |
| | | <div class="wb-control-subtitle">{{ controlPanelHint }}</div> |
| | | |
| | | <div v-if="activeCard === 'crn'" class="wb-form-grid"> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">堆垛机号</span> |
| | | <input class="wb-input" v-model="controlForms.crn.crnNo" placeholder="1" /> |
| | | </label> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">源库位</span> |
| | | <input class="wb-input" v-model="controlForms.crn.sourceLocNo" placeholder="源点" /> |
| | | </label> |
| | | <label class="wb-field wb-field-span-2"> |
| | | <span class="wb-field-label">目标库位</span> |
| | | <input class="wb-input" v-model="controlForms.crn.targetLocNo" placeholder="目标点" /> |
| | | </label> |
| | | <div class="wb-action-row"> |
| | | <button type="button" class="wb-btn wb-btn-primary" @click="submitControl('transport')">取放货</button> |
| | | <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('move')">移动</button> |
| | | <button type="button" class="wb-btn wb-btn-soft" @click="submitControl('taskComplete')">完成</button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else-if="activeCard === 'dualCrn'" class="wb-form-grid"> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">堆垛机号</span> |
| | | <input class="wb-input" v-model="controlForms.dualCrn.crnNo" placeholder="2" /> |
| | | </label> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">工位</span> |
| | | <select class="wb-select" v-model="controlForms.dualCrn.station"> |
| | | <option :value="1">工位1</option> |
| | | <option :value="2">工位2</option> |
| | | </select> |
| | | </label> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">源库位</span> |
| | | <input class="wb-input" v-model="controlForms.dualCrn.sourceLocNo" placeholder="源点" /> |
| | | </label> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">目标库位</span> |
| | | <input class="wb-input" v-model="controlForms.dualCrn.targetLocNo" placeholder="目标点" /> |
| | | </label> |
| | | <div class="wb-action-row"> |
| | | <button type="button" class="wb-btn wb-btn-primary" @click="submitControl('transport')">取放货</button> |
| | | <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('pickup')">取货</button> |
| | | <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('putdown')">放货</button> |
| | | <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('move')">移动</button> |
| | | <button type="button" class="wb-btn wb-btn-soft" @click="submitControl('taskComplete')">完成</button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else-if="activeCard === 'devp'" class="wb-form-grid"> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">站号</span> |
| | | <input class="wb-input" v-model="controlForms.devp.stationId" placeholder="101" /> |
| | | </label> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">工作号</span> |
| | | <input class="wb-input" v-model="controlForms.devp.taskNo" placeholder="工作号" /> |
| | | </label> |
| | | <label class="wb-field wb-field-span-2"> |
| | | <span class="wb-field-label">目标站</span> |
| | | <input class="wb-input" v-model="controlForms.devp.targetStationId" placeholder="目标站号" /> |
| | | </label> |
| | | <div class="wb-action-row"> |
| | | <button type="button" class="wb-btn wb-btn-primary" @click="submitControl('move')">下发</button> |
| | | <button type="button" class="wb-btn wb-btn-soft" @click="submitControl('reset')">复位</button> |
| | | <button |
| | | v-if="selectedItem" |
| | | type="button" |
| | | class="wb-btn wb-btn-ghost" |
| | | @click="editBarcode" |
| | | >条码</button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else-if="activeCard === 'rgv'" class="wb-form-grid"> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">RGV号</span> |
| | | <input class="wb-input" v-model="controlForms.rgv.rgvNo" placeholder="1" /> |
| | | </label> |
| | | <label class="wb-field"> |
| | | <span class="wb-field-label">源点</span> |
| | | <input class="wb-input" v-model="controlForms.rgv.sourcePos" placeholder="源点" /> |
| | | </label> |
| | | <label class="wb-field wb-field-span-2"> |
| | | <span class="wb-field-label">目标点</span> |
| | | <input class="wb-input" v-model="controlForms.rgv.targetPos" placeholder="目标点" /> |
| | | </label> |
| | | <div class="wb-action-row"> |
| | | <button type="button" class="wb-btn wb-btn-primary" @click="submitControl('transport')">取放货</button> |
| | | <button type="button" class="wb-btn wb-btn-ghost" @click="submitControl('move')">移动</button> |
| | | <button type="button" class="wb-btn wb-btn-soft" @click="submitControl('taskComplete')">完成</button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="wb-detail-panel"> |
| | | <div class="wb-detail" v-if="selectedItem"> |
| | | <div class="wb-detail-header"> |
| | | <div> |
| | | <div class="wb-section-title">{{ getItemTitle(activeCard, selectedItem) }}</div> |
| | | <div class="wb-detail-subtitle">{{ getItemMeta(activeCard, selectedItem) }}</div> |
| | | </div> |
| | | <div class="wb-detail-actions"> |
| | | <button |
| | | v-if="activeCard === 'dualCrn'" |
| | | type="button" |
| | | class="wb-link" |
| | | @click="editDualTask(1)" |
| | | >工位1任务号</button> |
| | | <button |
| | | v-if="activeCard === 'dualCrn'" |
| | | type="button" |
| | | class="wb-link" |
| | | @click="editDualTask(2)" |
| | | >工位2任务号</button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="wb-detail-grid"> |
| | | <div v-for="entry in detailEntries" :key="entry.label" class="wb-detail-cell"> |
| | | <div class="wb-detail-label">{{ entry.label }}</div> |
| | | <div class="wb-detail-value">{{ entry.value }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="wb-detail wb-detail-empty" v-else> |
| | | <div class="wb-empty">请先从左侧选择一个设备</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="noticeMessage" :class="['wb-notice', 'is-' + noticeType]">{{ noticeMessage }}</div> |
| | | </div> |
| | | `, |
| | | props: { |
| | | activeCard: { type: String, default: "crn" }, |
| | | crnParam: { type: Object, default: function () { return {}; } }, |
| | | dualCrnParam: { type: Object, default: function () { return {}; } }, |
| | | devpParam: { type: Object, default: function () { return {}; } }, |
| | | rgvParam: { type: Object, default: function () { return {}; } }, |
| | | crnList: { type: Array, default: function () { return []; } }, |
| | | dualCrnList: { type: Array, default: function () { return []; } }, |
| | | stationList: { type: Array, default: function () { return []; } }, |
| | | rgvList: { type: Array, default: function () { return []; } } |
| | | }, |
| | | data() { |
| | | return { |
| | | tabs: [ |
| | | { key: "crn", label: "堆垛机" }, |
| | | { key: "dualCrn", label: "双工位" }, |
| | | { key: "devp", label: "输送站" }, |
| | | { key: "rgv", label: "RGV" } |
| | | ], |
| | | searchMap: { |
| | | crn: "", |
| | | dualCrn: "", |
| | | devp: "", |
| | | rgv: "" |
| | | }, |
| | | selectedIdMap: { |
| | | crn: null, |
| | | dualCrn: null, |
| | | devp: null, |
| | | rgv: null |
| | | }, |
| | | showControlMap: { |
| | | crn: false, |
| | | dualCrn: false, |
| | | devp: false, |
| | | rgv: false |
| | | }, |
| | | controlForms: { |
| | | crn: { crnNo: "", sourceLocNo: "", targetLocNo: "" }, |
| | | dualCrn: { crnNo: "", sourceLocNo: "", targetLocNo: "", station: 1 }, |
| | | devp: { stationId: "", taskNo: "", targetStationId: "" }, |
| | | rgv: { rgvNo: "", sourcePos: "", targetPos: "" } |
| | | }, |
| | | noticeMessage: "", |
| | | noticeType: "info", |
| | | noticeTimer: null |
| | | }; |
| | | }, |
| | | computed: { |
| | | currentSearch() { |
| | | return this.searchMap[this.activeCard] || ""; |
| | | }, |
| | | currentShowControl() { |
| | | return !!this.showControlMap[this.activeCard]; |
| | | }, |
| | | currentList() { |
| | | return this.getListByType(this.activeCard); |
| | | }, |
| | | filteredList() { |
| | | const keyword = String(this.currentSearch || "").trim().toLowerCase(); |
| | | if (!keyword) { return this.currentList; } |
| | | return this.currentList.filter((item) => this.matchesKeyword(this.activeCard, item, keyword)); |
| | | }, |
| | | selectedItem() { |
| | | return this.getSelectedItem(this.activeCard); |
| | | }, |
| | | detailEntries() { |
| | | return this.buildDetailEntries(this.activeCard, this.selectedItem); |
| | | }, |
| | | controlPanelTitle() { |
| | | if (this.activeCard === "crn") { return "堆垛机控制"; } |
| | | if (this.activeCard === "dualCrn") { return "双工位控制"; } |
| | | if (this.activeCard === "devp") { return "输送站控制"; } |
| | | if (this.activeCard === "rgv") { return "RGV控制"; } |
| | | return "控制操作"; |
| | | }, |
| | | controlPanelHint() { |
| | | if (this.activeCard === "crn") { return "先确认设备号,再填写源库位和目标库位。"; } |
| | | if (this.activeCard === "dualCrn") { return "先选择工位,再下发取货、放货或移动指令。"; } |
| | | if (this.activeCard === "devp") { return "用于站点下发、复位和条码维护。"; } |
| | | if (this.activeCard === "rgv") { return "用于轨道车取放货、移动和任务完成。"; } |
| | | return ""; |
| | | }, |
| | | controlTargetText() { |
| | | if (!this.selectedItem) { return "未选中设备"; } |
| | | return "当前目标: " + this.getItemTitle(this.activeCard, this.selectedItem); |
| | | } |
| | | }, |
| | | watch: { |
| | | activeCard: { |
| | | immediate: true, |
| | | handler(type) { |
| | | this.ensureSelection(type); |
| | | } |
| | | }, |
| | | crnList() { this.ensureSelection("crn"); }, |
| | | dualCrnList() { this.ensureSelection("dualCrn"); }, |
| | | stationList() { this.ensureSelection("devp"); }, |
| | | rgvList() { this.ensureSelection("rgv"); }, |
| | | crnParam: { |
| | | deep: true, |
| | | handler(v) { this.applyExternalFocus("crn", v && v.crnNo); } |
| | | }, |
| | | dualCrnParam: { |
| | | deep: true, |
| | | handler(v) { this.applyExternalFocus("dualCrn", v && v.crnNo); } |
| | | }, |
| | | devpParam: { |
| | | deep: true, |
| | | handler(v) { this.applyExternalFocus("devp", v && v.stationId); } |
| | | }, |
| | | rgvParam: { |
| | | deep: true, |
| | | handler(v) { this.applyExternalFocus("rgv", v && v.rgvNo); } |
| | | } |
| | | }, |
| | | beforeDestroy() { |
| | | if (this.noticeTimer) { |
| | | clearTimeout(this.noticeTimer); |
| | | this.noticeTimer = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | changeTab(type) { |
| | | if (type === this.activeCard) { return; } |
| | | this.$emit("change-tab", type); |
| | | }, |
| | | updateSearch(value) { |
| | | this.$set(this.searchMap, this.activeCard, value); |
| | | this.ensureSelection(this.activeCard); |
| | | }, |
| | | refreshCurrent() { |
| | | this.$emit("refresh-request", this.activeCard); |
| | | }, |
| | | toggleControl() { |
| | | this.$set(this.showControlMap, this.activeCard, !this.currentShowControl); |
| | | if (this.currentShowControl && this.selectedItem) { |
| | | this.hydrateControlForm(this.activeCard, this.selectedItem); |
| | | } |
| | | }, |
| | | getListByType(type) { |
| | | if (type === "crn") { return this.crnList || []; } |
| | | if (type === "dualCrn") { return this.dualCrnList || []; } |
| | | if (type === "devp") { return this.stationList || []; } |
| | | if (type === "rgv") { return this.rgvList || []; } |
| | | return []; |
| | | }, |
| | | getItemId(type, item) { |
| | | if (!item) { return null; } |
| | | if (type === "crn" || type === "dualCrn") { return item.crnNo; } |
| | | if (type === "devp") { return item.stationId; } |
| | | if (type === "rgv") { return item.rgvNo; } |
| | | return null; |
| | | }, |
| | | getSelectedItem(type) { |
| | | const list = this.filteredListForType(type); |
| | | if (!list.length) { return null; } |
| | | const selectedId = this.selectedIdMap[type]; |
| | | for (let i = 0; i < list.length; i++) { |
| | | if (String(this.getItemId(type, list[i])) === String(selectedId)) { |
| | | return list[i]; |
| | | } |
| | | } |
| | | return list[0]; |
| | | }, |
| | | filteredListForType(type) { |
| | | const keyword = String(this.searchMap[type] || "").trim().toLowerCase(); |
| | | const list = this.getListByType(type); |
| | | if (!keyword) { return list; } |
| | | return list.filter((item) => this.matchesKeyword(type, item, keyword)); |
| | | }, |
| | | ensureSelection(type) { |
| | | const list = this.filteredListForType(type); |
| | | if (!list.length) { |
| | | this.$set(this.selectedIdMap, type, null); |
| | | return; |
| | | } |
| | | const currentId = this.selectedIdMap[type]; |
| | | const exists = list.some((item) => String(this.getItemId(type, item)) === String(currentId)); |
| | | if (!exists) { |
| | | this.$set(this.selectedIdMap, type, this.getItemId(type, list[0])); |
| | | } |
| | | }, |
| | | applyExternalFocus(type, rawId) { |
| | | if (rawId == null || rawId === "" || rawId === 0) { return; } |
| | | this.$set(this.selectedIdMap, type, rawId); |
| | | if (this.activeCard === type) { |
| | | const item = this.getSelectedItem(type); |
| | | if (item) { this.hydrateControlForm(type, item); } |
| | | } |
| | | }, |
| | | selectItem(type, item) { |
| | | this.$set(this.selectedIdMap, type, this.getItemId(type, item)); |
| | | this.hydrateControlForm(type, item); |
| | | }, |
| | | hydrateControlForm(type, item) { |
| | | if (!item) { return; } |
| | | if (type === "crn") { |
| | | this.controlForms.crn.crnNo = this.orEmpty(item.crnNo); |
| | | } else if (type === "dualCrn") { |
| | | this.controlForms.dualCrn.crnNo = this.orEmpty(item.crnNo); |
| | | } else if (type === "devp") { |
| | | this.controlForms.devp.stationId = this.orEmpty(item.stationId); |
| | | this.controlForms.devp.taskNo = this.orEmpty(item.taskNo); |
| | | this.controlForms.devp.targetStationId = this.orEmpty(item.targetStaNo); |
| | | } else if (type === "rgv") { |
| | | this.controlForms.rgv.rgvNo = this.orEmpty(item.rgvNo); |
| | | } |
| | | }, |
| | | matchesKeyword(type, item, keyword) { |
| | | const fields = []; |
| | | if (type === "crn" || type === "dualCrn") { |
| | | fields.push(item.crnNo, item.taskNo, item.taskNoTwo, item.locNo, item.sourceLocNo, item.status, item.mode); |
| | | } else if (type === "devp") { |
| | | fields.push(item.stationId, item.taskNo, item.targetStaNo, item.barcode, item.errorMsg, item.extend); |
| | | } else if (type === "rgv") { |
| | | fields.push(item.rgvNo, item.taskNo, item.trackSiteNo, item.status, item.mode, item.alarm); |
| | | } |
| | | return fields.some((field) => String(field == null ? "" : field).toLowerCase().indexOf(keyword) !== -1); |
| | | }, |
| | | currentSearchPlaceholder() { |
| | | if (this.activeCard === "crn") { return "搜索堆垛机号"; } |
| | | if (this.activeCard === "dualCrn") { return "搜索双工位堆垛机号"; } |
| | | if (this.activeCard === "devp") { return "搜索站号 / 条码"; } |
| | | if (this.activeCard === "rgv") { return "搜索RGV号"; } |
| | | return "搜索"; |
| | | }, |
| | | getItemTitle(type, item) { |
| | | if (!item) { return "-"; } |
| | | if (type === "crn") { return item.crnNo + "号堆垛机"; } |
| | | if (type === "dualCrn") { return item.crnNo + "号双工位堆垛机"; } |
| | | if (type === "devp") { return item.stationId + "号站点"; } |
| | | if (type === "rgv") { return item.rgvNo + "号RGV"; } |
| | | return "-"; |
| | | }, |
| | | getItemMeta(type, item) { |
| | | if (!item) { return "-"; } |
| | | if (type === "crn") { return "任务 " + this.orDash(item.workNo) + " | 目标 " + this.orDash(item.locNo); } |
| | | if (type === "dualCrn") { return "工位1 " + this.orDash(item.taskNo) + " | 工位2 " + this.orDash(item.taskNoTwo); } |
| | | if (type === "devp") { return "任务 " + this.orDash(item.taskNo) + " | 目标站 " + this.orDash(item.targetStaNo); } |
| | | if (type === "rgv") { return "轨道位 " + this.orDash(item.trackSiteNo) + " | 任务 " + this.orDash(item.taskNo); } |
| | | return "-"; |
| | | }, |
| | | getStatusLabel(type, item) { |
| | | if (!item) { return "未知"; } |
| | | if (type === "devp") { return item.autoing ? "自动" : "手动"; } |
| | | const status = String(item.deviceStatus || "").toUpperCase(); |
| | | if (status === "AUTO") { return "自动"; } |
| | | if (status === "WORKING") { return "作业中"; } |
| | | if (status === "ERROR") { return "故障"; } |
| | | return "离线"; |
| | | }, |
| | | getStatusTone(type, item) { |
| | | const label = this.getStatusLabel(type, item); |
| | | if (label === "自动") { return "success"; } |
| | | if (label === "作业中") { return "working"; } |
| | | if (label === "手动") { return "warning"; } |
| | | if (label === "故障") { return "danger"; } |
| | | return "muted"; |
| | | }, |
| | | isSelected(type, item) { |
| | | return String(this.getItemId(type, item)) === String(this.selectedIdMap[type]); |
| | | }, |
| | | buildDetailEntries(type, item) { |
| | | if (!item) { return []; } |
| | | if (type === "crn") { |
| | | return [ |
| | | { label: "编号", value: this.orDash(item.crnNo) }, |
| | | { label: "工作号", value: this.orDash(item.workNo) }, |
| | | { label: "模式", value: this.orDash(item.mode) }, |
| | | { label: "状态", value: this.orDash(item.status) }, |
| | | { label: "源库位", value: this.orDash(item.sourceLocNo) }, |
| | | { label: "目标库位", value: this.orDash(item.locNo) }, |
| | | { label: "是否有物", value: this.yesNo(item.loading) }, |
| | | { label: "任务接收", value: this.orDash(item.taskReceive) }, |
| | | { label: "列", value: this.orDash(item.bay) }, |
| | | { label: "层", value: this.orDash(item.lev) }, |
| | | { label: "货叉定位", value: this.orDash(item.forkOffset) }, |
| | | { label: "载货台定位", value: this.orDash(item.liftPos) }, |
| | | { label: "走行定位", value: this.orDash(item.walkPos) }, |
| | | { label: "走行速度", value: this.orDash(item.xspeed) }, |
| | | { label: "升降速度", value: this.orDash(item.yspeed) }, |
| | | { label: "叉牙速度", value: this.orDash(item.zspeed) }, |
| | | { label: "称重数据", value: this.orDash(item.weight) }, |
| | | { label: "条码数据", value: this.orDash(item.barcode) }, |
| | | { label: "故障代码", value: this.orDash(item.warnCode) }, |
| | | { label: "故障描述", value: this.orDash(item.alarm) } |
| | | ]; |
| | | } |
| | | if (type === "dualCrn") { |
| | | return [ |
| | | { label: "模式", value: this.orDash(item.mode) }, |
| | | { label: "异常码", value: this.orDash(item.warnCode) }, |
| | | { label: "工位1任务号", value: this.orDash(item.taskNo) }, |
| | | { label: "工位2任务号", value: this.orDash(item.taskNoTwo) }, |
| | | { label: "工位1状态", value: this.orDash(item.status) }, |
| | | { label: "工位2状态", value: this.orDash(item.statusTwo) }, |
| | | { label: "工位1有物", value: this.yesNo(item.loading) }, |
| | | { label: "工位2有物", value: this.yesNo(item.loadingTwo) }, |
| | | { label: "列", value: this.orDash(item.bay) }, |
| | | { label: "层", value: this.orDash(item.lev) }, |
| | | { label: "载货台定位", value: this.orDash(item.liftPos) }, |
| | | { label: "走行定位", value: this.orDash(item.walkPos) }, |
| | | { label: "走行速度", value: this.orDash(item.xspeed) }, |
| | | { label: "升降速度", value: this.orDash(item.yspeed) }, |
| | | { label: "叉牙速度", value: this.orDash(item.zspeed) }, |
| | | { label: "扩展数据", value: this.orDash(item.extend) } |
| | | ]; |
| | | } |
| | | if (type === "devp") { |
| | | return [ |
| | | { label: "编号", value: this.orDash(item.stationId) }, |
| | | { label: "工作号", value: this.orDash(item.taskNo) }, |
| | | { label: "目标站", value: this.orDash(item.targetStaNo) }, |
| | | { label: "模式", value: item.autoing ? "自动" : "手动" }, |
| | | { label: "有物", value: this.yesNo(item.loading) }, |
| | | { label: "可入", value: this.yesNo(item.inEnable) }, |
| | | { label: "可出", value: this.yesNo(item.outEnable) }, |
| | | { label: "空板信号", value: this.yesNo(item.emptyMk) }, |
| | | { label: "满板信号", value: this.yesNo(item.fullPlt) }, |
| | | { label: "运行阻塞", value: this.yesNo(item.runBlock) }, |
| | | { label: "启动入库", value: this.yesNo(item.enableIn) }, |
| | | { label: "托盘高度", value: this.orDash(item.palletHeight) }, |
| | | { label: "条码", value: this.orDash(item.barcode) }, |
| | | { label: "重量", value: this.orDash(item.weight) }, |
| | | { label: "任务可写区", value: this.orDash(item.taskWriteIdx) }, |
| | | { label: "故障代码", value: this.orDash(item.error) }, |
| | | { label: "故障信息", value: this.orDash(item.errorMsg) } |
| | | ]; |
| | | } |
| | | if (type === "rgv") { |
| | | return [ |
| | | { label: "编号", value: this.orDash(item.rgvNo) }, |
| | | { label: "工作号", value: this.orDash(item.taskNo) }, |
| | | { label: "模式", value: this.orDash(item.mode) }, |
| | | { label: "状态", value: this.orDash(item.status) }, |
| | | { label: "轨道位", value: this.orDash(item.trackSiteNo) }, |
| | | { label: "是否有物", value: this.yesNo(item.loading) }, |
| | | { label: "故障代码", value: this.orDash(item.warnCode) }, |
| | | { label: "故障描述", value: this.orDash(item.alarm) }, |
| | | { label: "扩展数据", value: this.orDash(item.extend) } |
| | | ]; |
| | | } |
| | | return []; |
| | | }, |
| | | submitControl(action) { |
| | | const config = this.getControlConfig(this.activeCard, action); |
| | | if (!config) { return; } |
| | | $.ajax({ |
| | | url: baseUrl + config.url, |
| | | headers: { token: localStorage.getItem("token") }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(config.payload), |
| | | success: (res) => { |
| | | if (res && res.code === 200) { |
| | | this.showNotice(res.msg || "操作成功", "success"); |
| | | this.$emit("refresh-request", this.activeCard); |
| | | } else { |
| | | this.showNotice((res && res.msg) || "操作失败", "warning"); |
| | | } |
| | | }, |
| | | error: () => { |
| | | this.showNotice("请求失败", "danger"); |
| | | } |
| | | }); |
| | | }, |
| | | getControlConfig(type, action) { |
| | | if (type === "crn") { |
| | | return { |
| | | url: { transport: "/crn/command/take", move: "/crn/command/move", taskComplete: "/crn/command/taskComplete" }[action], |
| | | payload: this.controlForms.crn |
| | | }; |
| | | } |
| | | if (type === "dualCrn") { |
| | | return { |
| | | url: { |
| | | transport: "/dualcrn/command/take", |
| | | pickup: "/dualcrn/command/pick", |
| | | putdown: "/dualcrn/command/put", |
| | | move: "/dualcrn/command/move", |
| | | taskComplete: "/dualcrn/command/taskComplete" |
| | | }[action], |
| | | payload: this.controlForms.dualCrn |
| | | }; |
| | | } |
| | | if (type === "devp") { |
| | | return { |
| | | url: { move: "/station/command/move", reset: "/station/command/reset" }[action], |
| | | payload: this.controlForms.devp |
| | | }; |
| | | } |
| | | if (type === "rgv") { |
| | | return { |
| | | url: { transport: "/rgv/command/transport", move: "/rgv/command/move", taskComplete: "/rgv/command/taskComplete" }[action], |
| | | payload: this.controlForms.rgv |
| | | }; |
| | | } |
| | | return null; |
| | | }, |
| | | editBarcode() { |
| | | const item = this.selectedItem; |
| | | if (!item || item.stationId == null) { return; } |
| | | const barcode = window.prompt("请输入新的条码值(可留空清空)", item.barcode || ""); |
| | | if (barcode === null) { return; } |
| | | $.ajax({ |
| | | url: baseUrl + "/station/command/barcode", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify({ stationId: item.stationId, barcode: String(barcode).trim() }), |
| | | success: (res) => { |
| | | if (res && res.code === 200) { |
| | | this.showNotice("条码修改成功", "success"); |
| | | this.$emit("refresh-request", "devp"); |
| | | } else { |
| | | this.showNotice((res && res.msg) || "条码修改失败", "warning"); |
| | | } |
| | | }, |
| | | error: () => { this.showNotice("条码修改失败", "danger"); } |
| | | }); |
| | | }, |
| | | editDualTask(station) { |
| | | const item = this.selectedItem; |
| | | if (!item || item.crnNo == null) { return; } |
| | | const currentValue = station === 1 ? item.taskNo : item.taskNoTwo; |
| | | const value = window.prompt("请输入工位" + station + "任务号", currentValue == null ? "" : String(currentValue)); |
| | | if (value === null) { return; } |
| | | if (!/^\d+$/.test(String(value).trim())) { |
| | | this.showNotice("任务号必须是非负整数", "warning"); |
| | | return; |
| | | } |
| | | $.ajax({ |
| | | url: baseUrl + "/dualcrn/command/updateTaskNo", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify({ crnNo: item.crnNo, station: station, taskNo: Number(value) }), |
| | | success: (res) => { |
| | | if (res && res.code === 200) { |
| | | this.showNotice("任务号更新成功", "success"); |
| | | this.$emit("refresh-request", "dualCrn"); |
| | | } else { |
| | | this.showNotice((res && res.msg) || "任务号更新失败", "warning"); |
| | | } |
| | | }, |
| | | error: () => { this.showNotice("任务号更新失败", "danger"); } |
| | | }); |
| | | }, |
| | | showNotice(message, type) { |
| | | this.noticeMessage = message; |
| | | this.noticeType = type || "info"; |
| | | if (this.noticeTimer) { clearTimeout(this.noticeTimer); } |
| | | this.noticeTimer = setTimeout(() => { |
| | | this.noticeMessage = ""; |
| | | this.noticeTimer = null; |
| | | }, 2200); |
| | | }, |
| | | yesNo(value) { |
| | | if (value === true || value === "Y" || value === "y" || value === 1 || value === "1") { return "Y"; } |
| | | if (value === false || value === 0 || value === "0") { return "N"; } |
| | | return value ? "Y" : "N"; |
| | | }, |
| | | orDash(value) { |
| | | return value == null || value === "" ? "-" : String(value); |
| | | }, |
| | | orEmpty(value) { |
| | | return value == null ? "" : String(value); |
| | | } |
| | | } |
| | | }); |
| | |
| | | Vue.component("watch-crn-card", { |
| | | template: ` |
| | | <div> |
| | | <div style="display: flex;margin-bottom: 10px;"> |
| | | <div style="width: 100%;">堆垛机监控</div> |
| | | <div style="width: 100%;text-align: right;display: flex;"><el-input size="mini" v-model="searchCrnNo" placeholder="请输入堆垛机号"></el-input><el-button @click="getCrnStateInfo" size="mini">查询</el-button></div> |
| | | <div class="mc-root"> |
| | | <div class="mc-toolbar"> |
| | | <div class="mc-title">堆垛机监控</div> |
| | | <div class="mc-search"> |
| | | <input class="mc-input" v-model="searchCrnNo" placeholder="请输入堆垛机号" /> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="getCrnStateInfo">查询</button> |
| | | </div> |
| | | <div style="margin-bottom: 10px;" v-if="!readOnly"> |
| | | <div style="margin-bottom: 5px;"> |
| | | <el-button v-if="showControl" @click="openControl" size="mini">关闭控制中心</el-button> |
| | | <el-button v-else @click="openControl" size="mini">打开控制中心</el-button> |
| | | </div> |
| | | |
| | | <div v-if="!readOnly" class="mc-control-toggle"> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="openControl"> |
| | | {{ showControl ? '收起控制中心' : '打开控制中心' }} |
| | | </button> |
| | | </div> |
| | | |
| | | <div v-if="showControl" class="mc-control"> |
| | | <div class="mc-control-grid"> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">堆垛机号</span> |
| | | <input class="mc-input" v-model="controlParam.crnNo" placeholder="例如 1" /> |
| | | </label> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">源库位</span> |
| | | <input class="mc-input" v-model="controlParam.sourceLocNo" placeholder="输入源点" /> |
| | | </label> |
| | | <label class="mc-field mc-span-2"> |
| | | <span class="mc-field-label">目标库位</span> |
| | | <input class="mc-input" v-model="controlParam.targetLocNo" placeholder="输入目标点" /> |
| | | </label> |
| | | <div class="mc-action-row"> |
| | | <button type="button" class="mc-btn" @click="controlCommandTransport">取放货</button> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandMove">移动</button> |
| | | <button type="button" class="mc-btn mc-btn-soft" @click="controlCommandTaskComplete">任务完成</button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mc-collapse"> |
| | | <div |
| | | v-for="item in displayCrnList" |
| | | :key="item.crnNo" |
| | | :class="['mc-item', { 'is-open': isActive(item.crnNo) }]" |
| | | > |
| | | <button type="button" class="mc-head" @click="toggleItem(item)"> |
| | | <div class="mc-head-main"> |
| | | <div class="mc-head-title">{{ item.crnNo }}号堆垛机</div> |
| | | <div class="mc-head-subtitle">工作号 {{ orDash(item.workNo) }} | 目标 {{ orDash(item.locNo) }}</div> |
| | | </div> |
| | | <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;"> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.crnNo" placeholder="堆垛机号"></el-input></div> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.sourceLocNo" placeholder="源点"></el-input></div> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetLocNo" placeholder="目标点"></el-input></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandTransport()" size="mini">取放货</el-button></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandMove()" size="mini">移动</el-button></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandTaskComplete()" size="mini">任务完成</el-button></div> |
| | | <div class="mc-head-right"> |
| | | <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span> |
| | | <span class="mc-chevron">{{ isActive(item.crnNo) ? '▾' : '▸' }}</span> |
| | | </div> |
| | | </button> |
| | | |
| | | <div v-if="isActive(item.crnNo)" class="mc-body"> |
| | | <div class="mc-detail-grid"> |
| | | <div v-for="entry in buildDetailEntries(item)" :key="entry.label" class="mc-detail-cell"> |
| | | <div class="mc-detail-label">{{ entry.label }}</div> |
| | | <div class="mc-detail-value">{{ entry.value }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div style="max-height: 55vh; overflow:auto;"> |
| | | <el-collapse v-model="activeNames" accordion> |
| | | <el-collapse-item v-for="(item) in displayCrnList" :name="item.crnNo"> |
| | | <template slot="title"> |
| | | <div style="width: 100%;display: flex;"> |
| | | <div style="width: 50%;">{{ item.crnNo }}号堆垛机</div> |
| | | <div style="width: 50%;text-align: right;"> |
| | | <el-tag v-if="item.deviceStatus == 'AUTO'" type="success" size="small">自动</el-tag> |
| | | <el-tag v-else-if="item.deviceStatus == 'WORKING'" size="small">作业中</el-tag> |
| | | <el-tag v-else-if="item.deviceStatus == 'ERROR'" type="danger" size="small">故障</el-tag> |
| | | <el-tag v-else type="warning" size="small">离线</el-tag> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <el-descriptions border direction="vertical"> |
| | | <el-descriptions-item label="编号">{{ item.crnNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="工作号">{{ item.workNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="模式">{{ item.mode }}</el-descriptions-item> |
| | | <el-descriptions-item label="状态">{{ item.status }}</el-descriptions-item> |
| | | <el-descriptions-item label="源库位">{{ item.sourceLocNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="目标库位">{{ item.locNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="是否有物">{{ item.loading }}</el-descriptions-item> |
| | | <el-descriptions-item label="任务接收">{{ item.taskReceive }}</el-descriptions-item> |
| | | <el-descriptions-item label="列">{{ item.bay }}</el-descriptions-item> |
| | | <el-descriptions-item label="层">{{ item.lev }}</el-descriptions-item> |
| | | <el-descriptions-item label="货叉定位">{{ item.forkOffset }}</el-descriptions-item> |
| | | <el-descriptions-item label="载货台定位">{{ item.liftPos }}</el-descriptions-item> |
| | | <el-descriptions-item label="走行在定位">{{ item.walkPos }}</el-descriptions-item> |
| | | <el-descriptions-item label="走行速度(m/min)">{{ item.xspeed }}</el-descriptions-item> |
| | | <el-descriptions-item label="升降速度(m/min)">{{ item.yspeed }}</el-descriptions-item> |
| | | <el-descriptions-item label="叉牙速度(m/min)">{{ item.zspeed }}</el-descriptions-item> |
| | | <el-descriptions-item label="走行距离(Km)">{{ item.xdistance }}</el-descriptions-item> |
| | | <el-descriptions-item label="升降距离(Km)">{{ item.ydistance }}</el-descriptions-item> |
| | | <el-descriptions-item label="走行时长(H)">{{ item.xduration }}</el-descriptions-item> |
| | | <el-descriptions-item label="升降时长(H)">{{ item.yduration }}</el-descriptions-item> |
| | | <el-descriptions-item label="称重数据">{{ item.weight }}</el-descriptions-item> |
| | | <el-descriptions-item label="条码数据">{{ item.barcode }}</el-descriptions-item> |
| | | <el-descriptions-item label="故障代码">{{ item.warnCode }}</el-descriptions-item> |
| | | <el-descriptions-item label="故障描述">{{ item.alarm }}</el-descriptions-item> |
| | | <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-collapse-item> |
| | | </el-collapse> |
| | | </div> |
| | | <div style="display:flex; justify-content:flex-end; margin-top:8px;"> |
| | | <el-pagination |
| | | @current-change="handlePageChange" |
| | | @size-change="handleSizeChange" |
| | | :current-page="currentPage" |
| | | :page-size="pageSize" |
| | | :page-sizes="[10,20,50,100]" |
| | | layout="total, prev, pager, next" |
| | | :total="crnList.length"> |
| | | </el-pagination> |
| | | </div> |
| | | |
| | | <div v-if="displayCrnList.length === 0" class="mc-empty">当前没有可展示的堆垛机数据</div> |
| | | </div> |
| | | |
| | | <div class="mc-footer"> |
| | | <button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">上一页</button> |
| | | <span>{{ currentPage }} / {{ totalPages }}</span> |
| | | <button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">下一页</button> |
| | | </div> |
| | | </div> |
| | | `, |
| | | `, |
| | | props: { |
| | | param: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | }, |
| | | autoRefresh: { |
| | | type: Boolean, |
| | | default: true |
| | | }, |
| | | readOnly: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | param: { type: Object, default: function () { return {}; } }, |
| | | items: { type: Array, default: null }, |
| | | autoRefresh: { type: Boolean, default: true }, |
| | | readOnly: { type: Boolean, default: false } |
| | | }, |
| | | data() { |
| | | data: function () { |
| | | return { |
| | | crnList: [], |
| | | activeNames: "", |
| | |
| | | controlParam: { |
| | | crnNo: "", |
| | | sourceLocNo: "", |
| | | targetLocNo: "", |
| | | targetLocNo: "" |
| | | }, |
| | | pageSize: 25, |
| | | pageSize: 12, |
| | | currentPage: 1, |
| | | timer: null |
| | | }; |
| | | }, |
| | | created() { |
| | | if (this.autoRefresh) { |
| | | this.timer = setInterval(() => { |
| | | this.getCrnStateInfo(); |
| | | }, 1000); |
| | | } |
| | | }, |
| | | beforeDestroy() { |
| | | if (this.timer) { |
| | | clearInterval(this.timer); |
| | | } |
| | | }, |
| | | computed: { |
| | | displayCrnList() { |
| | | const start = (this.currentPage - 1) * this.pageSize; |
| | | const end = start + this.pageSize; |
| | | return this.crnList.slice(start, end); |
| | | sourceList: function () { |
| | | return Array.isArray(this.items) ? this.items : this.crnList; |
| | | }, |
| | | filteredCrnList: function () { |
| | | var keyword = String(this.searchCrnNo || "").trim(); |
| | | if (!keyword) { |
| | | return this.sourceList; |
| | | } |
| | | return this.sourceList.filter(function (item) { |
| | | return String(item.crnNo) === keyword; |
| | | }); |
| | | }, |
| | | displayCrnList: function () { |
| | | var start = (this.currentPage - 1) * this.pageSize; |
| | | return this.filteredCrnList.slice(start, start + this.pageSize); |
| | | }, |
| | | totalPages: function () { |
| | | return Math.max(1, Math.ceil(this.filteredCrnList.length / this.pageSize) || 1); |
| | | } |
| | | }, |
| | | watch: { |
| | | param: { |
| | | handler(newVal, oldVal) { |
| | | if (newVal && newVal.crnNo && newVal.crnNo != 0) { |
| | | this.activeNames = newVal.crnNo; |
| | | this.searchCrnNo = newVal.crnNo; |
| | | const idx = this.crnList.findIndex(i => i.crnNo == newVal.crnNo); |
| | | if (idx >= 0) { this.currentPage = Math.floor(idx / this.pageSize) + 1; } |
| | | } |
| | | }, |
| | | deep: true, // 深度监听嵌套属性 |
| | | immediate: true, // 立即触发一次(可选) |
| | | items: function () { |
| | | this.afterDataRefresh(); |
| | | }, |
| | | param: { |
| | | deep: true, |
| | | immediate: true, |
| | | handler: function (newVal) { |
| | | if (newVal && newVal.crnNo && newVal.crnNo !== 0) { |
| | | this.focusCrn(newVal.crnNo); |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | created: function () { |
| | | MonitorCardKit.ensureStyles(); |
| | | if (this.autoRefresh) { |
| | | this.timer = setInterval(this.getCrnStateInfo, 1000); |
| | | } |
| | | }, |
| | | beforeDestroy: function () { |
| | | if (this.timer) { |
| | | clearInterval(this.timer); |
| | | this.timer = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | handlePageChange(page) { |
| | | orDash: function (value) { |
| | | return MonitorCardKit.orDash(value); |
| | | }, |
| | | getStatusLabel: function (item) { |
| | | return MonitorCardKit.deviceStatusLabel(item && item.deviceStatus); |
| | | }, |
| | | getStatusTone: function (item) { |
| | | return MonitorCardKit.statusTone(this.getStatusLabel(item)); |
| | | }, |
| | | isActive: function (crnNo) { |
| | | return String(this.activeNames) === String(crnNo); |
| | | }, |
| | | toggleItem: function (item) { |
| | | var next = String(item.crnNo); |
| | | this.activeNames = this.activeNames === next ? "" : next; |
| | | }, |
| | | focusCrn: function (crnNo) { |
| | | this.searchCrnNo = String(crnNo); |
| | | var index = this.filteredCrnList.findIndex(function (item) { |
| | | return String(item.crnNo) === String(crnNo); |
| | | }); |
| | | if (index >= 0) { |
| | | this.currentPage = Math.floor(index / this.pageSize) + 1; |
| | | } else { |
| | | this.currentPage = 1; |
| | | } |
| | | this.activeNames = String(crnNo); |
| | | }, |
| | | afterDataRefresh: function () { |
| | | if (this.currentPage > this.totalPages) { |
| | | this.currentPage = this.totalPages; |
| | | } |
| | | if (this.activeNames) { |
| | | var exists = this.filteredCrnList.some(function (item) { |
| | | return String(item.crnNo) === String(this.activeNames); |
| | | }, this); |
| | | if (!exists) { |
| | | this.activeNames = ""; |
| | | } |
| | | } |
| | | }, |
| | | handlePageChange: function (page) { |
| | | if (page < 1 || page > this.totalPages) { |
| | | return; |
| | | } |
| | | this.currentPage = page; |
| | | }, |
| | | handleSizeChange(size) { |
| | | this.pageSize = size; |
| | | this.currentPage = 1; |
| | | }, |
| | | getCrnStateInfo() { |
| | | if (this.$root.sendWs) { |
| | | getCrnStateInfo: function () { |
| | | if (this.$root && this.$root.sendWs) { |
| | | this.$root.sendWs(JSON.stringify({ |
| | | "url": "/crn/table/crn/state", |
| | | "data": {} |
| | | url: "/crn/table/crn/state", |
| | | data: {} |
| | | })); |
| | | } |
| | | }, |
| | | setCrnList(res) { |
| | | let that = this; |
| | | if (res.code == 200) { |
| | | let list = res.data; |
| | | |
| | | if (that.searchCrnNo == "") { |
| | | that.crnList = list; |
| | | } else { |
| | | let tmp = []; |
| | | list.forEach((item) => { |
| | | if (item.crnNo == that.searchCrnNo) { |
| | | tmp.push(item); |
| | | } |
| | | }); |
| | | that.crnList = tmp; |
| | | that.currentPage = 1; |
| | | } |
| | | setCrnList: function (res) { |
| | | if (res && res.code === 200) { |
| | | this.crnList = res.data || []; |
| | | this.afterDataRefresh(); |
| | | } |
| | | }, |
| | | openControl() { |
| | | openControl: function () { |
| | | this.showControl = !this.showControl; |
| | | }, |
| | | controlCommandTransport() { |
| | | let that = this; |
| | | //取放货 |
| | | buildDetailEntries: function (item) { |
| | | return [ |
| | | { label: "编号", value: this.orDash(item.crnNo) }, |
| | | { label: "工作号", value: this.orDash(item.workNo) }, |
| | | { label: "模式", value: this.orDash(item.mode) }, |
| | | { label: "状态", value: this.orDash(item.status) }, |
| | | { label: "源库位", value: this.orDash(item.sourceLocNo) }, |
| | | { label: "目标库位", value: this.orDash(item.locNo) }, |
| | | { label: "是否有物", value: MonitorCardKit.yesNo(item.loading) }, |
| | | { label: "任务接收", value: this.orDash(item.taskReceive) }, |
| | | { label: "列", value: this.orDash(item.bay) }, |
| | | { label: "层", value: this.orDash(item.lev) }, |
| | | { label: "货叉定位", value: this.orDash(item.forkOffset) }, |
| | | { label: "载货台定位", value: this.orDash(item.liftPos) }, |
| | | { label: "走行定位", value: this.orDash(item.walkPos) }, |
| | | { label: "走行速度", value: this.orDash(item.xspeed) }, |
| | | { label: "升降速度", value: this.orDash(item.yspeed) }, |
| | | { label: "叉牙速度", value: this.orDash(item.zspeed) }, |
| | | { label: "称重数据", value: this.orDash(item.weight) }, |
| | | { label: "条码数据", value: this.orDash(item.barcode) }, |
| | | { label: "故障代码", value: this.orDash(item.warnCode) }, |
| | | { label: "故障描述", value: this.orDash(item.alarm) }, |
| | | { label: "扩展数据", value: this.orDash(item.extend) } |
| | | ]; |
| | | }, |
| | | postControl: function (url, payload) { |
| | | $.ajax({ |
| | | url: baseUrl + "/crn/command/take", |
| | | url: baseUrl + url, |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | token: localStorage.getItem("token") |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | data: JSON.stringify(payload), |
| | | success: function (res) { |
| | | if (res && res.code === 200) { |
| | | MonitorCardKit.showMessage(this, res.msg || "操作成功", "success"); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | MonitorCardKit.showMessage(this, (res && res.msg) || "操作失败", "warning"); |
| | | } |
| | | }, |
| | | }.bind(this) |
| | | }); |
| | | }, |
| | | controlCommandMove() { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/crn/command/move", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | controlCommandTransport: function () { |
| | | this.postControl("/crn/command/take", this.controlParam); |
| | | }, |
| | | controlCommandTaskComplete() { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/crn/command/taskComplete", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | controlCommandMove: function () { |
| | | this.postControl("/crn/command/move", this.controlParam); |
| | | }, |
| | | }, |
| | | controlCommandTaskComplete: function () { |
| | | this.postControl("/crn/command/taskComplete", this.controlParam); |
| | | } |
| | | } |
| | | }); |
| | |
| | | Vue.component("watch-dual-crn-card", { |
| | | template: ` |
| | | <div> |
| | | <div style="display: flex;margin-bottom: 10px;"> |
| | | <div style="width: 100%;">双工位堆垛机监控</div> |
| | | <div style="width: 100%;text-align: right;display: flex;"> |
| | | <el-input size="mini" v-model="searchCrnNo" placeholder="请输入堆垛机号"></el-input> |
| | | <el-button @click="getDualCrnStateInfo" size="mini">查询</el-button> |
| | | </div> |
| | | <div class="mc-root"> |
| | | <div class="mc-toolbar"> |
| | | <div class="mc-title">双工位堆垛机监控</div> |
| | | <div class="mc-search"> |
| | | <input class="mc-input" v-model="searchCrnNo" placeholder="请输入堆垛机号" /> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="getDualCrnStateInfo">查询</button> |
| | | </div> |
| | | <div style="margin-bottom: 10px;" v-if="!readOnly"> |
| | | <div style="margin-bottom: 5px;"> |
| | | <el-button v-if="showControl" @click="openControl" size="mini">关闭控制中心</el-button> |
| | | <el-button v-else @click="openControl" size="mini">打开控制中心</el-button> |
| | | </div> |
| | | <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;"> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.crnNo" placeholder="堆垛机号"></el-input></div> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.sourceLocNo" placeholder="源点"></el-input></div> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetLocNo" placeholder="目标点"></el-input></div> |
| | | <div style="margin-bottom: 10px;width: 33%;"> |
| | | <el-select size="mini" v-model="controlParam.station" placeholder="工位"> |
| | | <el-option :label="'工位1'" :value="1"></el-option> |
| | | <el-option :label="'工位2'" :value="2"></el-option> |
| | | </el-select> |
| | | </div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandTransport()" size="mini">取放货</el-button></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandPickup()" size="mini">取货</el-button></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandPutdown()" size="mini">放货</el-button></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandMove()" size="mini">移动</el-button></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandTaskComplete()" size="mini">任务完成</el-button></div> |
| | | </div> |
| | | |
| | | <div v-if="!readOnly" class="mc-control-toggle"> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="openControl"> |
| | | {{ showControl ? '收起控制中心' : '打开控制中心' }} |
| | | </button> |
| | | </div> |
| | | |
| | | <div v-if="showControl" class="mc-control"> |
| | | <div class="mc-control-grid"> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">堆垛机号</span> |
| | | <input class="mc-input" v-model="controlParam.crnNo" placeholder="例如 2" /> |
| | | </label> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">工位</span> |
| | | <select class="mc-select" v-model="controlParam.station"> |
| | | <option :value="1">工位1</option> |
| | | <option :value="2">工位2</option> |
| | | </select> |
| | | </label> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">源库位</span> |
| | | <input class="mc-input" v-model="controlParam.sourceLocNo" placeholder="输入源点" /> |
| | | </label> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">目标库位</span> |
| | | <input class="mc-input" v-model="controlParam.targetLocNo" placeholder="输入目标点" /> |
| | | </label> |
| | | <div class="mc-action-row"> |
| | | <button type="button" class="mc-btn" @click="controlCommandTransport">取放货</button> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandPickup">取货</button> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandPutdown">放货</button> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandMove">移动</button> |
| | | <button type="button" class="mc-btn mc-btn-soft" @click="controlCommandTaskComplete">任务完成</button> |
| | | </div> |
| | | </div> |
| | | <div style="max-height: 55vh; overflow:auto;"> |
| | | <el-collapse v-model="activeNames" accordion> |
| | | <el-collapse-item v-for="(item) in displayCrnList" :name="item.crnNo"> |
| | | <template slot="title"> |
| | | <div style="width: 100%;display: flex;"> |
| | | <div style="width: 50%;">{{ item.crnNo }}号双工位堆垛机</div> |
| | | <div style="width: 50%;text-align: right;"> |
| | | <el-tag v-if="item.deviceStatus == 'AUTO'" type="success" size="small">自动</el-tag> |
| | | <el-tag v-else-if="item.deviceStatus == 'WORKING'" size="small">作业中</el-tag> |
| | | <el-tag v-else-if="item.deviceStatus == 'ERROR'" type="danger" size="small">故障</el-tag> |
| | | <el-tag v-else type="warning" size="small">离线</el-tag> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <el-descriptions border direction="vertical"> |
| | | <el-descriptions-item label="模式">{{ item.mode }}</el-descriptions-item> |
| | | <el-descriptions-item label="异常码">{{ item.warnCode }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位1任务号"> |
| | | <span v-if="readOnly">{{ item.taskNo }}</span> |
| | | <el-button |
| | | v-else |
| | | type="text" |
| | | size="mini" |
| | | style="padding:0;" |
| | | @click.stop="editTaskNo(item, 1)" |
| | | >{{ item.taskNo }}</el-button> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="工位2任务号"> |
| | | <span v-if="readOnly">{{ item.taskNoTwo }}</span> |
| | | <el-button |
| | | v-else |
| | | type="text" |
| | | size="mini" |
| | | style="padding:0;" |
| | | @click.stop="editTaskNo(item, 2)" |
| | | >{{ item.taskNoTwo }}</el-button> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="设备工位1任务号">{{ item.deviceTaskNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="设备工位2任务号">{{ item.deviceTaskNoTwo }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位1状态">{{ item.status }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位2状态">{{ item.statusTwo }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位1是否有物">{{ item.loading }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位2是否有物">{{ item.loadingTwo }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位1货叉定位">{{ item.forkOffset }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位2货叉定位">{{ item.forkOffsetTwo }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位1任务接收">{{ item.taskReceive }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位2任务接收">{{ item.taskReceiveTwo }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位1下发数据">{{ item.taskSend }}</el-descriptions-item> |
| | | <el-descriptions-item label="工位2下发数据">{{ item.taskSendTwo }}</el-descriptions-item> |
| | | <el-descriptions-item label="列">{{ item.bay }}</el-descriptions-item> |
| | | <el-descriptions-item label="层">{{ item.lev }}</el-descriptions-item> |
| | | <el-descriptions-item label="载货台定位">{{ item.liftPos }}</el-descriptions-item> |
| | | <el-descriptions-item label="走行在定位">{{ item.walkPos }}</el-descriptions-item> |
| | | <el-descriptions-item label="走行速度(m/min)">{{ item.xspeed }}</el-descriptions-item> |
| | | <el-descriptions-item label="升降速度(m/min)">{{ item.yspeed }}</el-descriptions-item> |
| | | <el-descriptions-item label="叉牙速度(m/min)">{{ item.zspeed }}</el-descriptions-item> |
| | | <el-descriptions-item label="走行距离(Km)">{{ item.xdistance }}</el-descriptions-item> |
| | | <el-descriptions-item label="升降距离(Km)">{{ item.ydistance }}</el-descriptions-item> |
| | | <el-descriptions-item label="走行时长(H)">{{ item.xduration }}</el-descriptions-item> |
| | | <el-descriptions-item label="升降时长(H)">{{ item.yduration }}</el-descriptions-item> |
| | | <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-collapse-item> |
| | | </el-collapse> |
| | | </div> |
| | | |
| | | <div class="mc-collapse"> |
| | | <div |
| | | v-for="item in displayCrnList" |
| | | :key="item.crnNo" |
| | | :class="['mc-item', { 'is-open': isActive(item.crnNo) }]" |
| | | > |
| | | <button type="button" class="mc-head" @click="toggleItem(item)"> |
| | | <div class="mc-head-main"> |
| | | <div class="mc-head-title">{{ item.crnNo }}号双工位堆垛机</div> |
| | | <div class="mc-head-subtitle">工位1 {{ orDash(item.taskNo) }} | 工位2 {{ orDash(item.taskNoTwo) }}</div> |
| | | </div> |
| | | <div class="mc-head-right"> |
| | | <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span> |
| | | <span class="mc-chevron">{{ isActive(item.crnNo) ? '▾' : '▸' }}</span> |
| | | </div> |
| | | </button> |
| | | |
| | | <div v-if="isActive(item.crnNo)" class="mc-body"> |
| | | <div class="mc-inline-actions" v-if="!readOnly"> |
| | | <button type="button" class="mc-link" @click.stop="editTaskNo(item, 1)">编辑工位1任务号</button> |
| | | <button type="button" class="mc-link" @click.stop="editTaskNo(item, 2)">编辑工位2任务号</button> |
| | | </div> |
| | | <div class="mc-detail-grid"> |
| | | <div v-for="entry in buildDetailEntries(item)" :key="entry.label" class="mc-detail-cell"> |
| | | <div class="mc-detail-label">{{ entry.label }}</div> |
| | | <div class="mc-detail-value">{{ entry.value }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div style="display:flex; justify-content:flex-end; margin-top:8px;"> |
| | | <el-pagination |
| | | @current-change="handlePageChange" |
| | | @size-change="handleSizeChange" |
| | | :current-page="currentPage" |
| | | :page-size="pageSize" |
| | | :page-sizes="[10,20,50,100]" |
| | | layout="total, prev, pager, next" |
| | | :total="crnList.length"> |
| | | </el-pagination> |
| | | </div> |
| | | |
| | | <div v-if="displayCrnList.length === 0" class="mc-empty">当前没有可展示的双工位堆垛机数据</div> |
| | | </div> |
| | | |
| | | <div class="mc-footer"> |
| | | <button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">上一页</button> |
| | | <span>{{ currentPage }} / {{ totalPages }}</span> |
| | | <button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">下一页</button> |
| | | </div> |
| | | </div> |
| | | `, |
| | | props: { |
| | | param: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | }, |
| | | autoRefresh: { |
| | | type: Boolean, |
| | | default: true |
| | | }, |
| | | readOnly: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | param: { type: Object, default: function () { return {}; } }, |
| | | items: { type: Array, default: null }, |
| | | autoRefresh: { type: Boolean, default: true }, |
| | | readOnly: { type: Boolean, default: false } |
| | | }, |
| | | data() { |
| | | data: function () { |
| | | return { |
| | | crnList: [], |
| | | activeNames: "", |
| | |
| | | crnNo: "", |
| | | sourceLocNo: "", |
| | | targetLocNo: "", |
| | | station: 1, |
| | | station: 1 |
| | | }, |
| | | pageSize: 25, |
| | | pageSize: 12, |
| | | currentPage: 1, |
| | | timer: null |
| | | }; |
| | | }, |
| | | created() { |
| | | if (this.autoRefresh) { |
| | | this.timer = setInterval(() => { |
| | | this.getDualCrnStateInfo(); |
| | | }, 1000); |
| | | } |
| | | }, |
| | | beforeDestroy() { |
| | | if (this.timer) { |
| | | clearInterval(this.timer); |
| | | } |
| | | }, |
| | | computed: { |
| | | displayCrnList() { |
| | | const start = (this.currentPage - 1) * this.pageSize; |
| | | const end = start + this.pageSize; |
| | | return this.crnList.slice(start, end); |
| | | sourceList: function () { |
| | | return Array.isArray(this.items) ? this.items : this.crnList; |
| | | }, |
| | | filteredCrnList: function () { |
| | | var keyword = String(this.searchCrnNo || "").trim(); |
| | | if (!keyword) { |
| | | return this.sourceList; |
| | | } |
| | | return this.sourceList.filter(function (item) { |
| | | return String(item.crnNo) === keyword; |
| | | }); |
| | | }, |
| | | displayCrnList: function () { |
| | | var start = (this.currentPage - 1) * this.pageSize; |
| | | return this.filteredCrnList.slice(start, start + this.pageSize); |
| | | }, |
| | | totalPages: function () { |
| | | return Math.max(1, Math.ceil(this.filteredCrnList.length / this.pageSize) || 1); |
| | | } |
| | | }, |
| | | watch: { |
| | | items: function () { |
| | | this.afterDataRefresh(); |
| | | }, |
| | | param: { |
| | | handler(newVal) { |
| | | if (newVal && newVal.crnNo && newVal.crnNo != 0) { |
| | | this.activeNames = newVal.crnNo; |
| | | this.searchCrnNo = newVal.crnNo; |
| | | const idx = this.crnList.findIndex(i => i.crnNo == newVal.crnNo); |
| | | if (idx >= 0) { this.currentPage = Math.floor(idx / this.pageSize) + 1; } |
| | | } |
| | | }, |
| | | deep: true, |
| | | immediate: true, |
| | | }, |
| | | handler: function (newVal) { |
| | | if (newVal && newVal.crnNo && newVal.crnNo !== 0) { |
| | | this.focusCrn(newVal.crnNo); |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | created: function () { |
| | | MonitorCardKit.ensureStyles(); |
| | | if (this.autoRefresh) { |
| | | this.timer = setInterval(this.getDualCrnStateInfo, 1000); |
| | | } |
| | | }, |
| | | beforeDestroy: function () { |
| | | if (this.timer) { |
| | | clearInterval(this.timer); |
| | | this.timer = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | handlePageChange(page) { |
| | | orDash: function (value) { |
| | | return MonitorCardKit.orDash(value); |
| | | }, |
| | | getStatusLabel: function (item) { |
| | | return MonitorCardKit.deviceStatusLabel(item && item.deviceStatus); |
| | | }, |
| | | getStatusTone: function (item) { |
| | | return MonitorCardKit.statusTone(this.getStatusLabel(item)); |
| | | }, |
| | | isActive: function (crnNo) { |
| | | return String(this.activeNames) === String(crnNo); |
| | | }, |
| | | toggleItem: function (item) { |
| | | var next = String(item.crnNo); |
| | | this.activeNames = this.activeNames === next ? "" : next; |
| | | }, |
| | | focusCrn: function (crnNo) { |
| | | this.searchCrnNo = String(crnNo); |
| | | var index = this.filteredCrnList.findIndex(function (item) { |
| | | return String(item.crnNo) === String(crnNo); |
| | | }); |
| | | this.currentPage = index >= 0 ? Math.floor(index / this.pageSize) + 1 : 1; |
| | | this.activeNames = String(crnNo); |
| | | }, |
| | | afterDataRefresh: function () { |
| | | if (this.currentPage > this.totalPages) { |
| | | this.currentPage = this.totalPages; |
| | | } |
| | | if (this.activeNames) { |
| | | var exists = this.filteredCrnList.some(function (item) { |
| | | return String(item.crnNo) === String(this.activeNames); |
| | | }, this); |
| | | if (!exists) { |
| | | this.activeNames = ""; |
| | | } |
| | | } |
| | | }, |
| | | handlePageChange: function (page) { |
| | | if (page < 1 || page > this.totalPages) { |
| | | return; |
| | | } |
| | | this.currentPage = page; |
| | | }, |
| | | handleSizeChange(size) { |
| | | this.pageSize = size; |
| | | this.currentPage = 1; |
| | | getDualCrnStateInfo: function () { |
| | | if (this.$root && this.$root.sendWs) { |
| | | this.$root.sendWs(JSON.stringify({ |
| | | url: "/dualcrn/table/crn/state", |
| | | data: {} |
| | | })); |
| | | } |
| | | }, |
| | | openControl() { |
| | | setDualCrnList: function (res) { |
| | | if (res && res.code === 200) { |
| | | this.crnList = res.data || []; |
| | | this.afterDataRefresh(); |
| | | } |
| | | }, |
| | | openControl: function () { |
| | | this.showControl = !this.showControl; |
| | | }, |
| | | editTaskNo(item, station) { |
| | | let that = this; |
| | | const isStationOne = station === 1; |
| | | const fieldName = isStationOne ? "taskNo" : "taskNoTwo"; |
| | | const stationName = isStationOne ? "工位1" : "工位2"; |
| | | const currentTaskNo = item[fieldName] == null ? "" : String(item[fieldName]); |
| | | that.$prompt("请输入" + stationName + "任务号", "编辑任务号", { |
| | | buildDetailEntries: function (item) { |
| | | return [ |
| | | { label: "模式", value: this.orDash(item.mode) }, |
| | | { label: "异常码", value: this.orDash(item.warnCode) }, |
| | | { label: "工位1任务号", value: this.orDash(item.taskNo) }, |
| | | { label: "工位2任务号", value: this.orDash(item.taskNoTwo) }, |
| | | { label: "设备工位1任务号", value: this.orDash(item.deviceTaskNo) }, |
| | | { label: "设备工位2任务号", value: this.orDash(item.deviceTaskNoTwo) }, |
| | | { label: "工位1状态", value: this.orDash(item.status) }, |
| | | { label: "工位2状态", value: this.orDash(item.statusTwo) }, |
| | | { label: "工位1是否有物", value: MonitorCardKit.yesNo(item.loading) }, |
| | | { label: "工位2是否有物", value: MonitorCardKit.yesNo(item.loadingTwo) }, |
| | | { label: "工位1货叉定位", value: this.orDash(item.forkOffset) }, |
| | | { label: "工位2货叉定位", value: this.orDash(item.forkOffsetTwo) }, |
| | | { label: "工位1任务接收", value: this.orDash(item.taskReceive) }, |
| | | { label: "工位2任务接收", value: this.orDash(item.taskReceiveTwo) }, |
| | | { label: "工位1下发数据", value: this.orDash(item.taskSend) }, |
| | | { label: "工位2下发数据", value: this.orDash(item.taskSendTwo) }, |
| | | { label: "列", value: this.orDash(item.bay) }, |
| | | { label: "层", value: this.orDash(item.lev) }, |
| | | { label: "载货台定位", value: this.orDash(item.liftPos) }, |
| | | { label: "走行定位", value: this.orDash(item.walkPos) }, |
| | | { label: "走行速度", value: this.orDash(item.xspeed) }, |
| | | { label: "升降速度", value: this.orDash(item.yspeed) }, |
| | | { label: "叉牙速度", value: this.orDash(item.zspeed) }, |
| | | { label: "扩展数据", value: this.orDash(item.extend) } |
| | | ]; |
| | | }, |
| | | postControl: function (url, payload) { |
| | | $.ajax({ |
| | | url: baseUrl + url, |
| | | headers: { |
| | | token: localStorage.getItem("token") |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(payload), |
| | | success: function (res) { |
| | | if (res && res.code === 200) { |
| | | MonitorCardKit.showMessage(this, res.msg || "操作成功", "success"); |
| | | } else { |
| | | MonitorCardKit.showMessage(this, (res && res.msg) || "操作失败", "warning"); |
| | | } |
| | | }.bind(this) |
| | | }); |
| | | }, |
| | | editTaskNo: function (item, station) { |
| | | var currentValue = station === 1 ? item.taskNo : item.taskNoTwo; |
| | | this.$prompt("请输入工位" + station + "任务号", "编辑任务号", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | inputValue: currentTaskNo, |
| | | inputValue: currentValue == null ? "" : String(currentValue), |
| | | inputPattern: /^\d+$/, |
| | | inputErrorMessage: "任务号必须是非负整数", |
| | | }).then(({ value }) => { |
| | | const taskNo = Number(value); |
| | | inputErrorMessage: "任务号必须是非负整数" |
| | | }).then(function (result) { |
| | | $.ajax({ |
| | | url: baseUrl + "/dualcrn/command/updateTaskNo", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | token: localStorage.getItem("token") |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify({ |
| | | crnNo: item.crnNo, |
| | | station: station, |
| | | taskNo: taskNo, |
| | | taskNo: Number(result.value) |
| | | }), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | item[fieldName] = taskNo; |
| | | that.$message({ |
| | | message: stationName + "任务号更新成功", |
| | | type: "success", |
| | | }); |
| | | that.getDualCrnStateInfo(); |
| | | success: function (res) { |
| | | if (res && res.code === 200) { |
| | | MonitorCardKit.showMessage(this, "任务号更新成功", "success"); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | MonitorCardKit.showMessage(this, (res && res.msg) || "任务号更新失败", "warning"); |
| | | } |
| | | }, |
| | | }.bind(this) |
| | | }); |
| | | }).catch(() => {}); |
| | | }.bind(this)).catch(function () {}); |
| | | }, |
| | | getDualCrnStateInfo() { |
| | | if (this.$root.sendWs) { |
| | | this.$root.sendWs(JSON.stringify({ |
| | | "url": "/dualcrn/table/crn/state", |
| | | "data": {} |
| | | })); |
| | | } |
| | | controlCommandTransport: function () { |
| | | this.postControl("/dualcrn/command/take", this.controlParam); |
| | | }, |
| | | controlCommandTransport() { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/dualcrn/command/take", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | controlCommandPickup: function () { |
| | | this.postControl("/dualcrn/command/pick", this.controlParam); |
| | | }, |
| | | controlCommandPickup() { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/dualcrn/command/pick", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | controlCommandPutdown: function () { |
| | | this.postControl("/dualcrn/command/put", this.controlParam); |
| | | }, |
| | | controlCommandPutdown() { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/dualcrn/command/put", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | controlCommandMove: function () { |
| | | this.postControl("/dualcrn/command/move", this.controlParam); |
| | | }, |
| | | controlCommandMove() { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/dualcrn/command/move", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }, |
| | | controlCommandTaskComplete() { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/dualcrn/command/taskComplete", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }, |
| | | setDualCrnList(res) { |
| | | let that = this; |
| | | if (res.code == 200) { |
| | | let list = res.data; |
| | | if (that.searchCrnNo == "") { |
| | | that.crnList = list; |
| | | } else { |
| | | let tmp = []; |
| | | list.forEach((item) => { |
| | | if (item.crnNo == that.searchCrnNo) { |
| | | tmp.push(item); |
| | | } |
| | | }); |
| | | that.crnList = tmp; |
| | | that.currentPage = 1; |
| | | } |
| | | } |
| | | }, |
| | | }, |
| | | controlCommandTaskComplete: function () { |
| | | this.postControl("/dualcrn/command/taskComplete", this.controlParam); |
| | | } |
| | | } |
| | | }); |
| | |
| | | Vue.component("watch-rgv-card", { |
| | | template: ` |
| | | <div> |
| | | <div style="display: flex;margin-bottom: 10px;"> |
| | | <div style="width: 100%;">RGV监控</div> |
| | | <div style="width: 100%;text-align: right;display: flex;"> |
| | | <el-input size="mini" v-model="searchRgvNo" placeholder="请输入RGV号"></el-input> |
| | | <el-button @click="getRgvStateInfo" size="mini">查询</el-button> |
| | | <div class="mc-root"> |
| | | <div class="mc-toolbar"> |
| | | <div class="mc-title">RGV监控</div> |
| | | <div class="mc-search"> |
| | | <input class="mc-input" v-model="searchRgvNo" placeholder="请输入RGV号" /> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="getRgvStateInfo">查询</button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="!readOnly" class="mc-control-toggle"> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="openControl"> |
| | | {{ showControl ? '收起控制中心' : '打开控制中心' }} |
| | | </button> |
| | | </div> |
| | | |
| | | <div v-if="showControl" class="mc-control"> |
| | | <div class="mc-control-grid"> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">RGV号</span> |
| | | <input class="mc-input" v-model="controlParam.rgvNo" placeholder="例如 1" /> |
| | | </label> |
| | | <label class="mc-field"> |
| | | <span class="mc-field-label">源点</span> |
| | | <input class="mc-input" v-model="controlParam.sourcePos" placeholder="输入源点" /> |
| | | </label> |
| | | <label class="mc-field mc-span-2"> |
| | | <span class="mc-field-label">目标点</span> |
| | | <input class="mc-input" v-model="controlParam.targetPos" placeholder="输入目标点" /> |
| | | </label> |
| | | <div class="mc-action-row"> |
| | | <button type="button" class="mc-btn" @click="controlCommandTransport">取放货</button> |
| | | <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandMove">移动</button> |
| | | <button type="button" class="mc-btn mc-btn-soft" @click="controlCommandTaskComplete">任务完成</button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mc-collapse"> |
| | | <div |
| | | v-for="item in displayRgvList" |
| | | :key="item.rgvNo" |
| | | :class="['mc-item', { 'is-open': isActive(item.rgvNo) }]" |
| | | > |
| | | <button type="button" class="mc-head" @click="toggleItem(item)"> |
| | | <div class="mc-head-main"> |
| | | <div class="mc-head-title">{{ item.rgvNo }}号RGV</div> |
| | | <div class="mc-head-subtitle">轨道位 {{ orDash(item.trackSiteNo) }} | 任务 {{ orDash(item.taskNo) }}</div> |
| | | </div> |
| | | </div> |
| | | <div style="margin-bottom: 10px;" v-if="!readOnly"> |
| | | <div style="margin-bottom: 5px;"> |
| | | <el-button v-if="showControl" @click="openControl" size="mini">关闭控制中心</el-button> |
| | | <el-button v-else @click="openControl" size="mini">打开控制中心</el-button> |
| | | <div class="mc-head-right"> |
| | | <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span> |
| | | <span class="mc-chevron">{{ isActive(item.rgvNo) ? '▾' : '▸' }}</span> |
| | | </div> |
| | | <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;"> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.rgvNo" placeholder="RGV号"></el-input></div> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.sourcePos" placeholder="源点"></el-input></div> |
| | | <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetPos" placeholder="目标点"></el-input></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandTransport()" size="mini">取放货</el-button></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandMove()" size="mini">移动</el-button></div> |
| | | <div style="margin-bottom: 10px;"><el-button @click="controlCommandTaskComplete()" size="mini">任务完成</el-button></div> |
| | | </button> |
| | | |
| | | <div v-if="isActive(item.rgvNo)" class="mc-body"> |
| | | <div class="mc-detail-grid"> |
| | | <div v-for="entry in buildDetailEntries(item)" :key="entry.label" class="mc-detail-cell"> |
| | | <div class="mc-detail-label">{{ entry.label }}</div> |
| | | <div class="mc-detail-value">{{ entry.value }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div style="max-height: 55vh; overflow:auto;"> |
| | | <el-collapse v-model="activeNames" accordion> |
| | | <el-collapse-item v-for="(item) in displayRgvList" :name="item.rgvNo"> |
| | | <template slot="title"> |
| | | <div style="width: 100%;display: flex;"> |
| | | <div style="width: 50%;">{{ item.rgvNo }}号RGV</div> |
| | | <div style="width: 50%;text-align: right;"> |
| | | <el-tag v-if="item.deviceStatus === 'AUTO'" type="success" size="small">自动</el-tag> |
| | | <el-tag v-else-if="item.deviceStatus === 'WORKING'" size="small">作业中</el-tag> |
| | | <el-tag v-else-if="item.deviceStatus === 'ERROR'" type="danger" size="small">报警</el-tag> |
| | | <el-tag v-else type="warning" size="small">离线</el-tag> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <el-descriptions border direction="vertical"> |
| | | <el-descriptions-item label="编号">{{ item.rgvNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="工作号">{{ item.taskNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="模式">{{ item.mode }}</el-descriptions-item> |
| | | <el-descriptions-item label="状态">{{ item.status }}</el-descriptions-item> |
| | | <el-descriptions-item label="轨道位">{{ item.trackSiteNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="是否有物">{{ item.loading }}</el-descriptions-item> |
| | | <el-descriptions-item label="故障代码">{{ item.warnCode }}</el-descriptions-item> |
| | | <el-descriptions-item label="故障描述">{{ item.alarm }}</el-descriptions-item> |
| | | <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-collapse-item> |
| | | </el-collapse> |
| | | </div> |
| | | <div style="display:flex; justify-content:flex-end; margin-top:8px;"> |
| | | <el-pagination |
| | | @current-change="handlePageChange" |
| | | @size-change="handleSizeChange" |
| | | :current-page="currentPage" |
| | | :page-size="pageSize" |
| | | :page-sizes="[10,20,50,100]" |
| | | layout="total, prev, pager, next" |
| | | :total="rgvList.length"> |
| | | </el-pagination> |
| | | </div> |
| | | |
| | | <div v-if="displayRgvList.length === 0" class="mc-empty">当前没有可展示的RGV数据</div> |
| | | </div> |
| | | |
| | | <div class="mc-footer"> |
| | | <button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">上一页</button> |
| | | <span>{{ currentPage }} / {{ totalPages }}</span> |
| | | <button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">下一页</button> |
| | | </div> |
| | | </div> |
| | | `, |
| | | `, |
| | | props: { |
| | | param: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | }, |
| | | autoRefresh: { |
| | | type: Boolean, |
| | | default: true |
| | | }, |
| | | readOnly: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | param: { type: Object, default: function () { return {}; } }, |
| | | items: { type: Array, default: null }, |
| | | autoRefresh: { type: Boolean, default: true }, |
| | | readOnly: { type: Boolean, default: false } |
| | | }, |
| | | data() { |
| | | data: function () { |
| | | return { |
| | | rgvList: [], |
| | | activeNames: "", |
| | |
| | | sourcePos: "", |
| | | targetPos: "" |
| | | }, |
| | | pageSize: 25, |
| | | pageSize: 12, |
| | | currentPage: 1, |
| | | timer: null |
| | | }; |
| | | }, |
| | | created() { |
| | | if (this.autoRefresh) { |
| | | this.timer = setInterval(() => { |
| | | this.getRgvStateInfo(); |
| | | }, 1000); |
| | | } |
| | | }, |
| | | beforeDestroy() { |
| | | if (this.timer) { |
| | | clearInterval(this.timer); |
| | | } |
| | | }, |
| | | computed: { |
| | | displayRgvList() { |
| | | const start = (this.currentPage - 1) * this.pageSize; |
| | | const end = start + this.pageSize; |
| | | return this.rgvList.slice(start, end); |
| | | sourceList: function () { |
| | | return Array.isArray(this.items) ? this.items : this.rgvList; |
| | | }, |
| | | filteredRgvList: function () { |
| | | var keyword = String(this.searchRgvNo || "").trim(); |
| | | if (!keyword) { |
| | | return this.sourceList; |
| | | } |
| | | return this.sourceList.filter(function (item) { |
| | | return String(item.rgvNo) === keyword; |
| | | }); |
| | | }, |
| | | displayRgvList: function () { |
| | | var start = (this.currentPage - 1) * this.pageSize; |
| | | return this.filteredRgvList.slice(start, start + this.pageSize); |
| | | }, |
| | | totalPages: function () { |
| | | return Math.max(1, Math.ceil(this.filteredRgvList.length / this.pageSize) || 1); |
| | | } |
| | | }, |
| | | watch: { |
| | | param: { |
| | | handler(newVal) { |
| | | if (newVal && newVal.rgvNo && newVal.rgvNo != 0) { |
| | | this.activeNames = newVal.rgvNo; |
| | | this.searchRgvNo = newVal.rgvNo; |
| | | const idx = this.rgvList.findIndex(i => i.rgvNo == newVal.rgvNo); |
| | | if (idx >= 0) { this.currentPage = Math.floor(idx / this.pageSize) + 1; } |
| | | } |
| | | }, |
| | | deep: true, |
| | | immediate: true |
| | | items: function () { |
| | | this.afterDataRefresh(); |
| | | }, |
| | | param: { |
| | | deep: true, |
| | | immediate: true, |
| | | handler: function (newVal) { |
| | | if (newVal && newVal.rgvNo && newVal.rgvNo !== 0) { |
| | | this.focusRgv(newVal.rgvNo); |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | created: function () { |
| | | MonitorCardKit.ensureStyles(); |
| | | if (this.autoRefresh) { |
| | | this.timer = setInterval(this.getRgvStateInfo, 1000); |
| | | } |
| | | }, |
| | | beforeDestroy: function () { |
| | | if (this.timer) { |
| | | clearInterval(this.timer); |
| | | this.timer = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | handlePageChange(page) { |
| | | orDash: function (value) { |
| | | return MonitorCardKit.orDash(value); |
| | | }, |
| | | getStatusLabel: function (item) { |
| | | return MonitorCardKit.deviceStatusLabel(item && item.deviceStatus); |
| | | }, |
| | | getStatusTone: function (item) { |
| | | return MonitorCardKit.statusTone(this.getStatusLabel(item)); |
| | | }, |
| | | isActive: function (rgvNo) { |
| | | return String(this.activeNames) === String(rgvNo); |
| | | }, |
| | | toggleItem: function (item) { |
| | | var next = String(item.rgvNo); |
| | | this.activeNames = this.activeNames === next ? "" : next; |
| | | }, |
| | | focusRgv: function (rgvNo) { |
| | | this.searchRgvNo = String(rgvNo); |
| | | var index = this.filteredRgvList.findIndex(function (item) { |
| | | return String(item.rgvNo) === String(rgvNo); |
| | | }); |
| | | this.currentPage = index >= 0 ? Math.floor(index / this.pageSize) + 1 : 1; |
| | | this.activeNames = String(rgvNo); |
| | | }, |
| | | afterDataRefresh: function () { |
| | | if (this.currentPage > this.totalPages) { |
| | | this.currentPage = this.totalPages; |
| | | } |
| | | if (this.activeNames) { |
| | | var exists = this.filteredRgvList.some(function (item) { |
| | | return String(item.rgvNo) === String(this.activeNames); |
| | | }, this); |
| | | if (!exists) { |
| | | this.activeNames = ""; |
| | | } |
| | | } |
| | | }, |
| | | handlePageChange: function (page) { |
| | | if (page < 1 || page > this.totalPages) { |
| | | return; |
| | | } |
| | | this.currentPage = page; |
| | | }, |
| | | handleSizeChange(size) { |
| | | this.pageSize = size; |
| | | this.currentPage = 1; |
| | | }, |
| | | getRgvStateInfo() { |
| | | if (this.$root.sendWs) { |
| | | getRgvStateInfo: function () { |
| | | if (this.$root && this.$root.sendWs) { |
| | | this.$root.sendWs(JSON.stringify({ |
| | | "url": "/rgv/table/rgv/state", |
| | | "data": {} |
| | | url: "/rgv/table/rgv/state", |
| | | data: {} |
| | | })); |
| | | } |
| | | }, |
| | | setRgvList(res) { |
| | | let that = this; |
| | | if (res.code == 200) { |
| | | let list = res.data || []; |
| | | if (that.searchRgvNo == "") { |
| | | that.rgvList = list; |
| | | } else { |
| | | let tmp = []; |
| | | list.forEach((item) => { |
| | | if (item.rgvNo == that.searchRgvNo) { |
| | | tmp.push(item); |
| | | } |
| | | }); |
| | | that.rgvList = tmp; |
| | | that.currentPage = 1; |
| | | } |
| | | setRgvList: function (res) { |
| | | if (res && res.code === 200) { |
| | | this.rgvList = res.data || []; |
| | | this.afterDataRefresh(); |
| | | } |
| | | }, |
| | | openControl() { |
| | | openControl: function () { |
| | | this.showControl = !this.showControl; |
| | | }, |
| | | controlCommandTransport() { |
| | | let that = this; |
| | | buildDetailEntries: function (item) { |
| | | return [ |
| | | { label: "编号", value: this.orDash(item.rgvNo) }, |
| | | { label: "工作号", value: this.orDash(item.taskNo) }, |
| | | { label: "模式", value: this.orDash(item.mode) }, |
| | | { label: "状态", value: this.orDash(item.status) }, |
| | | { label: "轨道位", value: this.orDash(item.trackSiteNo) }, |
| | | { label: "是否有物", value: MonitorCardKit.yesNo(item.loading) }, |
| | | { label: "故障代码", value: this.orDash(item.warnCode) }, |
| | | { label: "故障描述", value: this.orDash(item.alarm) }, |
| | | { label: "扩展数据", value: this.orDash(item.extend) } |
| | | ]; |
| | | }, |
| | | postControl: function (url, payload) { |
| | | $.ajax({ |
| | | url: baseUrl + "/rgv/command/transport", |
| | | url: baseUrl + url, |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | token: localStorage.getItem("token") |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | data: JSON.stringify(payload), |
| | | success: function (res) { |
| | | if (res && res.code === 200) { |
| | | MonitorCardKit.showMessage(this, res.msg || "操作成功", "success"); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | MonitorCardKit.showMessage(this, (res && res.msg) || "操作失败", "warning"); |
| | | } |
| | | }, |
| | | }.bind(this) |
| | | }); |
| | | }, |
| | | controlCommandMove() { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/rgv/command/move", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | controlCommandTransport: function () { |
| | | this.postControl("/rgv/command/transport", this.controlParam); |
| | | }, |
| | | controlCommandTaskComplete() { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/rgv/command/taskComplete", |
| | | headers: { |
| | | token: localStorage.getItem("token"), |
| | | }, |
| | | contentType: "application/json", |
| | | method: "post", |
| | | data: JSON.stringify(that.controlParam), |
| | | success: (res) => { |
| | | if (res.code == 200) { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "success", |
| | | }); |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: "warning", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | controlCommandMove: function () { |
| | | this.postControl("/rgv/command/move", this.controlParam); |
| | | }, |
| | | }, |
| | | controlCommandTaskComplete: function () { |
| | | this.postControl("/rgv/command/taskComplete", this.controlParam); |
| | | } |
| | | } |
| | | }); |
| New file |
| | |
| | | var app = new Vue({ |
| | | el: '#app', |
| | | data: { |
| | | loading: false, |
| | | saving: false, |
| | | items: [], |
| | | predefineColors: [ |
| | | '#78FF81', |
| | | '#FA51F6', |
| | | '#C4C400', |
| | | '#30BFFC', |
| | | '#18C7B8', |
| | | '#97B400', |
| | | '#E69138', |
| | | '#B8B8B8', |
| | | '#FF6B6B', |
| | | '#FFD166', |
| | | '#06D6A0', |
| | | '#118AB2' |
| | | ] |
| | | }, |
| | | mounted: function () { |
| | | this.reloadData(); |
| | | }, |
| | | methods: { |
| | | reloadData: function () { |
| | | var that = this; |
| | | this.loading = true; |
| | | $.ajax({ |
| | | url: baseUrl + '/watch/stationColor/config/auth', |
| | | headers: { token: localStorage.getItem('token') }, |
| | | method: 'GET', |
| | | success: function (res) { |
| | | that.loading = false; |
| | | if (res.code === 200) { |
| | | var items = (res.data && res.data.items) ? res.data.items : []; |
| | | that.items = items.map(function (item) { |
| | | return { |
| | | status: item.status, |
| | | name: item.name, |
| | | desc: item.desc, |
| | | color: that.normalizeColor(item.color || item.defaultColor), |
| | | defaultColor: that.normalizeColor(item.defaultColor) |
| | | }; |
| | | }); |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + '/'; |
| | | } else { |
| | | that.$message.error(res.msg || '加载站点颜色配置失败'); |
| | | } |
| | | }, |
| | | error: function () { |
| | | that.loading = false; |
| | | that.$message.error('加载站点颜色配置失败'); |
| | | } |
| | | }); |
| | | }, |
| | | resetDefaults: function () { |
| | | this.items = this.items.map(function (item) { |
| | | return Object.assign({}, item, { |
| | | color: item.defaultColor |
| | | }); |
| | | }); |
| | | this.$message.success('已恢复默认颜色'); |
| | | }, |
| | | applyDefaultColor: function (item) { |
| | | item.color = item.defaultColor; |
| | | }, |
| | | handleColorInput: function (item) { |
| | | var normalized = this.normalizeColor(item.color); |
| | | if (normalized !== String(item.color || '').trim().toUpperCase()) { |
| | | this.$message.warning('颜色格式已自动修正为十六进制'); |
| | | } |
| | | item.color = normalized; |
| | | }, |
| | | saveConfig: function () { |
| | | var that = this; |
| | | this.saving = true; |
| | | $.ajax({ |
| | | url: baseUrl + '/watch/stationColor/config/save/auth', |
| | | headers: { token: localStorage.getItem('token') }, |
| | | method: 'POST', |
| | | contentType: 'application/json;charset=UTF-8', |
| | | dataType: 'json', |
| | | data: JSON.stringify({ |
| | | items: this.items.map(function (item) { |
| | | return { |
| | | status: item.status, |
| | | color: that.normalizeColor(item.color) |
| | | }; |
| | | }) |
| | | }), |
| | | success: function (res) { |
| | | that.saving = false; |
| | | if (res.code === 200) { |
| | | that.$message.success('站点颜色配置已保存'); |
| | | that.reloadData(); |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + '/'; |
| | | } else { |
| | | that.$message.error(res.msg || '保存站点颜色配置失败'); |
| | | } |
| | | }, |
| | | error: function () { |
| | | that.saving = false; |
| | | that.$message.error('保存站点颜色配置失败'); |
| | | } |
| | | }); |
| | | }, |
| | | normalizeColor: function (color) { |
| | | var value = String(color || '').trim(); |
| | | if (!value) { |
| | | return '#B8B8B8'; |
| | | } |
| | | if (/^#[0-9a-fA-F]{6}$/.test(value)) { |
| | | return value.toUpperCase(); |
| | | } |
| | | if (/^#[0-9a-fA-F]{3}$/.test(value)) { |
| | | return ('#' + value.charAt(1) + value.charAt(1) + value.charAt(2) + value.charAt(2) + value.charAt(3) + value.charAt(3)).toUpperCase(); |
| | | } |
| | | if (/^0x[0-9a-fA-F]{6}$/.test(value)) { |
| | | return ('#' + value.substring(2)).toUpperCase(); |
| | | } |
| | | return '#B8B8B8'; |
| | | } |
| | | } |
| | | }); |
| | |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script src="../../static/vue/js/vue.min.js"></script> |
| | | <script src="../../static/vue/element/element.js"></script> |
| | | <script src="../../components/MonitorCardKit.js"></script> |
| | | <script src="../../components/WatchCrnCard.js"></script> |
| | | <script src="../../components/WatchRgvCard.js"></script> |
| | | <script src="../../components/WatchDualCrnCard.js"></script> |
| | | <script src="../../components/DevpCard.js"></script> |
| | | <script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js" charset="utf-8"></script> |
| | | </body> |
| | | </html> |
| | | </html> |
| | |
| | | <meta charset="UTF-8"> |
| | | <title>WCS控制中心</title> |
| | | <link rel="stylesheet" href="../../static/css/animate.min.css"> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css"> |
| | | <link rel="stylesheet" href="../../static/css/watch/console_vue.css"> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css"> |
| | | <style> |
| | | html, body, #app { |
| | | width: 100%; |
| | |
| | | margin: 0; |
| | | overflow: hidden; |
| | | } |
| | | body { |
| | | background: linear-gradient(180deg, #eef4f8 0%, #e7edf4 100%); |
| | | } |
| | | #app { |
| | | position: relative; |
| | | } |
| | | .monitor-shell { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | overflow: hidden; |
| | | } |
| | | .monitor-map { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | .monitor-panel-wrap { |
| | | position: absolute; |
| | | top: 18px; |
| | | left: 18px; |
| | | bottom: 18px; |
| | | z-index: 40; |
| | | pointer-events: none; |
| | | } |
| | | .monitor-panel { |
| | | width: min(max(360px, 30vw), calc(100vw - 92px)); |
| | | max-width: calc(100vw - 92px); |
| | | height: calc(100vh - 36px); |
| | | display: flex; |
| | | flex-direction: column; |
| | | border-radius: 20px; |
| | | border: 1px solid rgba(255, 255, 255, 0.42); |
| | | background: rgba(248, 251, 253, 0.94); |
| | | box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08); |
| | | overflow: hidden; |
| | | pointer-events: auto; |
| | | transform-origin: left center; |
| | | will-change: transform, opacity; |
| | | backface-visibility: hidden; |
| | | contain: layout paint style; |
| | | transition: transform .26s cubic-bezier(0.22, 1, 0.36, 1), opacity .2s ease, box-shadow .2s ease, border-color .2s ease; |
| | | } |
| | | .monitor-panel.is-collapsed { |
| | | opacity: 0; |
| | | transform: translate3d(calc(-100% - 14px), 0, 0); |
| | | border-color: transparent; |
| | | box-shadow: 0 6px 16px rgba(88, 110, 136, 0.02); |
| | | pointer-events: none; |
| | | } |
| | | .monitor-panel-header { |
| | | padding: 14px 16px 10px; |
| | | border-bottom: 1px solid rgba(226, 232, 240, 0.72); |
| | | background: rgba(255, 255, 255, 0.24); |
| | | } |
| | | .monitor-panel-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #243447; |
| | | line-height: 1.2; |
| | | } |
| | | .monitor-panel-desc { |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: #6b7b8d; |
| | | } |
| | | .monitor-panel-body { |
| | | flex: 1; |
| | | min-height: 0; |
| | | padding: 12px; |
| | | overflow: hidden; |
| | | } |
| | | .monitor-panel-body { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | .monitor-card-host { |
| | | flex: 1; |
| | | min-height: 0; |
| | | display: flex; |
| | | align-items: stretch; |
| | | width: 100%; |
| | | } |
| | | .wb-root { |
| | | position: relative; |
| | | flex: 1; |
| | | min-width: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 100%; |
| | | gap: 10px; |
| | | color: #395066; |
| | | } |
| | | .wb-main { |
| | | flex: 1; |
| | | min-height: 0; |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | .wb-side { |
| | | flex: 0 0 38%; |
| | | min-width: 220px; |
| | | max-width: 44%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | min-height: 0; |
| | | } |
| | | .wb-side-title { |
| | | font-size: 11px; |
| | | font-weight: 700; |
| | | letter-spacing: 0.06em; |
| | | text-transform: uppercase; |
| | | color: #7d8fa2; |
| | | margin-bottom: 8px; |
| | | } |
| | | .wb-list-card, |
| | | .wb-detail-panel { |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | .wb-detail-panel { |
| | | flex: 1 1 62%; |
| | | min-width: 0; |
| | | } |
| | | .wb-tabs { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, 1fr); |
| | | gap: 6px; |
| | | padding: 6px; |
| | | border-radius: 14px; |
| | | background: rgba(242, 246, 250, 0.78); |
| | | border: 1px solid rgba(224, 232, 239, 0.9); |
| | | } |
| | | .wb-tab { |
| | | height: 34px; |
| | | border: none; |
| | | border-radius: 10px; |
| | | background: transparent; |
| | | color: #73869b; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | cursor: pointer; |
| | | transition: all .16s ease; |
| | | } |
| | | .wb-tab.is-active { |
| | | background: rgba(255, 255, 255, 0.92); |
| | | color: #24405c; |
| | | box-shadow: 0 8px 16px rgba(148, 163, 184, 0.08); |
| | | } |
| | | .wb-toolbar { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .wb-toolbar-actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | flex-shrink: 0; |
| | | } |
| | | .wb-input, |
| | | .wb-select { |
| | | width: 100%; |
| | | height: 36px; |
| | | padding: 0 12px; |
| | | border-radius: 10px; |
| | | border: 1px solid rgba(224, 232, 239, 0.96); |
| | | background: rgba(255, 255, 255, 0.76); |
| | | color: #334155; |
| | | box-sizing: border-box; |
| | | outline: none; |
| | | transition: border-color .16s ease, box-shadow .16s ease, background .16s ease; |
| | | } |
| | | .wb-input:focus, |
| | | .wb-select:focus { |
| | | border-color: rgba(128, 168, 208, 0.66); |
| | | box-shadow: 0 0 0 3px rgba(128, 168, 208, 0.1); |
| | | background: rgba(255, 255, 255, 0.92); |
| | | } |
| | | .wb-btn { |
| | | height: 36px; |
| | | padding: 0 14px; |
| | | border: none; |
| | | border-radius: 10px; |
| | | background: #6f95bd; |
| | | color: #fff; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | cursor: pointer; |
| | | box-shadow: 0 6px 14px rgba(111, 149, 189, 0.18); |
| | | transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease, background .16s ease; |
| | | } |
| | | .wb-btn:hover { |
| | | transform: translateY(-1px); |
| | | } |
| | | .wb-btn-primary { |
| | | background: linear-gradient(135deg, #5e89b4 0%, #6f95bd 100%); |
| | | box-shadow: 0 10px 20px rgba(111, 149, 189, 0.22); |
| | | } |
| | | .wb-btn.wb-btn-ghost { |
| | | background: rgba(255, 255, 255, 0.76); |
| | | color: #4c6177; |
| | | border: 1px solid rgba(224, 232, 239, 0.96); |
| | | box-shadow: none; |
| | | } |
| | | .wb-btn.wb-btn-soft { |
| | | background: rgba(230, 237, 244, 0.92); |
| | | color: #4c6177; |
| | | border: 1px solid rgba(210, 221, 232, 0.98); |
| | | box-shadow: none; |
| | | } |
| | | .wb-control-card, |
| | | .wb-list-card, |
| | | .wb-detail { |
| | | border-radius: 16px; |
| | | border: 1px solid rgba(224, 232, 239, 0.92); |
| | | background: rgba(255, 255, 255, 0.62); |
| | | box-shadow: 0 8px 18px rgba(148, 163, 184, 0.06); |
| | | } |
| | | .wb-list-card { |
| | | flex: 1; |
| | | padding: 10px 8px 8px; |
| | | } |
| | | .wb-control-card { |
| | | padding: 14px; |
| | | background: linear-gradient(180deg, rgba(255, 255, 255, 0.82) 0%, rgba(246, 250, 253, 0.78) 100%); |
| | | overflow: auto; |
| | | } |
| | | .wb-control-subtitle { |
| | | margin-top: 8px; |
| | | margin-bottom: 10px; |
| | | font-size: 11px; |
| | | line-height: 1.45; |
| | | color: #6f8194; |
| | | } |
| | | .wb-control-target { |
| | | padding: 7px 9px; |
| | | border-radius: 12px; |
| | | background: rgba(234, 241, 247, 0.96); |
| | | border: 1px solid rgba(214, 224, 234, 0.96); |
| | | color: #43607c; |
| | | font-size: 11px; |
| | | font-weight: 700; |
| | | line-height: 1.4; |
| | | } |
| | | .wb-form-grid { |
| | | display: grid; |
| | | grid-template-columns: 1fr; |
| | | gap: 8px; |
| | | } |
| | | .wb-field { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 5px; |
| | | } |
| | | .wb-field-label { |
| | | font-size: 11px; |
| | | font-weight: 700; |
| | | letter-spacing: 0.02em; |
| | | color: #6d8197; |
| | | } |
| | | .wb-action-row { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | margin-top: 2px; |
| | | padding-top: 8px; |
| | | border-top: 1px dashed rgba(216, 226, 235, 0.92); |
| | | } |
| | | .wb-section-title { |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: #28425d; |
| | | margin-bottom: 10px; |
| | | } |
| | | .wb-list { |
| | | flex: 1; |
| | | min-height: 0; |
| | | padding: 6px; |
| | | overflow: auto; |
| | | } |
| | | .wb-list-item { |
| | | width: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | justify-content: flex-start; |
| | | gap: 7px; |
| | | padding: 10px; |
| | | margin-bottom: 8px; |
| | | border: 1px solid transparent; |
| | | border-radius: 12px; |
| | | background: rgba(248, 250, 252, 0.72); |
| | | cursor: pointer; |
| | | text-align: left; |
| | | color: inherit; |
| | | } |
| | | .wb-list-item:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | .wb-list-item.is-active { |
| | | border-color: rgba(135, 166, 198, 0.38); |
| | | background: rgba(236, 243, 249, 0.94); |
| | | } |
| | | .wb-list-main { |
| | | min-width: 0; |
| | | } |
| | | .wb-list-title { |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | color: #27425c; |
| | | line-height: 1.35; |
| | | } |
| | | .wb-list-meta { |
| | | margin-top: 4px; |
| | | font-size: 11px; |
| | | color: #7b8b9c; |
| | | line-height: 1.4; |
| | | display: -webkit-box; |
| | | -webkit-line-clamp: 2; |
| | | -webkit-box-orient: vertical; |
| | | overflow: hidden; |
| | | } |
| | | .wb-badge { |
| | | flex-shrink: 0; |
| | | padding: 3px 8px; |
| | | border-radius: 999px; |
| | | font-size: 10px; |
| | | font-weight: 700; |
| | | } |
| | | .wb-badge.is-success { |
| | | background: rgba(82, 177, 126, 0.12); |
| | | color: #2d7650; |
| | | } |
| | | .wb-badge.is-working { |
| | | background: rgba(111, 149, 189, 0.12); |
| | | color: #3f6286; |
| | | } |
| | | .wb-badge.is-warning { |
| | | background: rgba(214, 162, 94, 0.14); |
| | | color: #9b6a24; |
| | | } |
| | | .wb-badge.is-danger { |
| | | background: rgba(207, 126, 120, 0.14); |
| | | color: #a14e4a; |
| | | } |
| | | .wb-badge.is-muted { |
| | | background: rgba(148, 163, 184, 0.14); |
| | | color: #748397; |
| | | } |
| | | .wb-empty { |
| | | padding: 28px 12px; |
| | | text-align: center; |
| | | color: #8b9aad; |
| | | font-size: 12px; |
| | | } |
| | | .wb-detail { |
| | | flex: 1; |
| | | min-height: 0; |
| | | padding: 12px; |
| | | overflow: auto; |
| | | } |
| | | .wb-detail-empty { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | .wb-detail-header { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | } |
| | | .wb-detail-subtitle { |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: #7c8c9d; |
| | | } |
| | | .wb-detail-actions { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | .wb-link { |
| | | padding: 0; |
| | | border: none; |
| | | background: transparent; |
| | | color: #4677a4; |
| | | font-size: 12px; |
| | | cursor: pointer; |
| | | } |
| | | .wb-detail-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); |
| | | gap: 8px; |
| | | } |
| | | .wb-detail-cell { |
| | | padding: 10px 12px; |
| | | border-radius: 12px; |
| | | background: rgba(247, 250, 252, 0.86); |
| | | border: 1px solid rgba(233, 239, 244, 0.96); |
| | | } |
| | | .wb-detail-label { |
| | | font-size: 11px; |
| | | color: #8090a2; |
| | | } |
| | | .wb-detail-value { |
| | | margin-top: 5px; |
| | | font-size: 13px; |
| | | color: #31485f; |
| | | word-break: break-all; |
| | | } |
| | | .wb-notice { |
| | | position: absolute; |
| | | right: 18px; |
| | | bottom: 18px; |
| | | padding: 10px 14px; |
| | | border-radius: 12px; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: #fff; |
| | | box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12); |
| | | } |
| | | .wb-notice.is-success { |
| | | background: rgba(82, 177, 126, 0.92); |
| | | } |
| | | .wb-notice.is-warning { |
| | | background: rgba(214, 162, 94, 0.92); |
| | | } |
| | | .wb-notice.is-danger { |
| | | background: rgba(207, 126, 120, 0.92); |
| | | } |
| | | .floor-switch-button { |
| | | min-width: 44px; |
| | | height: 30px; |
| | | padding: 0 12px; |
| | | border: 1px solid rgba(185, 197, 210, 0.84); |
| | | border-radius: 999px; |
| | | background: rgba(255, 255, 255, 0.82); |
| | | color: #4b6177; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | cursor: pointer; |
| | | } |
| | | .floor-switch-button.is-active { |
| | | border-color: rgba(111, 149, 189, 0.4); |
| | | background: rgba(236, 243, 249, 0.94); |
| | | color: #27425c; |
| | | } |
| | | .monitor-panel-toggle { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 50%; |
| | | margin-left: 0; |
| | | transform: translateY(-50%); |
| | | width: 30px; |
| | | min-height: 108px; |
| | | padding: 10px 4px; |
| | | border: 1px solid rgba(148, 163, 184, 0.22); |
| | | border-left: none; |
| | | border-radius: 0 14px 14px 0; |
| | | background: rgba(255, 255, 255, 0.96); |
| | | color: #3e5974; |
| | | box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08); |
| | | cursor: pointer; |
| | | pointer-events: auto; |
| | | display: inline-flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 10px; |
| | | font-size: 12px; |
| | | line-height: 1; |
| | | white-space: nowrap; |
| | | backface-visibility: hidden; |
| | | transition: left .26s cubic-bezier(0.22, 1, 0.36, 1), transform .26s cubic-bezier(0.22, 1, 0.36, 1), box-shadow .18s ease, background .18s ease; |
| | | } |
| | | .monitor-panel-toggle.is-panel-open { |
| | | left: calc(100% + 10px); |
| | | } |
| | | .monitor-panel-toggle i { |
| | | font-size: 14px; |
| | | } |
| | | .monitor-panel-toggle span { |
| | | writing-mode: vertical-rl; |
| | | text-orientation: mixed; |
| | | letter-spacing: 0.08em; |
| | | user-select: none; |
| | | } |
| | | </style> |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/layui/layui.js"></script> |
| | | <script type="text/javascript" src="../../static/js/handlebars/handlebars-v4.5.3.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.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> |
| | | <script src="../../static/js/gsap.min.js"></script> |
| | |
| | | </head> |
| | | <body> |
| | | <div id="app"> |
| | | <div style="display: flex;margin-left: 20px;"> |
| | | <div style="width: 20%;height: 60vh;margin-right: 20px;margin-top: 30px;"> |
| | | <el-tabs type="border-card" v-model="activateCard" @tab-click="handleCardClick"> |
| | | <el-tab-pane label="堆垛机" name="crn"> |
| | | <watch-crn-card ref="watchCrnCard" :param="crnParam"></watch-crn-card> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="双工位堆垛机" name="dualCrn"> |
| | | <watch-dual-crn-card ref="watchDualCrnCard" :param="dualCrnParam"></watch-dual-crn-card> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="输送站" name="devp"> |
| | | <devp-card ref="devpCard" :param="devpParam"></devp-card> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="RGV" name="rgv"> |
| | | <watch-rgv-card ref="watchRgvCard" :param="rgvParam"></watch-rgv-card> |
| | | </el-tab-pane> |
| | | <!-- <el-tab-pane label="地图配置" name="mapSetting"> |
| | | <map-setting-card :param="mapSettingParam"></map-setting-card> |
| | | </el-tab-pane> --> |
| | | </el-tabs> |
| | | </div> |
| | | <div class="monitor-shell" ref="monitorShell"> |
| | | <map-canvas :lev="currentLev" :lev-list="levList" :crn-param="crnParam" :rgv-param="rgvParam" :devp-param="devpParam" :station-task-range="stationTaskRange" :viewport-padding="mapViewportPadding" :hud-padding="mapHudPadding" @switch-lev="switchLev" @crn-click="openCrn" @dual-crn-click="openDualCrn" @station-click="openSite" @rgv-click="openRgv" class="monitor-map"></map-canvas> |
| | | |
| | | <map-canvas :lev="currentLev" :crn-param="crnParam" :rgv-param="rgvParam" :devp-param="devpParam" @crn-click="openCrn" @dual-crn-click="openDualCrn" @station-click="openSite" @rgv-click="openRgv" style="width: 80%; height: 100vh;"></map-canvas> |
| | | |
| | | <div style="position: absolute;top: 15px;left: 50%;display: flex;"> |
| | | <div v-if="levList.length > 1" v-for="(lev,index) in levList" :key="index" style="margin-right: 10px;"> |
| | | <el-button :type="currentLev == lev ? 'primary' : ''" @click="switchLev(lev)" size="mini">{{ lev }}F</el-button> |
| | | <div class="monitor-panel-wrap" ref="monitorPanelWrap"> |
| | | <div class="monitor-panel" ref="monitorPanel" :class="{ 'is-collapsed': panelCollapsed }"> |
| | | <div class="monitor-panel-header"> |
| | | <div class="monitor-panel-title">监控工作台</div> |
| | | <div class="monitor-panel-desc">围绕地图做操作,设备点击后自动切换到对应面板</div> |
| | | </div> |
| | | <div class="monitor-panel-body"> |
| | | <div class="wb-tabs" role="tablist"> |
| | | <button type="button" :class="['wb-tab', { 'is-active': activateCard === 'crn' }]" @click="handleWorkbenchTabChange('crn')">堆垛机</button> |
| | | <button type="button" :class="['wb-tab', { 'is-active': activateCard === 'dualCrn' }]" @click="handleWorkbenchTabChange('dualCrn')">双工位</button> |
| | | <button type="button" :class="['wb-tab', { 'is-active': activateCard === 'devp' }]" @click="handleWorkbenchTabChange('devp')">输送站</button> |
| | | <button type="button" :class="['wb-tab', { 'is-active': activateCard === 'rgv' }]" @click="handleWorkbenchTabChange('rgv')">RGV</button> |
| | | </div> |
| | | <div class="monitor-card-host"> |
| | | <watch-crn-card v-if="activateCard === 'crn'" ref="watchCrnCard" :param="crnParam" :items="crnStateList" :auto-refresh="false"></watch-crn-card> |
| | | <watch-dual-crn-card v-else-if="activateCard === 'dualCrn'" ref="watchDualCrnCard" :param="dualCrnParam" :items="dualCrnStateList" :auto-refresh="false"></watch-dual-crn-card> |
| | | <devp-card v-else-if="activateCard === 'devp'" ref="devpCard" :param="devpParam" :items="stationStateList" :auto-refresh="false"></devp-card> |
| | | <watch-rgv-card v-else ref="watchRgvCard" :param="rgvParam" :items="rgvStateList" :auto-refresh="false"></watch-rgv-card> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <button class="monitor-panel-toggle" :class="{ 'is-panel-open': !panelCollapsed }" ref="monitorToggle" @click="toggleMonitorPanel"> |
| | | <i>{{ panelCollapsed ? '>' : '<' }}</i> |
| | | <span>{{ panelCollapsed ? '展开面板' : '收起面板' }}</span> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | | </div> |
| | | |
| | | <script src="../../components/MonitorCardKit.js"></script> |
| | | <script src="../../components/WatchCrnCard.js"></script> |
| | | <script src="../../components/WatchDualCrnCard.js"></script> |
| | | <script src="../../components/DevpCard.js"></script> |
| | | <script src="../../components/MapSettingCard.js"></script> |
| | | <script src="../../components/WatchRgvCard.js"></script> |
| | | <script src="../../components/MapCanvas.js"></script> |
| | | <script> |
| | |
| | | systemStatus: true,//系统运行状态 |
| | | consoleInterval: null,//定时器存储变量 |
| | | rgvPosition: [], |
| | | panelCollapsed: false, |
| | | mapViewportPadding: { |
| | | top: 0, |
| | | right: 0, |
| | | bottom: 0, |
| | | left: 0 |
| | | }, |
| | | mapHudPadding: { |
| | | left: 14 |
| | | }, |
| | | stationTaskRange: { |
| | | inbound: null, |
| | | outbound: null |
| | | }, |
| | | panelTransitionTimer: null, |
| | | panelPollTimer: null, |
| | | activateCard: 'crn', |
| | | crnParam: { |
| | | crnNo: 0 |
| | |
| | | dualCrnParam: { |
| | | crnNo: 0 |
| | | }, |
| | | mapSettingParam: { |
| | | zoom: 70 |
| | | }, |
| | | devpParam: { |
| | | stationId: 0 |
| | | }, |
| | | rgvParam: { |
| | | rgvNo: 0 |
| | | }, |
| | | crnStateList: [], |
| | | dualCrnStateList: [], |
| | | stationStateList: [], |
| | | rgvStateList: [], |
| | | locMastData: [],//库位数据 |
| | | wsReconnectTimer: null, |
| | | wsReconnectAttempts: 0, |
| | |
| | | this.init() |
| | | }, |
| | | mounted() { |
| | | this.$nextTick(() => { |
| | | this.updateMapViewportPadding(); |
| | | }); |
| | | window.addEventListener('resize', this.updateMapViewportPadding); |
| | | this.panelPollTimer = setInterval(() => { |
| | | this.refreshWorkbench(this.activateCard); |
| | | }, 1000); |
| | | }, |
| | | beforeDestroy() { |
| | | if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; } |
| | | if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { try { ws.close(); } catch (e) {} } |
| | | window.removeEventListener('resize', this.updateMapViewportPadding); |
| | | if (this.panelTransitionTimer) { clearTimeout(this.panelTransitionTimer); this.panelTransitionTimer = null; } |
| | | if (this.panelPollTimer) { clearInterval(this.panelPollTimer); this.panelPollTimer = null; } |
| | | }, |
| | | watch: { |
| | | |
| | |
| | | ws.send(data); |
| | | } |
| | | }, |
| | | webSocketOnOpen() { |
| | | console.log("WebSocket连接成功"); |
| | | if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; } |
| | | this.wsReconnectAttempts = 0; |
| | | this.getMap(); |
| | | }, |
| | | webSocketOnOpen() { |
| | | console.log("WebSocket连接成功"); |
| | | if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; } |
| | | this.wsReconnectAttempts = 0; |
| | | this.getMap(); |
| | | this.refreshWorkbench(this.activateCard); |
| | | }, |
| | | webSocketOnError() { |
| | | console.log("WebSocket连接发生错误"); |
| | | this.scheduleWsReconnect(); |
| | |
| | | console.log("WebSocket连接关闭"); |
| | | this.scheduleWsReconnect(); |
| | | }, |
| | | webSocketOnMessage(e) { |
| | | const result = JSON.parse(e.data); |
| | | if (result.url == "/crn/table/crn/state") { |
| | | if(this.$refs.watchCrnCard) { |
| | | this.$refs.watchCrnCard.setCrnList(JSON.parse(result.data)); |
| | | } |
| | | } else if (result.url == "/dualcrn/table/crn/state") { |
| | | if(this.$refs.watchDualCrnCard) { |
| | | this.$refs.watchDualCrnCard.setDualCrnList(JSON.parse(result.data)); |
| | | } |
| | | } else if (result.url == "/console/latest/data/station") { |
| | | if(this.$refs.devpCard) { |
| | | this.$refs.devpCard.setStationList(JSON.parse(result.data)); |
| | | } |
| | | } else if (result.url == "/rgv/table/rgv/state") { |
| | | if(this.$refs.watchRgvCard) { |
| | | this.$refs.watchRgvCard.setRgvList(JSON.parse(result.data)); |
| | | } |
| | | } else if (result.url == "/basMap/lev/" + this.currentLev + "/auth") { |
| | | // 地图数据 |
| | | let res = JSON.parse(result.data); |
| | | webSocketOnMessage(e) { |
| | | const result = JSON.parse(e.data); |
| | | if (result.url == "/crn/table/crn/state") { |
| | | const res = JSON.parse(result.data); |
| | | this.crnStateList = res && res.code === 200 ? (res.data || []) : []; |
| | | } else if (result.url == "/dualcrn/table/crn/state") { |
| | | const res = JSON.parse(result.data); |
| | | this.dualCrnStateList = res && res.code === 200 ? (res.data || []) : []; |
| | | } else if (result.url == "/console/latest/data/station") { |
| | | const res = JSON.parse(result.data); |
| | | this.stationStateList = res && res.code === 200 ? (res.data || []) : []; |
| | | } else if (result.url == "/rgv/table/rgv/state") { |
| | | const res = JSON.parse(result.data); |
| | | this.rgvStateList = res && res.code === 200 ? (res.data || []) : []; |
| | | } else if (result.url == "/basMap/lev/" + this.currentLev + "/auth") { |
| | | // 地图数据 |
| | | let res = JSON.parse(result.data); |
| | | if (res.code === 200) { |
| | | this.map = res.data; |
| | | } |
| | |
| | | |
| | | this.getSystemRunningStatus() //获取系统运行状态 |
| | | this.getLevList() //获取地图层级列表 |
| | | this.getStationTaskRange() // 获取入库/出库工作号范围 |
| | | this.getLocMastData() //获取库位数据 |
| | | }, |
| | | getStationTaskRange() { |
| | | this.fetchWrkLastnoRange(1, 'inbound'); |
| | | this.fetchWrkLastnoRange(101, 'outbound'); |
| | | }, |
| | | fetchWrkLastnoRange(id, key) { |
| | | $.ajax({ |
| | | url: baseUrl + "/wrkLastno/" + id + "/auth", |
| | | headers: { |
| | | 'token': localStorage.getItem('token') |
| | | }, |
| | | method: "get", |
| | | success: (res) => { |
| | | if (!res || res.code !== 200 || !res.data) { return; } |
| | | const data = res.data; |
| | | this.stationTaskRange = Object.assign({}, this.stationTaskRange, { |
| | | [key]: { |
| | | start: data.sNo, |
| | | end: data.eNo |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | }, |
| | | connectWs() { |
| | | if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { return; } |
| | |
| | | this.currentLev = lev; |
| | | this.getMap() |
| | | this.getLocMastData() |
| | | this.refreshWorkbench(this.activateCard) |
| | | }, |
| | | handleWorkbenchTabChange(type) { |
| | | this.activateCard = type; |
| | | this.refreshWorkbench(type); |
| | | }, |
| | | refreshWorkbench(type) { |
| | | if (!type) { return; } |
| | | if (type === 'crn') { |
| | | this.sendWs(JSON.stringify({ url: "/crn/table/crn/state", data: {} })); |
| | | } else if (type === 'dualCrn') { |
| | | this.sendWs(JSON.stringify({ url: "/dualcrn/table/crn/state", data: {} })); |
| | | } else if (type === 'devp') { |
| | | this.sendWs(JSON.stringify({ url: "/console/latest/data/station", data: {} })); |
| | | } else if (type === 'rgv') { |
| | | this.sendWs(JSON.stringify({ url: "/rgv/table/rgv/state", data: {} })); |
| | | } |
| | | }, |
| | | updateMapViewportPadding() { |
| | | const shell = this.$refs.monitorShell; |
| | | const panelWrap = this.$refs.monitorPanelWrap; |
| | | if (!shell) { return; } |
| | | const shellRect = shell.getBoundingClientRect(); |
| | | let leftPadding = 0; |
| | | let hudLeft = 14; |
| | | if (!this.panelCollapsed && this.$refs.monitorPanel && panelWrap) { |
| | | const wrapRect = panelWrap.getBoundingClientRect(); |
| | | const panelWidth = this.$refs.monitorPanel.offsetWidth || this.$refs.monitorPanel.getBoundingClientRect().width || 0; |
| | | const panelLeft = Math.max(0, Math.ceil(wrapRect.left - shellRect.left)); |
| | | const panelBaseRight = panelLeft + Math.ceil(panelWidth); |
| | | const overlapCompensation = Math.min(56, panelWidth * 0.18); |
| | | leftPadding = Math.max(0, Math.ceil(panelBaseRight - overlapCompensation)); |
| | | hudLeft = Math.max(14, Math.ceil(panelBaseRight + 34)); |
| | | } else { |
| | | leftPadding = 0; |
| | | hudLeft = 14; |
| | | } |
| | | this.mapViewportPadding = { |
| | | top: 0, |
| | | right: 0, |
| | | bottom: 0, |
| | | left: leftPadding |
| | | }; |
| | | this.mapHudPadding = { |
| | | left: hudLeft |
| | | }; |
| | | }, |
| | | scheduleMapViewportPaddingUpdate(delay) { |
| | | if (this.panelTransitionTimer) { |
| | | clearTimeout(this.panelTransitionTimer); |
| | | this.panelTransitionTimer = null; |
| | | } |
| | | this.panelTransitionTimer = setTimeout(() => { |
| | | this.panelTransitionTimer = null; |
| | | this.updateMapViewportPadding(); |
| | | }, delay == null ? 0 : delay); |
| | | }, |
| | | toggleMonitorPanel() { |
| | | this.panelCollapsed = !this.panelCollapsed; |
| | | this.scheduleMapViewportPaddingUpdate(0); |
| | | }, |
| | | openCrn(id) { |
| | | this.panelCollapsed = false; |
| | | this.crnParam.crnNo = id; |
| | | this.activateCard = 'crn'; |
| | | this.scheduleMapViewportPaddingUpdate(0); |
| | | this.refreshWorkbench('crn'); |
| | | }, |
| | | openDualCrn(id) { |
| | | this.panelCollapsed = false; |
| | | this.dualCrnParam.crnNo = id; |
| | | this.activateCard = 'dualCrn'; |
| | | this.scheduleMapViewportPaddingUpdate(0); |
| | | this.refreshWorkbench('dualCrn'); |
| | | }, |
| | | openRgv(id) { |
| | | this.panelCollapsed = false; |
| | | this.rgvParam.rgvNo = id; |
| | | this.activateCard = 'rgv'; |
| | | this.scheduleMapViewportPaddingUpdate(0); |
| | | this.refreshWorkbench('rgv'); |
| | | }, |
| | | openSite(id) { |
| | | this.panelCollapsed = false; |
| | | this.devpParam.stationId = id; |
| | | this.activateCard = 'devp'; |
| | | this.scheduleMapViewportPaddingUpdate(0); |
| | | this.refreshWorkbench('devp'); |
| | | }, |
| | | systemSwitch() { |
| | | // 系统开关 |
| | | let that = this |
| | | if (this.systemStatus) { |
| | | this.$prompt('请输入口令,并停止WCS系统', '提示', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | }).then(({ |
| | | value |
| | | }) => { |
| | | that.doSwitch(0, value) |
| | | }).catch(() => { |
| | | |
| | | }); |
| | | const password = window.prompt('请输入口令,并停止WCS系统', ''); |
| | | if (password === null) { |
| | | return; |
| | | } |
| | | this.doSwitch(0, password); |
| | | } else { |
| | | this.doSwitch(1) |
| | | } |
| | | }, |
| | | showPageMessage(message, type) { |
| | | if (!message) { |
| | | return; |
| | | } |
| | | if (typeof this.$message === 'function') { |
| | | this.$message({ |
| | | message: message, |
| | | type: type || 'info' |
| | | }); |
| | | return; |
| | | } |
| | | if (window.ELEMENT && typeof window.ELEMENT.Message === 'function') { |
| | | window.ELEMENT.Message({ |
| | | message: message, |
| | | type: type || 'info' |
| | | }); |
| | | return; |
| | | } |
| | | if (window.layer && typeof window.layer.msg === 'function') { |
| | | const iconMap = { |
| | | success: 1, |
| | | error: 2, |
| | | warning: 0 |
| | | }; |
| | | window.layer.msg(message, { |
| | | icon: iconMap[type] != null ? iconMap[type] : 0, |
| | | time: 1800 |
| | | }); |
| | | return; |
| | | } |
| | | console[type === 'error' ? 'error' : 'log'](message); |
| | | }, |
| | | doSwitch(operatorType, password) { |
| | | let that = this |
| | |
| | | } else if (res.code === 403) { |
| | | parent.location.href = baseUrl + "/login"; |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: 'error' |
| | | }); |
| | | that.showPageMessage(res.msg, 'error'); |
| | | } |
| | | } |
| | | }); |
| | |
| | | } else if (res.code === 403) { |
| | | parent.location.href = baseUrl + "/login"; |
| | | } else { |
| | | that.$message({ |
| | | message: res.msg, |
| | | type: 'error' |
| | | }); |
| | | that.showPageMessage(res.msg, 'error'); |
| | | } |
| | | } |
| | | }); |
| | |
| | | return false; |
| | | } |
| | | }, |
| | | handleCardClick(tab, event) { |
| | | |
| | | }, |
| | | //获取库位数据 |
| | | getLocMastData() { |
| | | let that = this; |
| | |
| | | return locInfo.row1 + '-' + locInfo.bay1; |
| | | } |
| | | return ''; |
| | | }, |
| | | } |
| | | }, |
| | | } |
| | | }) |
| | | </script> |
| | | </body> |
| New file |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="utf-8"> |
| | | <title>站点颜色配置</title> |
| | | <meta name="renderer" content="webkit"> |
| | | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css"> |
| | | <link rel="stylesheet" href="../../static/css/cool.css"> |
| | | <style> |
| | | html, body { |
| | | height: 100%; |
| | | margin: 0; |
| | | background: |
| | | radial-gradient(circle at top left, rgba(70, 136, 214, 0.14), transparent 34%), |
| | | radial-gradient(circle at bottom right, rgba(37, 198, 178, 0.12), transparent 28%), |
| | | linear-gradient(180deg, #eef4fa 0%, #e8eef5 100%); |
| | | } |
| | | body { |
| | | font-family: "PingFang SC", "Microsoft YaHei", sans-serif; |
| | | } |
| | | [v-cloak] { |
| | | display: none; |
| | | } |
| | | #app { |
| | | height: 100%; |
| | | padding: 22px; |
| | | box-sizing: border-box; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | } |
| | | .page-hero { |
| | | padding: 22px 24px; |
| | | border-radius: 20px; |
| | | border: 1px solid rgba(255, 255, 255, 0.78); |
| | | background: linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(244, 249, 255, 0.82)); |
| | | box-shadow: 0 18px 50px rgba(48, 74, 104, 0.08); |
| | | } |
| | | .page-title { |
| | | font-size: 26px; |
| | | font-weight: 700; |
| | | color: #223548; |
| | | letter-spacing: 0.01em; |
| | | } |
| | | .page-subtitle { |
| | | margin-top: 8px; |
| | | max-width: 860px; |
| | | font-size: 13px; |
| | | line-height: 1.8; |
| | | color: #66788c; |
| | | } |
| | | .page-panel { |
| | | flex: 1; |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | border-radius: 22px; |
| | | border: 1px solid rgba(221, 231, 242, 0.92); |
| | | background: rgba(251, 253, 255, 0.88); |
| | | box-shadow: 0 20px 48px rgba(49, 76, 106, 0.08); |
| | | overflow: hidden; |
| | | } |
| | | .panel-toolbar { |
| | | padding: 18px 22px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | border-bottom: 1px solid rgba(224, 233, 244, 0.9); |
| | | background: rgba(255, 255, 255, 0.72); |
| | | } |
| | | .panel-tip { |
| | | font-size: 12px; |
| | | color: #74879b; |
| | | line-height: 1.8; |
| | | } |
| | | .color-grid { |
| | | flex: 1; |
| | | min-height: 0; |
| | | padding: 20px 22px 24px; |
| | | overflow: auto; |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); |
| | | grid-auto-rows: max-content; |
| | | align-content: start; |
| | | gap: 16px; |
| | | } |
| | | .color-card { |
| | | position: relative; |
| | | border-radius: 18px; |
| | | border: 1px solid rgba(223, 232, 242, 0.94); |
| | | background: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(246,250,255,0.92)); |
| | | box-shadow: 0 12px 28px rgba(76, 101, 130, 0.07); |
| | | padding: 16px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | overflow: visible; |
| | | } |
| | | .color-card::after { |
| | | content: ""; |
| | | position: absolute; |
| | | top: -30px; |
| | | right: -20px; |
| | | width: 120px; |
| | | height: 120px; |
| | | border-radius: 50%; |
| | | background: rgba(255,255,255,0.42); |
| | | pointer-events: none; |
| | | } |
| | | .color-card-head { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 14px; |
| | | } |
| | | .color-name { |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | color: #24374a; |
| | | } |
| | | .color-status { |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: #7a8da2; |
| | | word-break: break-all; |
| | | } |
| | | .color-chip { |
| | | width: 58px; |
| | | height: 58px; |
| | | border-radius: 18px; |
| | | border: 1px solid rgba(89, 109, 134, 0.18); |
| | | box-shadow: inset 0 0 0 1px rgba(255,255,255,0.46); |
| | | flex-shrink: 0; |
| | | } |
| | | .color-desc { |
| | | font-size: 12px; |
| | | line-height: 1.8; |
| | | color: #66798d; |
| | | } |
| | | .color-editor { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | .color-picker-inline { |
| | | flex-shrink: 0; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 0 10px; |
| | | height: 40px; |
| | | border-radius: 12px; |
| | | border: 1px solid rgba(214, 225, 238, 0.95); |
| | | background: rgba(247, 250, 255, 0.9); |
| | | color: #567089; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | } |
| | | .color-actions { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | } |
| | | .color-default { |
| | | font-size: 12px; |
| | | color: #7890a8; |
| | | } |
| | | .color-meta { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 10px; |
| | | } |
| | | .empty-state { |
| | | padding: 60px 20px; |
| | | text-align: center; |
| | | color: #7b8c9f; |
| | | font-size: 14px; |
| | | } |
| | | .footer-note { |
| | | padding: 0 22px 18px; |
| | | font-size: 12px; |
| | | color: #8696a8; |
| | | } |
| | | .el-color-picker__trigger { |
| | | width: 52px; |
| | | height: 40px; |
| | | padding: 0; |
| | | border-radius: 12px; |
| | | border-color: rgba(211, 223, 237, 0.95); |
| | | background: #fff; |
| | | } |
| | | .el-input__inner { |
| | | border-radius: 12px; |
| | | height: 40px; |
| | | line-height: 40px; |
| | | } |
| | | .el-button + .el-button { |
| | | margin-left: 10px; |
| | | } |
| | | @media (max-width: 900px) { |
| | | #app { |
| | | padding: 14px; |
| | | } |
| | | .page-hero, |
| | | .panel-toolbar, |
| | | .color-grid, |
| | | .footer-note { |
| | | padding-left: 16px; |
| | | padding-right: 16px; |
| | | } |
| | | .color-editor, |
| | | .color-meta { |
| | | flex-wrap: wrap; |
| | | } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div id="app" v-cloak> |
| | | <div class="page-hero"> |
| | | <div class="page-title">站点颜色配置</div> |
| | | <div class="page-subtitle"> |
| | | 单独维护监控地图里站点状态的显示颜色。颜色通过调色盘任意选择,配置保存在 Redis 中; |
| | | 未配置时默认使用当前地图代码里的颜色值。 |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="page-panel"> |
| | | <div class="panel-toolbar"> |
| | | <div class="panel-tip"> |
| | | 建议让“启动入库 / 入库任务 / 出库任务 / 堵塞”保持明显区分,避免现场值守时误判。 |
| | | </div> |
| | | <div> |
| | | <el-button size="small" @click="reloadData" :loading="loading">刷新</el-button> |
| | | <el-button size="small" @click="resetDefaults">恢复默认</el-button> |
| | | <el-button type="primary" size="small" @click="saveConfig" :loading="saving">保存配置</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="items.length" class="color-grid"> |
| | | <div v-for="item in items" :key="item.status" class="color-card"> |
| | | <div class="color-card-head"> |
| | | <div> |
| | | <div class="color-name">{{ item.name }}</div> |
| | | <div class="color-status">{{ item.status }}</div> |
| | | </div> |
| | | <div class="color-chip" :style="{ backgroundColor: item.color }"></div> |
| | | </div> |
| | | |
| | | <div class="color-desc">{{ item.desc }}</div> |
| | | |
| | | <div class="color-editor"> |
| | | <div class="color-picker-inline"> |
| | | <el-color-picker |
| | | v-model="item.color" |
| | | :predefine="predefineColors" |
| | | ></el-color-picker> |
| | | <span>调色盘</span> |
| | | </div> |
| | | <el-input |
| | | v-model="item.color" |
| | | @change="handleColorInput(item)" |
| | | placeholder="#FFFFFF" |
| | | ></el-input> |
| | | </div> |
| | | |
| | | <div class="color-meta"> |
| | | <div class="color-default">默认值:{{ item.defaultColor }}</div> |
| | | <div class="color-actions"> |
| | | <el-button size="mini" @click="applyDefaultColor(item)">恢复默认</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-else class="empty-state">暂无站点颜色配置项</div> |
| | | |
| | | <div class="footer-note">保存后,新打开的监控地图会直接读取 Redis 配置;已打开页面刷新后即可生效。</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.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> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/watch/stationColorConfig.js" charset="utf-8"></script> |
| | | </body> |
| | | </html> |