LedDebug.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <template>
  2. <div class="card-debug">
  3. <a-row :gutter="[24, 24]">
  4. <a-col :xs="24" :lg="10">
  5. <a-card title="设置卡号(V4 协议 0x0003)">
  6. <a-form layout="vertical">
  7. <a-form-item label="选择终端">
  8. <a-select
  9. v-model:value="selectedDevice"
  10. placeholder="请选择终端"
  11. style="width: 100%"
  12. >
  13. <a-select-option
  14. v-for="t in terminals"
  15. :key="t.address"
  16. :value="t.address"
  17. >
  18. 地址 {{ t.address }} ({{ t.uid_readable || t.uid }})
  19. </a-select-option>
  20. </a-select>
  21. </a-form-item>
  22. <a-form-item label="选择端口">
  23. <a-select
  24. v-model:value="selectedPort"
  25. style="width: 100%"
  26. >
  27. <a-select-option v-for="n in 24" :key="n" :value="n">端口 P{{ n }}</a-select-option>
  28. </a-select>
  29. </a-form-item>
  30. <a-form-item label="卡号(12 位十六进制,硬件 6 字节)">
  31. <a-input
  32. v-model:value="cardUidInput"
  33. placeholder="如 ED9A57F95001"
  34. :maxlength="12"
  35. style="font-family: 'Monaco', 'Menlo', monospace; text-transform: uppercase; letter-spacing: 1px"
  36. allow-clear
  37. />
  38. <div style="font-size: 12px; color: #999; margin-top: 4px">
  39. 无分隔符,自动转大写;留空点击「清除」可移除该端口卡号
  40. </div>
  41. </a-form-item>
  42. <a-form-item label="操作">
  43. <a-space wrap>
  44. <a-button
  45. type="primary"
  46. :disabled="!canSet"
  47. :loading="setting"
  48. @click="setCard"
  49. >
  50. 设置卡号
  51. </a-button>
  52. <a-button
  53. :disabled="!canControl || setting"
  54. :loading="setting"
  55. @click="clearCard"
  56. >
  57. 清除该端口卡号
  58. </a-button>
  59. </a-space>
  60. </a-form-item>
  61. </a-form>
  62. <a-alert
  63. v-if="feedbackMsg"
  64. :type="feedbackType"
  65. :message="feedbackMsg"
  66. show-icon
  67. closable
  68. style="margin-top: 12px"
  69. @close="feedbackMsg = ''"
  70. />
  71. </a-card>
  72. <a-card title="读卡号验证(V4 批量 24 路)" style="margin-top: 16px">
  73. <a-button type="primary" @click="readAllCards" :loading="readingCard" :disabled="!canControl">
  74. 读取全部卡号
  75. </a-button>
  76. <a-table
  77. v-if="cardList.length > 0"
  78. :data-source="cardList"
  79. :columns="cardColumns"
  80. :pagination="false"
  81. size="small"
  82. row-key="antenna"
  83. style="margin-top: 12px"
  84. >
  85. <template #bodyCell="{ column, record }">
  86. <template v-if="column.key === 'antenna'">
  87. P{{ record.antenna }}
  88. </template>
  89. <template v-else-if="column.key === 'present'">
  90. <a-tag :color="record.present ? 'green' : 'default'">
  91. {{ record.present ? '有卡' : '无卡' }}
  92. </a-tag>
  93. </template>
  94. <template v-else-if="column.key === 'card_str'">
  95. <span class="card-uid">{{ record.present ? formatUid(record.card_str) : '-' }}</span>
  96. </template>
  97. </template>
  98. </a-table>
  99. <a-empty v-else-if="!readingCard" description="设置卡号后点击「读取全部卡号」验证结果" style="margin-top: 16px" />
  100. </a-card>
  101. </a-col>
  102. <a-col :xs="24" :lg="14">
  103. <a-card title="24 路端口卡号状态">
  104. <a-alert
  105. v-if="!dataStore.serialConnected"
  106. message="串口未连接"
  107. type="warning"
  108. show-icon
  109. style="margin-bottom: 16px"
  110. />
  111. <div class="port-grid">
  112. <div
  113. v-for="n in 24"
  114. :key="n"
  115. class="port-cell"
  116. :class="{ active: selectedPort === n }"
  117. @click="selectedPort = n"
  118. >
  119. <div class="port-dot" :class="portClass(n)">
  120. <span class="port-label">{{ n }}</span>
  121. </div>
  122. <div class="port-info">
  123. <div class="port-status-text">{{ portStatusText(n) }}</div>
  124. <div class="port-uid-text">{{ portUidText(n) }}</div>
  125. </div>
  126. </div>
  127. </div>
  128. <div class="legend">
  129. <span class="legend-item">
  130. <span class="legend-dot present"></span> 有卡
  131. </span>
  132. <span class="legend-item">
  133. <span class="legend-dot empty"></span> 无卡
  134. </span>
  135. <span class="legend-item">
  136. <span class="legend-dot unknown"></span> 未读取
  137. </span>
  138. </div>
  139. <div style="margin-top: 16px; color: #999; font-size: 13px; line-height: 1.8">
  140. <p>使用说明:</p>
  141. <ol>
  142. <li>选择终端地址(从已配置设备列表加载)</li>
  143. <li>选择要设置卡号的端口(P1 ~ P24)</li>
  144. <li>输入 12 位十六进制卡号,点击「设置卡号」</li>
  145. <li>点击「读取全部卡号」验证硬件是否写入成功</li>
  146. </ol>
  147. </div>
  148. </a-card>
  149. </a-col>
  150. </a-row>
  151. <a-row :gutter="[24, 24]" style="margin-top: 16px;">
  152. <a-col :span="24">
  153. <SerialDataDisplay />
  154. </a-col>
  155. </a-row>
  156. </div>
  157. </template>
  158. <script setup>
  159. import { ref, computed, onMounted } from 'vue'
  160. import apiService from '../api/apiService'
  161. import { useDataStore } from '../stores/dataStore'
  162. import SerialDataDisplay from '../components/SerialDataDisplay.vue'
  163. const dataStore = useDataStore()
  164. // V4 卡号统一格式:12 位大写 hex,无分隔
  165. function formatUid(uid) {
  166. if (!uid) return '--'
  167. const s = String(uid).toUpperCase().replace(/[:\s-]/g, '')
  168. return s || '--'
  169. }
  170. const selectedDevice = ref(undefined)
  171. const selectedPort = ref(1)
  172. const terminals = ref([])
  173. const cardUidInput = ref('')
  174. const setting = ref(false)
  175. const readingCard = ref(false)
  176. const cardList = ref([])
  177. const feedbackMsg = ref('')
  178. const feedbackType = ref('success')
  179. const cardColumns = [
  180. { title: '端口', key: 'antenna', dataIndex: 'antenna', width: 70 },
  181. { title: '状态', key: 'present', dataIndex: 'present', width: 90 },
  182. { title: '卡号', key: 'card_str', dataIndex: 'card_str' }
  183. ]
  184. const canControl = computed(() => terminals.value.length > 0)
  185. const canSet = computed(() => {
  186. if (!canControl.value) return false
  187. const uid = (cardUidInput.value || '').replace(/[:\s-]/g, '')
  188. return uid.length === 12 && /^[0-9A-Fa-f]{12}$/.test(uid)
  189. })
  190. // 端口状态网格
  191. function portClass(n) {
  192. const card = cardList.value.find(c => c.antenna === n)
  193. if (!card) return 'unknown'
  194. return card.present ? 'present' : 'empty'
  195. }
  196. function portStatusText(n) {
  197. const card = cardList.value.find(c => c.antenna === n)
  198. if (!card) return '未读取'
  199. return card.present ? '有卡' : '无卡'
  200. }
  201. function portUidText(n) {
  202. const card = cardList.value.find(c => c.antenna === n)
  203. if (!card || !card.present) return ''
  204. return formatUid(card.card_str)
  205. }
  206. function showFeedback(msg, type = 'success') {
  207. feedbackMsg.value = msg
  208. feedbackType.value = type
  209. setTimeout(() => { feedbackMsg.value = '' }, 4000)
  210. }
  211. async function setCard() {
  212. const uid = (cardUidInput.value || '').replace(/[:\s-]/g, '').toUpperCase()
  213. if (!/^[0-9A-F]{12}$/.test(uid)) {
  214. showFeedback('卡号必须是 12 位十六进制', 'error')
  215. return
  216. }
  217. setting.value = true
  218. feedbackMsg.value = ''
  219. try {
  220. const res = await apiService.modbus.setCardMapping({
  221. device_address: selectedDevice.value,
  222. port_id: selectedPort.value,
  223. card_uid: uid
  224. })
  225. if (res.data.success) {
  226. showFeedback(`端口 P${selectedPort.value} 卡号已下发硬件: ${uid}`, 'success')
  227. } else {
  228. showFeedback('设置失败: ' + (res.data.message || '未知错误'), 'error')
  229. }
  230. } catch (e) {
  231. showFeedback('设置错误: ' + (e.response?.data?.message || e.message), 'error')
  232. } finally {
  233. setting.value = false
  234. }
  235. }
  236. async function clearCard() {
  237. setting.value = true
  238. feedbackMsg.value = ''
  239. try {
  240. const res = await apiService.modbus.setCardMapping({
  241. device_address: selectedDevice.value,
  242. port_id: selectedPort.value,
  243. card_uid: null
  244. })
  245. if (res.data.success) {
  246. showFeedback(`端口 P${selectedPort.value} 卡号已清除(填 0xFF)`, 'success')
  247. } else {
  248. showFeedback('清除失败: ' + (res.data.message || '未知错误'), 'error')
  249. }
  250. } catch (e) {
  251. showFeedback('清除错误: ' + (e.response?.data?.message || e.message), 'error')
  252. } finally {
  253. setting.value = false
  254. }
  255. }
  256. async function loadTerminals() {
  257. try {
  258. const res = await apiService.modbus.getStoredDevices()
  259. if (res.data.success && res.data.devices) {
  260. const entries = Object.entries(res.data.devices)
  261. terminals.value = entries.map(([uid, addr]) => ({
  262. uid, uid_readable: uid.match(/.{1,2}/g)?.join(':') || uid, address: addr
  263. }))
  264. if (terminals.value.length) {
  265. selectedDevice.value = terminals.value[0].address
  266. }
  267. }
  268. } catch (e) { console.error(e) }
  269. }
  270. async function readAllCards() {
  271. readingCard.value = true
  272. cardList.value = []
  273. try {
  274. const res = await apiService.modbus.readAllCards({
  275. device_address: selectedDevice.value
  276. })
  277. if (res.data.success && res.data.data && res.data.data.cards) {
  278. cardList.value = res.data.data.cards.map(c => ({
  279. key: c.antenna,
  280. antenna: c.antenna,
  281. present: !!c.present,
  282. card_str: c.card_str || '',
  283. uid: c.uid || ''
  284. }))
  285. const has = cardList.value.filter(c => c.present).length
  286. showFeedback(`读取成功,24 路中 ${has} 路有卡`, 'success')
  287. } else {
  288. showFeedback('读卡失败: ' + (res.data.message || '未知错误'), 'error')
  289. }
  290. } catch (e) {
  291. showFeedback('读卡错误: ' + (e.response?.data?.message || e.message), 'error')
  292. } finally {
  293. readingCard.value = false
  294. }
  295. }
  296. onMounted(() => {
  297. loadTerminals()
  298. })
  299. </script>
  300. <style scoped>
  301. .card-debug {
  302. width: 100%;
  303. }
  304. .port-grid {
  305. display: grid;
  306. grid-template-columns: repeat(4, 1fr);
  307. gap: 12px;
  308. }
  309. @media (max-width: 768px) {
  310. .port-grid { grid-template-columns: repeat(3, 1fr); }
  311. }
  312. @media (max-width: 480px) {
  313. .port-grid { grid-template-columns: repeat(2, 1fr); }
  314. }
  315. .port-cell {
  316. display: flex;
  317. align-items: center;
  318. gap: 10px;
  319. cursor: pointer;
  320. padding: 8px;
  321. border-radius: 10px;
  322. border: 2px solid transparent;
  323. transition: all 0.2s;
  324. }
  325. .port-cell.active {
  326. border-color: #1890ff;
  327. background: #e6f7ff;
  328. }
  329. .port-dot {
  330. width: 48px;
  331. height: 48px;
  332. border-radius: 10px;
  333. display: flex;
  334. align-items: center;
  335. justify-content: center;
  336. border: 2px solid rgba(0,0,0,0.1);
  337. flex-shrink: 0;
  338. }
  339. .port-dot.present { background: #52c41a; }
  340. .port-dot.empty { background: #d9d9d9; }
  341. .port-dot.unknown { background: #bfbfbf; }
  342. .port-label {
  343. font-size: 16px;
  344. font-weight: 700;
  345. color: #fff;
  346. text-shadow: 0 1px 3px rgba(0,0,0,0.4);
  347. }
  348. .port-info {
  349. flex: 1;
  350. min-width: 0;
  351. }
  352. .port-status-text {
  353. font-size: 13px;
  354. font-weight: 600;
  355. color: #333;
  356. }
  357. .port-uid-text {
  358. font-size: 11px;
  359. font-family: 'Monaco', 'Menlo', monospace;
  360. color: #666;
  361. word-break: break-all;
  362. }
  363. .legend {
  364. display: flex;
  365. gap: 20px;
  366. margin-top: 16px;
  367. flex-wrap: wrap;
  368. font-size: 13px;
  369. color: #666;
  370. }
  371. .legend-item {
  372. display: flex;
  373. align-items: center;
  374. gap: 6px;
  375. }
  376. .legend-dot {
  377. width: 14px;
  378. height: 14px;
  379. border-radius: 4px;
  380. border: 1px solid rgba(0,0,0,0.1);
  381. }
  382. .legend-dot.present { background: #52c41a; }
  383. .legend-dot.empty { background: #d9d9d9; }
  384. .legend-dot.unknown { background: #bfbfbf; }
  385. .card-uid {
  386. font-family: 'Monaco', 'Menlo', monospace;
  387. font-size: 13px;
  388. color: #333;
  389. word-break: break-all;
  390. }
  391. </style>