yeg
2026-09-05 dbefaa087017ba2056ebc13bcd4d41de47ddb4e6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { createDeviceCollector, getDeviceCollector, getDiscoveredCollectorList, updateDeviceCollector } from '@/api/deviceCollectors'
import type { CollectorPayload } from '@/stores/deviceCollectors'
import DeviceRelatedTests from '@/components/DeviceRelatedTests.vue'
 
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const saving = ref(false)
const deviceDialogVisible = ref(false)
const deviceLoading = ref(false)
const devicePage = ref(1)
const devicePageSize = 5
const devices = ref<CollectorPayload[]>([])
const deviceTotal = ref(0)
const channelOptions = Array.from({ length: 99 }, (_, index) => index + 1)
const editingId = computed(() => Number(route.params.id) || null)
const isEditing = computed(() => editingId.value !== null)
 
const form = reactive<CollectorPayload>({
  deviceCode: '', ip: '', mac: '', name: '', manufacturer: '', model: '', channelCount: 1,
  remark: '', onlineStatus: '离线', relatedTests: [], status: '启用',
})
 
async function loadEditingCollector() {
  if (!editingId.value) return
  loading.value = true
  try {
    Object.assign(form, await getDeviceCollector(editingId.value))
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '采集器详情加载失败')
    await router.replace('/device-collector')
  } finally {
    loading.value = false
  }
}
 
onMounted(loadEditingCollector)
 
async function loadDiscoveredCollectors() {
  deviceLoading.value = true
  try {
    const result = await getDiscoveredCollectorList(devicePage.value, devicePageSize)
    devices.value = result.rows
    deviceTotal.value = result.total
  } catch (error) {
    devices.value = []
    deviceTotal.value = 0
    ElMessage.error(error instanceof Error ? error.message : '可发现采集器加载失败')
  } finally {
    deviceLoading.value = false
  }
}
 
async function openDeviceDialog() {
  devicePage.value = 1
  deviceDialogVisible.value = true
  await loadDiscoveredCollectors()
}
 
async function handleDevicePageChange(page: number) {
  devicePage.value = page
  await loadDiscoveredCollectors()
}
 
function selectDevice(device: CollectorPayload) {
  Object.assign(form, {
    deviceCode: device.deviceCode,
    ip: device.ip,
    mac: device.mac,
    name: device.name,
    manufacturer: device.manufacturer,
    model: device.model,
    channelCount: device.channelCount,
    onlineStatus: device.onlineStatus,
    relatedTests: [],
  })
  deviceDialogVisible.value = false
  ElMessage.success(`已选择设备 ${device.deviceCode}`)
}
 
function validate() {
  if (!form.deviceCode) return '请先选择设备'
  if (!form.name.trim()) return '请输入采集器名称'
  if (!form.manufacturer.trim()) return '请输入制造商'
  if (!form.model.trim()) return '请输入型号'
  if (!Number.isInteger(form.channelCount) || form.channelCount < 1 || form.channelCount > 99) return '请选择 1~99 的通道数'
  return ''
}
 
async function submit() {
  const error = validate()
  if (error) {
    ElMessage.warning(error)
    return
  }
 
  saving.value = true
  try {
    if (editingId.value) await updateDeviceCollector(editingId.value, form)
    else await createDeviceCollector(form)
    ElMessage.success(isEditing.value ? '采集器已更新' : '采集器已添加')
    await router.push('/device-collector')
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '采集器保存失败')
  } finally {
    saving.value = false
  }
}
</script>
 
