feat: 切换城市

This commit is contained in:
2025-10-15 20:34:08 +08:00
parent f63295db13
commit fcd9cc7d4c
13 changed files with 472 additions and 118 deletions

View File

@@ -1,8 +1,8 @@
@use '~@/scss/themeColor.scss' as theme;
@use "~@/scss/themeColor.scss" as theme;
.common-popup {
position: fixed;
z-index: 9999!important;
z-index: 9999 !important;
.common-popup__drag-handle-container {
position: position;
}
@@ -12,7 +12,7 @@
left: 50%;
width: 32px;
height: 4px;
background-color: rgba(22, 24, 35, 0.20);
background-color: rgba(22, 24, 35, 0.2);
border-radius: 2px;
z-index: 10;
cursor: pointer;
@@ -48,13 +48,18 @@
padding: 8px 10px 0 10px;
display: flex;
gap: 8px;
background: #FFF;
background: #fff;
padding-bottom: env(safe-area-inset-bottom);
}
.common-popup__btn {
flex: 1;
font-feature-settings: "liga" off, "clig" off;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: normal;
}
.common-popup__btn-cancel {
@@ -63,7 +68,7 @@
border: none;
width: 154px;
height: 44px;
border-radius: 12px!important;
border-radius: 12px !important;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #fff;
padding: 4px 10px;
@@ -75,7 +80,7 @@
height: 44px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #000;
border-radius: 12px!important;
border-radius: 12px !important;
padding: 4px 10px;
}
}

View File

@@ -2,9 +2,8 @@
position: fixed;
top: 0;
left: 0;
z-index: 999;
overflow: hidden;
z-index: 999;
z-index: 9991;
width: 100%;
background-color: #fff;
}

View File

