| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- <template>
- <div class="card-debug">
- <a-row :gutter="[24, 24]">
- <a-col :xs="24" :lg="10">
- <a-card title="设置卡号(V4 协议 0x0003)">
- <a-form layout="vertical">
- <a-form-item label="选择终端">
- <a-select
- v-model:value="selectedDevice"
- placeholder="请选择终端"
- style="width: 100%"
- >
- <a-select-option
- v-for="t in terminals"
- :key="t.address"
- :value="t.address"
- >
- 地址 {{ t.address }} ({{ t.uid_readable || t.uid }})
- </a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="选择端口">
- <a-select
- v-model:value="selectedPort"
- style="width: 100%"
- >
- <a-select-option v-for="n in 24" :key="n" :value="n">端口 P{{ n }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="卡号(12 位十六进制,硬件 6 字节)">
- <a-input
- v-model:value="cardUidInput"
- placeholder="如 ED9A57F95001"
- :maxlength="12"
- style="font-family: 'Monaco', 'Menlo', monospace; text-transform: uppercase; letter-spacing: 1px"
- allow-clear
- />
- <div style="font-size: 12px; color: #999; margin-top: 4px">
- 无分隔符,自动转大写;留空点击「清除」可移除该端口卡号
- </div>
- </a-form-item>
- <a-form-item label="操作">
- <a-space wrap>
- <a-button
- type="primary"
- :disabled="!canSet"
- :loading="setting"
- @click="setCard"
- >
- 设置卡号
- </a-button>
- <a-button
- :disabled="!canControl || setting"
- :loading="setting"
- @click="clearCard"
- >
- 清除该端口卡号
- </a-button>
- </a-space>
- </a-form-item>
- </a-form>
- <a-alert
- v-if="feedbackMsg"
- :type="feedbackType"
- :message="feedbackMsg"
- show-icon
- closable
- style="margin-top: 12px"
- @close="feedbackMsg = ''"
- />
- </a-card>
- <a-card title="读卡号验证(V4 批量 24 路)" style="margin-top: 16px">
- <a-button type="primary" @click="readAllCards" :loading="readingCard" :disabled="!canControl">
- 读取全部卡号
- </a-button>
- <a-table
- v-if="cardList.length > 0"
- :data-source="cardList"
- :columns="cardColumns"
- :pagination="false"
- size="small"
- row-key="antenna"
- style="margin-top: 12px"
- >
- <template #bodyCell="{ column, record }">
- <template v-if="column.key === 'antenna'">
- P{{ record.antenna }}
- </template>
- <template v-else-if="column.key === 'present'">
- <a-tag :color="record.present ? 'green' : 'default'">
- {{ record.present ? '有卡' : '无卡' }}
- </a-tag>
- </template>
- <template v-else-if="column.key === 'card_str'">
- <span class="card-uid">{{ record.present ? formatUid(record.card_str) : '-' }}</span>
- </template>
- </template>
- </a-table>
- <a-empty v-else-if="!readingCard" description="设置卡号后点击「读取全部卡号」验证结果" style="margin-top: 16px" />
- </a-card>
- </a-col>
- <a-col :xs="24" :lg="14">
- <a-card title="24 路端口卡号状态">
- <a-alert
- v-if="!dataStore.serialConnected"
- message="串口未连接"
- type="warning"
- show-icon
- style="margin-bottom: 16px"
- />
- <div class="port-grid">
- <div
- v-for="n in 24"
- :key="n"
- class="port-cell"
- :class="{ active: selectedPort === n }"
- @click="selectedPort = n"
- >
- <div class="port-dot" :class="portClass(n)">
- <span class="port-label">{{ n }}</span>
- </div>
- <div class="port-info">
- <div class="port-status-text">{{ portStatusText(n) }}</div>
- <div class="port-uid-text">{{ portUidText(n) }}</div>
- </div>
- </div>
- </div>
- <div class="legend">
- <span class="legend-item">
- <span class="legend-dot present"></span> 有卡
- </span>
- <span class="legend-item">
- <span class="legend-dot empty"></span> 无卡
- </span>
- <span class="legend-item">
- <span class="legend-dot unknown"></span> 未读取
- </span>
- </div>
- <div style="margin-top: 16px; color: #999; font-size: 13px; line-height: 1.8">
- <p>使用说明:</p>
- <ol>
- <li>选择终端地址(从已配置设备列表加载)</li>
- <li>选择要设置卡号的端口(P1 ~ P24)</li>
- <li>输入 12 位十六进制卡号,点击「设置卡号」</li>
- <li>点击「读取全部卡号」验证硬件是否写入成功</li>
- </ol>
- </div>
- </a-card>
- </a-col>
- </a-row>
- <a-row :gutter="[24, 24]" style="margin-top: 16px;">
- <a-col :span="24">
- <SerialDataDisplay />
- </a-col>
- </a-row>
- </div>
- </template>
- <script setup>
- import { ref, computed, onMounted } from 'vue'
- import apiService from '../api/apiService'
- import { useDataStore } from '../stores/dataStore'
- import SerialDataDisplay from '../components/SerialDataDisplay.vue'
- const dataStore = useDataStore()
- // V4 卡号统一格式:12 位大写 hex,无分隔
- function formatUid(uid) {
- if (!uid) return '--'
- const s = String(uid).toUpperCase().replace(/[:\s-]/g, '')
- return s || '--'
- }
- const selectedDevice = ref(undefined)
- const selectedPort = ref(1)
- const terminals = ref([])
- const cardUidInput = ref('')
- const setting = ref(false)
- const readingCard = ref(false)
- const cardList = ref([])
- const feedbackMsg = ref('')
- const feedbackType = ref('success')
- const cardColumns = [
- { title: '端口', key: 'antenna', dataIndex: 'antenna', width: 70 },
- { title: '状态', key: 'present', dataIndex: 'present', width: 90 },
- { title: '卡号', key: 'card_str', dataIndex: 'card_str' }
- ]
- const canControl = computed(() => terminals.value.length > 0)
- const canSet = computed(() => {
- if (!canControl.value) return false
- const uid = (cardUidInput.value || '').replace(/[:\s-]/g, '')
- return uid.length === 12 && /^[0-9A-Fa-f]{12}$/.test(uid)
- })
- // 端口状态网格
- function portClass(n) {
- const card = cardList.value.find(c => c.antenna === n)
- if (!card) return 'unknown'
- return card.present ? 'present' : 'empty'
- }
- function portStatusText(n) {
- const card = cardList.value.find(c => c.antenna === n)
- if (!card) return '未读取'
- return card.present ? '有卡' : '无卡'
- }
- function portUidText(n) {
- const card = cardList.value.find(c => c.antenna === n)
- if (!card || !card.present) return ''
- return formatUid(card.card_str)
- }
- function showFeedback(msg, type = 'success') {
- feedbackMsg.value = msg
- feedbackType.value = type
- setTimeout(() => { feedbackMsg.value = '' }, 4000)
- }
- async function setCard() {
- const uid = (cardUidInput.value || '').replace(/[:\s-]/g, '').toUpperCase()
- if (!/^[0-9A-F]{12}$/.test(uid)) {
- showFeedback('卡号必须是 12 位十六进制', 'error')
- return
- }
- setting.value = true
- feedbackMsg.value = ''
- try {
- const res = await apiService.modbus.setCardMapping({
- device_address: selectedDevice.value,
- port_id: selectedPort.value,
- card_uid: uid
- })
- if (res.data.success) {
- showFeedback(`端口 P${selectedPort.value} 卡号已下发硬件: ${uid}`, 'success')
- } else {
- showFeedback('设置失败: ' + (res.data.message || '未知错误'), 'error')
- }
- } catch (e) {
- showFeedback('设置错误: ' + (e.response?.data?.message || e.message), 'error')
- } finally {
- setting.value = false
- }
- }
- async function clearCard() {
- setting.value = true
- feedbackMsg.value = ''
- try {
- const res = await apiService.modbus.setCardMapping({
- device_address: selectedDevice.value,
- port_id: selectedPort.value,
- card_uid: null
- })
- if (res.data.success) {
- showFeedback(`端口 P${selectedPort.value} 卡号已清除(填 0xFF)`, 'success')
- } else {
- showFeedback('清除失败: ' + (res.data.message || '未知错误'), 'error')
- }
- } catch (e) {
- showFeedback('清除错误: ' + (e.response?.data?.message || e.message), 'error')
- } finally {
- setting.value = false
- }
- }
- async function loadTerminals() {
- try {
- const res = await apiService.modbus.getStoredDevices()
- if (res.data.success && res.data.devices) {
- const entries = Object.entries(res.data.devices)
- terminals.value = entries.map(([uid, addr]) => ({
- uid, uid_readable: uid.match(/.{1,2}/g)?.join(':') || uid, address: addr
- }))
- if (terminals.value.length) {
- selectedDevice.value = terminals.value[0].address
- }
- }
- } catch (e) { console.error(e) }
- }
- async function readAllCards() {
- readingCard.value = true
- cardList.value = []
- try {
- const res = await apiService.modbus.readAllCards({
- device_address: selectedDevice.value
- })
- if (res.data.success && res.data.data && res.data.data.cards) {
- cardList.value = res.data.data.cards.map(c => ({
- key: c.antenna,
- antenna: c.antenna,
- present: !!c.present,
- card_str: c.card_str || '',
- uid: c.uid || ''
- }))
- const has = cardList.value.filter(c => c.present).length
- showFeedback(`读取成功,24 路中 ${has} 路有卡`, 'success')
- } else {
- showFeedback('读卡失败: ' + (res.data.message || '未知错误'), 'error')
- }
- } catch (e) {
- showFeedback('读卡错误: ' + (e.response?.data?.message || e.message), 'error')
- } finally {
- readingCard.value = false
- }
- }
- onMounted(() => {
- loadTerminals()
- })
- </script>
- <style scoped>
- .card-debug {
- width: 100%;
- }
- .port-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 12px;
- }
- @media (max-width: 768px) {
- .port-grid { grid-template-columns: repeat(3, 1fr); }
- }
- @media (max-width: 480px) {
- .port-grid { grid-template-columns: repeat(2, 1fr); }
- }
- .port-cell {
- display: flex;
- align-items: center;
- gap: 10px;
- cursor: pointer;
- padding: 8px;
- border-radius: 10px;
- border: 2px solid transparent;
- transition: all 0.2s;
- }
- .port-cell.active {
- border-color: #1890ff;
- background: #e6f7ff;
- }
- .port-dot {
- width: 48px;
- height: 48px;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- border: 2px solid rgba(0,0,0,0.1);
- flex-shrink: 0;
- }
- .port-dot.present { background: #52c41a; }
- .port-dot.empty { background: #d9d9d9; }
- .port-dot.unknown { background: #bfbfbf; }
- .port-label {
- font-size: 16px;
- font-weight: 700;
- color: #fff;
- text-shadow: 0 1px 3px rgba(0,0,0,0.4);
- }
- .port-info {
- flex: 1;
- min-width: 0;
- }
- .port-status-text {
- font-size: 13px;
- font-weight: 600;
- color: #333;
- }
- .port-uid-text {
- font-size: 11px;
- font-family: 'Monaco', 'Menlo', monospace;
- color: #666;
- word-break: break-all;
- }
- .legend {
- display: flex;
- gap: 20px;
- margin-top: 16px;
- flex-wrap: wrap;
- font-size: 13px;
- color: #666;
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .legend-dot {
- width: 14px;
- height: 14px;
- border-radius: 4px;
- border: 1px solid rgba(0,0,0,0.1);
- }
- .legend-dot.present { background: #52c41a; }
- .legend-dot.empty { background: #d9d9d9; }
- .legend-dot.unknown { background: #bfbfbf; }
- .card-uid {
- font-family: 'Monaco', 'Menlo', monospace;
- font-size: 13px;
- color: #333;
- word-break: break-all;
- }
- </style>
|