yeg
2026-08-27 6499535aa03e0f581f87f936185996321e895a4f
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { getUserInfo } from '@/api/auth'
import { changeBatteryGroupFieldStatus, createBatteryGroupField, deleteBatteryGroupField, fieldTypeCode, getBatteryGroupFields, updateBatteryGroupField, type BatteryGroupField, type BatteryGroupFieldStatus, type BatteryGroupFieldType } from '@/api/batteryGroupFields'
 
interface Filters {
  id: string
  name: string
  status: string
}
 
const router = useRouter()
const fields = ref<BatteryGroupField[]>([])
const creators = ref<string[]>([])
const statuses: BatteryGroupFieldStatus[] = ['启用', '禁用']
const fieldTypes: BatteryGroupFieldType[] = ['文本', '数字', '时间', '备注']
const filters = reactive<Filters>({ id: '', name: '', status: '' })
const appliedFilters = ref<Filters>({ id: '', name: '', status: '' })
const currentPage = ref(1)
const pageSize = ref(10)
const dialogVisible = ref(false)
const editingId = ref<number | null>(null)
const loading = ref(false)
const saving = ref(false)
const fieldForm = reactive<{ name: string, type: BatteryGroupFieldType, creator: string, status: BatteryGroupFieldStatus }>({ name: '', type: '文本', creator: '', status: '启用' })
 
const dialogTitle = computed(() => editingId.value ? '编辑电池组字段' : '新增电池组字段')
const filteredFields = computed(() => {
  const query = appliedFilters.value
  return fields.value
    .filter(item => !query.id || String(item.id).includes(query.id.trim()))
    .filter(item => !query.name || item.name.toLowerCase().includes(query.name.trim().toLowerCase()))
    .filter(item => !query.status || query.status === '全部' || item.status === query.status)
    .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
})
const pagedFields = computed(() => {
  const start = (currentPage.value - 1) * pageSize.value
  return filteredFields.value.slice(start, start + pageSize.value)
})
 
function applySearch() {
  appliedFilters.value = { ...filters }
  currentPage.value = 1
  ElMessage.success(`已找到 ${filteredFields.value.length} 个字段`)
}
 
function resetSearch() {
  Object.assign(filters, { id: '', name: '', status: '' })
  appliedFilters.value = { id: '', name: '', status: '' }
  currentPage.value = 1
}
 
function openCreate() {
  editingId.value = null
  Object.assign(fieldForm, { name: '', type: '文本', creator: creators.value[0] || '', status: '启用' })
  dialogVisible.value = true
}
 
function openEdit(item: BatteryGroupField) {
  editingId.value = item.id
  Object.assign(fieldForm, { name: item.name, type: item.type, creator: item.creator, status: item.status })
  dialogVisible.value = true
}
 
async function saveField() {
  const name = fieldForm.name.trim()
  if (!name) {
    ElMessage.warning('请输入字段名称')
    return
  }
  if (!fieldForm.creator) {
    ElMessage.warning('请选择添加人')
    return
  }
  const duplicated = fields.value.some(item => item.id !== editingId.value && item.name.trim().toLowerCase() === name.toLowerCase())
  if (duplicated) {
    ElMessage.warning('字段名称已存在')
    return
  }
  saving.value = true
  try {
    const payload = { fieldName: name, fieldType: fieldTypeCode[fieldForm.type], status: fieldForm.status === '启用' ? '0' as const : '1' as const }
    if (editingId.value) await updateBatteryGroupField(editingId.value, payload)
    else await createBatteryGroupField(payload)
    dialogVisible.value = false
    ElMessage.success(editingId.value ? '字段已更新' : '字段已添加')
    await loadFields()
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '保存字段失败')
  } finally {
    saving.value = false
  }
}
 