@@ -0,0 +1,72 @@
import React, { useState } from "react";
import CommonPopup from "@/components/CommonPopup";
import Picker from "./Picker";
interface PickerOption {
text: string | number;
value: string | number;
}
interface PickerProps {
visible: boolean;
setvisible: (visible: boolean) => void;
options?: PickerOption[][] | PickerOption[];
value?: (string | number)[];
type?: "month" | "day" | "hour" | "ntrp" | null;
img?: string;
onConfirm?: (options: PickerOption[], values: (string | number)[]) => void;
onChange?: (value: (string | number)[]) => void;
style?: React.CSSProperties;
}
const PopupPicker = ({
visible,
setvisible,
value = [],
onConfirm,
onChange,
options = [],
style,
}: PickerProps) => {
const [defaultValue, setDefaultValue] = useState<(string | number)[]>(value);
const changePicker = (_options: any[], values: any, _columnIndex: number) => {
setDefaultValue(values);
};
const handleConfirm = () => {
console.log(defaultValue, "defaultValue");
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 PopupPicker;

View File

@@ -4,3 +4,4 @@ export { default as PickerCommon } from './PickerCommon'
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'

View File

@@ -1,13 +1,15 @@
import { useEffect, useState } from "react";
import { View, Text, Image } from "@tarojs/components";
import img from "@/config/images";
import { useGlobalState } from "@/store/global";
import { useUserInfo, } from '@/store/userStore'
import { useUserInfo } from "@/store/userStore";
import { useListState } from "@/store/listStore";
import CustomNavbar from "@/components/CustomNavbar";
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";
interface IProps {
config?: {
@@ -18,30 +20,60 @@ interface IProps {
};
}
function CityPicker(props) {
const { visible, setVisible, cities, area, setArea } = props;
console.log(cities, "cities");
const [index, setIndex] = useState(0);
const [value, setValue] = useState(area);
useEffect(() => {
if (visible) {
setIndex(index + 1);
}
}, [visible]);
function onChange(value: any) {
console.log(value, "value");
setValue(value);
setArea(value);
}
return (
visible && (
<PopupPicker
key={index}
options={cities}
visible={visible}
setvisible={setVisible}
value={value}
onChange={onChange}
style={{ zIndex: 9991 }}
/>
)
);
}
const ListHeader = (props: IProps) => {
const { config } = props;
const {
showInput = false,
inputLeftIcon,
leftIconClick,
} = config || {};
const {
getLocationLoading,
statusNavbarHeightInfo,
} = useGlobalState();
const { gamesNum, searchValue } = useListState();
const { showInput = false, inputLeftIcon, leftIconClick } = config || {};
const { getLocationLoading, statusNavbarHeightInfo } = useGlobalState();
const { gamesNum, searchValue, cities, area, updateArea } = useListState();
const { navBarHeight } = statusNavbarHeightInfo;
const userInfo = useUserInfo()
const city = (userInfo as any)?.city || ''
const district = (userInfo as any)?.district || ''
const [cityPopupVisible, setCityPopupVisible] = useState(false);
console.log("useUserInfo",city,district )
const userInfo = useUserInfo();
const province = (userInfo as any)?.province || "";
const city = (userInfo as any)?.city || "";
// const district = (userInfo as any)?.district || "";
const currentAddress = city + district
useEffect(() => {
updateArea(["中国", province, city]);
}, [province, city]);
// const currentAddress = city + district;
const handleInputClick = () => {
const currentPagePath = getCurrentFullPath()
const currentPagePath = getCurrentFullPath();
if (currentPagePath === "/game_pages/searchResult/index") {
Taro.navigateBack();
} else {
@@ -77,6 +109,11 @@ const ListHeader = (props: IProps) => {
height: `${navBarHeight}px`,
};
function handleToggleCity() {
setCityPopupVisible(true);
}
const area_city = area.at(-1);
return (
<CustomNavbar>
@@ -96,10 +133,10 @@ const ListHeader = (props: IProps) => {
/>
<View className="listNavLine" />
<View className="listNavContent">
<View className="listNavCityWrapper">
<View className="listNavCityWrapper" onClick={handleToggleCity}>
{/* 位置 */}
<Text className="listNavCity">{currentAddress}</Text>
{!getLocationLoading && currentAddress && (
<Text className="listNavCity">{area_city}</Text>
{!getLocationLoading && area_city && (
<Image src={img.ICON_CHANGE} className="listNavChange" />
)}
</View>
@@ -111,7 +148,8 @@ const ListHeader = (props: IProps) => {
</View>
{/* 搜索导航 */}
<View
className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${showInput && "visible"
className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${
showInput && "visible"
} ${showInput ? "inputCustomerNavbarShowInput" : ""}`}
style={navbarStyle}
>
@@ -139,6 +177,13 @@ const ListHeader = (props: IProps) => {
</View>
</View>
</View>
<CityPicker
visible={cityPopupVisible}
setVisible={setCityPopupVisible}
cities={cities}
area={area}
setArea={updateArea}
/>
</CustomNavbar>
);
};

View File

@@ -4,6 +4,79 @@
}
}
.cqContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// gap: 24px;
.tips {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
.tip1 {
color: #000;
text-align: center;
font-family: "PingFang SC";
font-size: 18px;
font-style: normal;
font-weight: 600;
line-height: 28px;
}
.tip2 {
color: rgba(0, 0, 0, 0.65);
text-align: center;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
}
.qrcodeWrappper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
.qrcode {
width: 180px;
height: 180px;
// border-radius: 12px;
// border: 1px solid rgba(0, 0, 0, 0.06);
// background: lightgray 50% / cover no-repeat;
// box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.16);
}
.qrcodeTip {
color: rgba(0, 0, 0, 0.65);
text-align: center;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
margin-top: -30px;
}
}
}
}
.listPage {
background-color: #fefefe;

View File

@@ -9,7 +9,7 @@ import Taro, {
} from "@tarojs/taro";
import { useListStore } from "@/store/listStore";
import { useGlobalState } from "@/store/global";
import { View } from "@tarojs/components";
import { View, Image, Text } from "@tarojs/components";
import CustomerNavBar from "@/container/listCustomNavbar";
import GuideBar from "@/components/GuideBar";
import ListContainer from "@/container/listContainer";
@@ -19,7 +19,7 @@ import { updateUserLocation } from "@/services/userService";
// import ShareCardCanvas from "@/components/ShareCardCanvas";
import { useUserActions } from "@/store/userStore";
import { useDictionaryStore } from "@/store/dictionaryStore";
// import generateShareImage from '@/utils/share'
import { saveImage } from "@/utils";
const ListPage = () => {
// 从 store 获取数据和方法
@@ -47,6 +47,10 @@ const ListPage = () => {
loadMoreMatches,
fetchGetGamesCount,
updateDistanceQuickFilter,
getCities,
getCityQrCode,
area,
cityQrCode,
} = store;
const {
@@ -104,6 +108,8 @@ const ListPage = () => {
useEffect(() => {
getLocation();
fetchUserInfo();
getCities();
getCityQrCode();
}, []);
// 监听数据变化,如果是第一页就滚动到顶部
@@ -317,6 +323,44 @@ const ListPage = () => {
// })
// }, [])
const area_city = area.at(-2);
function renderCityQrcode() {
let item = cityQrCode.find((item) => item.city_name === area_city);
if (!item) item = cityQrCode.find((item) => item.city_name === "其他");
return (
<View className={styles.cqContainer}>
{item ? (
<View className={styles.wrapper}>
<View className={styles.tips}>
<Text className={styles.tip1}></Text>
<Text className={styles.tip2}>
</Text>
</View>
<View className={styles.qrcodeWrappper}>
<Image
className={styles.qrcode}
src={item.qr_code_url}
mode="widthFix"
onClick={() => {
saveImage(item.qr_code_url);
}}
/>
<Text className={styles.qrcodeTip}>
使
</Text>
</View>
</View>
) : (
<View>
<Text>, </Text>
</View>
)}
</View>
);
}
return (
<>
{/* 自定义导航 */}
@@ -325,6 +369,9 @@ const ListPage = () => {
showInput: isShowInputCustomerNavBar,
}}
/>
{area_city !== "上海" ? (
renderCityQrcode()
) : (
<View ref={scrollContextRef}>
{/* 列表内容 */}
<View className={styles.listPage} style={{ paddingTop: totalHeight }}>
@@ -384,23 +431,7 @@ const ListPage = () => {
/>
</View>
</View>
{/* 测试分享功能 */}
{/* <ShareCardCanvas data={
{
userAvatar: "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg",
userNickname: "华巴抡卡",
gameType: "单打",
skillLevel: "NTRP 2.5 - 3.0",
gameDate: "6月20日(周五)",
gameTime: "下午5点 2小时",
venueName: "因乐驰网球俱乐部(嘉定江桥万达店)",
venueImages: ["https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg",
"https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg"
],
}
}
onGenerated={handleShare}
/> */}
)}
<GuideBar currentPage="list" />
</>
);

View File

@@ -553,8 +553,13 @@ function Result() {
},
});
} else {
try {
const url = await genCardImage();
Taro.saveImageToPhotosAlbum({ filePath: url });
Taro.showToast({ title: "保存成功" });
} catch (e) {
Taro.showToast({ title: "图片保存失败", icon: "none" });
}
}
});
}

View File

@@ -109,3 +109,26 @@ export const searchSuggestion = async (params) => {
throw error;
}
}
export const getCities = async () => {
try {
// 调用HTTP服务获取城市列表
return httpService.post('/cities/tree', {})
} catch (error) {
// 捕获并打印错误信息
console.error("城市列表获取失败:", error);
// 抛出错误以便上层处理
throw error;
}
}
export const getCityQrCode = async () => {
try {
// 调用HTTP服务获取城市二维码
return httpService.post('/hot_city_qr/list', {})
} catch (error) {
// 捕获并打印错误信息
console.error("城市二维码获取失败:", error);
// 抛出错误以便上层处理
throw error;
}
}

View File

@@ -7,6 +7,8 @@ import {
clearHistory,
searchSuggestion,
getGamesCount,
getCities,
getCityQrCode,
} from "../services/listApi";
import {
ListActions,
@@ -15,6 +17,19 @@ import {
IPayload,
} from "../../types/list/types";
function translateCityData(dataTree) {
return dataTree.map((item) => {
const { children, ...rest } = item;
return {
...rest,
text: rest.name,
label: rest.name,
value: rest.name,
children: children?.length > 0 ? translateCityData(children) : null,
};
});
}
// 完整的 Store 类型
type TennisStore = ListState & ListActions;
@@ -138,6 +153,9 @@ const commonStateDefaultValue = {
{ id: 5, label: "晚上 18:00-22:00", value: "18:00-22:00" },
{ id: 6, label: "夜间 22:00-24:00", value: "22:00-24:00" },
],
cities: [],
cityQrCode: [],
area: ['', '', ''] as [string, string, string],
}
// 创建 store
@@ -486,6 +504,32 @@ export const useListStore = create<TennisStore>()((set, get) => ({
});
console.log("===更新搜索页状态:", state);
},
async getCities() {
const res = await getCities();
const state = get();
set({
...state,
cities: translateCityData(res.data),
})
},
async getCityQrCode() {
const res = await getCityQrCode();
const state = get();
set({
...state,
cityQrCode: res.data,
})
},
updateArea(payload: [string, string, string]) {
const state = get();
set({
...state,
area: payload,
})
},
}));
// 导出便捷的 hooks

View File

@@ -9,3 +9,4 @@ export * from './orderActions';
export * from './routeUtil';
export * from './share'
export * from './genPoster'
export * from './wx_helper'

35
src/utils/wx_helper.ts Normal file
View File

@@ -0,0 +1,35 @@
import Taro from "@tarojs/taro";
export function saveImage(url) {
Taro.getSetting().then(async (res) => {
if (!res.authSetting["scope.writePhotosAlbum"]) {
Taro.authorize({
scope: "scope.writePhotosAlbum",
success: async () => {
try {
Taro.saveImageToPhotosAlbum({ filePath: url });
Taro.showToast({ title: "保存成功" });
} catch (e) {
Taro.showToast({ title: "图片保存失败", icon: "none" });
}
},
fail: () => {
Taro.showModal({
title: "提示",
content: "需要开启相册权限才能保存图片",
success: (r) => {
if (r.confirm) Taro.openSetting();
},
});
},
});
} else {
try {
Taro.saveImageToPhotosAlbum({ filePath: url });
Taro.showToast({ title: "保存成功" });
} catch (e) {
Taro.showToast({ title: "图片保存失败", icon: "none" });
}
}
});
}

View File

@@ -51,10 +51,24 @@ export interface SearchPageState extends PageState {
data: TennisMatch[]
suggestionList: string[]
isShowSuggestion: boolean
searchHistory: {id: number, title: string}[]
searchHistory: { id: number, title: string }[]
searchHistoryParams: Record<string, any>
}
export interface CityTree {
id?: number;
name: string;
children: CityTree[];
}
export interface CityQrCodeItem {
id: number,
city_name: string,
qr_code_url: string,
description: string,
sort_order: number
}
// 主状态接口
export interface ListState {
currentPage: string
@@ -73,6 +87,9 @@ export interface ListState {
timeBubbleData: BubbleOption[]
dateRangeOptions: BubbleOption[]
gamesNum: number
cities: CityTree[]
cityQrCode: CityQrCodeItem[]
area: [string, string, string]
}
export interface ListActions {
@@ -96,6 +113,9 @@ export interface ListActions {
getCurrentPageState: () => { currentPageState: any; currentPageKey: string }
updateCurrentPageState: (payload: Record<string, any>) => void
updateDistanceQuickFilter: (payload: Record<string, any>) => void
getCities: () => Promise<void>
getCityQrCode: () => Promise<void>
updateArea: (payload: [string, string, string]) => void
}
export interface IPayload {