yeg
2026-08-27 2b4fb46f9153b8a25d9e493279bfa722b653cd9e
fix: connect battery group management APIs
2个文件已添加
6个文件已修改
447 ■■■■■ 已修改文件
docs/pages/battery-group.md 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/batteryGroupFields.ts 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/batteryGroupPayload.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/batteryGroupTemplates.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/batteryGroups.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/BatteryGroupField.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/BatteryGroupForm.vue 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/battery-group-persistence.contract.test.mjs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/pages/battery-group.md
@@ -22,7 +22,7 @@
- 电池号支持逐行添加和删除,至少保留并填写一个电池号,编号不可重复。
- 右侧模板编辑面板支持收起、选择模板,并从“电池组字段管理”的启用字段中勾选要展示的字段。
- 模板面板展示字段管理中的全部字段,禁用字段保留可见但不可勾选;勾选变化会实时控制样品详情中的动态字段。
- 保存模板和另存模板会在浏览器本地持久化当前字段勾选结果。
- 保存模板和另存模板通过后端持久化当前字段勾选结果,电池组保存时同时保存字段值。
- 模板编辑面板固定在页面右侧,底部操作按钮始终可见,仅中间字段列表区域独立滚动。
- 字段类型由字段管理统一维护,支持文本、数字、时间和备注,并实时映射到样品详情表单。
- 电池组名称不可重复;名称、型号、用途、创建人及至少一个电池编号必填。
@@ -44,4 +44,4 @@
## 待明确
接入电池组和模板 API、持久化列设置、同步“新建测试”的电池组数据源、增加测试任务引用关系及被引用后的删除约束。
持久化列设置,并继续完善测试任务引用关系及被引用后的删除约束。
src/api/batteryGroupFields.ts
New file
@@ -0,0 +1,69 @@
import { request, type ApiResponse } from './request'
export type BatteryGroupFieldStatus = '启用' | '禁用'
export type BatteryGroupFieldType = '文本' | '数字' | '时间' | '备注'
interface BatteryGroupFieldDto {
  fieldId: number
  fieldName: string | null
  fieldType: string | null
  createBy: string | null
  createTime: string | null
  status: string | null
}
interface BatteryGroupFieldListResponse extends ApiResponse {
  total: number
  rows: BatteryGroupFieldDto[]
}
export interface BatteryGroupField {
  id: number
  name: string
  type: BatteryGroupFieldType
  creator: string
  createdAt: string
  status: BatteryGroupFieldStatus
}
export interface BatteryGroupFieldPayload {
  fieldName: string
  fieldType: string
  status: '0' | '1'
}
const typeMap: Record<string, BatteryGroupFieldType> = {
  text: '文本', number: '数字', time: '时间', date: '时间', remark: '备注',
}
export async function getBatteryGroupFields(): Promise<BatteryGroupField[]> {
  const result = await request<BatteryGroupFieldListResponse>('/system/batteryGroupField/list?pageNum=1&pageSize=1000')
  return (result.rows || []).map(item => ({
    id: item.fieldId,
    name: item.fieldName || '',
    type: typeMap[item.fieldType || ''] || '文本',
    creator: item.createBy || '-',
    createdAt: item.createTime || '',
    status: item.status === '1' ? '禁用' : '启用',
  }))
}
export function createBatteryGroupField(payload: BatteryGroupFieldPayload): Promise<ApiResponse> {
  return request<ApiResponse>('/system/batteryGroupField', { method: 'POST', body: JSON.stringify(payload) })
}
export function updateBatteryGroupField(fieldId: number, payload: BatteryGroupFieldPayload): Promise<ApiResponse> {
  return request<ApiResponse>('/system/batteryGroupField', {
    method: 'PUT', body: JSON.stringify({ fieldId, ...payload }),
  })
}
export function changeBatteryGroupFieldStatus(fieldId: number, status: '0' | '1'): Promise<ApiResponse> {
  return request<ApiResponse>('/system/batteryGroupField/changeStatus', {
    method: 'PUT', body: JSON.stringify({ fieldId, status }),
  })
}
export function deleteBatteryGroupField(fieldId: number): Promise<ApiResponse> {
  return request<ApiResponse>(`/system/batteryGroupField/${fieldId}`, { method: 'DELETE' })
}
src/api/batteryGroupPayload.ts
@@ -13,7 +13,7 @@
  b: string
  batteryNos: string[]
  templateId: number | null
  templateFieldRelations?: Array<{ batteryGroupFieldId: number }>
  templateFieldRelations?: Array<{ batteryGroupFieldId: number; fieldValue: string }>
}
interface BatteryGroupFormPayloadInput {
@@ -29,7 +29,7 @@
export function buildBatteryGroupPayload(
  form: BatteryGroupFormPayloadInput,
  templateId: number | null,
  templateFieldIds: number[],
  templateFields: Array<{ batteryGroupFieldId: number; fieldValue: string }>,
): BatteryGroupPayload {
  return {
    groupName: form.name.trim(),
@@ -45,7 +45,7 @@
    batteryNos: form.batteries.map(item => item.value.trim()).filter(Boolean),
    templateId,
    templateFieldRelations: templateId
      ? templateFieldIds.map(batteryGroupFieldId => ({ batteryGroupFieldId }))
      ? templateFields
      : undefined,
  }
}
src/api/batteryGroupTemplates.ts
New file
@@ -0,0 +1,56 @@
import { request, type ApiResponse } from './request'
interface BatteryGroupTemplateFieldDto {
  fieldId: number
  fieldName: string | null
  fieldType: string | null
}
interface BatteryGroupTemplateDto {
  templateId: number
  templateName: string | null
  fields?: BatteryGroupTemplateFieldDto[] | null
}
interface BatteryGroupTemplateResponse extends ApiResponse {
  data: BatteryGroupTemplateDto
}
interface BatteryGroupTemplateOptionsResponse extends ApiResponse {
  data: BatteryGroupTemplateDto[]
}
export interface BatteryGroupTemplateOption {
  id: number
  name: string
}
export interface BatteryGroupTemplateDetail extends BatteryGroupTemplateOption {
  fieldIds: number[]
}
export async function getBatteryGroupTemplateOptions(): Promise<BatteryGroupTemplateOption[]> {
  const result = await request<BatteryGroupTemplateOptionsResponse>('/system/batteryGroupTemplate/options')
  return (result.data || []).map(item => ({ id: item.templateId, name: item.templateName || `模板${item.templateId}` }))
}
export async function getBatteryGroupTemplate(templateId: number): Promise<BatteryGroupTemplateDetail> {
  const result = await request<BatteryGroupTemplateResponse>(`/system/batteryGroupTemplate/${templateId}`)
  return {
    id: result.data.templateId,
    name: result.data.templateName || `模板${result.data.templateId}`,
    fieldIds: (result.data.fields || []).map(field => field.fieldId),
  }
}
export function createBatteryGroupTemplate(templateName: string, fieldIds: number[]): Promise<ApiResponse> {
  return request<ApiResponse>('/system/batteryGroupTemplate', {
    method: 'POST', body: JSON.stringify({ templateName, fieldIds }),
  })
}
export function updateBatteryGroupTemplate(templateId: number, templateName: string, fieldIds: number[]): Promise<ApiResponse> {
  return request<ApiResponse>('/system/batteryGroupTemplate', {
    method: 'PUT', body: JSON.stringify({ templateId, templateName, fieldIds }),
  })
}
src/api/batteryGroups.ts
@@ -24,6 +24,7 @@
  batteryGroupFieldId: number | null
  fieldName: string | null
  fieldType: string | null
  fieldValue: string | null
}
interface BatteryGroupDto {
@@ -89,6 +90,7 @@
  batteryGroupFieldId: number
  fieldName: string
  fieldType: string
  fieldValue: string
}
export interface BatteryGroupDetail {
@@ -162,6 +164,7 @@
        batteryGroupFieldId: Number(field.batteryGroupFieldId),
        fieldName: field.fieldName || '',
        fieldType: field.fieldType || '文本',
        fieldValue: field.fieldValue || '',
      })),
  }
}
src/views/BatteryGroupField.vue
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { useBatteryGroupFields, type BatteryGroupField, type BatteryGroupFieldStatus } from '@/stores/batteryGroupFields'
import type { FieldType } from '@/stores/batteryGroups'
import { getUserInfo } from '@/api/auth'
import { changeBatteryGroupFieldStatus, createBatteryGroupField, deleteBatteryGroupField, getBatteryGroupFields, updateBatteryGroupField, type BatteryGroupField, type BatteryGroupFieldStatus, type BatteryGroupFieldType } from '@/api/batteryGroupFields'
interface Filters {
  id: string
@@ -12,17 +12,19 @@
}
const router = useRouter()
const { fields, createField, updateField, removeField, saveChanges } = useBatteryGroupFields()
const creators = ['张工', '李工', '周工', '王工', '赵工', '陈工', 'Admin']
const fields = ref<BatteryGroupField[]>([])
const creators = ref<string[]>([])
const statuses: BatteryGroupFieldStatus[] = ['启用', '禁用']
const fieldTypes: FieldType[] = ['文本', '数字', '时间', '备注']
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 fieldForm = reactive<{ name: string, type: FieldType, creator: string, status: BatteryGroupFieldStatus }>({ name: '', type: '文本', creator: 'Admin', status: '启用' })
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(() => {
@@ -52,7 +54,7 @@
function openCreate() {
  editingId.value = null
  Object.assign(fieldForm, { name: '', type: '文本', creator: 'Admin', status: '启用' })
  Object.assign(fieldForm, { name: '', type: '文本', creator: creators.value[0] || '', status: '启用' })
  dialogVisible.value = true
}
@@ -62,7 +64,7 @@
  dialogVisible.value = true
}
function saveField() {
async function saveField() {
  const name = fieldForm.name.trim()
  if (!name) {
    ElMessage.warning('请输入字段名称')
@@ -77,35 +79,70 @@
    ElMessage.warning('字段名称已存在')
    return
  }
  const payload = { name, type: fieldForm.type, creator: fieldForm.creator, status: fieldForm.status }
  if (editingId.value) updateField(editingId.value, payload)
  else createField(payload)
  dialogVisible.value = false
  ElMessage.success(editingId.value ? '字段已更新' : '字段已添加')
  saving.value = true
  try {
    const payload = { fieldName: name, fieldType: 'text', 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
    saveChanges()
    ElMessage.success(`字段已${nextStatus}`)
  } catch {
    // 用户取消状态变更。
  } 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' })
    removeField(item.id)
    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 {
    // 用户取消删除。
  } 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 {
    creators.value = []
  }
  await loadFields()
}
onMounted(loadPage)
</script>
<template>
@@ -122,7 +159,7 @@
      </div>
    </section>
    <section class="result-panel">
    <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>
@@ -140,10 +177,10 @@
      <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" placeholder="选择添加人"><el-option v-for="item in creators" :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" @click="saveField"><el-icon><Check /></el-icon>保存</button></template>
      <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>
src/views/BatteryGroupForm.vue
@@ -2,10 +2,19 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { getUserInfo } from '@/api/auth'
import { getBatteryGroupFields, type BatteryGroupField, type BatteryGroupFieldType } from '@/api/batteryGroupFields'
import { buildBatteryGroupPayload, type GroupStatus } from '@/api/batteryGroupPayload'
import { createBatteryGroup, getBatteryGroupDetail, updateBatteryGroup } from '@/api/batteryGroups'
import { buildBatteryGroupPayload } from '@/api/batteryGroupPayload'
import type { GroupStatus, TemplateField } from '@/stores/batteryGroups'
import { useBatteryGroupFields, type BatteryGroupField } from '@/stores/batteryGroupFields'
import { createBatteryGroupTemplate, getBatteryGroupTemplate, getBatteryGroupTemplateOptions, updateBatteryGroupTemplate, type BatteryGroupTemplateOption } from '@/api/batteryGroupTemplates'
interface TemplateField {
  key: string
  fieldId: number
  label: string
  type: BatteryGroupFieldType
  value: string
}
interface FormState {
  name: string
@@ -14,7 +23,7 @@
  remark: string
  creator: string
  status: GroupStatus
  templateName: string
  templateId: number
  specs: Record<string, string>
  customFields: TemplateField[]
  batteries: Array<{ key: number, value: string }>
@@ -22,91 +31,91 @@
const route = useRoute()
const router = useRouter()
const { fields: managedFields } = useBatteryGroupFields()
const creators = ['张工', '李工', '周工', '王工', '赵工', '陈工']
const creators = ref<string[]>([])
const managedFields = ref<BatteryGroupField[]>([])
const templateOptions = ref<BatteryGroupTemplateOption[]>([])
const templatePanelOpen = ref(false)
const loading = ref(false)
const saving = ref(false)
const persistedTemplateId = ref<number | null>(null)
const persistedTemplateFieldIds = ref<number[]>([])
const templateSaving = ref(false)
const selectedFieldIds = ref<number[]>([])
const loadedTemplateFieldIds = ref<number[]>([])
let batteryKey = 1
const editingId = computed(() => Number(route.params.id) || null)
const isEditing = computed(() => editingId.value !== null)
const pageTitle = computed(() => isEditing.value ? '编辑电池组' : '新建电池组')
const templateStorageKey = 'battery_detection_group_field_templates_v1'
const presetTemplateLabels: Record<string, string[]> = {
  '标准容量样品': ['额定容量', '电流', '内阻'],
  '标准循环样品': ['额定容量', '循环温度', '内阻'],
  '低温性能样品': ['目标温度', '静置时间', '环境备注'],
}
const savedTemplateSelections = (() => {
  try {
    const stored = localStorage.getItem(templateStorageKey)
    return stored ? JSON.parse(stored) as Record<string, number[]> : {}
  } catch {
    localStorage.removeItem(templateStorageKey)
    return {}
  }
})()
const templateSelections = reactive<Record<string, number[]>>(savedTemplateSelections)
const templateNames = ref(['自定义', '标准容量样品', '标准循环样品', '低温性能样品', ...Object.keys(savedTemplateSelections).filter(name => !['自定义', '标准容量样品', '标准循环样品', '低温性能样品'].includes(name))])
const templateFieldOptions = computed(() => [...managedFields.value].sort((a, b) => b.createdAt.localeCompare(a.createdAt)))
const selectableFields = computed(() => templateFieldOptions.value.filter(field => field.status === '启用'))
const selectedFieldIds = ref<number[]>([])
const form = reactive<FormState>({
  name: '', model: '', purpose: '', remark: '', creator: '', status: '启用', templateName: '自定义',
  name: '', model: '', purpose: '', remark: '', creator: '', status: '启用', templateId: 0,
  specs: { z: '', h: '', f: '', a: '', b: '' }, customFields: [], batteries: [{ key: batteryKey++, value: '' }],
})
function persistTemplateSelections() {
  localStorage.setItem(templateStorageKey, JSON.stringify(templateSelections))
}
function resolveTemplateFieldIds(name: string) {
  const savedIds = templateSelections[name]
  if (savedIds) return savedIds.filter(id => selectableFields.value.some(field => field.id === id))
  const labels = presetTemplateLabels[name] || []
  return selectableFields.value.filter(field => labels.includes(field.name)).map(field => field.id)
}
function syncSelectedFields() {
  const previousValues = new Map(form.customFields.map(field => [field.label, field.value]))
  const previousValues = new Map(form.customFields.map(field => [field.fieldId, field.value]))
  form.customFields = selectedFieldIds.value
    .map(id => selectableFields.value.find(field => field.id === id))
    .map(id => templateFieldOptions.value.find(field => field.id === id))
    .filter((field): field is BatteryGroupField => Boolean(field))
    .map(field => ({ key: `managed-field-${field.id}`, label: field.name, type: field.type, value: previousValues.get(field.name) || '' }))
    .map(field => ({
      key: `managed-field-${field.id}`, fieldId: field.id, label: field.name,
      type: field.type, value: previousValues.get(field.id) || '',
    }))
}
async function applyTemplate(templateId: number) {
  if (!templateId) {
    selectedFieldIds.value = []
    loadedTemplateFieldIds.value = []
    form.customFields = []
    return
  }
  try {
    const template = await getBatteryGroupTemplate(templateId)
    selectedFieldIds.value = [...template.fieldIds]
    loadedTemplateFieldIds.value = [...template.fieldIds]
    syncSelectedFields()
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '加载模板失败')
  }
}
async function loadEditingGroup() {
  if (!editingId.value) return
  const target = await getBatteryGroupDetail(editingId.value)
  const templateId = target.templateFields[0]?.templateId || 0
  const templateFields = target.templateFields.map(field => ({
    key: `persisted-field-${field.batteryGroupFieldId}`,
    fieldId: field.batteryGroupFieldId,
    label: field.fieldName,
    type: (field.fieldType === 'text' ? '文本' : field.fieldType) as BatteryGroupFieldType,
    value: field.fieldValue,
  }))
  Object.assign(form, {
    name: target.name, model: target.model, purpose: target.purpose, remark: target.remark,
    creator: target.creator, status: target.status, templateId, specs: { ...target.specs },
    customFields: templateFields,
    batteries: target.batteries.map(value => ({ key: batteryKey++, value })),
  })
  if (target.creator && !creators.value.includes(target.creator)) creators.value.push(target.creator)
  if (!form.batteries.length) form.batteries.push({ key: batteryKey++, value: '' })
  selectedFieldIds.value = templateFields.map(field => field.fieldId)
  loadedTemplateFieldIds.value = [...selectedFieldIds.value]
}
async function loadPage() {
  loading.value = true
  try {
    const target = await getBatteryGroupDetail(editingId.value)
    const templateFields = target.templateFields.map(field => ({
      key: `persisted-field-${field.batteryGroupFieldId}`,
      label: field.fieldName,
      type: field.fieldType as TemplateField['type'],
      value: '',
    }))
    persistedTemplateId.value = target.templateFields[0]?.templateId || null
    persistedTemplateFieldIds.value = target.templateFields.map(field => field.batteryGroupFieldId)
    Object.assign(form, {
      name: target.name,
      model: target.model,
      purpose: target.purpose,
      remark: target.remark,
      creator: target.creator,
      status: target.status,
      templateName: '自定义',
      specs: { ...target.specs },
      customFields: templateFields,
      batteries: target.batteries.map(value => ({ key: batteryKey++, value })),
    })
    if (!form.batteries.length) form.batteries.push({ key: batteryKey++, value: '' })
    selectedFieldIds.value = [...persistedTemplateFieldIds.value]
    const [fields, templates, userInfo] = await Promise.all([
      getBatteryGroupFields(), getBatteryGroupTemplateOptions(), getUserInfo(),
    ])
    managedFields.value = fields
    templateOptions.value = templates
    const currentUser = userInfo.user.userName?.trim() || ''
    creators.value = currentUser ? [currentUser] : []
    form.creator = currentUser
    await loadEditingGroup()
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '加载电池组失败')
    ElMessage.error(error instanceof Error ? error.message : '加载电池组表单失败')
    await router.replace('/battery-group')
  } finally {
    loading.value = false
@@ -125,45 +134,70 @@
  form.batteries.splice(index, 1)
}
function applyTemplate(name: string) {
  selectedFieldIds.value = resolveTemplateFieldIds(name)
  syncSelectedFields()
function templateName(templateId: number) {
  return templateOptions.value.find(item => item.id === templateId)?.name || ''
}
function saveTemplate() {
  templateSelections[form.templateName] = [...selectedFieldIds.value]
  persistTemplateSelections()
  ElMessage.success(`模板“${form.templateName}”已保存`)
async function saveTemplate() {
  if (!form.templateId) {
    ElMessage.warning('自定义字段请使用“另存模板”')
    return
  }
  if (!selectedFieldIds.value.length) {
    ElMessage.warning('请至少选择一个模板字段')
    return
  }
  templateSaving.value = true
  try {
    await updateBatteryGroupTemplate(form.templateId, templateName(form.templateId), selectedFieldIds.value)
    loadedTemplateFieldIds.value = [...selectedFieldIds.value]
    ElMessage.success(`模板“${templateName(form.templateId)}”已保存`)
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '保存模板失败')
  } finally {
    templateSaving.value = false
  }
}
async function saveTemplateAs() {
  if (!selectedFieldIds.value.length) {
    ElMessage.warning('请至少选择一个模板字段')
    return
  }
  try {
    const result = await ElMessageBox.prompt('输入新模板名称,当前动态字段会保存为新模板。', '另存模板', {
      confirmButtonText: '保存', cancelButtonText: '取消', inputPlaceholder: '输入模板名称',
      inputValidator: value => value.trim() ? true : '请输入模板名称',
    })
    const name = result.value.trim()
    if (!templateNames.value.includes(name)) templateNames.value.push(name)
    form.templateName = name
    templateSelections[name] = [...selectedFieldIds.value]
    persistTemplateSelections()
    templateSaving.value = true
    await createBatteryGroupTemplate(name, selectedFieldIds.value)
    templateOptions.value = await getBatteryGroupTemplateOptions()
    const created = templateOptions.value.find(item => item.name === name)
    if (!created) throw new Error('模板已保存,但未能重新加载')
    form.templateId = created.id
    loadedTemplateFieldIds.value = [...selectedFieldIds.value]
    ElMessage.success(`已另存为“${name}”`)
  } catch {
    // 用户取消另存。
  } catch (error) {
    if (error instanceof Error && error.message !== 'cancel') {
      ElMessage.error(error.message || '另存模板失败')
    }
  } finally {
    templateSaving.value = false
  }
}
function validate() {
  if (!form.name.trim()) return '请输入电池组名称'
  if (!form.model.trim()) return '请输入型号'
  if (!form.creator) return '请选择创建人'
  if (!form.creator) return '当前登录用户信息不可用'
  if (!form.purpose.trim()) return '请输入目的'
  const batteries = form.batteries.map(item => item.value.trim()).filter(Boolean)
  if (!batteries.length) return '请至少填写一个电池号'
  if (new Set(batteries).size !== batteries.length) return '电池号不能重复'
  if (form.customFields.some(field => !field.label.trim())) return '请填写全部模板字段名称'
  const fieldLabels = form.customFields.map(field => field.label.trim().toLowerCase())
  if (new Set(fieldLabels).size !== fieldLabels.length) return '模板字段名称不能重复'
  if (form.customFields.some(field => field.value.length > 500)) return '模板字段值不能超过500个字符'
  if (!form.templateId && selectedFieldIds.value.length) return '请先将自定义字段另存为模板'
  if (form.templateId && loadedTemplateFieldIds.value.join(',') !== selectedFieldIds.value.join(',')) return '模板字段已变更,请先保存当前模板'
  return ''
}
@@ -175,7 +209,10 @@
  }
  saving.value = true
  try {
    const payload = buildBatteryGroupPayload(form, persistedTemplateId.value, persistedTemplateFieldIds.value)
    const relations = form.customFields.map(field => ({
      batteryGroupFieldId: field.fieldId, fieldValue: field.value,
    }))
    const payload = buildBatteryGroupPayload(form, form.templateId || null, relations)
    if (editingId.value) await updateBatteryGroup(editingId.value, payload)
    else await createBatteryGroup(payload)
    ElMessage.success(isEditing.value ? '电池组已更新' : '电池组已创建')
@@ -187,7 +224,7 @@
  }
}
onMounted(loadEditingGroup)
onMounted(loadPage)
</script>
<template>
@@ -213,7 +250,7 @@
            <label class="textarea-field"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="输入备注" /></label>
            <label class="textarea-field"><span><b>*</b>目的</span><el-input v-model="form.purpose" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="输入目的" /></label>
          </div>
          <div class="template-actions"><label><span>当前模板</span><el-select v-model="form.templateName" @change="applyTemplate"><el-option v-for="name in templateNames" :key="name" :label="name" :value="name" /></el-select></label><button class="secondary-button" type="button" @click="templatePanelOpen = !templatePanelOpen"><el-icon><EditPen /></el-icon>{{ templatePanelOpen ? '收起模板编辑' : '编辑当前模板' }}</button></div>
          <div class="template-actions"><label><span>当前模板</span><el-select v-model="form.templateId" @change="applyTemplate"><el-option label="自定义" :value="0" /><el-option v-for="item in templateOptions" :key="item.id" :label="item.name" :value="item.id" /></el-select></label><button class="secondary-button" type="button" @click="templatePanelOpen = !templatePanelOpen"><el-icon><EditPen /></el-icon>{{ templatePanelOpen ? '收起模板编辑' : '编辑当前模板' }}</button></div>
        </section>
        <section class="battery-panel">
@@ -225,7 +262,7 @@
        </section>
        <section class="meta-panel">
          <label><span><b>*</b>创建人</span><el-select v-model="form.creator" placeholder="选择创建人"><el-option v-for="item in creators" :key="item" :label="item" :value="item" /></el-select></label>
          <label><span><b>*</b>创建人</span><el-select v-model="form.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="form.status"><el-radio-button value="启用">启用</el-radio-button><el-radio-button value="禁用">禁用</el-radio-button></el-radio-group></label>
        </section>
@@ -234,14 +271,14 @@
      <aside class="template-panel" :aria-hidden="!templatePanelOpen">
        <header><div><strong>模板字段</strong><span>TEMPLATE FIELDS</span></div><button type="button" aria-label="关闭模板编辑" title="关闭" @click="templatePanelOpen = false"><el-icon><Close /></el-icon></button></header>
        <label class="template-select"><span>当前模板</span><el-select v-model="form.templateName" @change="applyTemplate"><el-option v-for="name in templateNames" :key="name" :label="name" :value="name" /></el-select></label>
        <label class="template-select"><span>当前模板</span><el-select v-model="form.templateId" @change="applyTemplate"><el-option label="自定义" :value="0" /><el-option v-for="item in templateOptions" :key="item.id" :label="item.name" :value="item.id" /></el-select></label>
        <div class="field-checkbox-list">
          <el-checkbox-group v-if="templateFieldOptions.length" v-model="selectedFieldIds" class="field-checkbox-grid" @change="syncSelectedFields">
            <el-checkbox v-for="field in templateFieldOptions" :key="field.id" :value="field.id" :disabled="field.status === '禁用'" :title="`${field.name}(${field.type},${field.status})`">{{ field.name }}</el-checkbox>
          </el-checkbox-group>
          <div v-else class="field-empty">暂无字段,请先到电池组字段管理中添加字段</div>
        </div>
        <footer><button class="primary-button" type="button" @click="saveTemplate"><el-icon><Check /></el-icon>保存</button><button class="secondary-button" type="button" @click="saveTemplateAs"><el-icon><DocumentCopy /></el-icon>另存模板</button></footer>
        <footer><button class="primary-button" type="button" :disabled="templateSaving" @click="saveTemplate"><el-icon><Check /></el-icon>保存</button><button class="secondary-button" type="button" :disabled="templateSaving" @click="saveTemplateAs"><el-icon><DocumentCopy /></el-icon>另存模板</button></footer>
      </aside>
    </div>
  </div>
tests/battery-group-persistence.contract.test.mjs
@@ -29,3 +29,14 @@
    templateFieldRelations: undefined,
  })
})
test('battery group payload preserves template field values', () => {
  const payload = buildBatteryGroupPayload({
    name: '样品组', model: 'M-01', purpose: '验证', remark: '', status: '禁用',
    specs: {}, batteries: [{ value: 'BAT-01' }],
  }, 7, [{ batteryGroupFieldId: 3, fieldValue: '72Ah' }])
  assert.equal(payload.status, '1')
  assert.equal(payload.templateId, 7)
  assert.deepEqual(payload.templateFieldRelations, [{ batteryGroupFieldId: 3, fieldValue: '72Ah' }])
})