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
<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, updateDeviceCollector } from '@/api/deviceCollectors'
import { getDeviceAdapterOptions, type DeviceAdapterOption } from '@/api/deviceAdapters'
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 adapterOptions = ref<DeviceAdapterOption[]>([])
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>({
  adapterId: null, 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
  }
}
 
/**
 * 采集器不再有发现候选:真实来源不产出采集器候选,登记由操作员手工填写来源身份。
 * 这里只加载可选的数据来源实例(采集器的辅助测量由它提供);读不到时给出可见错误,
 * 不能把权限拒绝或网络失败显示成"没有来源"。
 */
async function loadAdapterOptions() {
  try {
    adapterOptions.value = await getDeviceAdapterOptions()
  } catch (error) {
    adapterOptions.value = []
    ElMessage.error(error instanceof Error ? error.message : '数据来源列表加载失败')
  }
}
 
onMounted(async () => {
  await Promise.all([loadEditingCollector(), loadAdapterOptions()])
})
 
function validate() {
  if (!form.deviceCode.trim()) return '请输入设备编码'
  if (!form.name.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"><span>采集器的测量按「同一来源的辅助测量」表达:选择它所属的数据来源,在线状态由该来源的连接结果决定。设备编码由操作员按现场叫法填写,系统不自动改写。</span></div>
 
      <div class="detail-form">
        <label><span><b>*</b>设备编码</span><el-input v-model="form.deviceCode" maxlength="64" :disabled="isEditing" placeholder="输入设备编码" /></label>
        <label><span>所属数据来源</span><el-select v-model="form.adapterId" clearable placeholder="选择来源实例"><el-option v-for="item in adapterOptions" :key="item.id" :label="`${item.name}(${item.type})`" :value="item.id" /></el-select></label>
        <label><span>IP</span><el-input v-model="form.ip" maxlength="45" placeholder="可留空" /></label>
        <label><span>MAC</span><el-input v-model="form.mac" maxlength="17" placeholder="可留空" /></label>
        <label><span><b>*</b>采集器名称</span><el-input v-model="form.name" maxlength="30" show-word-limit placeholder="输入采集器名称" /></label>
        <label><span>制造商</span><el-input v-model="form.manufacturer" maxlength="30" placeholder="输入制造商" /></label>
        <label><span>型号</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-input :model-value="form.deviceCode ? form.onlineStatus : '-'" disabled /></label>
        <label class="related-field"><span>关联测试</span><DeviceRelatedTests :tests="form.relatedTests" /></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>
  </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; }
.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; }
.related-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; }
@media (max-width: 960px) { .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; }
  .detail-form { grid-template-columns: minmax(0, 1fr); padding-left: 12px; padding-right: 12px; }
  .remark-field, .related-field { grid-column: auto; }
  .form-footer { padding: 0 12px; }
  .form-footer button { flex: 1; }
}
</style>