| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679 |
- <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>
|