<template>
  <div class="collector-form-page">
    <div class="page-tools"><button class="back-button" type="button" @click="router.push('/device-collector')"><el-icon><ArrowLeft /></el-icon>返回采集器管理</button><span>{{ isEditing ? 'EDIT TEST COLLECTOR' : 'NEW TEST COLLECTOR' }}</span></div>
 
    <section v-loading="loading" class="detail-panel">
      <header class="panel-heading"><div><i></i><strong>采集器详情</strong><span>TEST COLLECTOR DETAILS</span></div><small><b>*</b> 为必填项</small></header>
      <div class="device-select-row"><button class="primary-button" type="button" @click="openDeviceDialog"><el-icon><Connection /></el-icon>选择设备</button><span>显示当前网段全部可发现设备</span></div>
 
      <div class="device-summary">
        <div><span>设备编码</span><strong>{{ form.deviceCode || '-' }}</strong></div>
        <div><span>IP</span><strong class="mono-value">{{ form.ip || '-' }}</strong></div>
        <div><span>MAC</span><strong class="mono-value">{{ form.mac || '-' }}</strong></div>
        <div><span>在线状态</span><strong :class="form.onlineStatus === '在线' ? 'online-value' : 'offline-value'">{{ form.deviceCode ? form.onlineStatus : '-' }}</strong></div>
        <div><span>关联测试</span><DeviceRelatedTests :tests="form.relatedTests" /></div>
      </div>
 
      <div class="detail-form">
        <label><span><b>*</b>采集器名称</span><el-input v-model="form.name" maxlength="30" show-word-limit placeholder="输入采集器名称" /></label>
        <label><span><b>*</b>制造商</span><el-input v-model="form.manufacturer" maxlength="30" placeholder="输入制造商" /></label>
        <label><span><b>*</b>型号</span><el-input v-model="form.model" maxlength="30" placeholder="输入型号" /></label>
        <label><span>通道数</span><el-select v-model="form.channelCount"><el-option v-for="item in channelOptions" :key="item" :label="`${item} 通道`" :value="item" /></el-select></label>
        <label><span>状态</span><el-radio-group v-model="form.status"><el-radio-button value="启用">启用</el-radio-button><el-radio-button value="禁用">禁用</el-radio-button></el-radio-group></label>
        <label class="remark-field"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="输入内容" /></label>
      </div>
 
      <footer class="form-footer"><button class="secondary-button" type="button" @click="router.push('/device-collector')">取消</button><button class="primary-button save-button" type="button" :disabled="loading || saving" @click="submit"><el-icon><Check /></el-icon>{{ saving ? '保存中' : '保存' }}</button></footer>
    </section>
 
    <el-dialog v-model="deviceDialogVisible" class="device-dialog" title="选择设备" width="720px" align-center>
      <div v-loading="deviceLoading" class="device-table-wrap">
        <table class="device-table">
          <thead><tr><th>设备编码</th><th>IP</th><th>MAC</th><th>操作</th></tr></thead>
          <tbody>
            <tr v-for="device in devices" :key="device.deviceCode"><td>{{ device.deviceCode }}</td><td>{{ device.ip }}</td><td>{{ device.mac }}</td><td><button type="button" @click="selectDevice(device)">选择</button></td></tr>
            <tr v-if="!deviceLoading && !devices.length"><td colspan="4" class="empty-cell">暂无可发现采集器</td></tr>
          </tbody>
        </table>
      </div>
      <footer class="device-pagination"><span>共 {{ deviceTotal }} 个设备</span><el-pagination :current-page="devicePage" :page-size="devicePageSize" :total="deviceTotal" layout="prev, pager, next" background @current-change="handleDevicePageChange" /></footer>
    </el-dialog>
  </div>
</template>
 
