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。
### 待办 / 风险
- **后端需同步改造**:表加 `province_ids` / `city_ids`(varchar 逗号分隔),接口 add/update 接收这两个字段。前端已按此约定提交,后端未改则数据落不了库。
- 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 已实现前端)
- 需求:配送地区选择从「省+市单选级联」升级为「省+市两级多选穿梭弹窗」(仿客户截图双栏:左候选/右已选)。
- 前端组件:`src/components/RegionMultiSelect/index.vue`(树形可展开,省级"选择"=选该省全部市,市级可单独选;@update:value 城市ID数组 + @update:provinceIds 省份ID数组)。
- 数据字段:model 新增 `provinceIds` / `cityIds`逗号分隔字符串);保留旧 `provinceId`/`cityId` 兼容(编辑回显时旧单值会自动补进 cityIds)
- 列表多值展示:`index.vue` customRender 遍历 `cityIds` 反查 regionMap,超 22 字截断 + tooltip
- ⚠️ 后端待改:`shop_express_template_detail` 表加 `province_ids`/`city_ids`varchar 逗号分隔),add/update 接口接收这两个字段;前端已按此约定提交,后端未改前数据落不了库
- 注:与"不发货地区(黑名单) regionMode/regionIds"是不同需求,勿混淆
- 前端组件:`src/components/RegionMultiSelect/index.vue`(树形可展开,省级"选择"=选该省全部市,市级可单独选;@update:value 城市编码数组)。
- **直接对接后端已有的 `regionMode`(0=全国/1=指定配送白名单/2=不发货黑名单) + `regionIds`(逗号分隔城市编码串) 机制,无需后端改动**。城市编码即 `regions-data.json` 的 city value(6位),与 `regionIds` 完全一致
- 数据字段:model 用 `regionMode` / `regionIds`(已有字段,非新建);旧 `provinceId`/`cityId` 仅作回显兼容
- 列表多值展示:`index.vue` customRender 遍历 `regionIds` 反查 regionMap,超 22 字截断 + tooltip
- 下单校验 `DeliveryRegionChecker``regionIds` 直接生效(regionMode=2 为不发货黑名单)
- ⚠️ 教训:第一版误用 `provinceIds`/`cityIds` 字段名,与后端 `regionMode`/`regionIds` 不匹配,MyBatis-Plus 自动忽略未知字段导致"没存"。改字段名对接即可,不必新增后端字段。
@@ -16,10 +16,10 @@ export interface ShopExpressTemplateDetail {
provinceId?: number;
/** 城市ID(旧单值字段,兼容历史数据) */
cityId?: number;
/** 省份IDs(逗号分隔,多选配送区域) */
provinceIds?: string;
/** 城市IDs(逗号分隔,多选配送区域) */
cityIds?: string;
/** 配送区域模式: 0=全国, 1=指定配送(白名单), 2=指定不发货(黑名单) */
regionMode?: number;
/** 地区编码列表(6位行政区划码,逗号分隔),regionMode=1或2时有效 */
regionIds?: string;
/** 首件数量/重量 */
firstNum?: string;
/** 首件价格 */
@@ -129,7 +129,6 @@
const emit = defineEmits<{
(e: 'update:value', value: string[]): void;
(e: 'update:provinceIds', value: string[]): void;
}>();
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 s = new Set(selectedSet.value);
p.cities.forEach((c) => s.add(c.value));
@@ -275,7 +263,6 @@
const handleConfirm = () => {
emit('update:value', selectedCities.value);
emit('update:provinceIds', derivedProvinceIds.value);
visible.value = false;
};
@@ -24,7 +24,6 @@
<RegionMultiSelect
:value="cityIds"
@update:value="onCityIdsChange"
@update:provinceIds="onProvinceIdsChange"
/>
<div class="rms-tip">不选则默认全国</div>
</a-form-item>
@@ -128,10 +127,8 @@
const maxable = ref(true);
// 表单实例
const formRef = ref<FormInstance | null>(null);
// 已选城市ID数组
// 已选城市编码数组(对应后端的 regionIds)
const cityIds = ref<string[]>([]);
// 已选省份ID数组(由城市推导,随城市变化同步)
const provinceIds = ref<string[]>([]);
// 表单数据
const form = reactive<ShopExpressTemplateDetail>({
@@ -140,8 +137,8 @@
type: true,
provinceId: undefined,
cityId: undefined,
provinceIds: undefined,
cityIds: undefined,
regionMode: undefined,
regionIds: undefined,
firstNum: undefined,
firstAmount: undefined,
extraAmount: undefined,
@@ -190,16 +187,16 @@
const { resetFields } = useForm(form, rules);
/* 城市选中变化,同步到 form.cityIds */
/* 城市选中变化,同步到 form.regionIds / regionMode1=指定配送白名单,0=全国) */
const onCityIdsChange = (val: string[]) => {
cityIds.value = val;
form.cityIds = val.length ? val.join(',') : undefined;
};
/* 省份选中变化(由组件根据已选城市推导),同步到 form.provinceIds */
const onProvinceIdsChange = (val: string[]) => {
provinceIds.value = val;
form.provinceIds = val.length ? val.join(',') : undefined;
if (val.length) {
form.regionIds = val.join(',');
form.regionMode = 1;
} else {
form.regionIds = undefined;
form.regionMode = 0;
}
};
/* 保存编辑 */
@@ -215,13 +212,14 @@
// 确保templateId和type从父级继承
formData.templateId = props.templateId;
formData.type = props.templateType ?? true;
// 多选配送区域(逗号分隔,空则默认全国
formData.cityIds = cityIds.value.length
? cityIds.value.join(',')
: undefined;
formData.provinceIds = provinceIds.value.length
? provinceIds.value.join(',')
: undefined;
// 多选配送区域(regionMode=1 指定配送,0=全国;regionIds 存城市编码逗号串
if (cityIds.value.length) {
formData.regionIds = cityIds.value.join(',');
formData.regionMode = 1;
} else {
formData.regionIds = undefined;
formData.regionMode = 0;
}
const saveOrUpdate = isUpdate.value
? updateShopExpressTemplateDetail
: addShopExpressTemplateDetail;
@@ -252,26 +250,18 @@
form.templateId = props.templateId;
form.type = props.templateType ?? true;
}
// 回显多选配送区域
if (form.cityIds) {
cityIds.value = form.cityIds.split(',').filter(Boolean);
// 回显多选配送区域regionIds 存城市编码)
if (form.regionIds) {
cityIds.value = form.regionIds.split(',').filter(Boolean);
} else if (form.cityId) {
// 兼容旧单值数据
cityIds.value = [String(form.cityId)];
} else {
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 {
resetFields();
cityIds.value = [];
provinceIds.value = [];
}
},
{ immediate: true }
@@ -128,8 +128,8 @@
align: 'center',
width: 150,
customRender: ({ record }) => {
// 多选配送区域(cityIds 逗号分隔)
const cityIds = (record.cityIds || '')
// 多选配送区域(regionIds 逗号分隔的城市编码
const cityIds = (record.regionIds || '')
.split(',')
.filter(Boolean);
if (cityIds.length) {