async function toggleStatus(item: BatteryGroupField) {
  const nextStatus: BatteryGroupFieldStatus = item.status === '启用' ? '禁用' : '启用'
  try {
    await ElMessageBox.confirm(`确认${nextStatus}字段“${item.name}”?`, `${nextStatus}字段`, { confirmButtonText: nextStatus, cancelButtonText: '取消', type: 'warning' })
    await changeBatteryGroupFieldStatus(item.id, nextStatus === '启用' ? '0' : '1')
    item.status = nextStatus
    ElMessage.success(`字段已${nextStatus}`)
  } catch (error) {
    if (error instanceof Error) ElMessage.error(error.message || '更新字段状态失败')
  }
}
 
async function deleteField(item: BatteryGroupField) {
  try {
    await ElMessageBox.confirm(`删除字段“${item.name}”后无法恢复,是否继续?`, '确认删除字段', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm' })
    await deleteBatteryGroupField(item.id)
    fields.value = fields.value.filter(field => field.id !== item.id)
    currentPage.value = Math.min(currentPage.value, Math.max(1, Math.ceil(filteredFields.value.length / pageSize.value)))
    ElMessage.success('字段已删除')
  } catch (error) {
    if (error instanceof Error) ElMessage.error(error.message || '删除字段失败')
  }
}
 
async function loadFields() {
  loading.value = true
  try {
    fields.value = await getBatteryGroupFields()
  } catch (error) {
    fields.value = []
    ElMessage.error(error instanceof Error ? error.message : '加载字段失败')
  } finally {
    loading.value = false
  }
}
 
async function loadPage() {
  try {
    const userInfo = await getUserInfo()
    const currentUser = userInfo.user.userName?.trim() || ''
    creators.value = currentUser ? [currentUser] : []
    fieldForm.creator = currentUser
  } catch (error) {
    creators.value = []
    ElMessage.error(error instanceof Error ? error.message : '加载当前登录用户失败')
  }
  await loadFields()
}
 
onMounted(loadPage)
</script>
 
<template>
  <div class="group-field-page">
    <div class="page-tools"><button class="back-button" type="button" @click="router.push('/battery-group')"><el-icon><ArrowLeft /></el-icon>返回电池组管理</button><span>BATTERY GROUP FIELDS</span></div>
 
    <section class="filter-panel" aria-label="电池组字段查询条件">
      <header class="section-heading"><div><i></i><strong>查询条件</strong><span>FILTER CONDITIONS</span></div><small>共 {{ fields.length }} 个字段</small></header>
      <div class="filter-grid">
        <label><span>字段ID</span><el-input v-model="filters.id" clearable placeholder="输入字段ID" @keyup.enter="applySearch" /></label>
        <label><span>字段名称</span><el-input v-model="filters.name" clearable placeholder="输入字段名称" @keyup.enter="applySearch" /></label>
        <label><span>状态</span><el-select v-model="filters.status" clearable placeholder="全部状态"><el-option label="全部" value="全部" /><el-option v-for="item in statuses" :key="item" :label="item" :value="item" /></el-select></label>
        <div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div>
      </div>
    </section>
 
    <section v-loading="loading" class="result-panel">
      <div class="result-toolbar"><div><i></i><strong>电池组字段列表</strong><span>{{ filteredFields.length }} RESULTS · 按添加时间倒序</span></div><button class="primary-button" type="button" @click="openCreate"><el-icon><Plus /></el-icon>新增字段</button></div>
      <div class="table-wrap">
        <table>
          <thead><tr><th>字段ID</th><th>字段名称</th><th>字段类型</th><th>添加人</th><th>添加时间</th><th>状态</th><th class="action-column">操作</th></tr></thead>
          <tbody>
            <tr v-for="item in pagedFields" :key="item.id"><td class="id-cell">#{{ item.id }}</td><td><strong>{{ item.name }}</strong></td><td><span class="type-tag">{{ item.type }}</span></td><td>{{ item.creator }}</td><td>{{ item.createdAt }}</td><td><span class="status-tag" :class="item.status === '启用' ? 'enabled' : 'disabled'"><i></i>{{ item.status }}</span></td><td class="action-column"><div class="row-actions"><button type="button" @click="openEdit(item)">编辑</button><button type="button" :class="item.status === '启用' ? 'disable-action' : 'enable-action'" @click="toggleStatus(item)">{{ item.status === '启用' ? '禁用' : '启用' }}</button><button class="delete-action" type="button" @click="deleteField(item)">删除</button></div></td></tr>
            <tr v-if="!pagedFields.length"><td colspan="7" class="empty-cell">暂无符合条件的电池组字段</td></tr>
          </tbody>
        </table>
      </div>
      <footer class="pagination-bar"><span>显示 {{ pagedFields.length }} 条,共 {{ filteredFields.length }} 条</span><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50]" :total="filteredFields.length" layout="sizes, prev, pager, next" background /></footer>
    </section>
 
    <el-dialog v-model="dialogVisible" class="field-dialog" :title="dialogTitle" width="520px" align-center destroy-on-close>
      <div class="field-form">
        <label><span><b>*</b>字段名称</span><el-input v-model="fieldForm.name" maxlength="30" show-word-limit placeholder="输入字段名称" @keyup.enter="saveField" /></label>
        <label><span><b>*</b>字段类型</span><el-select v-model="fieldForm.type" placeholder="选择字段类型"><el-option v-for="item in fieldTypes" :key="item" :label="item" :value="item" /></el-select></label>
        <label><span><b>*</b>添加人</span><el-select v-model="fieldForm.creator" disabled placeholder="当前登录用户"><el-option v-for="item in creators" :key="item" :label="item" :value="item" /></el-select></label>
        <label><span>状态</span><el-radio-group v-model="fieldForm.status"><el-radio-button value="启用">启用</el-radio-button><el-radio-button value="禁用">禁用</el-radio-button></el-radio-group></label>
      </div>
      <template #footer><button class="secondary-button" type="button" @click="dialogVisible = false">取消</button><button class="primary-button" type="button" :disabled="saving" @click="saveField"><el-icon><Check /></el-icon>保存</button></template>
    </el-dialog>
  </div>
