fix(shopExpressTemplateDetail): 修正配送区域多选字段对接后端机制

- model中省市多选字段由provinceIds/cityIds改为regionMode/regionIds
- RegionMultiSelect组件去除update:provinceIds事件,改仅emit update:value
- 编辑弹窗中城市选择变化同步更新form.regionIds和form.regionMode字段
- 回显数据时从regionIds拆分赋值,不再使用provinceIds/cityIds
- 列表页customRender展示使用record.regionIds反查城市名
- 解决前端提交字段与后端实体不匹配导致数据未保存问题
- 确保本次改动完全兼容后端现有regionMode/regionIds机制,无需后端改动
This commit is contained in:
2026-08-11 10:32:57 +08:00
parent ae2ea4d841
commit a228dcb96b
6 changed files with 50 additions and 58 deletions
+16 -2
View File
@@ -23,6 +23,20 @@
- 新增 `h` 函数 import。 - 新增 `h` 函数 import。
### 待办 / 风险 ### 待办 / 风险
- **后端需同步改造**:表加 `province_ids` / `city_ids`(varchar 逗号分隔),接口 add/update 接收这两个字段。前端已按此约定提交,后端未改则数据落不了库。
- antd 为 v4`var(--ant-color-border)` 在暗色下可能回退浅灰(边框略浅,不影响功能);如要完美暗色适配可后续改 less 变量。 - antd 为 v4`var(--ant-color-border)` 在暗色下可能回退浅灰(边框略浅,不影响功能);如要完美暗色适配可后续改 less 变量。
- 构建验证(vite build)已触发,待结果确认无编译错误 - 构建验证(vite build)已通过
## ⚠️ 重大修订(同日):前端字段名与后端不匹配,导致"没存进库"
### 根因
- 后端 `ShopExpressTemplateDetail` 实体**已有**配送区域多选机制:`regionMode`(0=全国 / 1=指定配送白名单 / 2=不发货黑名单)+ `regionIds`(逗号分隔的 6 位城市编码串)。
- 旧代码 `provinceId`/`cityId` 是单值历史字段,已被迁移脚本 `sql/shop_express_template_detail_region.sql` 迁成 `regionMode=1, regionIds=城市编码`
- 我第一版前端提交的是 `provinceIds`/`cityIds`,后端实体无此字段 → MyBatis-Plus 自动忽略未知字段 → 看起来"没存没变化"。
- ORM 是 MyBatis-Plusadd/update 用 `save()`/`updateById()` 自动生成 SQL,接收的是实体类本身(非 Param);新增字段只需在**实体加属性 + 表加列**,无需改 Mapper XML。
### 修复(对接后端已有的 regionMode/regionIds,无需后端改动)
1. model`provinceIds`/`cityIds` → 改为 `regionMode?``regionIds?`
2. `RegionMultiSelect` 组件:去掉 `update:provinceIds` emit,只 emit `update:value`(城市编码数组)。
3. 编辑弹窗:城市选择变化 → `form.regionIds = 逗号串``form.regionMode = 1`(有选)或 `0`(全国);回显从 `form.regionIds` 拆出。
4. 列表页:customRender 读 `record.regionIds` 反查城市名。
- 结论:**本需求纯前端即可闭环,后端无需改**。城市编码(`regions-data.json` 的 city value6位)与 `regionIds` 完全一致,下单校验 `DeliveryRegionChecker``regionIds` 直接生效。
+6 -5
View File
@@ -57,9 +57,10 @@
### 运费模板配送地区多选(2026-08-11 已实现前端) ### 运费模板配送地区多选(2026-08-11 已实现前端)
- 需求:配送地区选择从「省+市单选级联」升级为「省+市两级多选穿梭弹窗」(仿客户截图双栏:左候选/右已选)。 - 需求:配送地区选择从「省+市单选级联」升级为「省+市两级多选穿梭弹窗」(仿客户截图双栏:左候选/右已选)。
- 前端组件:`src/components/RegionMultiSelect/index.vue`(树形可展开,省级"选择"=选该省全部市,市级可单独选;@update:value 城市ID数组 + @update:provinceIds 省份ID数组)。 - 前端组件:`src/components/RegionMultiSelect/index.vue`(树形可展开,省级"选择"=选该省全部市,市级可单独选;@update:value 城市编码数组)。
- 数据字段:model 新增 `provinceIds` / `cityIds`逗号分隔字符串);保留旧 `provinceId`/`cityId` 兼容(编辑回显时旧单值会自动补进 cityIds) - **直接对接后端已有的 `regionMode`(0=全国/1=指定配送白名单/2=不发货黑名单) + `regionIds`(逗号分隔城市编码串) 机制,无需后端改动**。城市编码即 `regions-data.json` 的 city value(6位),与 `regionIds` 完全一致
- 列表多值展示:`index.vue` customRender 遍历 `cityIds` 反查 regionMap,超 22 字截断 + tooltip - 数据字段:model 用 `regionMode` / `regionIds`(已有字段,非新建);旧 `provinceId`/`cityId` 仅作回显兼容
- ⚠️ 后端待改:`shop_express_template_detail` 表加 `province_ids`/`city_ids`varchar 逗号分隔),add/update 接口接收这两个字段;前端已按此约定提交,后端未改前数据落不了库 - 列表多值展示:`index.vue` customRender 遍历 `regionIds` 反查 regionMap,超 22 字截断 + tooltip
- 注:与"不发货地区(黑名单) regionMode/regionIds"是不同需求,勿混淆 - 下单校验 `DeliveryRegionChecker``regionIds` 直接生效(regionMode=2 为不发货黑名单)
- ⚠️ 教训:第一版误用 `provinceIds`/`cityIds` 字段名,与后端 `regionMode`/`regionIds` 不匹配,MyBatis-Plus 自动忽略未知字段导致"没存"。改字段名对接即可,不必新增后端字段。
@@ -16,10 +16,10 @@ export interface ShopExpressTemplateDetail {
provinceId?: number; provinceId?: number;
/** 城市ID(旧单值字段,兼容历史数据) */ /** 城市ID(旧单值字段,兼容历史数据) */
cityId?: number; cityId?: number;
/** 省份IDs(逗号分隔,多选配送区域) */ /** 配送区域模式: 0=全国, 1=指定配送(白名单), 2=指定不发货(黑名单) */
provinceIds?: string; regionMode?: number;
/** 城市IDs(逗号分隔,多选配送区域) */ /** 地区编码列表(6位行政区划码,逗号分隔),regionMode=1或2时有效 */
cityIds?: string; regionIds?: string;
/** 首件数量/重量 */ /** 首件数量/重量 */
firstNum?: string; firstNum?: string;
/** 首件价格 */ /** 首件价格 */
@@ -129,7 +129,6 @@
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:value', value: string[]): void; (e: 'update:value', value: string[]): void;
(e: 'update:provinceIds', value: string[]): void;
}>(); }>();
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE; const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
@@ -226,17 +225,6 @@
})) }))
); );
const derivedProvinceIds = computed(() => {
const set = new Set<string>();
selectedSet.value.forEach((cv) => {
const pv = cityToProvince.value[cv];
if (pv) {
set.add(pv);
}
});
return Array.from(set);
});
const selectProvince = (p: ProvinceItem) => { const selectProvince = (p: ProvinceItem) => {
const s = new Set(selectedSet.value); const s = new Set(selectedSet.value);
p.cities.forEach((c) => s.add(c.value)); p.cities.forEach((c) => s.add(c.value));
@@ -275,7 +263,6 @@
const handleConfirm = () => { const handleConfirm = () => {
emit('update:value', selectedCities.value); emit('update:value', selectedCities.value);
emit('update:provinceIds', derivedProvinceIds.value);
visible.value = false; visible.value = false;
}; };
@@ -24,7 +24,6 @@
<RegionMultiSelect <RegionMultiSelect
:value="cityIds" :value="cityIds"
@update:value="onCityIdsChange" @update:value="onCityIdsChange"
@update:provinceIds="onProvinceIdsChange"
/> />
<div class="rms-tip">不选则默认全国</div> <div class="rms-tip">不选则默认全国</div>
</a-form-item> </a-form-item>
@@ -128,10 +127,8 @@
const maxable = ref(true); const maxable = ref(true);
// 表单实例 // 表单实例
const formRef = ref<FormInstance | null>(null); const formRef = ref<FormInstance | null>(null);
// 已选城市ID数组 // 已选城市编码数组(对应后端的 regionIds)
const cityIds = ref<string[]>([]); const cityIds = ref<string[]>([]);
// 已选省份ID数组(由城市推导,随城市变化同步)
const provinceIds = ref<string[]>([]);
// 表单数据 // 表单数据
const form = reactive<ShopExpressTemplateDetail>({ const form = reactive<ShopExpressTemplateDetail>({
@@ -140,8 +137,8 @@
type: true, type: true,
provinceId: undefined, provinceId: undefined,
cityId: undefined, cityId: undefined,
provinceIds: undefined, regionMode: undefined,
cityIds: undefined, regionIds: undefined,
firstNum: undefined, firstNum: undefined,
firstAmount: undefined, firstAmount: undefined,
extraAmount: undefined, extraAmount: undefined,
@@ -190,16 +187,16 @@
const { resetFields } = useForm(form, rules); const { resetFields } = useForm(form, rules);
/* 城市选中变化,同步到 form.cityIds */ /* 城市选中变化,同步到 form.regionIds / regionMode1=指定配送白名单,0=全国) */
const onCityIdsChange = (val: string[]) => { const onCityIdsChange = (val: string[]) => {
cityIds.value = val; cityIds.value = val;
form.cityIds = val.length ? val.join(',') : undefined; if (val.length) {
}; form.regionIds = val.join(',');
form.regionMode = 1;
/* 省份选中变化(由组件根据已选城市推导),同步到 form.provinceIds */ } else {
const onProvinceIdsChange = (val: string[]) => { form.regionIds = undefined;
provinceIds.value = val; form.regionMode = 0;
form.provinceIds = val.length ? val.join(',') : undefined; }
}; };
/* 保存编辑 */ /* 保存编辑 */
@@ -215,13 +212,14 @@
// 确保templateId和type从父级继承 // 确保templateId和type从父级继承
formData.templateId = props.templateId; formData.templateId = props.templateId;
formData.type = props.templateType ?? true; formData.type = props.templateType ?? true;
// 多选配送区域(逗号分隔,空则默认全国 // 多选配送区域(regionMode=1 指定配送,0=全国;regionIds 存城市编码逗号串
formData.cityIds = cityIds.value.length if (cityIds.value.length) {
? cityIds.value.join(',') formData.regionIds = cityIds.value.join(',');
: undefined; formData.regionMode = 1;
formData.provinceIds = provinceIds.value.length } else {
? provinceIds.value.join(',') formData.regionIds = undefined;
: undefined; formData.regionMode = 0;
}
const saveOrUpdate = isUpdate.value const saveOrUpdate = isUpdate.value
? updateShopExpressTemplateDetail ? updateShopExpressTemplateDetail
: addShopExpressTemplateDetail; : addShopExpressTemplateDetail;
@@ -252,26 +250,18 @@
form.templateId = props.templateId; form.templateId = props.templateId;
form.type = props.templateType ?? true; form.type = props.templateType ?? true;
} }
// 回显多选配送区域 // 回显多选配送区域regionIds 存城市编码)
if (form.cityIds) { if (form.regionIds) {
cityIds.value = form.cityIds.split(',').filter(Boolean); cityIds.value = form.regionIds.split(',').filter(Boolean);
} else if (form.cityId) { } else if (form.cityId) {
// 兼容旧单值数据 // 兼容旧单值数据
cityIds.value = [String(form.cityId)]; cityIds.value = [String(form.cityId)];
} else { } else {
cityIds.value = []; cityIds.value = [];
} }
if (form.provinceIds) {
provinceIds.value = form.provinceIds.split(',').filter(Boolean);
} else if (form.provinceId) {
provinceIds.value = [String(form.provinceId)];
} else {
provinceIds.value = [];
}
} else { } else {
resetFields(); resetFields();
cityIds.value = []; cityIds.value = [];
provinceIds.value = [];
} }
}, },
{ immediate: true } { immediate: true }
@@ -128,8 +128,8 @@
align: 'center', align: 'center',
width: 150, width: 150,
customRender: ({ record }) => { customRender: ({ record }) => {
// 多选配送区域(cityIds 逗号分隔) // 多选配送区域(regionIds 逗号分隔的城市编码
const cityIds = (record.cityIds || '') const cityIds = (record.regionIds || '')
.split(',') .split(',')
.filter(Boolean); .filter(Boolean);
if (cityIds.length) { if (cityIds.length) {