This commit is contained in:
张成
2025-09-02 09:34:05 +08:00
135 changed files with 4954 additions and 3848 deletions

View File

@@ -29,6 +29,7 @@
color: #fff;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.10);
.detail-navigator-back {
border-right: 1px solid #444;
@@ -123,6 +124,7 @@
&-tags {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
&-tag {
@@ -545,6 +547,7 @@
&-scroll {
flex: 0 0 auto;
width: calc(100% - 116px);
&-content {
display: flex;
@@ -813,7 +816,7 @@
border-radius: 20px;
border: 1px solid rgba(33, 178, 0, 0.20);
background: rgba(255, 255, 255, 0.16);
padding: 12px 15px;
padding: 12px 0 12px 15px;
box-sizing: border-box;
&-title {
@@ -923,6 +926,52 @@
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.06);
background: #FFF;
.sticky-bottom-bar-share {
display: flex;
align-items: center;
flex-direction: column;
gap: 4px;
&-icon {
width: 16px;
height: 16px;
}
&-text {
color: rgba(0, 0, 0, 0.85);
font-size: 10px;
font-style: normal;
font-weight: 500;
line-height: 16px; /* 160% */
}
}
&-separator {
width: 1px;
height: 24px;
background: rgba(0, 0, 0, 0.10);
}
.sticky-bottom-bar-comment {
display: flex;
align-items: center;
flex-direction: column;
gap: 4px;
&-icon {
width: 16px;
height: 16px;
}
&-text {
color: rgba(0, 0, 0, 0.85);
font-size: 10px;
font-style: normal;
font-weight: 500;
line-height: 16px; /* 160% */
}
}
}
&-join-game {
@@ -938,8 +987,48 @@
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: #FFF;
&-price {
font-family: "PoetsenOne";
font-size: 28px;
font-weight: 400;
line-height: 24px; /* 114.286% */
letter-spacing: -0.56px;
color: #000;
}
}
}
}
}
.share-popup-content {
width: 100%;
height: 100%;
padding: 20px 16px env(safe-area-inset-bottom);
box-sizing: border-box;
// padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
display: flex;
justify-content: space-around;
align-items: center;
& > view {
width: 100px;
height: 64px;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
& > image {
width: 24px;
height: 24px;
}
& > text {
color: rgba(0, 0, 0, 0.85);
}
}
}

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useRef, useImperativeHandle, forwardRef } from 'react'
import { View, Text, Button, Swiper, SwiperItem, Image, Map, ScrollView } from '@tarojs/components'
import { Cell, Avatar, Progress, Popover } from '@nutui/nutui-react-taro'
import Taro, { useRouter } from '@tarojs/taro'
import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow } from '@tarojs/taro'
// 导入API服务
import DetailService from '../../services/detailService'
import {
@@ -9,8 +9,9 @@ import {
useUserActions
} from '../../store/userStore'
import img from '../../config/images'
import { getTextColorOnImage } from '../../utils/processImage'
import { getTextColorOnImage } from '../../utils'
import './index.scss'
import { CommonPopup } from '@/components'
const images = [
'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1a35ebbf-2361-44da-b338-7608561d0b31.png',
@@ -22,6 +23,71 @@ function insertDotInTags(tags: string[]) {
return tags.join('-·-').split('-')
}
const SharePopup = forwardRef(({ id, from }: { id: string, from: string }, ref) => {
const [visible, setVisible] = useState(false)
useImperativeHandle(ref, () => ({
show: () => {
setVisible(true)
}
}))
function handleShareToWechat() {
useShareAppMessage(() => {
return {
title: '分享',
path: `/pages/detail/index?id=${id}&from=${from}`,
}
})
}
function handleShareToWechatMoments() {
useShareTimeline(() => {
return {
title: '分享',
path: `/pages/detail/index?id=${id}&from=${from}`,
}
})
}
function handleSaveToLocal() {
Taro.saveImageToPhotosAlbum({
filePath: images[0],
success: () => {
Taro.showToast({ title: '保存成功', icon: 'success' })
},
fail: () => {
Taro.showToast({ title: '保存失败', icon: 'none' })
},
})
}
return (
<CommonPopup
title="分享"
visible={visible}
onClose={() => { setVisible(false) }}
hideFooter
style={{ minHeight: '100px' }}
>
<View catchMove className='share-popup-content'>
<View onClick={handleShareToWechat}>
<Image src={img.ICON_DETAIL_SHARE} />
<Text></Text>
</View>
<View onClick={handleShareToWechatMoments}>
<Image src={img.ICON_DETAIL_SHARE} />
<Text></Text>
</View>
<View onClick={handleSaveToLocal}>
<Image src={img.ICON_DETAIL_SHARE} />
<Text></Text>
</View>
</View>
</CommonPopup>
)
})
function Index() {
// 使用Zustand store
// const userStats = useUserStats()
@@ -31,16 +97,24 @@ function Index() {
const [colors, setColors] = useState<string []>([])
const [detail, setDetail] = useState<any>(null)
const { params } = useRouter()
const { id, share } = params
const { id, autoShare, from } = params
console.log('from', from)
// 本地状态管理
const [loading, setLoading] = useState(false)
const sharePopupRef = useRef<any>(null)
// 页面加载时获取数据
useEffect(() => {
// useEffect(() => {
// fetchDetail()
// calcBgMainColors()
// }, [])
useDidShow(() => {
fetchDetail()
calcBgMainColors()
}, [])
})
const fetchDetail = async () => {
const res = await DetailService.getDetail(Number(id))
@@ -59,6 +133,26 @@ function Index() {
setColors(textcolors)
}
function handleShare() {
sharePopupRef.current.show()
}
const openMap = () => {
Taro.openLocation({
latitude: detail?.latitude, // 纬度(必填)
longitude: detail?.longitude, // 经度(必填)
name: '上海体育场', // 位置名(可选)
address: '上海市徐汇区肇嘉浜路128号', // 地址详情(可选)
scale: 15, // 地图缩放级别1-28
})
}
const handleJoinGame = () => {
Taro.navigateTo({
url: `/pages/orderCheck/index?id=${id}`,
})
}
const tags = [{
name: '🕙 急招',
icon: '',
@@ -139,7 +233,7 @@ function Index() {
{/* custom navbar */}
<view className="custom-navbar">
<View className='detail-navigator'>
<View className='detail-navigator-back'>
<View className='detail-navigator-back' onClick={() => { Taro.navigateBack() }}>
<Image className='detail-navigator-back-icon' src={img.ICON_ARROW_LEFT} />
</View>
<View className='detail-navigator-icon'>
@@ -234,7 +328,7 @@ function Index() {
{/* location message */}
<View className='location-message-text'>
{/* venue name and distance */}
<View className='location-message-text-name-distance'>
<View className='location-message-text-name-distance' onClick={openMap}>
<Text></Text>
<Text>·</Text>
<Text>1.2km</Text>
@@ -325,7 +419,7 @@ function Index() {
</View>
{/* participants list */}
<ScrollView className='participants-list-scroll' scrollX>
<View className='participants-list-scroll-content' style={{ width: `${(participants.length + 1) * 108 + (participants.length) * 8 - 30}px` }}>
<View className='participants-list-scroll-content' style={{ width: `${participants.length * 103 + (participants.length - 1) * 8}px` }}>
{participants.map((participant) => (
<View key={participant.id} className='participants-list-item'>
{/* <Avatar className='participants-list-item-avatar' src={participant.user.avatar_url} /> */}
@@ -420,7 +514,7 @@ function Index() {
<Avatar className='recommend-games-list-item-addon-avatar' src={game.avatar} />
<View className='recommend-games-list-item-addon-message'>
<View className='recommend-games-list-item-addon-message-applications'>
<Text> {game.checkedApplications} / {game.applications}</Text>
<Text> {game.checkedApplications}/{game.applications}</Text>
</View>
<View className='recommend-games-list-item-addon-message-level-requirements'>
<Text>{game.levelRequirements}</Text>
@@ -439,10 +533,17 @@ function Index() {
{/* sticky bottom action bar */}
<View className="sticky-bottom-bar">
<View className="sticky-bottom-bar-share-and-comment">
<View></View>
<View></View>
<View className='sticky-bottom-bar-share' onClick={handleShare}>
<Image className='sticky-bottom-bar-share-icon' src={img.ICON_DETAIL_SHARE} />
<Text className='sticky-bottom-bar-share-text'></Text>
</View>
<View className='sticky-bottom-bar-share-and-comment-separator' />
<View className='sticky-bottom-bar-comment' onClick={() => { Taro.showToast({ title: 'To be continued', icon: 'none' }) }}>
<Image className='sticky-bottom-bar-comment-icon' src={img.ICON_DETAIL_COMMENT_DARK} />
<Text className='sticky-bottom-bar-comment-text'>32</Text>
</View>
</View>
<View className="sticky-bottom-bar-join-game">
<View className="sticky-bottom-bar-join-game" onClick={handleJoinGame}>
<Text>🎾</Text>
<Text></Text>
<View className='game-price'>
@@ -450,6 +551,8 @@ function Index() {
</View>
</View>
</View>
{/* share popup */}
<SharePopup ref={sharePopupRef} id={id} from={from} />
</View>
</View>
)

View File

@@ -1 +0,0 @@

View File

@@ -1,261 +0,0 @@
.index-page {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
.page-header {
text-align: center;
margin-bottom: 24px;
.page-title {
font-size: 28px;
font-weight: bold;
color: white;
display: block;
margin-bottom: 8px;
}
.page-subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
display: block;
}
}
.user-card {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
.user-header {
display: flex;
align-items: center;
gap: 16px;
.user-info {
flex: 1;
.username {
font-size: 20px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 4px;
}
.user-level {
font-size: 14px;
color: #666;
display: block;
margin-bottom: 4px;
}
.join-date {
font-size: 12px;
color: #999;
display: block;
}
}
}
}
.stats-section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
display: block;
}
:global {
.nut-cell {
background: #f8f9fa;
border-radius: 12px;
margin-bottom: 8px;
border: none;
&:last-child {
margin-bottom: 0;
}
.nut-cell__title {
font-weight: 500;
color: #555;
}
.nut-cell__value {
font-weight: bold;
color: #007bff;
}
}
}
}
.action-section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
display: block;
}
.button-group {
display: flex;
flex-direction: column;
gap: 12px;
.custom-button {
border-radius: 12px;
font-weight: 500;
height: 48px;
border: none;
margin-bottom: 12px;
font-size: 16px;
color: white;
&.primary-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.success-btn {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&.warning-btn {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
&:disabled {
opacity: 0.6;
}
}
// 保留 NutUI 按钮样式(备用)
:global {
.nut-button {
border-radius: 12px;
font-weight: 500;
height: 48px;
border: none;
&--primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&--success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&--warning {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
&:disabled {
opacity: 0.6;
}
}
}
}
}
.loading-section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
text-align: center;
.loading-text {
font-size: 16px;
color: #666;
margin-bottom: 12px;
display: block;
}
:global {
.nut-progress {
.nut-progress-outer {
background: #f0f0f0;
border-radius: 10px;
}
.nut-progress-inner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
}
}
}
}
.tips-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
.tips-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
display: block;
}
.tips-content {
.tip-item {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: 8px;
display: block;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.index-page {
padding: 16px;
.page-header {
.page-title {
font-size: 24px;
}
.page-subtitle {
font-size: 14px;
}
}
.user-card,
.stats-section,
.action-section,
.loading-section,
.tips-section {
padding: 16px;
margin-bottom: 16px;
}
}
}

View File

@@ -5,8 +5,9 @@ import Taro from '@tarojs/taro'
// 导入API服务
import demoApi from '../../services/demoApi'
import commonApi from '../../services/commonApi'
import {
useUserStats,
import PublishMenu from '../../components/PublishMenu'
import {
useUserStats,
useUserActions
} from '../../store/userStore'
import './index.scss'
@@ -15,7 +16,7 @@ function Index() {
// 使用Zustand store
const userStats = useUserStats()
const { incrementRequestCount, resetUserStats } = useUserActions()
// 本地状态管理
const [loading, setLoading] = useState(false)
const [userProfile, setUserProfile] = useState<any>(null)
@@ -43,19 +44,19 @@ function Index() {
const handleGetUserProfile = async () => {
console.log('获取用户信息...');
setLoading(true)
try {
const response = await demoApi.getUserProfile()
if (response.success) {
setUserProfile(response.data)
incrementRequestCount()
Taro.showToast({
title: '获取用户信息成功',
icon: 'success'
})
console.log('用户信息:', response.data)
}
} catch (error) {
@@ -64,7 +65,7 @@ function Index() {
title: '获取失败,使用模拟数据',
icon: 'none'
})
// 模拟数据
setUserProfile({
id: '123',
@@ -83,7 +84,7 @@ function Index() {
const handleSubmitStats = async () => {
console.log('提交统计数据...');
setLoading(true)
try {
const response = await commonApi.submitForm('userStats', [
{
@@ -97,21 +98,21 @@ function Index() {
}
}
])
if (response.success) {
incrementRequestCount()
Taro.showToast({
title: '统计数据提交成功',
icon: 'success'
})
console.log('提交结果:', response.data)
}
} catch (error) {
console.error('提交统计数据失败:', error)
incrementRequestCount() // 即使失败也计数,用于演示
Taro.showToast({
title: '网络模拟提交成功',
icon: 'success'
@@ -125,7 +126,7 @@ function Index() {
const handleSubmitFeedback = async () => {
console.log('提交用户反馈...');
setLoading(true)
try {
const response = await demoApi.submitFeedback({
matchId: 'demo_match_' + Date.now(),
@@ -134,21 +135,21 @@ function Index() {
aspects: ['场地环境', '服务质量', '价格合理'],
comments: `用户反馈 - 请求次数: ${userStats.requestCount + 1},体验良好!`
})
if (response.success) {
incrementRequestCount()
Taro.showToast({
title: '反馈提交成功',
icon: 'success'
})
console.log('反馈结果:', response.data)
}
} catch (error) {
console.error('提交反馈失败:', error)
incrementRequestCount() // 即使失败也计数,用于演示
Taro.showToast({
title: '网络模拟提交成功',
icon: 'success'
@@ -163,7 +164,7 @@ function Index() {
console.log('重置所有数据...');
resetUserStats()
setUserProfile(null)
Taro.showToast({
title: '数据已重置',
icon: 'success'
@@ -181,9 +182,9 @@ function Index() {
{/* 用户信息卡片 */}
<View className='user-card'>
<View className='user-header'>
<Avatar
size="large"
src={userProfile?.avatar || ''}
<Avatar
size="large"
src={userProfile?.avatar || ''}
style={{ backgroundColor: '#fa2c19' }}
>
{userProfile?.nickname?.charAt(0) || 'U'}
@@ -205,17 +206,17 @@ function Index() {
{/* 统计数据 */}
<View className='stats-section'>
<Text className='section-title'>📊 API </Text>
<Cell title="API 请求次数" extra={userStats.requestCount} />
<Cell title="创建的比赛" extra={userStats.matchesCreated} />
<Cell title="参加的比赛" extra={userStats.matchesJoined} />
<Cell
title="最后活跃时间"
<Cell
title="最后活跃时间"
extra={new Date(userStats.lastActiveTime).toLocaleTimeString()}
/>
{interests.length > 0 && (
<Cell
title="推荐兴趣"
<Cell
title="推荐兴趣"
extra={interests.slice(0, 2).join(', ')}
/>
)}
@@ -224,10 +225,10 @@ function Index() {
{/* API 请求按钮区域 */}
<View className='action-section'>
<Text className='section-title'>🚀 API </Text>
<View className='button-group'>
<Button
type="primary"
<Button
type="primary"
loading={loading}
onClick={handleGetUserProfile}
disabled={loading}
@@ -236,8 +237,8 @@ function Index() {
{loading ? '请求中...' : '获取用户信息'}
</Button>
<Button
type="default"
<Button
type="default"
loading={loading}
onClick={handleSubmitStats}
disabled={loading}
@@ -246,8 +247,8 @@ function Index() {
{loading ? '提交中...' : '提交统计数据'}
</Button>
<Button
type="default"
<Button
type="default"
loading={loading}
onClick={handleSubmitFeedback}
disabled={loading}
@@ -256,8 +257,8 @@ function Index() {
{loading ? '提交中...' : '提交用户反馈'}
</Button>
<Button
type="warn"
<Button
type="warn"
onClick={handleResetAllData}
disabled={loading}
className="custom-button warning-btn"
@@ -286,6 +287,13 @@ function Index() {
<Text className='tip-item'> 使</Text>
</View>
</View>
<PublishMenu
onPersonalPublish={() => {
Taro.navigateTo({
url: '/pages/publishBall/index'
})
}}
/>
</View>
)
}

View File

@@ -12,6 +12,8 @@ import { FilterPopupProps } from "../../../types/list/types";
import CourtType from "@/components/CourtType";
// 玩法
import GamePlayType from "@/components/GamePlayType";
import { useDictionaryActions } from "@/store/dictionaryStore";
import { useMemo } from "react";
const FilterPopup = (props: FilterPopupProps) => {
const {
@@ -27,7 +29,22 @@ const FilterPopup = (props: FilterPopupProps) => {
} = props;
const store = useListStore() || {};
const { timeBubbleData, locationOptions, gamePlayOptions } = store;
const { getDictionaryValue } = useDictionaryActions() || {};
const { timeBubbleData } = store;
const handleOptions = (dictionaryValue: []) => {
return dictionaryValue?.map((item) => ({ label: item, value: item })) || [];
};
const courtType = getDictionaryValue("court_type") || [];
const locationOptions = useMemo(() => {
return courtType ? handleOptions(courtType) : [];
}, [courtType]);
const gamePlay = getDictionaryValue("game_play") || [];
const gamePlayOptions = useMemo(() => {
return gamePlay ? handleOptions(gamePlay) : [];
}, [gamePlay]);
const handleFilterChange = (name, value) => {
onChange({ [name]: value });

View File

@@ -1,22 +1,25 @@
.listPage {
background-color: #fafafa;
background-color: #fefefe;
.listTopSearchWrapper {
padding: 0 15px;
// position: sticky;
// background: #fefefe;
// z-index: 999;
}
// .isScroll {
// border-bottom: 0.5px solid #0000000F;
// }
.listTopFilterWrapper {
display: flex;
align-items: center;
margin-top: 5px;
margin-bottom: 10px;
padding-top: 10px;
padding-bottom: 10px;
gap: 5px;
}
.listContentWrapper {
padding: 0 5px;
}
.menuFilter {
padding: 0;
}

View File

@@ -1,33 +1,34 @@
import ListCard from "../../components/ListCard";
import ListCardSkeleton from "../../components/ListCardSkeleton";
import List from "../../components/List";
import Menu from "../../components/Menu";
import CityFilter from "../../components/CityFilter";
import SearchBar from "../../components/SearchBar";
import FilterPopup from "./FilterPopup";
import styles from "./index.module.scss";
import { useEffect } from "react";
import Taro, { useReachBottom } from "@tarojs/taro";
import Taro, { usePageScroll, useReachBottom } from "@tarojs/taro";
import { useListStore } from "@/store/listStore";
import {useGlobalState} from '@/store/global'
import { useGlobalState } from "@/store/global";
import { View } from "@tarojs/components";
import CustomerNavBar from "@/components/CustomNavbar";
import CustomerNavBar from "@/container/listCustomNavbar";
import InputCustomerBar from "@/container/inputCustomerNavbar";
import GuideBar from "@/components/GuideBar";
import ListContainer from "@/container/listContainer";
import DistanceQuickFilter from "@/components/DistanceQuickFilter";
import img from "@/config/images";
const ListPage = () => {
// 从 store 获取数据和方法
const store = useListStore() || {};
const {statusNavbarHeightInfo } = useGlobalState() || {}
// console.log("===store===", store);
// console.log('===statusNavbarHeightInfo', statusNavbarHeightInfo)
const { statusNavbarHeightInfo } = useGlobalState() || {};
const { totalHeight } = statusNavbarHeightInfo || {};
const {
isShowFilterPopup,
error,
matches,
recommendList,
loading,
fetchMatches,
refreshMatches,
clearError,
updateState,
filterCount,
updateFilterOptions, // 更新筛选条件
@@ -36,8 +37,22 @@ const ListPage = () => {
distanceData,
quickFilterData,
distanceQuickFilter,
isScrollTop,
searchValue,
isShowInputCustomerNavBar,
} = store;
usePageScroll((res) => {
// if (res?.scrollTop > 0 && !isScrollTop) {
// updateState({ isScrollTop: true });
// }
if (res?.scrollTop >= totalHeight && !isScrollTop) {
updateState({ isShowInputCustomerNavBar: true });
} else {
updateState({ isShowInputCustomerNavBar: false });
}
});
useReachBottom(() => {
console.log("触底了");
// 调用 store 的加载更多方法
@@ -47,7 +62,7 @@ const ListPage = () => {
useEffect(() => {
// 页面加载时获取数据
fetchMatches();
}, [fetchMatches]);
}, []);
// 下拉刷新处理函数 - 使用Taro生命周期钩子
Taro.usePullDownRefresh(() => {
@@ -78,78 +93,6 @@ const ListPage = () => {
});
});
// 错误处理
useEffect(() => {
if (error) {
Taro.showToast({
title: error,
icon: "error",
duration: 2000,
});
// 3秒后自动清除错误
setTimeout(() => {
clearError();
}, 3000);
}
}, [error, clearError]);
// 加载状态显示
if (loading && matches.length === 0) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "200px",
fontSize: "14px",
color: "#999",
}}
>
<div style={{ marginBottom: "10px" }}>...</div>
<div style={{ fontSize: "12px", color: "#ccc" }}>
</div>
</div>
);
}
// 错误状态显示
if (error && matches.length === 0) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "200px",
fontSize: "14px",
color: "#999",
}}
>
<div style={{ marginBottom: "10px" }}></div>
<div style={{ marginBottom: "15px", fontSize: "12px", color: "#ccc" }}>
{error}
</div>
<button
onClick={() => fetchMatches()}
style={{
padding: "8px 16px",
fontSize: "12px",
color: "#fff",
backgroundColor: "#007aff",
border: "none",
borderRadius: "4px",
}}
>
</button>
</div>
);
}
const toggleShowPopup = () => {
updateState({ isShowFilterPopup: !isShowFilterPopup });
};
@@ -174,21 +117,40 @@ const ListPage = () => {
});
};
const handleSearchClick = () => {
Taro.navigateTo({
url: "/pages/search/index",
});
};
return (
<>
<CustomerNavBar />
{!isShowInputCustomerNavBar ? (
<CustomerNavBar />
) : (
<InputCustomerBar icon={img.ICON_LIST_INPUT_LOGO} />
)}
<View className={styles.listPage}>
<View className={styles.listTopSearchWrapper}>
<View
className={`${styles.listTopSearchWrapper} ${
isScrollTop ? styles.isScroll : ""
}`}
// style={{
// top: statusNavbarHeightInfo?.totalHeight,
// }}
>
<SearchBar
handleFilterIcon={toggleShowPopup}
isSelect={filterCount > 0}
filterCount={filterCount}
onChange={handleSearchChange}
value={searchValue}
onInputClick={handleSearchClick}
/>
{/* 综合筛选 */}
{isShowFilterPopup && (
<div>
<View>
<FilterPopup
loading={loading}
onCancel={toggleShowPopup}
@@ -200,47 +162,33 @@ const ListPage = () => {
onClose={toggleShowPopup}
statusNavbarHeigh={statusNavbarHeightInfo?.totalHeight}
/>
</div>
</View>
)}
{/* 筛选 */}
<div className={styles.listTopFilterWrapper}>
{/* 全城筛选 */}
<CityFilter
options={distanceData}
value={distanceQuickFilter?.distance}
wrapperClassName={styles.menuFilter}
onChange={handleDistanceOrQuickChange}
name="distance"
/>
{/* 智能排序 */}
<Menu
options={quickFilterData}
value={distanceQuickFilter?.quick}
onChange={handleDistanceOrQuickChange}
wrapperClassName={styles.menuFilter}
name="quick"
/>
</div>
</View>
{/* 筛选 */}
<View className={styles.listTopFilterWrapper}>
<DistanceQuickFilter
cityOptions={distanceData}
quickOptions={quickFilterData}
onChange={handleDistanceOrQuickChange}
cityName="distance"
quickName="quick"
cityValue={distanceQuickFilter?.distance}
quickValue={distanceQuickFilter?.quick}
/>
</View>
<View className={styles.listContentWrapper}>
{/* 列表内容 */}
<List>
{!loading &&
matches.length > 0 &&
matches.map((match, index) => (
<ListCard key={match.id || index} {...match} />
))}
</List>
{/* 空状态 */}
{loading &&
matches.length === 0 &&
new Array(10).fill(0).map(() => {
return <ListCardSkeleton />;
})}
</View>
{/* 列表内容 */}
<ListContainer
data={matches}
recommendList={recommendList}
loading={loading}
error={error}
reload={refreshMatches}
/>
</View>
<GuideBar currentPage="list" />
</>
);
};

View File

@@ -110,7 +110,7 @@ const LoginPage: React.FC = () => {
<View className="background_image">
<Image
className="bg_img"
src={require('../../../static/login/login_bg.png')}
src="http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/2e00dea1-8723-42fe-ae42-84fe38e9ac3f.png"
mode="aspectFill"
/>
<View className="bg_overlay"></View>
@@ -229,7 +229,7 @@ const LoginPage: React.FC = () => {
{agree_terms ? '已同意' : '同意并继续'}
</Button>
</View>
</View>
)}

View File

@@ -31,15 +31,15 @@ const VerificationPage: React.FC = () => {
try {
console.log('开始发送验证码,手机号:', phone);
// 调用发送短信接口
const result = await send_sms_code(phone);
console.log('发送验证码结果:', result);
if (result.success) {
console.log('验证码发送成功,开始倒计时');
Taro.showToast({
title: '验证码已发送',
icon: 'success',
@@ -49,7 +49,7 @@ const VerificationPage: React.FC = () => {
// 开始倒计时
setCanSendCode(false);
setCountdown(60);
console.log('设置状态: can_send_code = false, countdown = 60');
// 发送验证码成功后,让验证码输入框获得焦点并调用系统键盘
@@ -81,7 +81,7 @@ const VerificationPage: React.FC = () => {
// 倒计时效果
useEffect(() => {
console.log('倒计时 useEffect 触发countdown:', countdown);
if (countdown > 0) {
const timer = setTimeout(() => {
console.log('倒计时减少,从', countdown, '到', countdown - 1);
@@ -124,7 +124,7 @@ const VerificationPage: React.FC = () => {
setTimeout(() => {
Taro.redirectTo({
url: '/pages/index/index'
url: '/pages/list/index'
});
}, 200);
} else {
@@ -257,4 +257,4 @@ const VerificationPage: React.FC = () => {
);
};
export default VerificationPage;
export default VerificationPage;

View File

@@ -1,5 +0,0 @@
// import MapPlugin from "src/components/MapDisplay/mapPlugin";
import MapDisplay from "src/components/MapDisplay";
export default function MapDisplayPage() {
return <MapDisplay />
}

View File

@@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '消息',
// navigationBarBackgroundColor: '#FAFAFA',
navigationStyle: 'custom',
})

View File

@@ -0,0 +1,80 @@
@use '~@/scss/images.scss' as img;
$--Backgrounds-Primary: '#fff';
.message-container {
width: 100%;
height: 100vh;
background: radial-gradient(227.15% 100% at 50% 0%, #EEFFDC 0%, #FFF 36.58%), var(--Backgrounds-Primary, #FFF);
// padding-top: 100px;
box-sizing: border-box;
.custom-navbar {
height: 56px; /* 通常与原生导航栏高度一致 */
display: flex;
align-items: center;
justify-content: flex-start;
// background-color: #fff;
color: #000;
padding-top: 44px; /* 适配状态栏 */
position: sticky;
top: 0;
z-index: 100;
.message-navigator {
position: relative;
left: 15px;
top: -2px;
width: 80px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
.message-navigator-avatar {
width: 32px;
height: 32px;
}
.message-navigator-title {
font-size: 16px;
font-weight: 500;
color: #000;
}
}
}
.message-content {
.message-content-list {
display: flex;
flex-direction: column;
padding: 10px 15px;
box-sizing: border-box;
gap: 12px;
.message-item {
padding: 10px;
// border: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 10px;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
background-color: rgba(255, 255, 255, 0.5);
border-radius: 10px;
}
.message-item-title {
font-size: 16px;
font-weight: 500;
color: #000;
}
.message-item-content {
font-size: 14px;
color: #666;
}
}
}
}

View File

@@ -0,0 +1,43 @@
import React from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import { Avatar } from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import GuideBar from '@/components/GuideBar'
// import img from '@/config/images'
import './index.scss'
const Personal = () => {
const messageList = Array(10).fill(0).map((_, index) => ({
id: index + 1,
title: `消息${index + 1}消息${index + 1}消息${index + 1}消息${index + 1}`,
content: Array(Math.round(Math.random() * 40)).fill(0).map((_, index) => `消息${index + 1}`).join(''),
}))
return (
<View className='message-container'>
<View className='custom-navbar'>
<View className='message-navigator'>
<Avatar className='message-navigator-avatar' src="https://img.yzcdn.cn/vant/cat.jpeg" />
<Text className='message-navigator-title'></Text>
</View>
</View>
<ScrollView scrollY className='message-content'>
<View className='message-content-list'>
{messageList.map((item) => (
<View className='message-item' key={item.id}>
<View className='message-item-title'>
<Text>{item.title}</Text>
</View>
<View className='message-item-content'>
<Text>{item.content}</Text>
</View>
</View>
))}
</View>
</ScrollView>
<GuideBar currentPage='message' />
</View>
)
}
export default Personal

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '订单确认',
navigationBarBackgroundColor: '#FAFAFA'
})

View File

@@ -0,0 +1 @@
@use '~@/scss/images.scss' as img;

View File

@@ -0,0 +1,31 @@
import React from 'react'
import { View, Text, Button } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { delay } from '@/utils'
const OrderCheck = () => {
const handlePay = async () => {
Taro.showLoading({
title: '支付中...',
mask: true
})
await delay(2000)
Taro.hideLoading()
Taro.showToast({
title: '支付成功',
icon: 'success'
})
await delay(1000)
Taro.navigateBack({
delta: 1
})
}
return (
<View>
<Text>OrderCheck</Text>
<Button onClick={handlePay}></Button>
</View>
)
}
export default OrderCheck

View File

@@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '个人中心',
// navigationBarBackgroundColor: '#FAFAFA',
navigationStyle: 'custom',
})

View File

@@ -0,0 +1,35 @@
@use '~@/scss/images.scss' as img;
$--Backgrounds-Primary: '#fff';
.personal-container {
width: 100%;
height: 100vh;
background: radial-gradient(227.15% 100% at 50% 0%, #EEFFDC 0%, #FFF 36.58%), var(--Backgrounds-Primary, #FFF);
padding-top: 100px;
box-sizing: border-box;
.personal-navigator {
position: fixed;
left: 10px;
top: 54px;
width: 32px;
height: 32px;
.personal-navigator-back {
width: 100%;
height: 100%;
}
}
.personal-content {
width: 100%;
height: calc(100vh - 300px);
display: flex;
justify-content: center;
align-items: center;
font-size: 32px;
font-weight: 500;
color: #000;
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react'
import { View, Text, Image } from '@tarojs/components'
import Taro, { useRouter } from '@tarojs/taro'
import GuideBar from '@/components/GuideBar'
import img from '@/config/images'
import './index.scss'
const Personal = () => {
const { params } = useRouter()
const { id } = params
const handleBack = () => {
Taro.navigateBack()
}
return (
<View className='personal-container'>
{id && (
<View className='personal-navigator' onClick={handleBack}>
<Image className='personal-navigator-back' src={img.ICON_NAVIGATOR_BACK} />
</View>
)}
<View className='personal-content'>
<Text>Personal</Text>
</View>
<GuideBar currentPage='personal' />
</View>
)
}
export default Personal

View File

@@ -1,5 +1,5 @@
import React, { useState, useCallback, useEffect } from 'react'
import { View, Text, Input, Image, Picker } from '@tarojs/components'
import { View, Text, Input, Image } from '@tarojs/components'
import PopupGameplay from '../PopupGameplay'
import img from '@/config/images';
import { FormFieldConfig } from '@/config/formSchema/publishBallFormSchema';
@@ -65,10 +65,52 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
})
setShowStadiumSelector(false)
}
const handleChange = useCallback((key: string, costValue: any) => {
// 价格输入限制¥0.009999.99
console.log(costValue, 'valuevalue');
const handleChange = useCallback((key: string, value: any) => {
onChange({...value, [key]: value})
}, [onChange])
if (key === children[0]?.prop) {
// 允许清空
if (costValue === '') {
onChange({...value, [key]: ''});
return;
}
// 只允许数字和一个小数点
const filteredValue = costValue.replace(/[^\d.]/g, '');
// 确保只有一个小数点
const parts = filteredValue.split('.');
if (parts.length > 2) {
return; // 不更新,保持原值
}
// 限制小数点后最多2位
if (parts.length === 2 && parts[1].length > 2) {
return; // 不更新,保持原值
}
const numValue = parseFloat(filteredValue);
if (isNaN(numValue)) {
onChange({...value, [key]: ''});
return;
}
if (numValue < 0) {
onChange({...value, [key]: '0'});
return;
}
if (numValue > 9999.99) {
onChange({...value, [key]: '9999.99'});
return;
}
// 使用过滤后的值
onChange({...value, [key]: filteredValue});
return;
}
onChange({...value, [key]: costValue})
}, [onChange, children])
useEffect(() => {
if (children.length > 2) {
@@ -76,6 +118,10 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
setPlayGame(options)
}
}, [children])
useEffect(() => {
console.log(value, 'valuevalue');
}, [value])
const renderChildren = () => {
return children.map((child: any, index: number) => {
return <View className='form-item'>
@@ -91,6 +137,7 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
placeholder='请输入'
placeholderClass='title-placeholder'
type='digit'
maxlength={7}
value={value[child.prop]}
onInput={(e) => handleChange(child.prop, e.detail.value)}
/>

View File

@@ -13,7 +13,7 @@ export interface Stadium {
id?: string
name: string
address?: string
istance?: string
distance_km?: number | null | undefined
longitude?: number
latitude?: number
}
@@ -78,6 +78,15 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
setShowDetail(true)
}
const calculateDistance = (stadium: Stadium) => {
const distance_km = stadium.distance_km
if (!distance_km) return ''
if (distance_km && distance_km > 1) {
return distance_km.toFixed(1) + 'km'
}
return (distance_km * 1000).toFixed(0) + 'm'
}
// 处理搜索框输入
const handleSearchInput = (e: any) => {
@@ -253,7 +262,7 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
handleItemLocation(stadium)
}}
>
{stadium.istance} ·
{calculateDistance(stadium)} ·
</Text>
<Text
className='stadium-address-text'

View File

@@ -14,7 +14,7 @@ export interface Stadium {
address?: string
longitude?: number
latitude?: number
istance?: string
distance_km?: number | null
court_type?: string
court_surface?: string
description?: string
@@ -100,7 +100,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
address: stadium.address,
latitude: stadium.longitude,
longitude: stadium.latitude,
istance: stadium.istance,
istance: stadium.distance_km,
court_type: court_type[0] || '',
court_surface: court_surface[0] || '',
additionalInfo: '',
@@ -117,6 +117,13 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
setFormData: (data: any) => setFormData(data)
}), [formData, stadium])
const calculateDistance = (distance_km: number | null) => {
if (!distance_km) return ''
if (distance_km && distance_km > 1) {
return distance_km.toFixed(1) + 'km'
}
return (distance_km * 1000).toFixed(0) + 'm'
}
const handleMapLocation = () => {
@@ -128,7 +135,8 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
name: res.name,
address: res.address,
latitude: res.longitude,
longitude: res.latitude
longitude: res.latitude,
istance: null
})
},
fail: (err) => {
@@ -166,7 +174,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
<View className='stadium-item-right'>
<View className='stadium-name'>{formData.name}</View>
<View className='stadium-address'>
<Text>{formData.istance} · </Text>
<Text>{calculateDistance(formData.istance || null)} · </Text>
<Text>{formData.address}</Text>
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
</View>

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'
import { View, Text } from '@tarojs/components'
import React, { useCallback, useState } from 'react'
import { View, Text, Input } from '@tarojs/components'
import { Checkbox } from '@nutui/nutui-react-taro'
import styles from './index.module.scss'
interface FormSwitchProps {
@@ -10,7 +10,14 @@ interface FormSwitchProps {
}
const FormSwitch: React.FC<FormSwitchProps> = ({ value, onChange, subTitle, wechatId }) => {
const [editWechat, setEditWechat] = useState(false)
const editWechatId = () => {
}
const setWechatId = useCallback((e: any) => {
const value = e.target.value
onChange(value)
}, [])
return (
<>
<View className={styles['wechat-contact-section']}>
@@ -28,7 +35,14 @@ const FormSwitch: React.FC<FormSwitchProps> = ({ value, onChange, subTitle, wech
wechatId && (
<View className={styles['wechat-contact-id']}>
<Text className={styles['wechat-contact-text']}>: {wechatId.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')}</Text>
<View className={styles['wechat-contact-edit']}></View>
<View className={styles['wechat-contact-edit']} onClick={editWechatId}></View>
</View>
)
}
{
editWechat && (
<View className={styles['wechat-contact-edit']}>
<Input value={wechatId} onInput={setWechatId} />
</View>
)
}

View File

@@ -25,7 +25,8 @@
align-items: center;
gap: 4px;
color: rgba(60, 60, 67, 0.50);
font-size: 14px;
&-icon{
width: 16px;
height: 16px;
@@ -183,6 +184,9 @@
border: 1px solid rgba(0, 0, 0, 0.06);
background: #000;
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
&.submit-btn-disabled {
color: rgba(255, 255, 255, 0.30);
}
}
.submit-tip {
@@ -191,12 +195,21 @@
color: #999;
line-height: 1.4;
display: flex;
justify-content: center;
padding: 12px 0;
justify-content: center;
padding: 12px 0;
align-items: center;
.link {
color: #007AFF;
}
}
.submit-checkbox {
width: 11px;
height: 11px;
:global(.nut-icon-Checked){
background: rgba(22, 24, 35, 0.75)!important;
}
}
}
// 加载状态遮罩保持原样
@@ -230,74 +243,7 @@
}
}
// 删除确认弹窗
.delete-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
&__content {
background: white;
border-radius: 16px;
padding: 24px;
margin: 0 32px;
max-width: 320px;
width: 100%;
text-align: center;
}
&__title {
display: block;
font-size: 18px;
font-weight: 600;
color: theme.$primary-color;
margin-bottom: 8px;
}
&__desc {
display: block;
font-size: 14px;
color: rgba(60, 60, 67, 0.6);
margin-bottom: 24px;
}
&__actions {
display: flex;
gap: 12px;
.delete-modal__btn {
flex: 1;
height: 44px;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.2s ease;
&:first-child {
background: rgba(0, 0, 0, 0.04);
color: rgba(60, 60, 67, 0.8);
}
&:last-child {
background: #FF3B30;
color: white;
}
&:hover {
opacity: 0.8;
}
}
}
}
}
// 旋转动画

View File

@@ -1,53 +1,59 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { View, Text, Button, Image } from '@tarojs/components'
import { Checkbox } from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import ActivityTypeSwitch, { type ActivityType } from '../../components/ActivityTypeSwitch'
import { type ActivityType } from '../../components/ActivityTypeSwitch'
import CommonDialog from '../../components/CommonDialog'
import PublishForm from './publishForm'
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
import { FormFieldConfig, publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
import { PublishBallFormData } from '../../../types/publishBall';
import PublishService from '@/services/publishService';
import { getNextHourTime, getEndTime } from '@/utils/timeUtils';
import { getNextHourTime, getEndTime, delay } from '@/utils';
import images from '@/config/images'
import styles from './index.module.scss'
const defaultFormData: PublishBallFormData = {
title: '',
image_list: ['https://static-o.oss-cn-shenzhen.aliyuncs.com/images/tpbj/tpss10.jpg'],
title: '',
image_list: [],
timeRange: {
start_time: getNextHourTime(),
start_time: getNextHourTime(),
end_time: getEndTime(getNextHourTime())
},
activityInfo: {
activityInfo: {
play_type: '不限',
price: '',
venue_id: null,
venue_id: null,
location_name: '',
location: '',
latitude: '',
location: '',
latitude: '',
longitude: '',
court_type: '',
court_surface: '',
venue_description_tag: [],
venue_description: '',
venue_image_list: [],
court_type: '',
court_surface: '',
venue_description_tag: [],
venue_description: '',
venue_image_list: [],
},
players: [1, 4],
players: [1, 1],
skill_level: [1.0, 5.0],
descriptionInfo: {
description: '',
description_tag: [],
description: '',
description_tag: [],
},
is_substitute_supported: true,
is_wechat_contact: true,
wechat_contact: '14223332214'
is_substitute_supported: true,
is_wechat_contact: true,
wechat_contact: '14223332214'
}
const PublishBall: React.FC = () => {
const [activityType, setActivityType] = useState<ActivityType>('individual')
const [isSubmitDisabled, setIsSubmitDisabled] = useState(false)
// 获取页面参数并设置导航标题
const [optionsConfig, setOptionsConfig] = useState<FormFieldConfig[]>(publishBallFormSchema)
const [formData, setFormData] = useState<PublishBallFormData[]>([
defaultFormData
])
const [checked, setChecked] = useState(true)
// 删除确认弹窗状态
const [deleteConfirm, setDeleteConfirm] = useState<{
visible: boolean;
@@ -69,21 +75,49 @@ const PublishBall: React.FC = () => {
}
// 处理活动类型变化
const handleActivityTypeChange = (type: ActivityType) => {
setActivityType(type)
if (type === 'group') {
setFormData([defaultFormData])
} else {
setFormData([defaultFormData])
}
}
// 检查相邻两组数据是否相同
const checkAdjacentDataSame = (formDataArray: PublishBallFormData[]) => {
if (formDataArray.length < 2) return false
const lastIndex = formDataArray.length - 1
const secondLastIndex = formDataArray.length - 2
const lastData = formDataArray[lastIndex]
const secondLastData = formDataArray[secondLastIndex]
// 比较关键字段是否相同
return (JSON.stringify(lastData) === JSON.stringify(secondLastData))
}
const handleAdd = () => {
// 检查最后两组数据是否相同
if (checkAdjacentDataSame(formData)) {
Taro.showToast({
title: '信息不可与前序场完全一致',
icon: 'none'
})
return
}
const newStartTime = getNextHourTime()
setFormData(prev => [...prev, {
...defaultFormData,
title: '',
start_time: newStartTime,
end_time: getEndTime(newStartTime)
timeRange: {
start_time: newStartTime,
end_time: getEndTime(newStartTime)
}
}])
}
// 复制上一场数据
const handleCopyPrevious = (index: number) => {
@@ -94,7 +128,7 @@ const PublishBall: React.FC = () => {
return newData
})
Taro.showToast({
title: '复制上一场数据',
title: '复制上一场填入',
icon: 'success'
})
}
@@ -128,42 +162,59 @@ const PublishBall: React.FC = () => {
}
}
const validateFormData = (formData: PublishBallFormData) => {
const validateFormData = (formData: PublishBallFormData, isOnSubmit: boolean = false) => {
const { activityInfo, image_list, title } = formData;
const { play_type, price, location_name } = activityInfo;
if (!image_list.length) {
Taro.showToast({
title: `请上传活动封面`,
icon: 'none'
})
if (!image_list?.length) {
if (!isOnSubmit) {
Taro.showToast({
title: `请上传活动封面`,
icon: 'none'
})
}
return false
}
if (!title) {
Taro.showToast({
title: `请输入活动标题`,
icon: 'none'
})
if (!isOnSubmit) {
Taro.showToast({
title: `请输入活动标题`,
icon: 'none'
})
}
return false
}
if (!price) {
Taro.showToast({
title: `请输入费用`,
icon: 'none'
})
if (!price || (typeof price === 'number' && price <= 0) || (typeof price === 'string' && !price.trim())) {
if (!isOnSubmit) {
Taro.showToast({
title: `请输入费用`,
icon: 'none'
})
}
return false
}
if (!play_type) {
Taro.showToast({
title: `请选择玩法类型`,
icon: 'none'
})
if (!play_type || !play_type.trim()) {
if (!isOnSubmit) {
Taro.showToast({
title: `请选择玩法类型`,
icon: 'none'
})
}
return false
}
if (!location_name) {
Taro.showToast({
title: `请选择场地`,
icon: 'none'
})
if (!location_name || !location_name.trim()) {
if (!isOnSubmit) {
Taro.showToast({
title: `请选择场地`,
icon: 'none'
})
}
return false
}
return true
}
const validateOnSubmit = () => {
const isValid = activityType === 'individual' ? validateFormData(formData[0], true) : formData.every(item => validateFormData(item, true))
if (!isValid) {
return false
}
return true
@@ -178,7 +229,7 @@ const PublishBall: React.FC = () => {
if (!isValid) {
return
}
const { activityInfo, descriptionInfo, timeRange, players, skill_level, ...rest } = formData[0];
const { activityInfo, descriptionInfo, timeRange, players, skill_level,image_list, ...rest } = formData[0];
const options = {
...rest,
...activityInfo,
@@ -187,7 +238,8 @@ const PublishBall: React.FC = () => {
max_players: players[1],
current_players: players[0],
skill_level_min: skill_level[0],
skill_level_max: skill_level[1]
skill_level_max: skill_level[1],
image_list: image_list.map(item => item.url)
}
const res = await PublishService.createPersonal(options);
if (res.code === 0 && res.data) {
@@ -195,6 +247,59 @@ const PublishBall: React.FC = () => {
title: '发布成功',
icon: 'success'
})
delay(1000)
// 如果是个人球局,则跳转到详情页,并自动分享
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
Taro.navigateTo({
// @ts-expect-error: id
url: `/pages/detail/index?id=${res.data.id || 1}&from=publish&autoShare=1`
})
} else {
Taro.showToast({
title: res.message,
icon: 'none'
})
}
}
if (activityType === 'group') {
const isValid = formData.every(item => validateFormData(item))
if (!isValid) {
return
}
if (checkAdjacentDataSame(formData)) {
Taro.showToast({
title: '信息不可与前序场完全一致',
icon: 'none'
})
return
}
const options = formData.map((item) => {
const { activityInfo, descriptionInfo, timeRange, players, skill_level, ...rest } = item;
return {
...rest,
...activityInfo,
...descriptionInfo,
...timeRange,
max_players: players[1],
current_players: players[0],
skill_level_min: skill_level[0],
skill_level_max: skill_level[1],
image_list: item.image_list.map(img => img.url)
}
})
const res = await PublishService.create_play_pmoothlys({rows: options});
if (res.code === 0 && res.data) {
Taro.showToast({
title: '发布成功',
icon: 'success'
})
delay(1000)
// 如果是个人球局,则跳转到详情页,并自动分享
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
Taro.navigateTo({
// @ts-expect-error: id
url: `/pages/detail/index?id=${res.data?.[0].id || 1}&from=publish&autoShare=1`
})
} else {
Taro.showToast({
title: res.message,
@@ -204,16 +309,75 @@ const PublishBall: React.FC = () => {
}
}
const initFormData = () => {
const currentInstance = Taro.getCurrentInstance()
const params = currentInstance.router?.params
if (params?.type) {
const type = params.type as ActivityType
if (type === 'individual' || type === 'group') {
setActivityType(type)
if (type === 'group') {
const newFormSchema = publishBallFormSchema.reduce((acc, item) => {
if (item.prop === 'is_wechat_contact') {
return acc
}
if (item.prop === 'image_list') {
if (item.props) {
item.props.source = ['album', 'history']
}
}
if (item.prop === 'players') {
if (item.props) {
item.props.max = 100
}
}
acc.push(item)
return acc
}, [] as FormFieldConfig[])
setOptionsConfig(newFormSchema)
setFormData([defaultFormData])
}
// 根据type设置导航标题
if (type === 'group') {
Taro.setNavigationBarTitle({
title: '发布畅打活动'
})
} else {
Taro.setNavigationBarTitle({
title: '发布'
})
}
}
handleActivityTypeChange(type)
}
}
const onCheckedChange = (checked: boolean) => {
setChecked(checked)
}
useEffect(() => {
const isValid = validateOnSubmit()
if (!isValid) {
setIsSubmitDisabled(true)
} else {
setIsSubmitDisabled(false)
}
console.log(formData, 'formData');
}, [formData])
useEffect(() => {
initFormData()
}, [])
return (
<View className={styles['publish-ball']}>
{/* 活动类型切换 */}
<View className={styles['activity-type-switch']}>
<ActivityTypeSwitch
{/* <ActivityTypeSwitch
value={activityType}
onChange={handleActivityTypeChange}
/>
/> */}
</View>
<View className={styles['publish-ball__scroll']}>
{
formData.map((item, index) => (
@@ -223,19 +387,19 @@ const PublishBall: React.FC = () => {
<View className={styles['session-header']}>
<View className={styles['session-title']}>
{index + 1}
<View
className={styles['session-delete']}
<View
className={styles['session-delete']}
onClick={() => showDeleteConfirm(index)}
>
<Image src={images.ICON_DELETE} className={styles['session-delete-icon']} />
</View>
</View>
<View className={styles['session-actions']}>
{index > 0 && (
<View
className={styles['session-action-btn']}
<View
className={styles['session-action-btn']}
onClick={() => handleCopyPrevious(index)}
>
@@ -244,10 +408,10 @@ const PublishBall: React.FC = () => {
</View>
</View>
)}
<PublishForm
formData={item}
onChange={(key, value) => updateFormData(key, value, index)}
optionsConfig={publishBallFormSchema}
<PublishForm
formData={item}
onChange={(key, value) => updateFormData(key, value, index)}
optionsConfig={optionsConfig}
/>
</View>
))
@@ -263,38 +427,40 @@ const PublishBall: React.FC = () => {
</View>
{/* 删除确认弹窗 */}
{deleteConfirm.visible && (
<View className={styles['delete-modal']}>
<View className={styles['delete-modal__content']}>
<Text className={styles['delete-modal__title']}></Text>
<Text className={styles['delete-modal__desc']}></Text>
<View className={styles['delete-modal__actions']}>
<Button
className={styles['delete-modal__btn']}
onClick={closeDeleteConfirm}
>
</Button>
<Button
className={styles['delete-modal__btn']}
onClick={confirmDelete}
>
</Button>
</View>
</View>
</View>
)}
<CommonDialog
visible={deleteConfirm.visible}
cancelText="再想想"
confirmText="确认移除"
onCancel={closeDeleteConfirm}
onConfirm={confirmDelete}
contentTitle="确认移除该场次?"
contentDesc="该操作不可恢复"
/>
{/* 完成按钮 */}
<View className={styles['submit-section']}>
<Button className={styles['submit-btn']} onClick={handleSubmit}>
<Button className={`${styles['submit-btn']} ${isSubmitDisabled ? styles['submit-btn-disabled'] : ''}`} onClick={handleSubmit}>
</Button>
<Text className={styles['submit-tip']}>
<Text className={styles['link']}></Text>
</Text>
{
activityType === 'individual' && (
<Text className={styles['submit-tip']}>
<Text className={styles['link']}></Text>
</Text>
)
}
{
activityType === 'group' && (
<View className={styles['submit-tip']}>
<Checkbox
className={styles['submit-checkbox']}
checked={checked}
onChange={onCheckedChange}
/>
</View>
)
}
</View>
</View>
)

View File

@@ -31,13 +31,16 @@ const PublishForm: React.FC<{
// 字典数据相关
const { getDictionaryValue } = useDictionaryActions()
useEffect(() => {
setCoverImages(formData.image_list)
}, [formData.image_list])
// 处理封面图片变化
const handleCoverImagesChange = (fn: (images: CoverImage[]) => CoverImage[]) => {
if (fn instanceof Function) {
setCoverImages(fn(coverImages))
} else {
setCoverImages(fn)
}
const newImages = fn instanceof Function ? fn(coverImages) : fn
setCoverImages(newImages)
onChange('image_list', newImages)
}
// 更新表单数据
@@ -88,9 +91,44 @@ const PublishForm: React.FC<{
})
}
const getNTRPText = (ntrp: [number, number]) => {
const [min, max] = ntrp
if (min === 1.0 && max === 5.0) {
return '不限'
}
if (min === 5.0 && max === 5.0) {
return '5.0 及以上'
}
if (min === 1.0 && max === 1.0) {
return `${min.toFixed(1)}`
}
if (min > 1.0 && max === 5.0) {
return `${min.toFixed(1)} 以上`
}
if (min === 1.0 && max < 5.0) {
return `${max.toFixed(1)} 以下`
}
if (min > 1.0 && max < 5.0) {
return `${min.toFixed(1)} - ${max.toFixed(1)}之间`
}
return '';
}
const getPlayersText = (players: [number, number]) => {
const [min, max] = players
return `最少${min}人,最多${max}`
}
const renderSummary = (item: FormFieldConfig) => {
if (item.props?.showSummary) {
return <Text className={styles['section-summary']}>{item.props?.summary}</Text>
if (item.prop === 'skill_level') {
return <Text className={styles['section-summary']}>{getNTRPText(formData.skill_level)}</Text>
}
if (item.prop === 'players') {
return <Text className={styles['section-summary']}>{getPlayersText(formData.players)}</Text>
}
}
return null
}

View File

@@ -1,3 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '首页'
navigationBarTitleText: ''
})

120
src/pages/search/index.scss Normal file
View File

@@ -0,0 +1,120 @@
.listSearchContainer {
padding: 0 15px;
.icon16 {
width: 16px;
height: 16px;
}
.topSearch {
padding: 10px 16px 5px 12px;
display: flex;
align-items: center;
height: 44px;
box-sizing: border-box;
gap: 10px;
border-radius: 44px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #FFF;
box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08);
.nut-input {
padding: 0;
height: 100%;
}
}
.searchRight {
display: flex;
align-items: center;
gap: 12px;
.searchLine {
width: 1px;
height: 20px;
border-radius: 20px;
background: rgba(0, 0, 0, 0.06);
}
.searchText {
color: #000000;
font-size: 16px;
font-weight: 600;
line-height: 20px
}
}
.searchIcon {
width: 20px;
height: 20px;
}
.historySearchTitleWrapper {
display: flex;
padding: 12px 15px;
justify-content: space-between;
align-items: flex-end;
align-self: stretch;
.historySearchTitle,
.historySearchClear {
color: #000;
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
.historySearchClear {
color: #9a9a9a;
display: flex;
align-items: center;
gap: 4px;
}
}
.historySearchList {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
.historySearchItem {
flex-shrink: 0;
flex-grow: 0;
display: flex;
height: 28px;
padding: 4px 12px;
justify-content: center;
align-items: center;
gap: 2px;
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.03);
}
}
.searchSuggestion {
padding: 6px 0;
.searchSuggestionItem {
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
.searchSuggestionItemLeft {
display: flex;
align-items: center;
gap: 12px;
color: rgba(60, 60, 67, 0.60);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.highlight {
color: #000000;
}
}
}
}

200
src/pages/search/index.tsx Normal file
View File

@@ -0,0 +1,200 @@
import CustomerNavbarBack from "@/components/CustomerNavbarBack";
import { View, Image, Text } from "@tarojs/components";
import { Input } from "@nutui/nutui-react-taro";
import { useEffect, useMemo, useRef } from "react";
import { useListState } from "@/store/listStore";
import img from "@/config/images";
import "./index.scss";
import Taro from "@tarojs/taro";
const ListSearch = () => {
const {
searchValue,
updateState,
getSearchHistory,
searchHistory = [],
clearHistory,
searchSuggestion,
suggestionList,
isShowSuggestion,
} = useListState() || {};
const ref = useRef<any>(null);
useEffect(() => {
getSearchHistory();
}, []);
useEffect(() => {
if (ref?.current) {
ref.current.focus();
}
}, [ref.current]);
const regex = useMemo(() => {
return new RegExp(searchValue, "gi");
}, [searchValue]);
/**
* @description 输入
* @param value
*/
const handleChange = (value: string) => {
updateState({ searchValue: value });
if (value) {
searchSuggestion(value);
} else {
updateState({
isShowSuggestion: false,
});
}
};
/**
* @description 点击清空输入内容
*/
const handleClear = () => {
updateState({ searchValue: "" });
};
/**
* @description 点击历史搜索
* @param value
*/
const handleHistoryClick = (value: string) => {
updateState({ searchValue: value });
handleSearch();
};
/**
* @description 清空历史搜索
*/
const handleClearHistory = () => {
clearHistory();
};
/**
* @description 点击联想词
*/
const handleSuggestionSearch = (val: string) => {
updateState({
searchValue: val,
isShowSuggestion: false,
});
handleSearch();
};
/**
* @description 点击搜索
*/
const handleSearch = () => {
Taro.navigateTo({
url: `/pages/searchResult/index`,
});
};
// 是否显示清空图标
const isShowClearIcon = searchValue && searchValue?.length > 0;
// 是否显示搜索历史
const isShowHistory =
!isShowClearIcon && searchHistory && searchHistory?.length > 0;
return (
<>
<View className="listSearchContainer">
{/* 搜索 */}
<View className="topSearch">
<Image className="searchIcon" src={img.ICON_LIST_SEARCH_SEARCH} />
<Input
placeholder="搜索上海的球局和场地"
value={searchValue}
defaultValue={searchValue}
onChange={handleChange}
onClear={handleClear}
autoFocus
clearable={false}
ref={ref}
/>
<View className="searchRight">
{isShowClearIcon && (
<Image
className="clearIcon icon16"
src={img.ICON_LIST_SEARCH_CLEAR}
onClick={handleClear}
/>
)}
<View className="searchLine" />
<Text className="searchText" onClick={handleSearch}>
</Text>
</View>
</View>
{/* 联想词 */}
{isShowSuggestion && (
<View className="searchSuggestion">
{(suggestionList || [])?.map((item) => {
// 替换匹配文本为高亮版本
const highlightedText = item.replace(regex, (match) => {
// 如果匹配不到,则返回原文本
if (!match) return match;
return `<Text class="highlight">${match}</Text>`;
});
return (
<View
className="searchSuggestionItem"
onClick={() => handleSuggestionSearch(item)}
>
<View className="searchSuggestionItemLeft">
<Image
className="icon16"
src={img.ICON_LIST_SEARCH_SEARCH}
/>
<Text
dangerouslySetInnerHTML={{ __html: highlightedText }}
></Text>
</View>
<Image
className="icon16"
src={img.ICON_LIST_SEARCH_SUGGESTION}
/>
</View>
);
})}
</View>
)}
{/* 历史搜索 */}
{!isShowClearIcon && (
<View className="historySearch">
<View className="historySearchTitleWrapper">
<View className="historySearchTitle"></View>
<View className="historySearchClear" onClick={handleClearHistory}>
<Text></Text>
<Image
className="clearIcon icon16"
src={img.ICON_LIST_SEARCH_CLEAR_HISTORY}
/>
</View>
</View>
{isShowHistory && (
<View className="historySearchList">
{(searchHistory || [])?.map((item) => {
return (
<Text
className="historySearchItem"
onClick={() => handleHistoryClick(item)}
>
{item}
</Text>
);
})}
</View>
)}
</View>
)}
</View>
</>
);
};
export default ListSearch;

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '搜索结果',
// navigationStyle: 'custom',
})

View File

@@ -0,0 +1,18 @@
.searchResultPage {
position: relative;
.searchResultFilterWrapper {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 0px 10px 0px;
position: sticky;
top: -1px;
background-color: #fefefe;
z-index: 123;
}
.menuFilter {
padding: 0;
}
}

View File

@@ -0,0 +1,71 @@
import { View } from "@tarojs/components";
import { useListState } from "@/store/listStore";
import { useGlobalState } from "@/store/global";
import ListContainer from "@/container/listContainer";
import "./index.scss";
import DistanceQuickFilter from "@/components/DistanceQuickFilter";
import { useEffect } from "react";
const SearchResult = () => {
const {
distanceData,
quickFilterData,
distanceQuickFilter,
updateState,
matches,
recommendList,
loading,
error,
refreshMatches,
fetchMatches,
} = useListState() || {};
const { statusNavbarHeightInfo } = useGlobalState() || {};
const { totalHeight } = statusNavbarHeightInfo || {}
useEffect(() => {
// 页面加载时获取数据
fetchMatches();
}, []);
// 距离筛选
const handleDistanceOrQuickChange = (name, value) => {
updateState({
distanceQuickFilter: {
...distanceQuickFilter,
[name]: value,
},
});
};
return (
<View className="searchResultPage">
{/* 筛选 */}
<View className='searchResultFilterWrapper' style={{
// top: `${totalHeight}px`
}}>
<DistanceQuickFilter
cityOptions={distanceData}
quickOptions={quickFilterData}
onChange={handleDistanceOrQuickChange}
cityName="distance"
quickName="quick"
cityValue={distanceQuickFilter?.distance}
quickValue={distanceQuickFilter?.quick}
/>
</View>
{/* 列表内容 */}
<ListContainer
data={matches}
recommendList={recommendList}
loading={loading}
error={error}
reload={refreshMatches}
/>
</View>
);
};
export default SearchResult;