</template>
 
<style scoped>
.group-field-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); }
.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; }
.secondary-button { color: #52606d; background: #fff; border-color: var(--line-strong); }
.secondary-button:hover { color: var(--blue); border-color: #9dc8f5; }
.filter-panel, .result-panel { background: #fff; border: 1px solid #d8e1e9; }
.filter-panel { margin-bottom: 16px; }
.section-heading, .result-toolbar { display: flex; align-items: center; justify-content: space-between; background: var(--surface-soft); border-bottom: 1px solid var(--line); }
.section-heading { height: 44px; padding: 0 16px; }
.section-heading > div, .result-toolbar > div { display: flex; align-items: center; gap: 8px; }
.section-heading > div > i, .result-toolbar > div:first-child > i { width: 3px; height: 16px; background: #2b78a3; }
.section-heading strong, .result-toolbar strong { color: var(--ink); font-size: 15px; font-weight: 600; }
.section-heading span, .result-toolbar span { color: #81909d; font-size: 10px; }
.section-heading small { color: #647380; font-size: 11px; }
.filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px 18px; padding: 16px; }
.filter-grid label { min-width: 0; display: grid; grid-template-columns: 72px minmax(0, 1fr); align-items: center; gap: 8px; }
.filter-grid label > span { color: #52606d; font-size: 12px; font-weight: 500; }
.filter-grid :deep(.el-select) { width: 100%; }
.filter-grid :deep(.el-input__wrapper), .filter-grid :deep(.el-select__wrapper) { min-height: 34px; border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; }
.filter-actions { min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.result-toolbar { height: 50px; padding: 0 16px; }
.table-wrap { overflow-x: auto; }
table { width: 100%; min-width: 900px; table-layout: fixed; border-collapse: collapse; color: #52606d; font-size: 13px; }
th, td { height: 50px; padding: 0 12px; border-right: 1px solid #dde7f0; border-bottom: 1px solid #dde7f0; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
th { height: 40px; color: #52606d; background: #eaf4ff; border-color: #c7e0ff; font-size: 12px; font-weight: 600; }
th:nth-child(1) { width: 100px; } th:nth-child(2) { width: 190px; } th:nth-child(3) { width: 110px; } th:nth-child(4) { width: 110px; } th:nth-child(5) { width: 180px; } th:nth-child(6) { width: 90px; } th:last-child { width: 190px; }
th:last-child, td:last-child { border-right: 0; text-align: center; }
.action-column { position: sticky; right: 0; z-index: 2; background: #fff; overflow: visible; box-shadow: inset 1px 0 0 #b4c7d9, -8px 0 14px rgba(23, 49, 74, .12); }
th.action-column { z-index: 3; background: #eaf4ff; }
tbody tr:hover, tbody tr:hover td.action-column { background: #f7fbff; }
tbody tr:last-child td { border-bottom: 0; }
.id-cell { color: #647380; font-family: 'Cascadia Mono', Consolas, monospace; font-size: 12px; }
td strong { color: #26333f; font-weight: 600; }
.type-tag { display: inline-flex; padding: 4px 8px; color: #37688e; background: #edf5fb; border: 1px solid #c9deed; font-size: 11px; line-height: 1; }
.status-tag { display: inline-flex; align-items: center; gap: 6px; padding: 4px 7px; border: 1px solid; font-size: 11px; line-height: 1; }
.status-tag i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.status-tag.enabled { color: #23865e; background: #edf9f3; border-color: #b8dfcd; }
.status-tag.disabled { color: #a64a4a; background: #fff1f1; border-color: #e9bcbc; }
.row-actions { display: flex; align-items: center; justify-content: center; gap: 12px; }
.row-actions button { border: 0; color: var(--blue); background: transparent; font-size: 12px; font-weight: 500; cursor: pointer; }
.row-actions .disable-action, .row-actions .delete-action { color: #ad4a4a; }
.row-actions .enable-action { color: #23865e; }
.empty-cell { height: 160px; color: #8795a3; text-align: center; }
.pagination-bar { min-height: 58px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 14px 8px 16px; border-top: 1px solid var(--line); }
.pagination-bar > span { color: #7b8794; font-size: 11px; }
.group-field-page :deep(.field-dialog) { max-width: calc(100vw - 28px); border-radius: 4px; }
.group-field-page :deep(.el-dialog__header) { margin: 0; padding: 17px 20px; border-bottom: 1px solid var(--line); }
.group-field-page :deep(.el-dialog__body) { padding: 22px 28px; }
.group-field-page :deep(.el-dialog__footer) { padding: 14px 20px; border-top: 1px solid var(--line); }
.field-form { display: grid; gap: 16px; }
.field-form label { display: grid; grid-template-columns: 80px minmax(0, 1fr); align-items: center; gap: 12px; }
.field-form label > span { color: #52606d; font-size: 12px; font-weight: 500; }
.field-form label > span b { color: #b34f4f; }
.field-form :deep(.el-select) { width: 100%; }
.field-form :deep(.el-input__wrapper), .field-form :deep(.el-select__wrapper) { min-height: 34px; border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; }
@media (max-width: 980px) { .filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 680px) { .filter-grid { grid-template-columns: minmax(0, 1fr); gap: 12px; padding: 14px 12px; } .filter-actions { justify-content: stretch; } .filter-actions button { flex: 1; } .section-heading, .result-toolbar { padding-left: 12px; } .section-heading span, .result-toolbar span { display: none; } .pagination-bar { align-items: flex-start; flex-direction: column; overflow-x: auto; } .field-form label { grid-template-columns: minmax(0, 1fr); gap: 7px; } }
</style>