添加行政区选择

This commit is contained in:
张成
2025-11-22 22:50:18 +08:00
parent 6eefcd27a9
commit 17015c0cca
11 changed files with 708 additions and 49 deletions

View File

@@ -81,7 +81,7 @@
// 标签样式
.bubbleLabel {
font-weight: 500;
font-weight: 600;
font-size: 12px !important;
}

View File

@@ -0,0 +1,265 @@
.distanceQuickFilterWrap {
width: 100%;
font-family: "PingFang SC";
position: relative;
// 全局覆盖 NutUI Menu 容器的 overflow 样式
.nut-menu-container-wrap {
position: fixed !important;
left: 0 !important;
right: 0 !important;
width: auto !important;
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
background-color: #fafafa !important;
z-index: 1100 !important;
box-sizing: border-box !important;
max-height: auto !important;
height: 380px !important;
}
.nut-menu-container-content {
overflow: visible !important;
padding-left: 0px !important;
padding-right: 0px !important;
padding-bottom: 0px !important;
background-color: transparent !important;
max-height: auto !important;
}
.nut-menu-bar {
--nutui-menu-bar-line-height: 30px;
background-color: #fafafa !important; // 明确设置背景色,避免组件默认样式覆盖
box-shadow: unset;
// padding: 0 15px;
gap: 5px;
justify-content: flex-start;
}
.nut-menu-title {
flex: unset;
box-sizing: border-box;
display: flex;
height: 28px;
padding: 4px 10px;
justify-content: center;
align-items: center;
gap: 2px;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.06); // 根据设计稿添加边框
background: #ffffff;
font-family: "PingFang SC"; // 根据设计稿设置字体
font-size: 14px;
font-weight: 600;
line-height: 20px; // 1.4285714285714286em ≈ 20px
letter-spacing: -0.23px; // -1.6428571726594652% of 14px
}
.nut-menu-title.active {
color: #000;
}
.positionWrap {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 16px;
padding-left: 20px ;
padding-right: 20px ;
}
.title {
font-size: 14px;
font-weight: 600;
}
.cityName {
font-size: 13px;
font-weight: 400;
color: #3c3c43;
}
.distanceWrap {
margin-bottom: 16px;
width: 100%;
padding-left: 20px;
padding-right: 20px;
}
.distanceBubbleItem {
display: flex;
width: 80px;
height: 28px;
padding: 4px 10px;
justify-content: center;
align-items: center;
gap: 2px;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
font-weight: 600;
}
.itemIcon {
width: 20px;
height: 20px;
}
// 新增:行政区选择区域样式
.districtWrap2 {
width: 100%;
margin-top: 0;
padding-top: 0;
display: flex;
flex-direction: column;
.districtContent {
display: flex;
align-items: stretch; // 让子元素高度一致
flex-direction: row;
width: 100%;
position: relative;
padding: 0;
gap: 0;
flex: 1; // 占满父容器剩余空间
overflow: hidden; // 确保子元素不会超出圆角
.districtTitleBox {
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: flex-start;
position: relative;
height: 100%; // 占满父容器高度
width: 82px;
.districtTitle {
flex-shrink: 0;
padding: 10px 0 10px 20px; // 左侧标题的 padding
z-index: 1;
font-family: "PingFang SC";
font-size: 14px;
font-weight: 600;
line-height: 1.4285714285714286em; // 20px
letter-spacing: -0.23px;
color: #000;
margin: 0;
white-space: nowrap;
position: relative;
}
.districtTitleBg {
width: 100%; // 自适应宽度
height: 100%; // 自适应高度
background-color: #f2f2f7; // 灰色背景
border-top-right-radius: 20px; // 右上角圆角
z-index: 0; // 在文字下方
}
}
.districtOptionsWrapper {
flex: 1;
max-height: 80vh;
overflow-x: hidden;
overflow-y: auto;
height: 290px;
-webkit-overflow-scrolling: touch;
.districtOptionsList {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0;
padding: 0;
.districtItem {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px 20px 8px 20px;
height: 38px;
color: rgba(60, 60, 67, 0.6);
font-family: "PingFang SC";
font-size: 14px;
font-weight: 600;
line-height: 1.2857142857142858em; // 18px
letter-spacing: -0.2px;
box-sizing: border-box;
cursor: pointer;
&:first-child {
padding-top: 10px;
}
&:not(:first-child) {
padding-top: 8px;
padding-bottom: 8px;
}
.districtItemText {
flex: 1;
}
&.active {
color: #000;
font-weight: 600;
}
}
}
}
}
}
.quickOptionsWrapper {
width: 100%;
display: flex;
flex-direction: column;
padding-left: 20px;
padding-right: 20px;
.quickItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
color: rgba(60, 60, 67, 0.6);
font-size: 14px;
font-weight: 600;
line-height: 20px;
&.active {
color: #000;
}
}
}
}
.distanceQuickFilterWrap_0 .nut-menu-title-0 {
background-color: #000;
color: #fff;
&.active {
color: #ffffff;
}
}
.distanceQuickFilterWrap_1 .nut-menu-title-1 {
background-color: #000;
color: #fff;
&.active {
color: #ffffff;
}
}

