|
|
@@ -0,0 +1,679 @@
|
|
|
+<template>
|
|
|
+ <u-popup :show="show" mode="bottom" round="30rpx" closeable @close="close">
|
|
|
+ <view class="up-picker-area d-flex flex-cln">
|
|
|
+ <!-- 标题 -->
|
|
|
+ <view class="area-title">{{ title }}</view>
|
|
|
+ <!-- 顶部链路 -->
|
|
|
+ <view class="area-breadcrumb">
|
|
|
+ <!-- 全国根:selectCodeMax 为空或 000000 时显示 -->
|
|
|
+ <template v-if="isNationRoot">
|
|
|
+ <view class="crumb disabled" @click="onClickNationBreadcrumb">全国</view>
|
|
|
+ <view class="sep" v-if="selectedCodes.length">/</view>
|
|
|
+ </template>
|
|
|
+ <!-- 基础链路(selectCodeMax 的完整链路),仅最后一个可点击以切换到第一页(市级) -->
|
|
|
+ <template v-for="(code, idx) in baseChain" :key="'base-' + code">
|
|
|
+ <view class="crumb" :class="idx === baseChain.length - 1 ? '' : 'disabled'" @click="idx === baseChain.length - 1 && onClickBaseBreadcrumb()">
|
|
|
+ {{ getNameByCode(code) }}
|
|
|
+ </view>
|
|
|
+ <view class="sep" v-if="idx < baseChain.length - 1 || selectedCodes.length">/</view>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 选中链路 -->
|
|
|
+ <template v-for="(code, idx) in selectedCodes" :key="'sel-' + code">
|
|
|
+ <view class="crumb" :class="{ active: idx === currentSwiper }" @click="jumpToLevel(idx)">
|
|
|
+ {{ getNameByCode(code) }}
|
|
|
+ </view>
|
|
|
+ <view class="sep" v-if="idx < selectedCodes.length - 1">/</view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 滑动列区域 -->
|
|
|
+ <view class="area-body">
|
|
|
+ <swiper :current="currentSwiper" @change="onSwiperChange" class="area-swiper">
|
|
|
+ <template v-for="(parent, colIdx) in columnParents" :key="'col-' + parent">
|
|
|
+ <swiper-item>
|
|
|
+ <scroll-view
|
|
|
+ :id="`sv-${colIdx}`"
|
|
|
+ scroll-y
|
|
|
+ class="area-scroll"
|
|
|
+ :scroll-into-view="scrollIntoViewArr[colIdx]"
|
|
|
+ >
|
|
|
+ <!-- 顶部特殊项:全国(仅全国范围且首列显示) -->
|
|
|
+ <view v-if="isNationRoot && colIdx === 0" class="picker-item" :id="`area-item-0-000000`" @click="onPickNation">
|
|
|
+ <view class="name" :class="{ selected: currentValue === '000000' }">全国</view>
|
|
|
+ <up-icon v-if="currentValue === '000000'" name="checkbox-mark" color="#000" size="36rpx" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 顶部特殊项:选择范围自身(非全国,首列显示),如 云南省 -->
|
|
|
+ <view v-else-if="!isNationRoot && colIdx === 0" class="picker-item" :id="`area-item-0-${canonical6(baseRoot)}`" @click="onPickBaseRoot">
|
|
|
+ <view class="name" :class="{ selected: currentValue === baseRoot }">{{ getNameByCode(baseRoot) }}</view>
|
|
|
+ <up-icon v-if="currentValue === baseRoot" name="checkbox-mark" color="#000" size="36rpx" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 正常子级项 -->
|
|
|
+ <template v-for="item in childrenMap[parent] || []" :key="item.adcdCode">
|
|
|
+ <view class="picker-item" :id="`area-item-${colIdx}-${canonical6(item.adcdCode)}`" @click="onPick(item, colIdx)">
|
|
|
+ <view class="name" :class="{ selected: canonical6(selectedCodes[colIdx]) === canonical6(item.adcdCode) }">
|
|
|
+ {{ item.adcdName }}
|
|
|
+ </view>
|
|
|
+ <up-icon v-if="canonical6(selectedCodes[colIdx]) === canonical6(item.adcdCode)" name="checkbox-mark" color="#000" size="36rpx" />
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ </scroll-view>
|
|
|
+ </swiper-item>
|
|
|
+ </template>
|
|
|
+ </swiper>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部操作 -->
|
|
|
+ <view class="area-footer">
|
|
|
+ <up-button class="btn" @click="close">取消</up-button>
|
|
|
+ <up-button class="btn" color="#2A6D52" @click="confirmPick">确定</up-button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </u-popup>
|
|
|
+ <up-toast ref="uToastRef" />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, watch, nextTick, getCurrentInstance } from 'vue';
|
|
|
+import { useClientRequest } from '@/utils/request';
|
|
|
+
|
|
|
+type MaybeStringNumber = string | number;
|
|
|
+interface AreaItem {
|
|
|
+ adcdCode: string;
|
|
|
+ adcdName: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface ConfirmPayload {
|
|
|
+ value: string;
|
|
|
+ name: string;
|
|
|
+ fullNames: string;
|
|
|
+ fullName: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface UtPickerAreaProps {
|
|
|
+ modelValue?: MaybeStringNumber;
|
|
|
+ show?: boolean;
|
|
|
+ title?: string;
|
|
|
+ maxLevel?: number;
|
|
|
+ selectCodeMax?: string;
|
|
|
+ selectedCodes?: string[];
|
|
|
+}
|
|
|
+
|
|
|
+const props = withDefaults(defineProps<UtPickerAreaProps>(), {
|
|
|
+ modelValue: '',
|
|
|
+ show: false,
|
|
|
+ title: '选择区域',
|
|
|
+ maxLevel: 3,
|
|
|
+ selectCodeMax: '',
|
|
|
+ selectedCodes: () => [] as string[],
|
|
|
+});
|
|
|
+
|
|
|
+const emits = defineEmits<{
|
|
|
+ (e: 'update:modelValue', value: string): void;
|
|
|
+ (e: 'update:show', value: boolean): void;
|
|
|
+ (e: 'confirm', payload: ConfirmPayload): void;
|
|
|
+ (e: 'update:selectedCodes', value: string[]): void;
|
|
|
+}>();
|
|
|
+
|
|
|
+const uToastRef = ref<{ show: (opts: any) => void } | null>(null);
|
|
|
+
|
|
|
+const close = () => emits('update:show', false);
|
|
|
+
|
|
|
+// ---------------- 工具与状态 ----------------
|
|
|
+// 统一为字符串,不再强制 6 位;全国仍使用 '000000'
|
|
|
+const normalize6 = (code: MaybeStringNumber): string => String(code ?? '');
|
|
|
+// 修复:全国返回空串用于层级与前缀判断,子级都视为全国的后代
|
|
|
+const trim00Pairs = (code: MaybeStringNumber): string => {
|
|
|
+ const s = normalize6(code);
|
|
|
+ if (s === '000000') return '';
|
|
|
+ if (s.length <= 6) {
|
|
|
+ let t = s;
|
|
|
+ while (t.endsWith('00')) t = t.slice(0, -2);
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+ // 6 位之后不做去 00 处理,直接保留扩展位(如 9/12 位)
|
|
|
+ let first6 = s.slice(0, 6);
|
|
|
+ while (first6.endsWith('00')) first6 = first6.slice(0, -2);
|
|
|
+ return first6 + s.slice(6);
|
|
|
+};
|
|
|
+// 层级:0 全国;<=6 位时按 2 位一层;>6 位时 9/12 位按 3 位一层
|
|
|
+const codeLevel = (code: MaybeStringNumber): number => {
|
|
|
+ const t = trim00Pairs(code);
|
|
|
+ if (!t) return 0;
|
|
|
+ if (t.length <= 6) return Math.floor(t.length / 2);
|
|
|
+ return 3 + Math.ceil((t.length - 6) / 3);
|
|
|
+};
|
|
|
+const isDescendant = (code: MaybeStringNumber, root: MaybeStringNumber): boolean => {
|
|
|
+ const p = trim00Pairs(root);
|
|
|
+ const c = trim00Pairs(code);
|
|
|
+ return c.startsWith(p); // p=='' 时,任何 c 都成立(全国范围)
|
|
|
+};
|
|
|
+
|
|
|
+// 生成用于 nameMap 的最小 6 位标准键:
|
|
|
+// - 全国固定 '000000'
|
|
|
+// - <=6 位:去掉末尾 00 对后右补齐到 6 位
|
|
|
+// - >6 位:取前 6 位,再按上述规则去 00 后补齐到 6 位
|
|
|
+const nameKey = (code: MaybeStringNumber): string => {
|
|
|
+ const s = String(code ?? '');
|
|
|
+ if (!s || s === '000000') return '000000';
|
|
|
+ const base6 = s.length <= 6 ? s : s.slice(0, 6);
|
|
|
+ let t = base6;
|
|
|
+ while (t.endsWith('00') && t.length >= 2) t = t.slice(0, -2);
|
|
|
+ return t.padEnd(6, '0');
|
|
|
+};
|
|
|
+// 规范化用于比较和锚点:
|
|
|
+// - 全国返回 '000000'
|
|
|
+// - <=6 位:去尾部 00 对后右补齐至 6 位
|
|
|
+// - >6 位:保持原始完整码
|
|
|
+const canonical6 = (code: MaybeStringNumber): string => {
|
|
|
+ const s = String(code ?? '');
|
|
|
+ if (!s) return '';
|
|
|
+ if (s === '000000') return '000000';
|
|
|
+ if (s.length <= 6) {
|
|
|
+ let t = s;
|
|
|
+ while (t.endsWith('00')) t = t.slice(0, -2);
|
|
|
+ return t.padEnd(6, '0');
|
|
|
+ }
|
|
|
+ return s;
|
|
|
+};
|
|
|
+const chainFromRoot = (code: MaybeStringNumber): string[] => {
|
|
|
+ const s = normalize6(code);
|
|
|
+ if (!s || s === '000000') return [];
|
|
|
+ const list: string[] = [];
|
|
|
+ // 省/市/区:统一输出为“最少6位”,少于补0,多余去掉末尾00(canonical6 内处理)
|
|
|
+ for (let i = 2; i <= Math.min(6, s.length); i += 2) {
|
|
|
+ list.push(canonical6(s.slice(0, i)));
|
|
|
+ }
|
|
|
+ // 扩展层级:9/12 位,保持真实长度;其 6 位基底已按 canonical6 规范化
|
|
|
+ if (s.length > 6) {
|
|
|
+ const base6 = canonical6(s).slice(0, 6);
|
|
|
+ for (let j = 9; j <= s.length; j += 3) {
|
|
|
+ list.push(base6 + s.slice(6, j));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return list;
|
|
|
+};
|
|
|
+
|
|
|
+// 根(选择范围)与基础链路
|
|
|
+const baseRoot = computed(() => {
|
|
|
+ const c = normalize6(props.selectCodeMax || '');
|
|
|
+ // 空值或显式全国,统一为 '000000'
|
|
|
+ if (!c || c === '000000') return '000000';
|
|
|
+ return trim00Pairs(c);
|
|
|
+});
|
|
|
+const baseChain = computed(() => {
|
|
|
+ // 完整显示 selectCodeMax 的链路(不包含全国 '000000')
|
|
|
+ return chainFromRoot(baseRoot.value);
|
|
|
+});
|
|
|
+const isNationRoot = computed(() => baseRoot.value === '000000');
|
|
|
+
|
|
|
+// 缓存:父code -> 子数组
|
|
|
+const childrenMap = ref<Record<string, AreaItem[] | undefined>>({});
|
|
|
+// 新增:进行中请求去重缓存(父code -> Promise)
|
|
|
+const pendingMap = ref<Record<string, Promise<AreaItem[]> | undefined>>({});
|
|
|
+// 名称缓存:code -> name
|
|
|
+const nameMap = ref<Record<string, string>>({ '000000': '全国' });
|
|
|
+
|
|
|
+// 选中链路(不包含 baseRoot),例如 baseRoot=省,选中市/区... 这里存 [市, 区, ...]
|
|
|
+const selectedCodes = ref<string[]>(props?.selectedCodes || []);
|
|
|
+const currentValue = ref<string>(''); // 当前选中最终 code
|
|
|
+
|
|
|
+// 计算列:每列的父节点仅为“下一列的父节点”,避免最后一级再多出一列
|
|
|
+const columnParents = computed<string[]>(() => {
|
|
|
+ const parents: string[] = [];
|
|
|
+ const baseL = codeLevel(baseRoot.value); // baseRoot 的绝对层级(全国=0,省=1,市=2...)
|
|
|
+ const maxCols = Math.max(0, props.maxLevel - baseL); // 允许渲染的最大列数(例如全国根且 maxLevel=3 -> 3 列:省/市/区)
|
|
|
+ if (maxCols <= 0) return parents;
|
|
|
+
|
|
|
+ // 第 0 列父节点固定为 baseRoot(展示 baseRoot 的子级)
|
|
|
+ parents.push(baseRoot.value);
|
|
|
+
|
|
|
+ // 后续列的父节点是“上一列选中的 code”,最多渲染到 maxCols 数量;没有选中则不再渲染,避免空列
|
|
|
+ for (let k = 1; k < maxCols; k++) {
|
|
|
+ const parentCode = selectedCodes.value[k - 1];
|
|
|
+ if (!parentCode) break; // 未选择上一列时,不渲染后续空列
|
|
|
+ parents.push(parentCode);
|
|
|
+ }
|
|
|
+ return parents;
|
|
|
+});
|
|
|
+
|
|
|
+// 当前 swiper 索引(指向当前操作的列)
|
|
|
+const currentSwiper = ref<number>(0);
|
|
|
+
|
|
|
+// ---------------- 异步数据获取(带缓存 + 去重) ----------------
|
|
|
+const getChildren = async (parentCode: string): Promise<AreaItem[]> => {
|
|
|
+ const key = normalize6(parentCode);
|
|
|
+ // 命中结果缓存
|
|
|
+ if (childrenMap.value[key]) return childrenMap.value[key] as AreaItem[];
|
|
|
+ // 命中请求去重缓存
|
|
|
+ if (pendingMap.value[key]) return pendingMap.value[key] as Promise<AreaItem[]>;
|
|
|
+
|
|
|
+ const p = (async () => {
|
|
|
+ const res: any = await useClientRequest.get('/app/adcd/listChildrenByCode', {
|
|
|
+ adcdCode: key,
|
|
|
+ leval: 1,
|
|
|
+ pageSize: 1000,
|
|
|
+ });
|
|
|
+ if (res?.code === 200) {
|
|
|
+ const rows: AreaItem[] = (res.rows || []).map((x: any) => ({
|
|
|
+ // 不再截断为 6 位,保留完整 code
|
|
|
+ adcdCode: nameKey(x.adcdCode),
|
|
|
+ adcdName: x.adcdName,
|
|
|
+ }));
|
|
|
+ childrenMap.value[key] = rows; // 写入结果缓存
|
|
|
+ rows.forEach((r) => {
|
|
|
+ // 名称缓存:精确键必存;仅当长度<=6时,才缓存到最少6位标准键,避免覆盖区县名称
|
|
|
+ nameMap.value[r.adcdCode] = r.adcdName;
|
|
|
+ nameMap.value[nameKey(r.adcdCode)] = r.adcdName;
|
|
|
+ });
|
|
|
+ delete pendingMap.value[key]; // 清除进行中缓存
|
|
|
+ return rows;
|
|
|
+ }
|
|
|
+ childrenMap.value[key] = [];
|
|
|
+ delete pendingMap.value[key];
|
|
|
+ return [];
|
|
|
+ })();
|
|
|
+
|
|
|
+ pendingMap.value[key] = p;
|
|
|
+ return p;
|
|
|
+};
|
|
|
+
|
|
|
+// 确保能显示 baseChain 的名称:逐级拉取上级的子级来命名
|
|
|
+const ensureBaseNames = async () => {
|
|
|
+ // 若 baseChain 为空无需处理
|
|
|
+ if (!baseChain.value.length) return;
|
|
|
+ // 第一层名称来自全国的子级
|
|
|
+ await getChildren('000000');
|
|
|
+ // 逐级往下,确保每一层的 name 显示
|
|
|
+ for (let i = 0; i < baseChain.value.length; i++) {
|
|
|
+ const parent = i === 0 ? '000000' : baseChain.value[i - 1];
|
|
|
+ await getChildren(parent);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取名称
|
|
|
+const getNameByCode = (code: string | number): string => {
|
|
|
+ const c = normalize6(code);
|
|
|
+ if (c === '000000') return '全国';
|
|
|
+ // 先查精确键
|
|
|
+ let val = nameMap.value[c];
|
|
|
+ if (val) return val;
|
|
|
+ // 对长度<=6的code再查最少6位标准键,避免用>6位的名称覆盖6位层级
|
|
|
+ if (c.length <= 6) {
|
|
|
+ const k6 = nameKey(c);
|
|
|
+ val = nameMap.value[k6];
|
|
|
+ if (val) return val;
|
|
|
+ }
|
|
|
+ // 即时回退:在已加载的 childrenMap 中查找匹配项并缓存
|
|
|
+ for (const rows of Object.values(childrenMap.value)) {
|
|
|
+ if (!rows) continue;
|
|
|
+ const found = rows.find((r) => r.adcdCode === c);
|
|
|
+ if (found) {
|
|
|
+ nameMap.value[c] = found.adcdName;
|
|
|
+ if (String(found.adcdCode).length <= 6) {
|
|
|
+ nameMap.value[nameKey(found.adcdCode)] = found.adcdName;
|
|
|
+ }
|
|
|
+ return found.adcdName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+};
|
|
|
+
|
|
|
+// ---------------- 回显与列构造 ----------------
|
|
|
+const toast = (msg: string) => {
|
|
|
+ uToastRef.value?.show({ message: msg, type: 'error' });
|
|
|
+};
|
|
|
+
|
|
|
+// 将传入值回显为 selectedCodes(不含 baseRoot)
|
|
|
+const applyModelValue = async (val: MaybeStringNumber): Promise<void> => {
|
|
|
+ const v = normalize6(val);
|
|
|
+ // 确保基础链路名称已就绪,避免回显时中文缺失
|
|
|
+ await ensureBaseNames();
|
|
|
+ if (!v) {
|
|
|
+ selectedCodes.value = [];
|
|
|
+ currentValue.value = '';
|
|
|
+ currentSwiper.value = Math.min(columnParents.value.length - 1, 0);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (v === '000000') {
|
|
|
+ selectedCodes.value = [];
|
|
|
+ currentValue.value = '000000';
|
|
|
+ await ensureColumnsData();
|
|
|
+ currentSwiper.value = 0;
|
|
|
+ scrollToAllSelected();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 新增:等于 baseRoot 自身时
|
|
|
+ if (v === baseRoot.value) {
|
|
|
+ selectedCodes.value = [];
|
|
|
+ currentValue.value = baseRoot.value;
|
|
|
+ await ensureColumnsData();
|
|
|
+ currentSwiper.value = 0;
|
|
|
+ scrollToAllSelected();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 必须在选择范围内
|
|
|
+ if (!isDescendant(v, baseRoot.value)) {
|
|
|
+ toast('不在选择范围内');
|
|
|
+ selectedCodes.value = [];
|
|
|
+ currentValue.value = '';
|
|
|
+ currentSwiper.value = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const full = chainFromRoot(v);
|
|
|
+ const base = baseChain.value;
|
|
|
+ const baseL = codeLevel(baseRoot.value);
|
|
|
+ const maxCols = Math.max(0, props.maxLevel - baseL);
|
|
|
+ const picked = full.slice(base.length, base.length + maxCols);
|
|
|
+
|
|
|
+
|
|
|
+ selectedCodes.value = picked;
|
|
|
+ currentValue.value = picked.length ? picked[picked.length - 1] : baseRoot.value;
|
|
|
+ await ensureColumnsData();
|
|
|
+ currentSwiper.value = Math.max(0, Math.min(selectedCodes.value.length - 1, columnParents.value.length - 1));
|
|
|
+ scrollToAllSelected();
|
|
|
+};
|
|
|
+
|
|
|
+// 确保每一列的 children 数据已拉取
|
|
|
+const ensureColumnsData = async (): Promise<void> => {
|
|
|
+ for (const parent of columnParents.value) {
|
|
|
+ await getChildren(parent);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// ---------------- 交互 ----------------
|
|
|
+const onSwiperChange = (e: any) => {
|
|
|
+ const idx = e?.detail?.current ?? 0;
|
|
|
+ const maxIdx = Math.max(0, columnParents.value.length - 1);
|
|
|
+ currentSwiper.value = Math.min(idx, maxIdx);
|
|
|
+};
|
|
|
+
|
|
|
+// 跳到链路的某一级进行重新选择
|
|
|
+const jumpToLevel = (idx: number) => {
|
|
|
+ // 只能跳选中链路,且 idx 为选中链路下标(不包含 baseChain)
|
|
|
+ currentSwiper.value = idx;
|
|
|
+};
|
|
|
+
|
|
|
+// 选择某列的一项(到达最后一级时自动确认)
|
|
|
+const onPick = async (item: AreaItem, colIdx: number) => {
|
|
|
+ const code = canonical6(item.adcdCode);
|
|
|
+ if (!isDescendant(code, baseRoot.value)) {
|
|
|
+ return toast('不在选择范围内');
|
|
|
+ }
|
|
|
+ // 特判:误点到全国(正常不会走到这,因为全国走 onPickNation)
|
|
|
+ if (code === '000000' && isNationRoot.value && colIdx === 0) {
|
|
|
+ return onPickNation();
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedCodes.value = [...selectedCodes.value.slice(0, colIdx), code];
|
|
|
+ currentValue.value = code;
|
|
|
+
|
|
|
+ // 是否达到允许的最后一列(相对 baseRoot 的列数)
|
|
|
+ const baseL = codeLevel(baseRoot.value);
|
|
|
+ const maxCols = Math.max(0, props.maxLevel - baseL);
|
|
|
+ const reachedFinalColumn = (colIdx + 1) >= maxCols;
|
|
|
+ if (reachedFinalColumn) {
|
|
|
+ scrollToAllSelected();
|
|
|
+ confirmPick();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await getChildren(code);
|
|
|
+ const nextChildren = childrenMap.value[code] || [];
|
|
|
+ currentSwiper.value = nextChildren.length ? colIdx + 1 : colIdx;
|
|
|
+ scrollToAllSelected();
|
|
|
+};
|
|
|
+
|
|
|
+// 选全国
|
|
|
+const onPickNation = (): void => {
|
|
|
+ if (!isNationRoot.value) return;
|
|
|
+ selectedCodes.value = [];
|
|
|
+ currentValue.value = '000000';
|
|
|
+ currentSwiper.value = 0;
|
|
|
+ scrollToAllSelected();
|
|
|
+};
|
|
|
+
|
|
|
+// 选“选择范围自身”(非全国时的顶部项,如:云南省)
|
|
|
+const onPickBaseRoot = async (): Promise<void> => {
|
|
|
+ if (isNationRoot.value) return; // 全国时不走这里
|
|
|
+ // 将当前值选为 baseRoot,自身级别
|
|
|
+ selectedCodes.value = [];
|
|
|
+ currentValue.value = baseRoot.value;
|
|
|
+ currentSwiper.value = 0;
|
|
|
+
|
|
|
+ // 若允许的列数仅 1(选择范围本身即最终列),直接确认
|
|
|
+ const baseL = codeLevel(baseRoot.value);
|
|
|
+ const maxCols = Math.max(0, props.maxLevel - baseL);
|
|
|
+ if (maxCols <= 1) {
|
|
|
+ scrollToAllSelected();
|
|
|
+ confirmPick();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 否则保持在第1列,滚动到该项
|
|
|
+ await ensureColumnsData();
|
|
|
+ scrollToAllSelected();
|
|
|
+};
|
|
|
+
|
|
|
+// 组装完整名称:包含基础链路与已选链路(全国仅返回“全国”)
|
|
|
+const buildFullName = (): string => {
|
|
|
+ if (currentValue.value === '000000') return '全国';
|
|
|
+ const baseNames = baseChain.value.map((code) => getNameByCode(code));
|
|
|
+ // 若当前值是 baseRoot 自身(选择范围自身),则不重复追加 selectedCodes
|
|
|
+ const selNames = selectedCodes.value.map((code) => getNameByCode(code));
|
|
|
+ return [...baseNames, ...selNames].filter(Boolean).join('');
|
|
|
+};
|
|
|
+
|
|
|
+const confirmPick = (): void => {
|
|
|
+ if (!currentValue.value && isNationRoot.value) currentValue.value = '000000';
|
|
|
+
|
|
|
+ const codeList = selectedCodes.value.slice();
|
|
|
+ const nameList = codeList.map((i) => getNameByCode(i));
|
|
|
+ const fullName = buildFullName();
|
|
|
+
|
|
|
+ emits('update:modelValue', currentValue.value);
|
|
|
+ emits('confirm', {
|
|
|
+ value: currentValue.value,
|
|
|
+ name: getNameByCode(currentValue.value),
|
|
|
+ fullNames: nameList.join(''),
|
|
|
+ fullName // 新增:完整地址名称
|
|
|
+ });
|
|
|
+ emits('update:selectedCodes', codeList);
|
|
|
+ close();
|
|
|
+};
|
|
|
+
|
|
|
+// ---------------- 滚动(H5 兼容)----------------
|
|
|
+// 选中项锚点
|
|
|
+const scrollIntoViewArr = ref<string[]>([]);
|
|
|
+const instance = getCurrentInstance();
|
|
|
+const proxy = instance && instance.proxy;
|
|
|
+
|
|
|
+const refreshScrollArrays = (): void => {
|
|
|
+ const len = columnParents.value.length;
|
|
|
+ // 初始化长度匹配列数
|
|
|
+ if (scrollIntoViewArr.value.length !== len) {
|
|
|
+ scrollIntoViewArr.value = Array(len).fill('');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 仅用锚点滚动
|
|
|
+const updateScrollIntoView = (): void => {
|
|
|
+ refreshScrollArrays();
|
|
|
+ scrollIntoViewArr.value = scrollIntoViewArr.value.map(() => '');
|
|
|
+ nextTick(() => {
|
|
|
+ scrollIntoViewArr.value = columnParents.value.map((_, i) => {
|
|
|
+ if (i === 0 && isNationRoot.value && currentValue.value === '000000') return 'area-item-0-000000';
|
|
|
+ if (i === 0 && !isNationRoot.value && currentValue.value === baseRoot.value) return `area-item-0-${canonical6(baseRoot.value)}`;
|
|
|
+ const code = selectedCodes.value[i];
|
|
|
+ return code ? `area-item-${i}-${canonical6(code)}` : '';
|
|
|
+ });
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const scrollToAllSelected = (): void => {
|
|
|
+ updateScrollIntoView();
|
|
|
+};
|
|
|
+
|
|
|
+// ---------------- 监听与初始化 ----------------
|
|
|
+const rebuildByInputs = async (): Promise<void> => {
|
|
|
+ await ensureBaseNames();
|
|
|
+ await ensureColumnsData();
|
|
|
+ await applyModelValue(String(props.modelValue || ''));
|
|
|
+};
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => [props.selectCodeMax, props.maxLevel],
|
|
|
+ async () => {
|
|
|
+ // 变更范围或层级,重建链路与列
|
|
|
+ selectedCodes.value = [];
|
|
|
+ currentValue.value = '';
|
|
|
+ currentSwiper.value = 0;
|
|
|
+ await rebuildByInputs();
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.modelValue,
|
|
|
+ async (nv) => {
|
|
|
+ await applyModelValue(String(nv || ''));
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+// 每次列父集合变化时,刷新列数据并滚动
|
|
|
+watch(
|
|
|
+ () => columnParents.value.slice(),
|
|
|
+ async () => {
|
|
|
+ await ensureColumnsData();
|
|
|
+ // 定位到当前列并滚动选中项
|
|
|
+ currentSwiper.value = Math.max(0, Math.min(currentSwiper.value, columnParents.value.length - 1));
|
|
|
+ scrollToAllSelected();
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+// 点击顶部“全国”:切到省级并滚动到省级区域
|
|
|
+const onClickNationBreadcrumb = async (): Promise<void> => {
|
|
|
+ if (!isNationRoot.value) return;
|
|
|
+ // 确保省级数据已加载
|
|
|
+ await getChildren('000000');
|
|
|
+
|
|
|
+ // 切到第 0 列(省级)
|
|
|
+ currentSwiper.value = 0;
|
|
|
+
|
|
|
+ // 保证滚动数组长度正确
|
|
|
+ refreshScrollArrays();
|
|
|
+
|
|
|
+ // 有已选省则对齐该省;否则滚到顶部
|
|
|
+ nextTick(() => {
|
|
|
+ const first = selectedCodes.value[0];
|
|
|
+ scrollIntoViewArr.value.splice(0, 1, first ? `area-item-0-${canonical6(first)}` : '');
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 点击顶部“选择范围自身”(如云南省):切到第0列(市级)并滚动到已选市;未选则回到顶部
|
|
|
+const onClickBaseBreadcrumb = async (): Promise<void> => {
|
|
|
+ if (isNationRoot.value) return; // 全国时不走这里
|
|
|
+ await getChildren(baseRoot.value); // 确保市级数据已加载
|
|
|
+ currentSwiper.value = 0; // 切到第0列(展示 baseRoot 的子级:市)
|
|
|
+ refreshScrollArrays();
|
|
|
+ nextTick(() => {
|
|
|
+ const selCity = selectedCodes.value[0]; // 若已有选中的市
|
|
|
+ scrollIntoViewArr.value.splice(0, 1, selCity ? `area-item-0-${canonical6(selCity)}` : '');
|
|
|
+ });
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.up-picker-area {
|
|
|
+ height: 70vh; /* 高度 70vh */
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.area-title {
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ text-align: center;
|
|
|
+ color: #000;
|
|
|
+ border-bottom: 1px solid #f2f2f2;
|
|
|
+}
|
|
|
+
|
|
|
+.area-breadcrumb {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ padding: 16rpx 24rpx;
|
|
|
+ gap: 8rpx;
|
|
|
+ border-bottom: 1px solid #f2f2f2;
|
|
|
+
|
|
|
+ .crumb {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+ padding: 8rpx 12rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ font-weight: 700;
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.disabled {
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .sep {
|
|
|
+ color: #bbb;
|
|
|
+ padding: 0 8rpx;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.area-body {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.area-swiper {
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.area-scroll {
|
|
|
+ height: 100%;
|
|
|
+ /* 移除 will-change 与像素滚动相关样式,仅保留稳定布局 */
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+.picker-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 24rpx 32rpx;
|
|
|
+ border-bottom: 1px solid #f5f5f5;
|
|
|
+
|
|
|
+ .name {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #333;
|
|
|
+
|
|
|
+ &.selected {
|
|
|
+ font-weight: 700;
|
|
|
+ color: #000; /* 选中项黑色加粗 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.area-footer {
|
|
|
+ display: flex;
|
|
|
+ gap: 20rpx;
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ border-top: 1px solid #f2f2f2;
|
|
|
+ background: #fff;
|
|
|
+
|
|
|
+ .btn {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|