<style scoped>
.collector-form-page { width: 100%; }
.page-tools { height: 34px; display: flex; align-items: center; justify-content: space-between; }
.page-tools > span { color: #7f8d99; font-size: 10px; font-weight: 600; }
.back-button { display: inline-flex; align-items: center; gap: 6px; border: 0; color: #52606d; background: transparent; font-size: 13px; cursor: pointer; }
.back-button:hover { color: var(--blue); }
.detail-panel { background: #fff; border: 1px solid #d8e1e9; }
.panel-heading { min-height: 48px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 16px; background: var(--surface-soft); border-bottom: 1px solid var(--line); }
.panel-heading > div { display: flex; align-items: center; gap: 8px; }
.panel-heading i { width: 3px; height: 16px; background: #2b78a3; }
.panel-heading strong { color: var(--ink); font-size: 15px; font-weight: 600; }
.panel-heading span, .panel-heading small { color: #81909d; font-size: 10px; }
.panel-heading small b, .detail-form label > span b { color: #b34f4f; }
.device-select-row { min-height: 62px; display: flex; align-items: center; gap: 10px; padding: 12px 18px 6px; }
.device-select-row > span { color: #83919e; font-size: 11px; }
.primary-button, .secondary-button { height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 13px; border: 1px solid transparent; border-radius: 3px; font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap; }
.primary-button { color: #fff; background: var(--blue); border-color: var(--blue); }
.primary-button:hover { background: #176fcb; border-color: #176fcb; }
.primary-button:disabled { opacity: .55; cursor: wait; }
.secondary-button { color: #52606d; background: #fff; border-color: var(--line-strong); }
.secondary-button:hover { color: var(--blue); border-color: #9dc8f5; }
.device-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 30px; padding: 16px 18px 20px; border-bottom: 1px solid var(--line); }
.device-summary > div { min-width: 0; display: grid; grid-template-columns: 82px minmax(0, 1fr); gap: 8px; align-items: center; }
.device-summary span { color: #7b8794; font-size: 12px; }
.device-summary strong { min-width: 0; color: #44515d; font-size: 12px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.device-summary .online-value { color: #23865e; }
.device-summary .offline-value { color: #a64a4a; }
.mono-value { font-family: 'Cascadia Mono', Consolas, monospace; }
.detail-form { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; gap: 20px 24px; padding: 22px 18px 28px; }
.detail-form label { min-width: 0; display: flex; flex-direction: column; gap: 7px; }
.detail-form label > span { color: #52606d; font-size: 12px; font-weight: 500; }
.detail-form :deep(.el-select) { width: 100%; }
.collector-form-page :deep(.el-input__wrapper), .collector-form-page :deep(.el-select__wrapper), .collector-form-page :deep(.el-textarea__inner) { border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; }
.collector-form-page :deep(.el-input__wrapper), .collector-form-page :deep(.el-select__wrapper) { min-height: 34px; }
.remark-field { grid-column: span 2; }
.form-footer { min-height: 70px; display: flex; align-items: center; gap: 10px; padding: 0 18px; border-top: 1px solid var(--line); }
.save-button { min-width: 82px; }
.collector-form-page :deep(.device-dialog) { max-width: calc(100vw - 28px); border-radius: 4px; }
.collector-form-page :deep(.el-dialog__header) { margin: 0; padding: 17px 20px; border-bottom: 1px solid var(--line); }
.collector-form-page :deep(.el-dialog__body) { padding: 20px; }
.device-table-wrap { overflow-x: auto; }
.device-table { width: 100%; min-width: 560px; table-layout: fixed; border-collapse: collapse; color: #52606d; font-size: 13px; }
.device-table th, .device-table td { height: 42px; padding: 0 12px; border: 1px solid #d8e1e9; text-align: left; }
.device-table th { height: 38px; color: #52606d; background: #eaf4ff; font-size: 12px; font-weight: 600; }
.device-table th:last-child, .device-table td:last-child { width: 88px; text-align: center; }
.device-table td:last-child button { border: 0; color: var(--blue); background: transparent; font-size: 12px; font-weight: 500; cursor: pointer; }
.device-table tbody tr:hover { background: #f7fbff; }
.device-table .empty-cell { height: 96px; color: #7b8794; text-align: center; }
.device-pagination { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-top: 18px; }
.device-pagination > span { color: #7b8794; font-size: 11px; }
.device-pagination :deep(.el-pagination) { --el-pagination-button-bg-color: #edf4fa; --el-pagination-hover-color: var(--blue); }
@media (max-width: 960px) { .device-summary, .detail-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px) {
  .panel-heading { padding: 0 12px; }
  .panel-heading span, .panel-heading small, .device-select-row > span { display: none; }
  .device-summary, .detail-form { grid-template-columns: minmax(0, 1fr); padding-left: 12px; padding-right: 12px; }
  .remark-field { grid-column: auto; }
  .form-footer { padding: 0 12px; }
  .form-footer button { flex: 1; }
  .collector-form-page :deep(.el-dialog__body) { padding: 14px 12px; }
}
</style>