View File

@@ -0,0 +1,176 @@
import { useRef, useState, useEffect } from "react";
import { Menu } from "@nutui/nutui-react-taro";
import { Image, View, ScrollView } from "@tarojs/components";
import img from "@/config/images";
import Bubble from "../Bubble";
import "./index.scss";
const DistanceQuickFilterV2 = (props) => {
const {
cityOptions,
quickOptions,
districtOptions = [], // 新增:行政区选项
onChange,
cityName,
quickName,
districtName = "district", // 新增:行政区字段名
cityValue,
quickValue,
districtValue, // 新增:行政区选中值
onMenuVisibleChange, // 菜单展开/收起回调
} = props;
const cityRef = useRef(null);
const quickRef = useRef(null);
const [changePosition, setChangePosition] = useState<number[]>([]);
const [isMenuOpen, setIsMenuOpen] = useState(false);
// 全城筛选显示的标题 - 如果选择了行政区,显示行政区名称
const getCityTitle = () => {
if (districtValue) {
const selectedDistrict = districtOptions.find((item) => item.value === districtValue);
if (selectedDistrict) {
return selectedDistrict.label;
}
}
return cityOptions.find((item) => item.value === cityValue)?.label;
};
const cityTitle = getCityTitle();
// 快捷筛选显示的标题
const quickTitle = quickOptions.find(
(item) => item.value === quickValue
)?.label;
// className
const filterWrapperClassName = changePosition.reduce((pre, cur) => {
return `${pre} distanceQuickFilterWrap_${cur}`;
}, "");
// 处理选择变化
const handleChange = (
name: string,
value: string | number,
index: number
) => {
setChangePosition((preState) => {
const newData = new Set([...preState, index]);
return Array.from(newData);
});
onChange && onChange(name, value);
// 控制隐藏
index === 0 && (cityRef.current as any)?.toggle(false);
index === 1 && (quickRef.current as any)?.toggle(false);
};
// 监听菜单状态变化,通知父组件
useEffect(() => {
onMenuVisibleChange?.(isMenuOpen);
}, [isMenuOpen, onMenuVisibleChange]);
return (
<View>
<Menu
className={`distanceQuickFilterWrap ${filterWrapperClassName}`}
onOpen={() => setIsMenuOpen(true)}
onClose={() => setIsMenuOpen(false)}
>
<Menu.Item
title={cityTitle}
ref={cityRef}
icon={<Image src={img.ICON_MENU_ITEM_SELECTED} />}
>
<div className="positionWrap">
<p className="title"></p>
<p className="cityName"></p>
</div>
<div className="distanceWrap">
<Bubble
options={cityOptions}
value={cityValue}
onChange={(name, value) => {
const singleValue = Array.isArray(value) ? value[0] : value;
handleChange(name, singleValue, 0);
}}
layout="grid"
size="small"
columns={4}
itemClassName="distanceBubbleItem"
name={cityName}
/>
</div>
{/* 新增:行政区选择区域 */}
{districtOptions.length > 0 && (
<div className="districtWrap2">
<div className="districtContent">
<div className="districtTitleBox">
<p className="districtTitle"></p>
<div className="districtTitleBg"></div>
</div>
<ScrollView
className="districtOptionsWrapper"
scrollY
enhanced
showScrollbar={true}
>
<View className="districtOptionsList">
{districtOptions.map((item) => {
const active = districtValue === item?.value;
return (
<View
key={item.value}
className={`districtItem ${active && "active"}`}
onClick={() => handleChange(districtName, item.value, 0)}
>
<View className="districtItemText">{item?.label}</View>
{active && (
<View>
<Image
className="itemIcon"
src={img.ICON_MENU_ITEM_SELECTED}
/>
</View>
)}
</View>
);
})}
</View>
</ScrollView>
</div>
</div>
)}
</Menu.Item>
<Menu.Item
title={quickTitle}
ref={quickRef}
defaultValue={quickValue}
>
<View className="quickOptionsWrapper">
{quickOptions.map((item) => {
const active = quickValue === item?.value;
return (
<View
key={item.value}
className={`quickItem ${active && "active"}`}
onClick={() => handleChange(quickName, item.value, 1)}
>
<View>{item?.label}</View>
{active && (
<View>
<Image
className="itemIcon"
src={img.ICON_MENU_ITEM_SELECTED}
/>
</View>
)}
</View>
);
})}
</View>
</Menu.Item>
</Menu>
</View>
);
};
export default DistanceQuickFilterV2;

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import { View, Text, Image } from "@tarojs/components";
import img from "@/config/images";
import { useGlobalState } from "@/store/global";
@@ -8,7 +8,10 @@ import { Input } from "@nutui/nutui-react-taro";
import Taro from "@tarojs/taro";
import "./index.scss";
import { getCurrentFullPath } from "@/utils";
import { CityPicker as PopupPicker } from "@/components/Picker";
import { CityPickerV2 as PopupPicker } from "@/components/Picker";
// 城市缓存 key
const CITY_CACHE_KEY = "USER_SELECTED_CITY";
interface IProps {
config?: {
@@ -81,6 +84,7 @@ const HomeNavbar = (props: IProps) => {
statusNavbarHeightInfo || {};
const [cityPopupVisible, setCityPopupVisible] = useState(false);
const hasShownLocationDialog = useRef(false); // 防止重复弹窗
// 监听城市选择器状态变化,通知父组件
useEffect(() => {
@@ -88,13 +92,59 @@ const HomeNavbar = (props: IProps) => {
}, [cityPopupVisible]);
const userInfo = useUserInfo();
const province = (userInfo as any)?.province || "";
const city = (userInfo as any)?.city || "";
// const district = (userInfo as any)?.district || "";
const locationProvince = (userInfo as any)?.province || ""; // 定位获取的省份
// 只使用省份不使用区域city、district
// 初始化城市:优先使用缓存的定位信息,其次使用当前定位
useEffect(() => {
updateArea(["中国", province, city]);
}, [province, city]);
// 1. 尝试从缓存中读取上次的定位信息
const cachedCity = (Taro as any).getStorageSync(CITY_CACHE_KEY);
if (cachedCity && cachedCity.length === 2) {
// 如果有缓存的定位信息,使用缓存
console.log("使用缓存的定位城市:", cachedCity);
updateArea(cachedCity);
// 如果当前定位省份已获取且与缓存不同,弹窗询问是否切换
if (locationProvince && cachedCity[1] !== locationProvince && !hasShownLocationDialog.current) {
hasShownLocationDialog.current = true;
showLocationConfirmDialog(locationProvince, cachedCity);
}
} else if (locationProvince) {
// 如果没有缓存但有定位信息,直接使用定位并保存到缓存
console.log("没有缓存,使用当前定位省份:", locationProvince);
const newArea: [string, string] = ["中国", locationProvince];
updateArea(newArea);
// 保存定位信息到缓存
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
}
}, [locationProvince]);
// 显示定位确认弹窗
const showLocationConfirmDialog = (detectedProvince: string, cachedCity: [string, string]) => {
(Taro as any).showModal({
title: "提示",
content: `检测到您当前位置在${detectedProvince},是否切换到${detectedProvince}`,
confirmText: "是",
cancelText: "否",
success: (res) => {
if (res.confirm) {
// 用户选择"是",切换到当前定位城市
const newArea: [string, string] = ["中国", detectedProvince];
updateArea(newArea);
// 更新缓存为新的定位信息(只有定位的才缓存)
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
console.log("切换到当前定位城市并更新缓存:", detectedProvince);
// 刷新数据
handleCityChangeWithoutCache();
} else {
// 用户选择"否",保持缓存的定位城市
console.log("保持缓存的定位城市:", cachedCity[1]);
}
}
});
};
// const currentAddress = city + district;
@@ -123,6 +173,7 @@ const HomeNavbar = (props: IProps) => {
duration: 300,
});
}
return; // 已经在列表页,只滚动到顶部,不需要跳转
}
(Taro as any).redirectTo({
url: "/main_pages/index", // 列表页
@@ -147,8 +198,8 @@ const HomeNavbar = (props: IProps) => {
const area_city = area.at(-1);
// 处理城市切换
const handleCityChange = async (_newArea: any) => {
// 处理城市切换(仅刷新数据,不保存缓存)
const handleCityChangeWithoutCache = async () => {
// 切换城市后,同时更新两个列表接口获取数据
if (refreshBothLists) {
await refreshBothLists();
@@ -159,6 +210,15 @@ const HomeNavbar = (props: IProps) => {
}
};
// 处理城市切换(用户手动选择)
const handleCityChange = async (_newArea: any) => {
// 用户手动选择的城市不保存到缓存
console.log("用户手动选择城市(不保存缓存):", _newArea);
// 切换城市后,同时更新两个列表接口获取数据
await handleCityChangeWithoutCache();
};
return (
<View
className="homeNavbar"

View File

@@ -0,0 +1,77 @@
import React, { useState, useEffect } from "react";
import CommonPopup from "@/components/CommonPopup";
import Picker from "./Picker";
interface PickerOption {
text: string | number;
value: string | number;
}
interface CityPickerV2Props {
visible: boolean;
setvisible: (visible: boolean) => void;
options?: PickerOption[][]; // 两级数据:[国家列表, 省份/城市列表]
value?: (string | number)[]; // [国家value, 省份/城市value]
onChange?: (value: (string | number)[]) => void;
style?: React.CSSProperties;
}
const CityPickerV2 = ({
visible,
setvisible,
value = [],
onChange,
options = [],
style,
}: CityPickerV2Props) => {
const [defaultValue, setDefaultValue] = useState<(string | number)[]>(value);
// 当外部 value 变化时同步更新
useEffect(() => {
if (value && value.length > 0) {
setDefaultValue(value);
}
}, [value]);
const changePicker = (_options: any[], values: any, _columnIndex: number) => {
setDefaultValue(values);
};
const handleConfirm = () => {
onChange?.(defaultValue);
setvisible(false);
};
const dialogClose = () => {
setvisible(false);
};
return (
<>
<CommonPopup
visible={visible}
onClose={dialogClose}
showHeader={false}
title={null}
hideFooter={false}
cancelText="取消"
confirmText="完成"
onConfirm={handleConfirm}
position="bottom"
round
zIndex={1000}
style={style}
>
<Picker
visible={visible}
options={options}
defaultValue={defaultValue}
onChange={changePicker}
/>
</CommonPopup>
</>
);
};
export default CityPickerV2;

View File

@@ -5,5 +5,6 @@ export type { PickerCommonRef } from './PickerCommon'
export { default as CalendarUI } from './CalendarUI/CalendarUI'
export { default as DialogCalendarCard } from './CalendarDialog/DialogCalendarCard'
export { default as CityPicker } from './CityPicker'
export { default as CityPickerV2 } from './CityPickerV2'
export { default as DayDialog } from './DayDialog';
export { default as HourDialog } from './HourDialog';