| | |
| | | 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 |
| | |
| | | remark: string |
| | | creator: string |
| | | status: GroupStatus |
| | | templateName: string |
| | | templateId: number |
| | | specs: Record<string, string> |
| | | customFields: TemplateField[] |
| | | batteries: Array<{ key: number, value: string }> |
| | |
| | | |
| | | 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 |
| | |
| | | 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 '' |
| | | } |
| | | |
| | |
| | | } |
| | | 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 ? '电池组已更新' : '电池组已创建') |
| | |
| | | } |
| | | } |
| | | |
| | | onMounted(loadEditingGroup) |
| | | onMounted(loadPage) |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | <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"> |
| | |
| | | </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> |
| | | |
| | |
| | | |
| | | <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> |