feat(shopExpressTemplateDetail): 支持省市两级多选配送区域
- 新增 model 字段 provinceIds 和 cityIds,兼容旧单值 provinceId 和 cityId - 引入 RegionMultiSelect 组件替代旧的单选级联选择器 - 编辑弹窗支持多选城市和省份,提交时转换为逗号分隔字符串 - 列表页配送区域列支持多选展示,超长文本截断并显示 tooltip - 新增 RegionMultiSelect 组件,实现省市两级树形多选穿梭弹窗 - 通过 computed 和 watch 同步组件选中状态和表单数据 - 更新相关类型定义和导入,确保编辑及展示逻辑正确 - 新增样式优化,提示默认配送全国规则说明
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
# 2026-08-11 工作记录
|
||||||
|
|
||||||
|
## 运费模板配送地区改为「省+市两级多选穿梭弹窗」
|
||||||
|
|
||||||
|
### 需求背景
|
||||||
|
客户要求运费模板的配送地区选择改成仿截图的双栏穿梭弹窗(左侧候选 + 右侧已选),
|
||||||
|
选择粒度从原「省+市单选(级联)」升级为「多条规则覆盖多城市」,并保留「不选=全国」规则。
|
||||||
|
|
||||||
|
### 改动清单(纯前端,后端字段由用户确认可改)
|
||||||
|
1. **新建组件** `src/components/RegionMultiSelect/index.vue`
|
||||||
|
- 树形可展开:省级可展开看市级;省级"选择"=选该省全部市,市级可单独选。
|
||||||
|
- 左栏搜索(按省/市名过滤)、右栏已选列表(单项是"省/市"+取消)。
|
||||||
|
- 底部:全部选择 / 取消 / 确认。
|
||||||
|
- `v-model:value` 绑定已选**城市ID数组**;额外 emit `update:provinceIds`(由城市推导的省份ID数组)。
|
||||||
|
- 复用 `src/components/RegionsSelect/load-data.ts` 的 getRegionsData()(public/json/regions-data.json,取省+市两级,丢弃区级)。
|
||||||
|
2. **model** `src/api/shop/shopExpressTemplateDetail/model/index.ts`
|
||||||
|
- 新增 `provinceIds?: string`(逗号分隔)、`cityIds?: string`(逗号分隔);保留旧 `provinceId/cityId` 兼容历史数据。
|
||||||
|
3. **编辑弹窗** `src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue`
|
||||||
|
- 移除 `RegionsSelect type="provinceCity"`,改用 `RegionMultiSelect`。
|
||||||
|
- 打开时从 `record.cityIds`(兼容旧 `cityId`)回填;保存时数组→逗号串提交 `cityIds/provinceIds`。
|
||||||
|
4. **列表页** `src/views/shop/shopExpressTemplateDetail/index.vue`
|
||||||
|
- "配送地区"列 customRender 改为遍历 `cityIds` 多值反查 regionMap;超22字截断+tooltip;旧单值数据兼容。
|
||||||
|
- 新增 `h` 函数 import。
|
||||||
|
|
||||||
|
### 待办 / 风险
|
||||||
|
- **后端需同步改造**:表加 `province_ids` / `city_ids`(varchar 逗号分隔),接口 add/update 接收这两个字段。前端已按此约定提交,后端未改则数据落不了库。
|
||||||
|
- antd 为 v4,`var(--ant-color-border)` 在暗色下可能回退浅灰(边框略浅,不影响功能);如要完美暗色适配可后续改 less 变量。
|
||||||
|
- 构建验证(vite build)已触发,待结果确认无编译错误。
|
||||||
@@ -55,3 +55,11 @@
|
|||||||
- 一键上下架、批量上下架均通过 `updateShopGoods({ ...record, status })` 直接改 status 实现(不额外用 isShow 字段)。
|
- 一键上下架、批量上下架均通过 `updateShopGoods({ ...record, status })` 直接改 status 实现(不额外用 isShow 字段)。
|
||||||
- 列表 tag 仍显示:0=已上架、1=待上架、2=待审核、3=审核不通过。
|
- 列表 tag 仍显示:0=已上架、1=待上架、2=待审核、3=审核不通过。
|
||||||
|
|
||||||
|
### 运费模板配送地区多选(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"是不同需求,勿混淆。
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ export interface ShopExpressTemplateDetail {
|
|||||||
templateId?: number;
|
templateId?: number;
|
||||||
/** 计费方式: true=按件, false=按重量 */
|
/** 计费方式: true=按件, false=按重量 */
|
||||||
type?: boolean;
|
type?: boolean;
|
||||||
/** 省份ID */
|
/** 省份ID(旧单值字段,兼容历史数据) */
|
||||||
provinceId?: number;
|
provinceId?: number;
|
||||||
/** 城市ID */
|
/** 城市ID(旧单值字段,兼容历史数据) */
|
||||||
cityId?: number;
|
cityId?: number;
|
||||||
|
/** 省份IDs(逗号分隔,多选配送区域) */
|
||||||
|
provinceIds?: string;
|
||||||
|
/** 城市IDs(逗号分隔,多选配送区域) */
|
||||||
|
cityIds?: string;
|
||||||
/** 首件数量/重量 */
|
/** 首件数量/重量 */
|
||||||
firstNum?: string;
|
firstNum?: string;
|
||||||
/** 首件价格 */
|
/** 首件价格 */
|
||||||
|
|||||||
@@ -0,0 +1,375 @@
|
|||||||
|
<!-- 省+市两级多选穿梭弹窗 -->
|
||||||
|
<template>
|
||||||
|
<div class="region-multi-select">
|
||||||
|
<!-- 触发区 -->
|
||||||
|
<div class="rms-trigger">
|
||||||
|
<a-button class="rms-trigger-btn" @click="openModal">
|
||||||
|
{{ triggerText }}
|
||||||
|
</a-button>
|
||||||
|
<span v-if="modelValue.length" class="rms-summary">
|
||||||
|
已选 {{ modelValue.length }} 个城市
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="visible"
|
||||||
|
title="区域选择"
|
||||||
|
width="760px"
|
||||||
|
:mask-closable="false"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<div class="rms-body">
|
||||||
|
<!-- 左侧候选 -->
|
||||||
|
<div class="rms-panel">
|
||||||
|
<a-input-search
|
||||||
|
v-model:value="keyword"
|
||||||
|
placeholder="输入地区关键字"
|
||||||
|
allow-clear
|
||||||
|
class="rms-search"
|
||||||
|
/>
|
||||||
|
<div class="rms-scroll">
|
||||||
|
<div
|
||||||
|
v-for="p in filteredProvinces"
|
||||||
|
:key="p.value"
|
||||||
|
class="rms-province"
|
||||||
|
>
|
||||||
|
<div class="rms-province-row">
|
||||||
|
<span class="rms-expand" @click="toggleExpand(p.value)">
|
||||||
|
{{ isExpanded(p.value) ? '▾' : '▸' }}
|
||||||
|
</span>
|
||||||
|
<span class="rms-label" @click="toggleExpand(p.value)">
|
||||||
|
{{ p.label }}
|
||||||
|
</span>
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
:disabled="isProvinceSelected(p)"
|
||||||
|
@click="selectProvince(p)"
|
||||||
|
>
|
||||||
|
{{ isProvinceSelected(p) ? '已选' : '选择' }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isExpanded(p.value)" class="rms-city-list">
|
||||||
|
<div
|
||||||
|
v-for="c in p.cities"
|
||||||
|
:key="c.value"
|
||||||
|
class="rms-city-row"
|
||||||
|
>
|
||||||
|
<span class="rms-label">{{ c.label }}</span>
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
:disabled="isCitySelected(c.value)"
|
||||||
|
@click="toggleCity(c.value)"
|
||||||
|
>
|
||||||
|
{{ isCitySelected(c.value) ? '已选' : '选择' }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧已选 -->
|
||||||
|
<div class="rms-panel">
|
||||||
|
<div class="rms-selected-title">
|
||||||
|
已选择 ({{ selectedCities.length }})
|
||||||
|
</div>
|
||||||
|
<div class="rms-scroll">
|
||||||
|
<div
|
||||||
|
v-for="item in selectedList"
|
||||||
|
:key="item.cityValue"
|
||||||
|
class="rms-selected-item"
|
||||||
|
>
|
||||||
|
<span class="rms-selected-label">
|
||||||
|
{{ item.provinceLabel }} / {{ item.cityLabel }}
|
||||||
|
</span>
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
@click="removeCity(item.cityValue)"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-empty
|
||||||
|
v-if="!selectedCities.length"
|
||||||
|
:image="simpleImage"
|
||||||
|
description="暂未选择"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<a-button @click="selectAll">全部选择</a-button>
|
||||||
|
<span class="rms-footer-spacer"></span>
|
||||||
|
<a-button @click="handleCancel">取消</a-button>
|
||||||
|
<a-button type="primary" @click="handleConfirm">确认</a-button>
|
||||||
|
</template>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { Empty } from 'ant-design-vue';
|
||||||
|
import { getRegionsData } from '@/components/RegionsSelect/load-data';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 已选城市ID数组(逗号分隔的行政区码) */
|
||||||
|
value?: string[];
|
||||||
|
placeholder?: string;
|
||||||
|
}>(),
|
||||||
|
{ value: () => [], placeholder: '选择配送地区' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:value', value: string[]): void;
|
||||||
|
(e: 'update:provinceIds', value: string[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const keyword = ref('');
|
||||||
|
const expandedSet = ref<Set<string>>(new Set());
|
||||||
|
const selectedSet = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
|
interface CityItem {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
interface ProvinceItem {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
cities: CityItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const provinceList = ref<ProvinceItem[]>([]);
|
||||||
|
const cityToProvince = ref<Record<string, string>>({});
|
||||||
|
const labelMap = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 加载省+市两级数据(丢弃区级)
|
||||||
|
getRegionsData().then((data) => {
|
||||||
|
const list: ProvinceItem[] = [];
|
||||||
|
const c2p: Record<string, string> = {};
|
||||||
|
const lm: Record<string, string> = {};
|
||||||
|
(data ?? []).forEach((p) => {
|
||||||
|
lm[p.value] = p.label;
|
||||||
|
const cities: CityItem[] = (p.children ?? []).map((c) => {
|
||||||
|
c2p[c.value] = p.value;
|
||||||
|
lm[c.value] = c.label;
|
||||||
|
return { value: c.value, label: c.label };
|
||||||
|
});
|
||||||
|
list.push({ value: p.value, label: p.label, cities });
|
||||||
|
});
|
||||||
|
provinceList.value = list;
|
||||||
|
cityToProvince.value = c2p;
|
||||||
|
labelMap.value = lm;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValue = computed(() => props.value ?? []);
|
||||||
|
|
||||||
|
const triggerText = computed(() =>
|
||||||
|
modelValue.value.length
|
||||||
|
? `已选 ${modelValue.value.length} 个城市`
|
||||||
|
: props.placeholder
|
||||||
|
);
|
||||||
|
|
||||||
|
const isExpanded = (v: string) => expandedSet.value.has(v);
|
||||||
|
const toggleExpand = (v: string) => {
|
||||||
|
const s = new Set(expandedSet.value);
|
||||||
|
if (s.has(v)) {
|
||||||
|
s.delete(v);
|
||||||
|
} else {
|
||||||
|
s.add(v);
|
||||||
|
}
|
||||||
|
expandedSet.value = s;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCitySelected = (v: string) => selectedSet.value.has(v);
|
||||||
|
const isProvinceSelected = (p: ProvinceItem) =>
|
||||||
|
p.cities.length > 0 && p.cities.every((c) => selectedSet.value.has(c.value));
|
||||||
|
|
||||||
|
const filteredProvinces = computed(() => {
|
||||||
|
const kw = keyword.value.trim();
|
||||||
|
if (!kw) {
|
||||||
|
return provinceList.value;
|
||||||
|
}
|
||||||
|
return provinceList.value
|
||||||
|
.map((p) => {
|
||||||
|
const matchProvince = p.label.includes(kw);
|
||||||
|
if (matchProvince) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
const cities = p.cities.filter((c) => c.label.includes(kw));
|
||||||
|
if (cities.length) {
|
||||||
|
return { ...p, cities };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as ProvinceItem[];
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedCities = computed(() => Array.from(selectedSet.value));
|
||||||
|
|
||||||
|
const selectedList = computed(() =>
|
||||||
|
selectedCities.value.map((cv) => ({
|
||||||
|
cityValue: cv,
|
||||||
|
cityLabel: labelMap.value[cv] || cv,
|
||||||
|
provinceValue: cityToProvince.value[cv],
|
||||||
|
provinceLabel: labelMap.value[cityToProvince.value[cv]] || ''
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
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));
|
||||||
|
selectedSet.value = s;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCity = (cv: string) => {
|
||||||
|
const s = new Set(selectedSet.value);
|
||||||
|
if (s.has(cv)) {
|
||||||
|
s.delete(cv);
|
||||||
|
} else {
|
||||||
|
s.add(cv);
|
||||||
|
}
|
||||||
|
selectedSet.value = s;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCity = (cv: string) => {
|
||||||
|
const s = new Set(selectedSet.value);
|
||||||
|
s.delete(cv);
|
||||||
|
selectedSet.value = s;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAll = () => {
|
||||||
|
const s = new Set<string>();
|
||||||
|
provinceList.value.forEach((p) =>
|
||||||
|
p.cities.forEach((c) => s.add(c.value))
|
||||||
|
);
|
||||||
|
selectedSet.value = s;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
selectedSet.value = new Set(modelValue.value);
|
||||||
|
keyword.value = '';
|
||||||
|
visible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emit('update:value', selectedCities.value);
|
||||||
|
emit('update:provinceIds', derivedProvinceIds.value);
|
||||||
|
visible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 外部清空时同步内部选中态
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(v) => {
|
||||||
|
if (!visible.value) {
|
||||||
|
selectedSet.value = new Set(v ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.region-multi-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.rms-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.rms-trigger-btn {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.rms-summary {
|
||||||
|
color: var(--ant-color-text-secondary, #999);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rms-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
height: 420px;
|
||||||
|
}
|
||||||
|
.rms-panel {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid var(--ant-color-border, #f0f0f0);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.rms-search {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.rms-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.rms-province-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.rms-expand {
|
||||||
|
width: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--ant-color-text-secondary, #999);
|
||||||
|
}
|
||||||
|
.rms-label {
|
||||||
|
flex: 1;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.rms-city-list {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
.rms-city-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
.rms-selected-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.rms-selected-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px dashed var(--ant-color-border, #f5f5f5);
|
||||||
|
}
|
||||||
|
.rms-selected-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.rms-footer-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+55
-28
@@ -20,13 +20,13 @@
|
|||||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<a-form-item label="配送地区" name="regions">
|
<a-form-item label="配送地区">
|
||||||
<RegionsSelect
|
<RegionMultiSelect
|
||||||
v-model:value="regions"
|
:value="cityIds"
|
||||||
type="provinceCity"
|
@update:value="onCityIdsChange"
|
||||||
:show-search="true"
|
@update:provinceIds="onProvinceIdsChange"
|
||||||
placeholder="请选择配送省份/城市(不选则默认全国)"
|
|
||||||
/>
|
/>
|
||||||
|
<div class="rms-tip">不选则默认全国</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="templateType ? '首件数量' : '首重重量'" name="firstNum">
|
<a-form-item :label="templateType ? '首件数量' : '首重重量'" name="firstNum">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
import {
|
import {
|
||||||
ShopExpressTemplateDetail
|
ShopExpressTemplateDetail
|
||||||
} from '@/api/shop/shopExpressTemplateDetail/model';
|
} from '@/api/shop/shopExpressTemplateDetail/model';
|
||||||
import RegionsSelect from '@/components/RegionsSelect/index.vue';
|
import RegionMultiSelect from '@/components/RegionMultiSelect/index.vue';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { FormInstance } from 'ant-design-vue/es/form';
|
import { FormInstance } from 'ant-design-vue/es/form';
|
||||||
@@ -128,8 +128,10 @@
|
|||||||
const maxable = ref(true);
|
const maxable = ref(true);
|
||||||
// 表单实例
|
// 表单实例
|
||||||
const formRef = ref<FormInstance | null>(null);
|
const formRef = ref<FormInstance | null>(null);
|
||||||
// 级联选择器选中值(字符串ID数组)
|
// 已选城市ID数组
|
||||||
const regions = ref<string[]>([]);
|
const cityIds = ref<string[]>([]);
|
||||||
|
// 已选省份ID数组(由城市推导,随城市变化同步)
|
||||||
|
const provinceIds = ref<string[]>([]);
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const form = reactive<ShopExpressTemplateDetail>({
|
const form = reactive<ShopExpressTemplateDetail>({
|
||||||
@@ -138,6 +140,8 @@
|
|||||||
type: true,
|
type: true,
|
||||||
provinceId: undefined,
|
provinceId: undefined,
|
||||||
cityId: undefined,
|
cityId: undefined,
|
||||||
|
provinceIds: undefined,
|
||||||
|
cityIds: undefined,
|
||||||
firstNum: undefined,
|
firstNum: undefined,
|
||||||
firstAmount: undefined,
|
firstAmount: undefined,
|
||||||
extraAmount: undefined,
|
extraAmount: undefined,
|
||||||
@@ -186,16 +190,17 @@
|
|||||||
|
|
||||||
const { resetFields } = useForm(form, rules);
|
const { resetFields } = useForm(form, rules);
|
||||||
|
|
||||||
/* 级联选择器值变化时,同步到 form.provinceId / form.cityId */
|
/* 城市选中变化,同步到 form.cityIds */
|
||||||
watch(regions, (val) => {
|
const onCityIdsChange = (val: string[]) => {
|
||||||
if (val && val.length > 0) {
|
cityIds.value = val;
|
||||||
form.provinceId = Number(val[0]);
|
form.cityIds = val.length ? val.join(',') : undefined;
|
||||||
form.cityId = val.length > 1 ? Number(val[1]) : undefined;
|
};
|
||||||
} else {
|
|
||||||
form.provinceId = undefined;
|
/* 省份选中变化(由组件根据已选城市推导),同步到 form.provinceIds */
|
||||||
form.cityId = undefined;
|
const onProvinceIdsChange = (val: string[]) => {
|
||||||
}
|
provinceIds.value = val;
|
||||||
});
|
form.provinceIds = val.length ? val.join(',') : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
/* 保存编辑 */
|
/* 保存编辑 */
|
||||||
const save = () => {
|
const save = () => {
|
||||||
@@ -210,6 +215,13 @@
|
|||||||
// 确保templateId和type从父级继承
|
// 确保templateId和type从父级继承
|
||||||
formData.templateId = props.templateId;
|
formData.templateId = props.templateId;
|
||||||
formData.type = props.templateType ?? true;
|
formData.type = props.templateType ?? true;
|
||||||
|
// 多选配送区域(逗号分隔,空则默认全国)
|
||||||
|
formData.cityIds = cityIds.value.length
|
||||||
|
? cityIds.value.join(',')
|
||||||
|
: undefined;
|
||||||
|
formData.provinceIds = provinceIds.value.length
|
||||||
|
? provinceIds.value.join(',')
|
||||||
|
: undefined;
|
||||||
const saveOrUpdate = isUpdate.value
|
const saveOrUpdate = isUpdate.value
|
||||||
? updateShopExpressTemplateDetail
|
? updateShopExpressTemplateDetail
|
||||||
: addShopExpressTemplateDetail;
|
: addShopExpressTemplateDetail;
|
||||||
@@ -240,21 +252,36 @@
|
|||||||
form.templateId = props.templateId;
|
form.templateId = props.templateId;
|
||||||
form.type = props.templateType ?? true;
|
form.type = props.templateType ?? true;
|
||||||
}
|
}
|
||||||
// 回显级联选择器
|
// 回显多选配送区域
|
||||||
if (form.provinceId) {
|
if (form.cityIds) {
|
||||||
const arr = [String(form.provinceId)];
|
cityIds.value = form.cityIds.split(',').filter(Boolean);
|
||||||
if (form.cityId) {
|
} else if (form.cityId) {
|
||||||
arr.push(String(form.cityId));
|
// 兼容旧单值数据
|
||||||
}
|
cityIds.value = [String(form.cityId)];
|
||||||
regions.value = arr;
|
|
||||||
} else {
|
} else {
|
||||||
regions.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();
|
||||||
regions.value = [];
|
cityIds.value = [];
|
||||||
|
provinceIds.value = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rms-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--ant-color-text-secondary, #999);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { createVNode, ref, computed, onMounted } from 'vue';
|
import { createVNode, ref, computed, onMounted, h } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import { message, Modal } from 'ant-design-vue';
|
||||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||||
@@ -128,6 +128,26 @@
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
width: 150,
|
width: 150,
|
||||||
customRender: ({ record }) => {
|
customRender: ({ record }) => {
|
||||||
|
// 多选配送区域(cityIds 逗号分隔)
|
||||||
|
const cityIds = (record.cityIds || '')
|
||||||
|
.split(',')
|
||||||
|
.filter(Boolean);
|
||||||
|
if (cityIds.length) {
|
||||||
|
const names = cityIds.map(
|
||||||
|
(id: string) => regionMap.value[id] || id
|
||||||
|
);
|
||||||
|
const text = names.join('、');
|
||||||
|
// 过长截断并支持 tooltip 悬浮查看全部
|
||||||
|
if (text.length > 22) {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{ title: text },
|
||||||
|
text.slice(0, 22) + '…'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
// 兼容旧单值数据
|
||||||
const province = record.provinceId
|
const province = record.provinceId
|
||||||
? regionMap.value[String(record.provinceId)]
|
? regionMap.value[String(record.provinceId)]
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
Reference in New Issue
Block a user