diff --git a/frontend/src/utils/replaceAllKeys.js b/frontend/src/utils/replaceAllKeys.js new file mode 100644 index 00000000..bb8ddb1e --- /dev/null +++ b/frontend/src/utils/replaceAllKeys.js @@ -0,0 +1,14 @@ +// 用 values 的键值对整体替换 target 的内容:先清空 target 已有的全部键,再赋值。 +// +// 不能直接用 Object.assign(target, values):assign 只覆盖 target 上**已存在**的键, +// 不会删除多余的键。表单对象上残留的 id / organization_id 这类字段就是这么漏进来的 —— +// values(新建用的空表单)里没有 id,assign 便原样保留了上一次编辑并进来的 id, +// 于是新增请求体里带上了别人的 id。这里先 delete 再赋值,保证结果只含 values 的键。 +// +// target 通常是 Vue 的 reactive 对象,delete 与赋值都会触发 Proxy 的拦截, +// 视图能正常同步;同样也适用于普通对象。 +export function replaceAllKeys(target, values) { + Object.keys(target).forEach(key => { delete target[key] }) + Object.assign(target, values) + return target +} diff --git a/frontend/src/utils/replaceAllKeys.test.js b/frontend/src/utils/replaceAllKeys.test.js new file mode 100644 index 00000000..0bca2b92 --- /dev/null +++ b/frontend/src/utils/replaceAllKeys.test.js @@ -0,0 +1,51 @@ +import { describe, expect, test } from 'vitest' +import { reactive } from 'vue' +import { replaceAllKeys } from './replaceAllKeys' + +describe('replaceAllKeys', () => { + test('删掉 values 里没有的键,不留残留字段', () => { + const form = { id: 1, code: 'A', name: '旧名称' } + + replaceAllKeys(form, { code: '', name: '' }) + + expect(form).toEqual({ code: '', name: '' }) + expect(Object.keys(form)).not.toContain('id') + }) + + test('这一点正是 Object.assign 做不到的', () => { + const assigned = { id: 1, code: 'A' } + Object.assign(assigned, { code: 'B' }) + + expect(assigned.id).toBe(1) + + const replaced = { id: 1, code: 'A' } + replaceAllKeys(replaced, { code: 'B' }) + + expect(replaced.id).toBeUndefined() + }) + + test('覆盖同名的键并写入新增的键', () => { + const form = { code: 'A', name: '旧名称' } + + replaceAllKeys(form, { code: 'A09', name: '新增类', parent_id: null }) + + expect(form).toEqual({ code: 'A09', name: '新增类', parent_id: null }) + }) + + test('用于 reactive 对象时,键集合与取值都同步更新', () => { + const form = reactive({ id: 7, code: 'A' }) + + replaceAllKeys(form, { code: 'B', status: 'active' }) + + expect(Object.keys(form)).toEqual(['code', 'status']) + expect(form.id).toBeUndefined() + expect(form.code).toBe('B') + expect(form.status).toBe('active') + }) + + test('返回 target,便于链式书写', () => { + const form = {} + + expect(replaceAllKeys(form, { code: 'A' })).toBe(form) + }) +}) diff --git a/frontend/src/views/master-data/MasterDataSettings.vue b/frontend/src/views/master-data/MasterDataSettings.vue index ecf49be3..9c0589ed 100644 --- a/frontend/src/views/master-data/MasterDataSettings.vue +++ b/frontend/src/views/master-data/MasterDataSettings.vue @@ -106,6 +106,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { ElMessage } from 'element-plus' import { codeRulesApi, materialCategoriesApi, unitsApi } from '../../api/masterData' +import { replaceAllKeys } from '../../utils/replaceAllKeys' const tabs = [ { key: 'categories', label: '物料分类', api: materialCategoriesApi }, @@ -150,16 +151,11 @@ async function ensureCategories() { categories.value = response.data || [] } -// 与 PartnerList.vue 同理:Object.assign 删不掉上一次编辑残留的 id, -// 新增前必须把表单整体清空。 -function resetForm(values) { - Object.keys(form).forEach(key => { delete form[key] }) - Object.assign(form, values) -} - async function openCreate() { editingId.value = null - resetForm(defaultForm()) + // 用 replaceAllKeys 而不是 Object.assign:openEdit 会把 row 的全部字段并进 form, + // 其中 id / organization_id 在新增时必须清掉,assign 做不到。 + replaceAllKeys(form, defaultForm()) if (activeTab.value === 'rules') await ensureCategories() dialogVisible.value = true } diff --git a/frontend/src/views/partners/PartnerList.vue b/frontend/src/views/partners/PartnerList.vue index f4946426..ab5cd4cc 100644 --- a/frontend/src/views/partners/PartnerList.vue +++ b/frontend/src/views/partners/PartnerList.vue @@ -38,6 +38,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue' import { ElMessage } from 'element-plus' import { partnersApi } from '../../api/partners' +import { replaceAllKeys } from '../../utils/replaceAllKeys' const props = defineProps({ role: { type: String, required: true }, title: { type: String, required: true } }) const rows = ref([]) @@ -54,13 +55,9 @@ const form = reactive(emptyForm()) function emptyForm() { return { partner_code: '', legal_name: '', unified_credit_code: '', contact_name: '', contact_phone: '', address: '', status: 'active', is_supplier: false, is_customer: false } } async function load() { loading.value = true; try { const response = await partnersApi.list(props.role); rows.value = response.data || [] } finally { loading.value = false } } -// Object.assign 只覆盖已存在的键,不会删掉上次编辑残留的 id, -// 所以这里先清空再整体赋值,保证新增提交的是一张干净的表单。 -function resetForm(values) { - Object.keys(form).forEach(key => { delete form[key] }) - Object.assign(form, values) -} -function openCreate() { editingId.value = null; resetForm(emptyForm()); dialogVisible.value = true } +// 用 replaceAllKeys 而不是 Object.assign:后者删不掉上次编辑残留的 id, +// 新增时会把别人的 id 一起提交上去。 +function openCreate() { editingId.value = null; replaceAllKeys(form, emptyForm()); dialogVisible.value = true } function openEdit(row) { editingId.value = row.id; Object.assign(form, row, { status: row[statusField.value] || 'active' }); dialogVisible.value = true } function payload() { return {