diff --git a/analyze.js b/analyze.js new file mode 100644 index 0000000..3b3453e --- /dev/null +++ b/analyze.js @@ -0,0 +1,50 @@ +const fs = require('fs'); +const path = require('path'); + +// dist 目录路径,根据你项目实际情况修改 +const DIST_DIR = path.join(__dirname, 'dist'); + +// 递归统计文件大小 +function getFiles(dir) { + let results = []; + const list = fs.readdirSync(dir); + list.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + if (stat && stat.isDirectory()) { + results = results.concat(getFiles(filePath)); + } else { + results.push({ path: filePath, size: stat.size }); + } + }); + return results; +} + +function formatSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; +} + +function analyze() { + if (!fs.existsSync(DIST_DIR)) { + console.error('dist 目录不存在,请先执行 taro build --type weapp'); + return; + } + + const files = getFiles(DIST_DIR); + const total = files.reduce((sum, f) => sum + f.size, 0); + + console.log('文件大小分析(按从大到小排序):'); + files + .sort((a, b) => b.size - a.size) + .forEach(f => { + console.log( + `${formatSize(f.size)} | ${(f.size / total * 100).toFixed(2)}% | ${path.relative(DIST_DIR, f.path)}` + ); + }); + + console.log(`\n总大小: ${formatSize(total)}`); +} + +analyze(); diff --git a/config/index.ts b/config/index.ts index 9e16ab3..56b2b5f 100644 --- a/config/index.ts +++ b/config/index.ts @@ -16,7 +16,7 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { 750: 1, 375: 2, 828: 1.81 / 2, - 390: 1.92 + 390: 1.92 }, sourceRoot: 'src', outputRoot: 'dist', @@ -66,9 +66,16 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { } } }, + miniCssExtractPluginOption: { + ignoreOrder: true, + // filename: 'css/[name].[hash].css', + // chunkFilename: 'css/[name].[chunkhash].css' + }, webpackChain(chain) { chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin) - } + }, + // @ts-expect-error: Taro 类型定义缺少 mini.hot + hot: true, }, h5: { publicPath: '/', diff --git a/package.json b/package.json index 9ef1f6d..4b34573 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "@tarojs/shared": "4.1.5", "@tarojs/taro": "4.1.5", "dayjs": "^1.11.13", - "qqmap-wx-jssdk": "^1.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "zustand": "^4.4.7" diff --git a/src/pages/index/index.module.scss b/postcss.config.js similarity index 100% rename from src/pages/index/index.module.scss rename to postcss.config.js diff --git a/src/app.config.ts b/src/app.config.ts index 901708f..eee34b8 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,14 +1,18 @@ export default defineAppConfig({ pages: [ - 'pages/list/index', - // 'pages/userInfo/myself/index', + // 'pages/publishBall/index', + 'pages/list/index', // 列表页 + 'pages/search/index', // 搜索页 + 'pages/searchResult/index', // 搜索结果页面 + 'pages/userInfo/myself/index', 'pages/login/index/index', 'pages/login/verification/index', 'pages/login/terms/index', - 'pages/publishBall/index', // 'pages/mapDisplay/index', - 'pages/index/index', 'pages/detail/index', + 'pages/message/index', + 'pages/personal/index', + 'pages/orderCheck/index', ], window: { backgroundTextStyle: 'light', diff --git a/src/app.scss b/src/app.scss index e69de29..3228f0b 100644 --- a/src/app.scss +++ b/src/app.scss @@ -0,0 +1,4 @@ +@font-face { + font-family: 'PoetsenOne'; + src: url('./static/asserts/fonts/PoetsenOne-Regular.ttf') format('truetype'); +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 6ef979c..7ce6cd4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,10 @@ import { Component, ReactNode } from 'react' -import './app.scss' import './nutui-theme.scss' +import './app.scss' import { useDictionaryStore } from './store/dictionaryStore' +import { useGlobalStore } from './store/global' + +// import { getNavbarHeight } from "@/utils/getNavbarHeight"; interface AppProps { children: ReactNode @@ -11,11 +14,12 @@ class App extends Component { componentDidMount() { // 初始化字典数据 this.initDictionaryData() + this.getNavBarHeight() } - componentDidShow() {} + componentDidShow() { } - componentDidHide() {} + componentDidHide() { } // 初始化字典数据 private async initDictionaryData() { @@ -27,6 +31,13 @@ class App extends Component { } } + // 获取导航高度 + getNavBarHeight = () => { + const { getNavbarHeightInfo } = useGlobalStore.getState() + getNavbarHeightInfo() + + } + render() { // this.props.children 是将要会渲染的页面 return this.props.children diff --git a/src/components/CalendarCard/CalendarCard.tsx b/src/components/CalendarCard/CalendarCard.tsx new file mode 100644 index 0000000..eb1e23c --- /dev/null +++ b/src/components/CalendarCard/CalendarCard.tsx @@ -0,0 +1,117 @@ +import React, { useMemo, useState } from 'react' +import { View, Text, Image } from '@tarojs/components' +import styles from './index.module.scss' +import images from '@/config/images' +interface CalendarCardProps { + value?: Date + minDate?: Date + maxDate?: Date + onChange?: (date: Date) => void + onNext?: (date: Date) => void + onHeaderClick?: (date: Date) => void +} + +const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1) +const endOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth() + 1, 0) +const addMonths = (date: Date, delta: number) => new Date(date.getFullYear(), date.getMonth() + delta, 1) + +const formatHeader = (date: Date) => `${date.getMonth() + 1}月 ${date.getFullYear()}` + +const CalendarCard: React.FC = ({ + value, + minDate, + maxDate, + onChange, + onHeaderClick +}) => { + const today = new Date() + const [current, setCurrent] = useState(value || startOfMonth(today)) + const [selected, setSelected] = useState(value || today) + + + const firstDay = useMemo(() => startOfMonth(current), [current]) + const lastDay = useMemo(() => endOfMonth(current), [current]) + + const days = useMemo(() => { + const startWeekday = firstDay.getDay() // 0 周日 + const prevPadding = startWeekday // 周日为第一列 + const total = prevPadding + lastDay.getDate() + const rows = Math.ceil(total / 7) + const grid: (Date | null)[] = [] + for (let i = 0; i < rows * 7; i++) { + const day = i - prevPadding + 1 + if (day < 1 || day > lastDay.getDate()) { + grid.push(null) + } else { + grid.push(new Date(current.getFullYear(), current.getMonth(), day)) + } + } + return grid + }, [firstDay, lastDay, current]) + + const isDisabled = (d: Date) => { + if (minDate && d < minDate) return true + if (maxDate && d > maxDate) return true + return false + } + + const gotoMonth = (delta: number) => setCurrent(prev => addMonths(prev, delta)) + + const handleHeaderClick = () => { + onHeaderClick && onHeaderClick(current) + } + + const handleSelectDay = (d: Date | null) => { + if (!d || isDisabled(d)) return + setSelected(d) + onChange && onChange(d) + } + + + + + + + + return ( + + + + {formatHeader(current)} + gotoMonth(1)} /> + + + gotoMonth(-1)} /> + gotoMonth(1)} /> + + + + + {['周日','周一','周二','周三','周四','周五','周六'].map((w) => ( + {w} + ))} + + + + {days.map((d, idx) => { + const isSelected = !!(d && selected && d.toDateString() === new Date(selected.getFullYear(), selected.getMonth(), selected.getDate()).toDateString()) + return ( + handleSelectDay(d)} + > + {d ? {d.getDate()} : null} + + ) + })} + + + + + + + ) +} + +export default CalendarCard diff --git a/src/components/CalendarCard/DialogCalendarCard.tsx b/src/components/CalendarCard/DialogCalendarCard.tsx new file mode 100644 index 0000000..92bfec4 --- /dev/null +++ b/src/components/CalendarCard/DialogCalendarCard.tsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react' +import CommonPopup from '@/components/CommonPopup' +import CalendarCard from './CalendarCard' +import DateTimePicker from '@/components/DateTimePicker' +import HourMinutePicker from '@/components/HourMinutePicker' +export interface DialogCalendarCardProps { + value?: Date + minDate?: Date + maxDate?: Date + onChange?: (date: Date) => void + onNext?: (date: Date) => void + visible: boolean + onClose: () => void + title?: React.ReactNode +} + +const DialogCalendarCard: React.FC = ({ + visible, + onClose, + title, + value, + minDate, + maxDate, + onChange, + onNext +}) => { + const [selected, setSelected] = useState(value || new Date()) + const [type, setType] = useState<'year' | 'month' | 'time'>('year'); + const [selectedHour, setSelectedHour] = useState(8) + const [selectedMinute, setSelectedMinute] = useState(0) + + const handleConfirm = () => { + if (type === 'year') { + // 年份选择完成后,进入月份选择 + setType('time') + } else if (type === 'month') { + // 月份选择完成后,进入时间选择 + setType('year') + } else if (type === 'time') { + // 时间选择完成后,调用onNext回调 + const finalDate = new Date(selected.getFullYear(), selected.getMonth(), selected.getDate(), selectedHour, selectedMinute) + console.log('finalDate', finalDate) + if (onChange) onChange(finalDate) + onClose() + } + } + + const handleChange = (d: Date) => { + console.log('handleChange', d) + setSelected(d) + // if (onChange) onChange(d) + } + const onHeaderClick = (date: Date) => { + console.log('onHeaderClick', date) + setSelected(date) + setType('month') + } + const getConfirmText = () => { + if (type === 'time' || type === 'month') return '完成' + return '下一步' + } + const handleDateTimePickerChange = (year: number, month: number) => { + console.log('year', year) + console.log('month', month) + setSelected(new Date(year, month - 1, 1)) + } + const dialogClose = () => { + if (type === 'month') { + setType('year') + } else if (type === 'time') { + setType('year') + } else { + onClose() + } + } + useEffect(() => { + setSelected(value || new Date()) + if (visible) { + setType('year') + setSelectedHour(8) + setSelectedMinute(0) + } + }, [value, visible]) + + + return ( + + { + type === 'year' && + } + { + type === 'month' && + } + { + type === 'time' && { + setSelectedHour(hour) + setSelectedMinute(minute) + }} + defaultHour={selectedHour} + defaultMinute={selectedMinute} + /> + } + + ) +} + +export default DialogCalendarCard diff --git a/src/components/CalendarCard/index.module.scss b/src/components/CalendarCard/index.module.scss new file mode 100644 index 0000000..e68e1ef --- /dev/null +++ b/src/components/CalendarCard/index.module.scss @@ -0,0 +1,105 @@ +.calendar-card { + background: #fff; + border-radius: 16px; + padding: 12px 12px 8px; + } + + .header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 16px 11px 16px; + height: 24px; + } + .header-left { + display: flex; + align-items: center; + gap: 6px; + } + .header-text { + font-size: 17px; + font-weight: 600; + color: #000; + } + .header-actions { + display: flex; + gap: 30px; + } + .month-arrow{ + width: 8px + } + .arrow { + width: 10px; + position: relative; + } + .arrow.left { + left: 9px; + transform: rotate(-180deg); + } + + .week-row { + display: grid; + grid-template-columns: repeat(7, 1fr); + padding: 0 0 4px 0; + } + .week-item { + text-align: center; + color: rgba(60, 60, 67, 0.30); + font-size: 13px; + } + + .grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 8px 0; + padding: 4px 0 16px; + } + .cell { + height: 44px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + } + .cell.empty { + opacity: 0; + } + .cell.disabled { + color: rgba(0,0,0,0.2); + } + .cell-text.selected { + width: 44px; + height: 44px; + border-radius: 22px; + background: rgba(0,0,0,0.9); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + } + + .footer { + display: flex; + gap: 12px; + } + .btn { + flex: 1; + height: 44px; + border-radius: 22px; + background: rgba(0,0,0,0.06); + display: flex; + align-items: center; + justify-content: center; + } + .btn.primary { + background: #000; + color: #fff; + } + + .hm-placeholder { + height: 240px; + display: flex; + align-items: center; + justify-content: center; + } + \ No newline at end of file diff --git a/src/components/CalendarCard/index.ts b/src/components/CalendarCard/index.ts new file mode 100644 index 0000000..15ceb7e --- /dev/null +++ b/src/components/CalendarCard/index.ts @@ -0,0 +1,2 @@ +export { default } from './CalendarCard' +export { default as DialogCalendarCard } from './DialogCalendarCard' diff --git a/src/components/CityFilter/index.tsx b/src/components/CityFilter/index.tsx index 9784915..5af6117 100644 --- a/src/components/CityFilter/index.tsx +++ b/src/components/CityFilter/index.tsx @@ -9,7 +9,7 @@ import {DistanceFilterProps} from '../../../types/list/types' const MenuComponent = (props: DistanceFilterProps) => { - const { value, onChange, wrapperClassName, itemClassName, options, name } = + const { value, onChange, wrapperClassName, itemClassName, options, name, onOpen, onClose } = props; const [isChange, setIsChange] = useState(false); const [iOpen, setIsOpen] = useState(false); @@ -23,10 +23,12 @@ const MenuComponent = (props: DistanceFilterProps) => { const handleOpen = () => { setIsOpen(true); + onOpen && typeof onOpen === "function" && onOpen(); }; const handleClose = () => { setIsOpen(false); + onClose && typeof onClose === "function" && onClose(); }; return ( diff --git a/src/components/CommonDialog/CommonDialog.tsx b/src/components/CommonDialog/CommonDialog.tsx new file mode 100644 index 0000000..e50d9ab --- /dev/null +++ b/src/components/CommonDialog/CommonDialog.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { Dialog } from '@nutui/nutui-react-taro' +import { View, Text } from '@tarojs/components' +import styles from './index.module.scss' + +export interface CommonDialogProps { + visible: boolean + title?: string + content?: React.ReactNode + cancelText?: string + confirmText?: string + onCancel?: () => void + onConfirm?: () => void + showCancel?: boolean + showConfirm?: boolean + children?: React.ReactNode + contentTitle?: string + contentDesc?: string +} + +const CommonDialog: React.FC = ({ + visible, + title, + content, + cancelText = '取消', + confirmText = '确认', + onCancel, + onConfirm, + children, + contentTitle, + contentDesc +}) => { + + const getContent = () => { + if (content) { + return content + } + if (children) { + return children + } + return ( + + {contentTitle} + {contentDesc} + + ) + } + return ( + + ) +} + +export default CommonDialog \ No newline at end of file diff --git a/src/components/CommonDialog/index.module.scss b/src/components/CommonDialog/index.module.scss new file mode 100644 index 0000000..10c6e0a --- /dev/null +++ b/src/components/CommonDialog/index.module.scss @@ -0,0 +1,86 @@ +@use '~@/scss/themeColor.scss' as theme; + +.custom-dialog { + :global(.nut-dialog) { + border-radius: 12px !important; + padding: 0 !important; + max-width: 320px !important; + width: 100% !important; + text-align: center !important; + } + :global(.nut-dialog-content) { + margin:0 !important; + } + + :global(.nut-dialog-header) { + margin-bottom: 8px !important; + } + + :global(.nut-dialog-title) { + font-size: 18px !important; + font-weight: 600 !important; + color: #000000 !important; + } + + :global(.nut-dialog-content) { + min-width: 280px !important; + } + + :global(.nut-dialog-footer) { + display: flex !important; + padding: 0 !important; + height: 47.5px; + justify-content: center; + align-items: center; + border-top: 1px solid theme.$primary-border-light-color; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + overflow: hidden; + } + + :global(.nut-dialog-footer .nut-button) { + flex: 1 !important; + font-size: 15px !important; + border-radius: 0; + padding: 0; + margin: 0; + border: none; + background: #fff; + border-right: 1px solid theme.$primary-border-light-color !important; + height: 100%; + &:last-child { + border-right: none !important; + } + } + + :global(.nut-dialog-footer .nut-button-default) { + color: rgba(22, 24, 35, 0.75) !important; + } + + :global(.nut-dialog-footer .nut-button-primary) { + color: #161823 !important; + + } + + :global(.nut-dialog-footer .nut-button:hover) { + opacity: 0.8 !important; + } + .confirm-content{ + padding: 24px 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + .confirm-content-title{ + font-size: 17px; + font-weight: 500; + line-height: 24px; + color: #161823; + } + .confirm-content-desc{ + font-size: 14px; + color: rgba(22, 24, 35, 0.75) + } + } +} \ No newline at end of file diff --git a/src/components/CommonDialog/index.ts b/src/components/CommonDialog/index.ts new file mode 100644 index 0000000..4ccbfef --- /dev/null +++ b/src/components/CommonDialog/index.ts @@ -0,0 +1,2 @@ +import CommonDialog from './CommonDialog.tsx' +export default CommonDialog diff --git a/src/components/CommonPopup/CommonPopup.tsx b/src/components/CommonPopup/CommonPopup.tsx index a8dbeef..d7b95da 100644 --- a/src/components/CommonPopup/CommonPopup.tsx +++ b/src/components/CommonPopup/CommonPopup.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useRef, useState } from 'react' import { View, Text } from '@tarojs/components' import { Popup, Button } from '@nutui/nutui-react-taro' import styles from './index.module.scss' @@ -18,6 +18,8 @@ export interface CommonPopupProps { zIndex?: number children?: React.ReactNode className?: string + style?: React.CSSProperties + enableDragToClose?: boolean } const CommonPopup: React.FC = ({ @@ -34,8 +36,15 @@ const CommonPopup: React.FC = ({ position = 'bottom', round = true, zIndex, - children + style, + children, + enableDragToClose = true }) => { + const [dragOffset, setDragOffset] = useState(0) + const [isDragging, setIsDragging] = useState(false) + const touchStartY = useRef(0) + const popupRef = useRef(null) + const handleCancel = () => { if (onCancel) { onCancel() @@ -44,6 +53,39 @@ const CommonPopup: React.FC = ({ } } + const handleTouchStart = (e: any) => { + if (!enableDragToClose) return + + touchStartY.current = e.touches[0].clientY + setIsDragging(true) + } + + const handleTouchMove = (e: any) => { + if (!enableDragToClose || !isDragging) return + + const currentY = e.touches[0].clientY + const deltaY = currentY - touchStartY.current + + // 只允许向下拖动,限制最大拖动距离 + if (deltaY > 0) { + setDragOffset(Math.min(deltaY, 200)) + } + } + + const handleTouchEnd = () => { + if (!enableDragToClose || !isDragging) return + + setIsDragging(false) + + // 如果拖动距离超过阈值,关闭弹窗 + if (dragOffset > 100) { + onClose() + } + + // 重置拖动偏移 + setDragOffset(0) + } + return ( = ({ closeable={false} onClose={onClose} className={`${styles['common-popup']} ${className ? className : ''}`} - style={zIndex ? { zIndex } : undefined} + style={{ + zIndex: zIndex ? zIndex : undefined, + ...style + }} > + {enableDragToClose && ( + + + + )} + {showHeader && ( {typeof title === 'string' ? {title} : title} @@ -78,4 +139,4 @@ const CommonPopup: React.FC = ({ ) } -export default CommonPopup \ No newline at end of file +export default CommonPopup \ No newline at end of file diff --git a/src/components/CommonPopup/index.module.scss b/src/components/CommonPopup/index.module.scss index fdfc417..1fb1e10 100644 --- a/src/components/CommonPopup/index.module.scss +++ b/src/components/CommonPopup/index.module.scss @@ -1,6 +1,25 @@ @use '~@/scss/themeColor.scss' as theme; .common-popup { + .common-popup__drag-handle-container { + position: position; + } + .common-popup__drag-handle { + position: absolute; + top: 6px; + left: 50%; + width: 32px; + height: 4px; + background-color: rgba(22, 24, 35, 0.20); + border-radius: 2px; + z-index: 10; + cursor: pointer; + transition: background-color 0.2s ease; + + &:active { + background-color: #9ca3af; + } + } padding: 0; box-sizing: border-box; max-height: calc(100vh - 10px); diff --git a/src/components/CustomNavbar/index.module.scss b/src/components/CustomNavbar/index.module.scss index b3a7de1..b15b2d5 100644 --- a/src/components/CustomNavbar/index.module.scss +++ b/src/components/CustomNavbar/index.module.scss @@ -1,47 +1,6 @@ .customerNavbar { - // background-color: red; - - .container { - padding-left: 17px; - display: flex; - align-items: center; - gap: 8px; - } - - .line { - width: 1px; - height: 25px; - background-color: #0000000F; - } - - .logo { - width: 60px; - height: 34px; - } - - .change { - width: 12px; - height: 12px; - } - - .cityWrapper { - line-height: 20px; - } - - .city { - font-weight: 600; - font-size: 13px; - line-height: 20px; - } - - .infoWrapper { - line-height: 12px; - } - - .info { - font-weight: 400; - font-size: 10px; - line-height: 12px; - color: #3C3C4399; - } + position: sticky; + top: 0; + z-index: 999; + background-color: #ffffff; } \ No newline at end of file diff --git a/src/components/CustomNavbar/index.tsx b/src/components/CustomNavbar/index.tsx index 4d2e368..5ad3864 100644 --- a/src/components/CustomNavbar/index.tsx +++ b/src/components/CustomNavbar/index.tsx @@ -1,73 +1,23 @@ -import { View, Text, Image } from "@tarojs/components"; -import img from "@/config/images"; -import { getCurrentLocation } from "@/utils/locationUtils"; -import { getNavbarHeight } from "@/utils/getNavbarHeight"; +import { View } from "@tarojs/components"; import styles from "./index.module.scss"; -import { useEffect } from "react"; import { useGlobalState } from "@/store/global"; -import { useListState } from "@/store/listStore"; -const ListHeader = () => { - const { statusBarHeight, navbarHeight, totalHeight } = getNavbarHeight(); - const { - updateState, - location, - getLocationText, - getLocationLoading, - getNavbarHeightInfo, - } = useGlobalState(); - const { gamesNum } = useListState(); +interface IProps { + children: any; +} - // 获取位置信息 - const getCurrentLocal = () => { - updateState({ - getLocationLoading: true, - }); - getCurrentLocation().then((res) => { - updateState({ - getLocationLoading: false, - location: res || {}, - }); - }); - }; - useEffect(() => { - getNavbarHeightInfo(); - getCurrentLocal(); - }, []); - - const currentAddress = getLocationLoading - ? getLocationText - : location?.address; +const CustomNavbar = (props: IProps) => { + const { children } = props; + const { statusNavbarHeightInfo } = useGlobalState(); + const { totalHeight } = statusNavbarHeightInfo; return ( - - {/* logo */} - - - - - {/* 位置 */} - {currentAddress} - {!getLocationLoading && ( - - )} - - - 附近${gamesNum}场球局 - - - + {children} ); }; -export default ListHeader; +export default CustomNavbar; diff --git a/src/components/CustomerNavbarBack/index.module.scss b/src/components/CustomerNavbarBack/index.module.scss new file mode 100644 index 0000000..bc09a72 --- /dev/null +++ b/src/components/CustomerNavbarBack/index.module.scss @@ -0,0 +1,50 @@ +.customerNavbarBack { + position: sticky; + top: 0; + z-index: 999; + background-color: #ffffff; + + .container { + padding-left: 17px; + display: flex; + align-items: center; + gap: 8px; + } + + .line { + width: 1px; + height: 25px; + background-color: #0000000F; + } + + .back { + width: 32px; + height: 32px; + } + + .change { + width: 12px; + height: 12px; + } + + .cityWrapper { + line-height: 20px; + } + + .city { + font-weight: 600; + font-size: 13px; + line-height: 20px; + } + + .infoWrapper { + line-height: 12px; + } + + .info { + font-weight: 400; + font-size: 10px; + line-height: 12px; + color: #3C3C4399; + } +} \ No newline at end of file diff --git a/src/components/CustomerNavbarBack/index.tsx b/src/components/CustomerNavbarBack/index.tsx new file mode 100644 index 0000000..338ad51 --- /dev/null +++ b/src/components/CustomerNavbarBack/index.tsx @@ -0,0 +1,32 @@ +import { View, Image } from "@tarojs/components"; +import img from "@/config/images"; +import styles from "./index.module.scss"; +import { useGlobalState } from "@/store/global"; +import Taro from "@tarojs/taro"; + +const ListHeader = () => { + const { statusNavbarHeightInfo } = useGlobalState(); + const { statusBarHeight, navbarHeight, totalHeight } = statusNavbarHeightInfo; + const handleBack = () => { + Taro.navigateBack(); + } + + return ( + + + {/* back */} + + + + ); +}; +export default ListHeader; diff --git a/src/components/DateTimePicker/DateTimePicker.tsx b/src/components/DateTimePicker/DateTimePicker.tsx index 9975fff..c5714e9 100644 --- a/src/components/DateTimePicker/DateTimePicker.tsx +++ b/src/components/DateTimePicker/DateTimePicker.tsx @@ -1,12 +1,11 @@ import React, { useState, useEffect } from 'react' -import { View, Text } from '@tarojs/components' -import { Picker, Popup } from '@nutui/nutui-react-taro' +import { View, Text, PickerView, PickerViewColumn } from '@tarojs/components' import styles from './index.module.scss' + + export interface DateTimePickerProps { - visible: boolean - onClose: () => void - onConfirm: (year: number, month: number) => void + onChange: (year: number, month: number) => void defaultYear?: number defaultMonth?: number minYear?: number @@ -14,101 +13,91 @@ export interface DateTimePickerProps { } const DateTimePicker: React.FC = ({ - visible, - onClose, - onConfirm, + + onChange, defaultYear = new Date().getFullYear(), defaultMonth = new Date().getMonth() + 1, minYear = 2020, maxYear = 2030 }) => { + console.log('defaultYear', defaultYear) + console.log('defaultMonth', defaultMonth) const [selectedYear, setSelectedYear] = useState(defaultYear) const [selectedMonth, setSelectedMonth] = useState(defaultMonth) + + // 计算当前选项在数组中的索引 + const getYearIndex = (year: number) => year - minYear + const getMonthIndex = (month: number) => month - 1 - // 生成年份选项 - const yearOptions = Array.from({ length: maxYear - minYear + 1 }, (_, index) => ({ - text: `${minYear + index}年`, - value: minYear + index - })) + // 生成多列选择器的选项数据 + const pickerOptions = [ + // 年份列 + Array.from({ length: maxYear - minYear + 1 }, (_, index) => ({ + text: `${minYear + index}年`, + value: minYear + index + })), + // 月份列 + Array.from({ length: 12 }, (_, index) => ({ + text: `${index + 1}月`, + value: index + 1 + })) + ] + - // 生成月份选项 - const monthOptions = Array.from({ length: 12 }, (_, index) => ({ - text: `${index + 1}月`, - value: index + 1 - })) useEffect(() => { - if (visible) { - setSelectedYear(defaultYear) - setSelectedMonth(defaultMonth) + setSelectedYear(defaultYear) + setSelectedMonth(defaultMonth) + }, [ defaultYear, defaultMonth]) + + const handlePickerChange = (event: any) => { + const values = event.detail.value + if (values && values.length >= 2) { + // 根据索引获取实际值 + const yearIndex = values[0] + const monthIndex = values[1] + const year = minYear + yearIndex + const month = monthIndex + 1 + setSelectedYear(year) + setSelectedMonth(month) + onChange(year, month) } - }, [visible, defaultYear, defaultMonth]) - - const handleYearChange = (value: any) => { - setSelectedYear(value[0]) - } - - const handleMonthChange = (value: any) => { - setSelectedMonth(value[0]) - } - - const handleConfirm = () => { - onConfirm(selectedYear, selectedMonth) - onClose() - } - - const handleCancel = () => { - onClose() } return ( - - {/* 拖拽手柄 */} - - - {/* 时间选择器 */} - - - {/* 年份选择 */} - - - - - - {/* 月份选择 */} - - - + + e.stopPropagation()}> + {/* 拖拽手柄 */} + + + {/* 时间选择器 */} + + {/* 多列选择器 */} + + + + {pickerOptions[0].map((option, index) => ( + + {option.text} + + ))} + + + {pickerOptions[1].map((option, index) => ( + + {option.text} + + ))} + + - - {/* 操作按钮 */} - - - 取消 - - - 完成 - - - + ) } diff --git a/src/components/DateTimePicker/README.md b/src/components/DateTimePicker/README.md deleted file mode 100644 index 61754dc..0000000 --- a/src/components/DateTimePicker/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# DateTimePicker 年月选择器 - -一个基于 NutUI 的年月切换弹窗组件,支持自定义年份范围和默认值。 - -## 功能特性 - -- 🎯 年月分别选择,操作简单直观 -- 🎨 遵循设计稿样式,美观易用 -- 📱 支持移动端手势操作 -- ⚙️ 可自定义年份范围 -- �� 基于 NutUI 组件库,稳定可靠 - -## 使用方法 - -```tsx -import { DateTimePicker } from '@/components' - -const MyComponent = () => { - const [visible, setVisible] = useState(false) - - const handleConfirm = (year: number, month: number) => { - console.log('选择的年月:', year, month) - setVisible(false) - } - - return ( - setVisible(false)} - onConfirm={handleConfirm} - defaultYear={2025} - defaultMonth={11} - minYear={2020} - maxYear={2030} - /> - ) -} -``` - -## API 参数 - -| 参数 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| visible | boolean | - | 是否显示弹窗 | -| onClose | () => void | - | 关闭弹窗的回调 | -| onConfirm | (year: number, month: number) => void | - | 确认选择的回调 | -| defaultYear | number | 当前年份 | 默认选中的年份 | -| defaultMonth | number | 当前月份 | 默认选中的月份 | -| minYear | number | 2020 | 可选择的最小年份 | -| maxYear | number | 2030 | 可选择的最大年份 | - -## 样式定制 - -组件使用 CSS Modules,可以通过修改 `index.module.scss` 文件来自定义样式。 - -主要样式类: -- `.date-time-picker-popup` - 弹窗容器 -- `.picker-columns` - 选择器列容器 -- `.picker-column` - 单列选择器 -- `.action-buttons` - 操作按钮区域 - -## 注意事项 - -1. 组件基于 NutUI 的 Picker 和 Popup 组件 -2. 年份范围建议不要设置过大,以免影响性能 -3. 月份固定为 1-12 月 -4. 组件会自动处理边界情况 diff --git a/src/components/DateTimePicker/example.tsx b/src/components/DateTimePicker/example.tsx deleted file mode 100644 index b9c44d8..0000000 --- a/src/components/DateTimePicker/example.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { useState } from 'react' -import { View, Button } from '@tarojs/components' -import DateTimePicker from './DateTimePicker' - -const DateTimePickerExample: React.FC = () => { - const [visible, setVisible] = useState(false) - const [selectedDate, setSelectedDate] = useState('') - - const handleOpen = () => { - setVisible(true) - } - - const handleClose = () => { - setVisible(false) - } - - const handleConfirm = (year: number, month: number) => { - setSelectedDate(`${year}年${month}月`) - console.log('选择的日期:', year, month) - } - - return ( - - - - {selectedDate && ( - - 已选择: {selectedDate} - - )} - - - - ) -} - -export default DateTimePickerExample diff --git a/src/components/DateTimePicker/index.module.scss b/src/components/DateTimePicker/index.module.scss index ea892ee..67c6587 100644 --- a/src/components/DateTimePicker/index.module.scss +++ b/src/components/DateTimePicker/index.module.scss @@ -1,102 +1,89 @@ +/* 日期选择器弹出层样式 */ .date-time-picker-popup { - :global(.nut-popup) { - border-radius: 16px 16px 0 0; - background: #fff; + .common-popup-content { + padding: 0; } } .popup-handle { - width: 40px; + width: 32px; height: 4px; - background: #e5e5e5; + background: #e0e0e0; border-radius: 2px; - margin: 12px auto 0; + margin: 12px auto; } .picker-container { - padding: 20px 0; + padding: 26px 16px 0 16px; + background: #fff; } -.picker-columns { +.picker-header { + text-align: center; + margin-bottom: 20px; +} + +.picker-title { + font-size: 18px; + font-weight: 500; + color: #333; +} + +.picker-wrapper { + position: relative; +} + +.multi-column-picker { + width: 100%; + height: 216px; + background: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: none; + /* 自定义选择器样式 */ + ::-webkit-scrollbar { + width: 0; + background: transparent; + } + + /* 选中项指示器 */ + &::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 48px; + background: rgba(22, 24, 35, 0.05); + transform: translateY(-50%); + pointer-events: none; + z-index: 1; + border-radius: 4px; + } +} + +.picker-item { display: flex; - justify-content: center; align-items: center; - gap: 60px; + justify-content: center; + font-size: 20px; + color: #161823; + transition: all 0.3s ease; + &.picker-item-active { + color: rgba(22, 24, 35, 0.05); + font-weight: 600; + transform: scale(1.05); + } } .picker-column { - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; + border: none; + outline: none; } -.picker-label { - font-size: 14px; - color: #999; - font-weight: 400; -} - -.year-picker, -.month-picker { - :global(.nut-picker) { - width: 80px; - } - - :global(.nut-picker__content) { - height: 200px; - } - - :global(.nut-picker-item) { - height: 40px; - line-height: 40px; - font-size: 16px; - color: #333; - } - - :global(.nut-picker-item--selected) { - color: #000; - font-weight: 500; - } - - :global(.nut-picker-item--disabled) { - color: #ccc; - } -} - -.action-buttons { - display: flex; - padding: 0 20px 20px; - gap: 12px; -} - -.cancel-btn, -.confirm-btn { - flex: 1; - height: 44px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; -} - -.cancel-btn { - background: #fff; - border: 1px solid #e5e5e5; -} - -.cancel-text { - color: #666; +.picker-item-text { font-size: 16px; + color: inherit; + text-align: center; } -.confirm-btn { - background: #000; - border: 1px solid #000; -} - -.confirm-text { - color: #fff; - font-size: 16px; -} diff --git a/src/components/DistanceQuickFilter/index.scss b/src/components/DistanceQuickFilter/index.scss new file mode 100644 index 0000000..8ce9f96 --- /dev/null +++ b/src/components/DistanceQuickFilter/index.scss @@ -0,0 +1,91 @@ +.distanceQuickFilterWrap { + width: 100%; + + .nut-menu-bar { + background-color: unset; + box-shadow: unset; + padding: 0 15px; + gap: 5px; + } + + .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-size: 14px; + font-weight: 600; + line-height: 20px; + } + + .nut-menu-title.active { + color: #000; + } + + .nut-menu-container-wrap { + width: 100vw; + border-bottom-left-radius: 30px; + border-bottom-right-radius: 30px; + } + + .nut-menu-container-item { + color: rgba(60, 60, 67, 0.60); + font-size: 14px; + font-weight: 600; + line-height: 20px; + } + + .nut-menu-container-item.active { + flex-direction: row-reverse; + justify-content: space-between; + color: #000; + } + + .positionWrap { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin-bottom: 16px; + } + + .title { + font-size: 14px; + font-weight: 600; + } + + .cityName { + font-size: 13px; + font-weight: 400; + color: #3c3c43; + } + + .distanceWrap { + margin-bottom: 16px; + width: 100%; + } + + .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); + } + + .itemIcon { + width: 20px; + height: 20px; + } +} \ No newline at end of file diff --git a/src/components/DistanceQuickFilter/index.tsx b/src/components/DistanceQuickFilter/index.tsx new file mode 100644 index 0000000..5d82195 --- /dev/null +++ b/src/components/DistanceQuickFilter/index.tsx @@ -0,0 +1,81 @@ +import React, { useRef, useState } from "react"; +import { Menu, Button } from "@nutui/nutui-react-taro"; +import { Image } from "@tarojs/components"; +import img from "@/config/images"; +import Bubble from "../Bubble"; +import "./index.scss"; + +const Demo3 = (props) => { + const { + cityOptions, + quickOptions, + onChange, + cityName, + quickName, + cityValue, + quickValue, + } = props; + + const itemRef = useRef(null); + + const handleChange = (name: string, value: string) => { + // setIsChange(true); + onChange && onChange(name, value); + (itemRef.current as any)?.toggle(false); + }; + + return ( + + } + > +
+

当前位置

+

上海市

+
+
+ +
+
+ handleChange(quickName, value)} + icon={} + /> + {/* +
+ 自定义内容 +
+ +
*/} +
+ ); +}; +export default Demo3; diff --git a/src/components/FormSwitch/index.scss b/src/components/FormSwitch/index.scss index d6f2e73..9ec3d43 100644 --- a/src/components/FormSwitch/index.scss +++ b/src/components/FormSwitch/index.scss @@ -33,8 +33,8 @@ .info-popover { position: absolute; bottom: 22px; - left: -65px; - width: 130px; + left: -92px; + width: 184px; padding:12px; background: rgba(57, 59, 68, 0.90); color: #fff; @@ -51,7 +51,7 @@ content: ''; position: absolute; bottom: -6px; - left: 68px; /* 对齐图标(宽12px),可按需微调 */ + left: 94px; /* 对齐图标(宽12px),可按需微调 */ width: 0; height: 0; border-left: 6px solid transparent; diff --git a/src/components/GuideBar/index.scss b/src/components/GuideBar/index.scss new file mode 100644 index 0000000..7cd40b1 --- /dev/null +++ b/src/components/GuideBar/index.scss @@ -0,0 +1,90 @@ +@use '~@/scss/images.scss' as img; + +.guide-bar-container { + padding-top: calc(60px + 20px + env(safe-area-inset-bottom)); +} + +.guide-bar { + position: fixed; + bottom: 0; + width: 100%; + height: calc(60px + 20px + env(safe-area-inset-bottom)); + box-sizing: border-box; + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 12px env(safe-area-inset-bottom); + z-index: 999; + + &-pages { + display: flex; + justify-content: space-between; + align-items: center; + display: inline-flex; + width: 240px; + height: 60px; + padding: 8px 6px; + box-sizing: border-box; + flex-shrink: 0; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.20); + background: rgba(255, 255, 255, 0.40); + box-shadow: 0 4px 64px 0 rgba(0, 0, 0, 0.16); + backdrop-filter: blur(16px); + + &-item { + display: flex; + width: 76px; + height: 48px; + // padding: 14px 0; + box-sizing: border-box; + justify-content: center; + align-items: center; + gap: 10px; + color: rgba(60, 60, 67, 0.60); + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 125% */ + } + + &-item-active { + display: flex; + width: 76px; + height: 48px; + // padding: 14px 22px; + box-sizing: border-box; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 999px; + border: 0.5px solid rgba(0, 0, 0, 0.06); + background: #FFF; + box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08); + color: #000; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 125% */ + } + } + + &-publish { + display: flex; + width: 60px; + height: 60px; + justify-content: center; + align-items: center; + flex-shrink: 0; + border-radius: 999px; + // border: 2px solid rgba(0, 0, 0, 0.06); + background: radial-gradient(75.92% 98.69% at 26.67% 8.33%, #BDFF4A 16.88%, #95F23E 54.19%, #32D838 100%); + box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08); + backdrop-filter: blur(16px); + + &-icon { + width: 36px; + height: 36px; + } + } +} \ No newline at end of file diff --git a/src/components/GuideBar/index.tsx b/src/components/GuideBar/index.tsx new file mode 100644 index 0000000..e2084e7 --- /dev/null +++ b/src/components/GuideBar/index.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react' +import { View, Text, Image } from '@tarojs/components' +import Taro from '@tarojs/taro' +import img from '@/config/images' +import './index.scss' +import PublishMenu from '../PublishMenu' +export type currentPageType = 'games' | 'message' | 'personal' + +const GuideBar = (props) => { + const { currentPage } = props + + const guideItems = [ + { + code: 'list', + text: '球局', + }, + { + code: 'message', + text: '消息', + }, + { + code: 'personal', + text: '我的', + }, + ] + + const handlePublish = () => { + Taro.navigateTo({ + url: '/pages/publishBall/index', + }) + } + + const handlePageChange = (code: string) => { + if (code === currentPage) { + return + } + Taro.navigateTo({ + url: `/pages/${code}/index`, + }).then(() => { + Taro.pageScrollTo({ + scrollTop: 0, + duration: 300, + }) + }) + } + + return ( + + + {/* guide area on the left */} + + {guideItems.map((item) => ( + handlePageChange(item.code)} + > + {item.text} + + ))} + + {/* publish button on the right */} + {/* + + */} + + + + ) +} + +export default GuideBar \ No newline at end of file diff --git a/src/components/HourMinutePicker/HourMinutePicker.tsx b/src/components/HourMinutePicker/HourMinutePicker.tsx new file mode 100644 index 0000000..d364829 --- /dev/null +++ b/src/components/HourMinutePicker/HourMinutePicker.tsx @@ -0,0 +1,95 @@ +import React, { useState, useEffect } from 'react' +import { View, Text, PickerView, PickerViewColumn } from '@tarojs/components' +import styles from './index.module.scss' + +export interface HourMinutePickerProps { + onChange: (hour: number, minute: number) => void + defaultHour?: number + defaultMinute?: number + minHour?: number + maxHour?: number +} + +const HourMinutePicker: React.FC = ({ + onChange, + defaultHour = new Date().getHours(), + defaultMinute = new Date().getMinutes(), + minHour = 0, + maxHour = 23 +}) => { + console.log('defaultHour', defaultHour) + console.log('defaultMinute', defaultMinute) + const [selectedHour, setSelectedHour] = useState(defaultHour) + const [selectedMinute, setSelectedMinute] = useState(defaultMinute) + + // 计算当前选项在数组中的索引 + const getHourIndex = (hour: number) => hour - minHour + const getMinuteIndex = (minute: number) => Math.floor(minute / 5) + + // 生成小时和分钟的选项数据 + const pickerOptions = [ + // 小时列 + Array.from({ length: maxHour - minHour + 1 }, (_, index) => ({ + text: `${minHour + index}时`, + value: minHour + index + })), + // 分钟列 (5分钟间隔) + Array.from({ length: 12 }, (_, index) => ({ + text: `${index * 5 < 10 ? '0' + index * 5 : index * 5}分`, + value: index * 5 + })) + ] + + useEffect(() => { + setSelectedHour(defaultHour) + setSelectedMinute(defaultMinute) + }, [defaultHour, defaultMinute]) + + const handlePickerChange = (event: any) => { + const values = event.detail.value + if (values && values.length >= 2) { + // 根据索引获取实际值 + const hourIndex = values[0] + const minuteIndex = values[1] + const hour = minHour + hourIndex + const minute = minuteIndex * 5 + setSelectedHour(hour) + setSelectedMinute(minute) + onChange(hour, minute) + } + } + + return ( + + {/* 拖拽手柄 */} + {/* 时间选择器 */} + + {/* 多列选择器 */} + + + + {pickerOptions[0].map((option, index) => ( + + {option.text} + + ))} + + + {pickerOptions[1].map((option, index) => ( + + {option.text} + + ))} + + + + + + ) +} + +export default HourMinutePicker \ No newline at end of file diff --git a/src/components/HourMinutePicker/index.module.scss b/src/components/HourMinutePicker/index.module.scss new file mode 100644 index 0000000..2433fbe --- /dev/null +++ b/src/components/HourMinutePicker/index.module.scss @@ -0,0 +1,44 @@ +.hour-minute-picker-popup { + background-color: #fff; + border-radius: 16px; + width: 100%; + position: relative; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + padding: 26px 16px 0 16px; + box-sizing: border-box; +} + +.picker-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.picker-wrapper { + width: 100%; + max-width: 400px; +} + +.multi-column-picker { + height: 216px; + width: 100%; +} + +.picker-column { + flex: 1; + text-align: center; +} + +.picker-item { + height: 48px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 16px; +} + +.picker-item-text { + font-size: 16px; + color: #333; + font-weight: 400; +} diff --git a/src/components/HourMinutePicker/index.ts b/src/components/HourMinutePicker/index.ts new file mode 100644 index 0000000..3d8bf3a --- /dev/null +++ b/src/components/HourMinutePicker/index.ts @@ -0,0 +1,2 @@ +export { default } from './HourMinutePicker' +export type { HourMinutePickerProps } from './HourMinutePicker' diff --git a/src/components/ListCard/index.scss b/src/components/ListCard/index.scss index 3cf2e16..bcb39aa 100644 --- a/src/components/ListCard/index.scss +++ b/src/components/ListCard/index.scss @@ -1,4 +1,10 @@ -.list-item { +.listCard { + background: linear-gradient(90deg, rgba(183, 248, 113, 0.5) 0%, rgba(183, 248, 113, 0.1) 100%); + border-radius: 20px; + border-width: 0.5px; +} + +.listItem { display: flex; padding: 12px 15px; background: #ffffff; @@ -247,4 +253,43 @@ width: 100%; height: 100%; object-fit: cover; +} + +// 底部 +.smoothPlayingGame { + padding: 5px 12px; + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + + .smoothWrapper, + .localAreaWrapper { + line-height: 18px; + display: flex; + align-items: center; + gap: 5px; + } + + .smoothTitle { + font-size: 14px; + } + + .line { + height: 8px; + width: 1px; + background: #00000040; + border-radius: 99px; + } + + .iconListPlayingGame, + .localArea { + width: 14px; + height: 14px; + } + + .localArea { + border: 0.5px solid #FFFFFFA6; + border-radius: 50%; + } } \ No newline at end of file diff --git a/src/components/ListCard/index.tsx b/src/components/ListCard/index.tsx index a5f5f28..3fb3538 100644 --- a/src/components/ListCard/index.tsx +++ b/src/components/ListCard/index.tsx @@ -1,10 +1,11 @@ import { View, Text, Image } from "@tarojs/components"; +import Taro from "@tarojs/taro"; import img from "../../config/images"; import { ListCardProps } from "../../../types/list/types"; import "./index.scss"; -// import SkeletonComponent from "../../components/Skeleton"; const ListCard: React.FC = ({ + id, title, dateTime, location, @@ -13,39 +14,37 @@ const ListCard: React.FC = ({ maxCount, skillLevel, matchType, - images, + images = [], shinei, }) => { const renderItemImage = (src: string) => { return ; }; + const handleViewDetail = () => { + console.log('id', id) + Taro.navigateTo({ + url: `/pages/detail/index?id=${id || 1}&from=list&autoShare=0`, + }); + }; + // 根据图片数量决定展示样式 const renderImages = () => { - if (images.length === 0) return null; + if (images?.length === 0) return null; - if (images.length === 1) { + if (images?.length === 1) { return ( - - {/* */} - {renderItemImage(images[0])} - + {renderItemImage(images[0])} ); } - if (images.length === 2) { + if (images?.length === 2) { return ( - - {/* */} - {renderItemImage(images[0])} - - - {/* */} - {renderItemImage(images[1])} - + {renderItemImage(images[0])} + {renderItemImage(images[1])} ); } @@ -53,79 +52,94 @@ const ListCard: React.FC = ({ // 3张或更多图片 return ( - {renderItemImage(images[0])} - {renderItemImage(images[1])} - {renderItemImage(images[2])} + {renderItemImage(images?.[0])} + {renderItemImage(images?.[1])} + {renderItemImage(images?.[2])} ); }; return ( - - {/* 左侧内容区域 */} - - {/* 标题 */} - - {title} - - - - {/* 时间信息 */} - - - {dateTime} - - - {/* 地点,室内外,距离 */} - - - {location} - - {shinei && `・${shinei}`} - {distance && `・${distance}`} - - - - {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} - - - - {Array.from({ length: Math.min(registeredCount, 3) }).map( - (_, index) => ( - - - - ) - )} - + + + {/* 左侧内容区域 */} + + {/* 标题 */} + + {title} + - - - - 报名人数 {registeredCount}/ - {maxCount} - + {/* 时间信息 */} + + + {dateTime} + + + {/* 地点,室内外,距离 */} + + + {location} + + {shinei && `・${shinei}`} + {distance && `・${distance}`} + + + + {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} + + + + {Array.from({ length: Math.min(registeredCount, 3) }).map( + (_, index) => ( + + + + ) + )} + - - {skillLevel} - - - {matchType} + + + + + 报名人数 {registeredCount}/ + {maxCount} + + + + {skillLevel} + + + {matchType} + + + {/* 右侧图片区域 */} + {renderImages()} + + {/* 畅打球局 */} + + + + 畅打球局 + + + 场馆方: + + + 仁恒河滨花园网球场 + - - {/* 右侧图片区域 */} - {renderImages()} ); }; diff --git a/src/components/ListCardSkeleton/index.tsx b/src/components/ListCardSkeleton/index.tsx index d19bf56..77441a0 100644 --- a/src/components/ListCardSkeleton/index.tsx +++ b/src/components/ListCardSkeleton/index.tsx @@ -13,7 +13,6 @@ const ListCard = () => { {/* 时间信息 */} - diff --git a/src/components/ListLoadError/index.module.scss b/src/components/ListLoadError/index.module.scss new file mode 100644 index 0000000..1629275 --- /dev/null +++ b/src/components/ListLoadError/index.module.scss @@ -0,0 +1,44 @@ +.listLoadError { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + + .listLoadErrorImg { + width: 154px; + height: 154px; + } + + .listLoadErrorText { + margin-top: 35px; + margin-bottom: 12px; + font-weight: 500; + font-style: Medium; + font-size: 14px; + line-height: 24px; + letter-spacing: 0px; + } + + .listLoadErrorBtn { + display: flex; + align-items: center; + justify-content: center; + width: 76px; + background: #00000008; + border: 0.5px solid #0000001F; + border-radius: 12px; + padding: 12px 0; + font-weight: 500; + font-style: Medium; + font-size: 14px; + line-height: 24px; + letter-spacing: 0px; + + } + + .reloadIcon { + width: 16px; + height: 16px; + } +} \ No newline at end of file diff --git a/src/components/ListLoadError/index.tsx b/src/components/ListLoadError/index.tsx new file mode 100644 index 0000000..420393d --- /dev/null +++ b/src/components/ListLoadError/index.tsx @@ -0,0 +1,24 @@ +import { Image, View, Text, Button } from "@tarojs/components"; +import styles from "./index.module.scss"; +import img from "@/config/images"; + +const ListLoadError = ({ reload }: { reload: () => void }) => { + const handleReload = () => { + reload && typeof reload === "function" && reload(); + }; + + return ( + + + 加载失败 + + + ); +}; +export default ListLoadError; diff --git a/src/components/MapDisplay/README.md b/src/components/MapDisplay/README.md deleted file mode 100644 index e4a89ba..0000000 --- a/src/components/MapDisplay/README.md +++ /dev/null @@ -1,215 +0,0 @@ -# 腾讯地图SDK使用说明 - -## 概述 - -本项目已集成腾讯地图SDK (`qqmap-wx-jssdk`),可以在小程序中使用腾讯地图的各种功能,包括地点搜索、地理编码等。现在已添加真实的腾讯地图组件,支持显示当前位置和交互功能。 - -## 安装依赖 - -项目已安装 `qqmap-wx-jssdk` 依赖: - -```bash -npm install qqmap-wx-jssdk -# 或 -yarn add qqmap-wx-jssdk -``` - -## 基本使用 - -### 1. 引入SDK - -```typescript -import QQMapWX from "qqmap-wx-jssdk"; -``` - -### 2. 初始化SDK - -```typescript -const qqmapsdk = new QQMapWX({ - key: 'YOUR_API_KEY' // 替换为你的腾讯地图API密钥 -}); -``` - -### 3. 使用search方法搜索地点 - -```typescript -// 搜索地点 -qqmapsdk.search({ - keyword: '关键词', // 搜索关键词 - location: '39.908802,116.397502', // 搜索中心点(可选) - page_size: 20, // 每页结果数量(可选) - page_index: 1, // 页码(可选) - success: (res) => { - console.log('搜索成功:', res.data); - // 处理搜索结果 - }, - fail: (err) => { - console.error('搜索失败:', err); - } -}); -``` - -## 在组件中使用 - -### MapDisplay组件 - -`MapDisplay` 组件已经封装了腾讯地图SDK的使用,包括: - -- **自动初始化SDK** -- **真实地图显示**: 使用Taro的Map组件显示腾讯地图 -- **当前位置显示**: 自动获取并显示用户当前位置 -- **地点搜索功能**: 支持关键词搜索地点 -- **搜索结果展示**: 在地图上标记搜索结果 -- **交互功能**: 支持地图缩放、拖动、标记点击等 -- **错误处理**: 完善的错误处理和用户提示 - -### 主要功能特性 - -#### 1. 地图显示 -- 使用真实的腾讯地图组件 -- 默认显示当前位置 -- 支持地图缩放、拖动、旋转 -- 响应式设计,适配不同屏幕尺寸 - -#### 2. 位置服务 -- 自动获取用户当前位置 -- 支持位置刷新 -- 逆地理编码获取地址信息 -- 位置信息悬浮显示 - -#### 3. 搜索功能 -- 实时搜索地点 -- 防抖优化(500ms) -- 搜索结果在地图上标记 -- 点击结果可移动地图中心 - -#### 4. 地图标记 -- 当前位置标记(蓝色) -- 搜索结果标记 -- 标记点击交互 -- 动态添加/移除标记 - -### 使用示例 - -```typescript -import { mapService } from './mapService'; - -// 搜索地点 -const results = await mapService.search({ - keyword: '体育馆', - location: '39.908802,116.397502' -}); - -console.log('搜索结果:', results); -``` - -## API密钥配置 - -在 `mapService.ts` 中配置你的腾讯地图API密钥: - -```typescript -this.qqmapsdk = new QQMapWX({ - key: 'YOUR_API_KEY' // 替换为你的实际API密钥 -}); -``` - -## 组件属性 - -### Map组件属性 -- `longitude`: 地图中心经度 -- `latitude`: 地图中心纬度 -- `scale`: 地图缩放级别(1-20) -- `markers`: 地图标记数组 -- `show-location`: 是否显示用户位置 -- `enable-zoom`: 是否支持缩放 -- `enable-scroll`: 是否支持拖动 -- `enable-rotate`: 是否支持旋转 - -### 标记属性 -```typescript -interface Marker { - id: string; // 标记唯一标识 - latitude: number; // 纬度 - longitude: number; // 经度 - title: string; // 标记标题 - iconPath?: string; // 图标路径 - width: number; // 图标宽度 - height: number; // 图标高度 -} -``` - -## 主要功能 - -### 1. 地点搜索 -- 支持关键词搜索 -- 支持按位置范围搜索 -- 分页显示结果 -- 搜索结果地图标记 - -### 2. 位置服务 -- 获取当前位置 -- 地理编码 -- 逆地理编码 -- 位置刷新 - -### 3. 地图交互 -- 地图缩放 -- 地图拖动 -- 地图旋转 -- 标记点击 -- 地图点击 - -### 4. 错误处理 -- SDK初始化失败处理 -- 搜索失败处理 -- 网络异常处理 -- 位置获取失败处理 - -## 注意事项 - -1. **API密钥**: 确保使用有效的腾讯地图API密钥 -2. **网络权限**: 小程序需要网络访问权限 -3. **位置权限**: 需要申请位置权限 (`scope.userLocation`) -4. **错误处理**: 建议添加适当的错误处理和用户提示 -5. **地图组件**: 使用Taro的Map组件,确保兼容性 - -## 权限配置 - -在 `app.config.ts` 中添加位置权限: - -```typescript -export default defineAppConfig({ - // ... 其他配置 - permission: { - 'scope.userLocation': { - desc: '你的位置信息将用于小程序位置接口的效果展示' - } - }, - requiredPrivateInfos: [ - 'getLocation' - ] -}) -``` - -## 常见问题 - -### Q: SDK初始化失败怎么办? -A: 检查API密钥是否正确,网络连接是否正常 - -### Q: 搜索没有结果? -A: 检查搜索关键词是否正确,API密钥是否有效 - -### Q: 如何获取用户当前位置? -A: 使用小程序的 `wx.getLocation` API,已集成到地图服务中 - -### Q: 地图不显示怎么办? -A: 检查网络连接,确保腾讯地图服务正常 - -### Q: 位置权限被拒绝? -A: 引导用户手动开启位置权限,或使用默认位置 - -## 更多信息 - -- [腾讯地图小程序SDK官方文档](https://lbs.qq.com/miniProgram/jsSdk/jsSdkGuide/jsSdkOverview) -- [API密钥申请](https://lbs.qq.com/dev/console/application/mine) -- [Taro Map组件文档](https://taro-docs.jd.com/docs/components/map) \ No newline at end of file diff --git a/src/components/MapDisplay/index.scss b/src/components/MapDisplay/index.scss deleted file mode 100644 index 4ca44ef..0000000 --- a/src/components/MapDisplay/index.scss +++ /dev/null @@ -1,382 +0,0 @@ -.map-display { - height: 100vh; - background-color: #f5f5f5; - display: flex; - flex-direction: column; - - .map-section { - flex: 1; - position: relative; - background-color: #e8f4fd; - - .map-container { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: relative; - - .map-component { - width: 100%; - height: calc(100vh - 50%); - border-radius: 0; - } - - .map-loading { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - - .map-loading-text { - font-size: 16px; - color: #666; - text-align: center; - padding: 20px; - } - } - - .map-placeholder { - font-size: 16px; - color: #666; - text-align: center; - padding: 20px; - } - - .location-info-overlay { - position: absolute; - top: 20px; - left: 20px; - right: 20px; - z-index: 10; - - .location-info { - background-color: rgba(255, 255, 255, 0.95); - padding: 12px 16px; - border-radius: 24px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - display: flex; - align-items: center; - justify-content: space-between; - backdrop-filter: blur(10px); - - .location-text { - font-size: 13px; - color: #333; - flex: 1; - margin-right: 12px; - line-height: 1.4; - } - - .refresh-btn { - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - background-color: #f0f0f0; - border-radius: 50%; - cursor: pointer; - transition: all 0.2s; - font-size: 12px; - - &:hover { - background-color: #e0e0e0; - transform: scale(1.1); - } - - &:active { - transform: scale(0.95); - } - } - } - } - - .center-info-overlay { - position: absolute; - bottom: 20px; - left: 20px; - right: 20px; - z-index: 10; - - .center-info { - background-color: rgba(255, 255, 255, 0.95); - padding: 12px 16px; - border-radius: 24px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - backdrop-filter: blur(10px); - - .center-text { - font-size: 13px; - color: #333; - text-align: center; - line-height: 1.4; - margin-bottom: 4px; - } - - .moving-indicator { - display: flex; - align-items: center; - justify-content: center; - padding: 4px 8px; - background-color: rgba(255, 193, 7, 0.9); - border-radius: 12px; - animation: pulse 1.5s ease-in-out infinite; - - .moving-text { - font-size: 11px; - color: #333; - font-weight: 500; - } - } - - @keyframes pulse { - 0%, 100% { - opacity: 1; - transform: scale(1); - } - 50% { - opacity: 0.8; - transform: scale(1.05); - } - } - } - } - - .fixed-center-indicator { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 15; - pointer-events: none; - - .center-dot { - width: 20px; - height: 20px; - background-color: #ff4757; - border: 3px solid #fff; - border-radius: 50%; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - animation: pulse 2s infinite; - } - - @keyframes pulse { - 0% { - transform: scale(1); - opacity: 1; - } - 50% { - transform: scale(1.2); - opacity: 0.8; - } - 100% { - transform: scale(1); - opacity: 1; - } - } - } - - .location-info { - position: absolute; - top: 20px; - left: 20px; - background-color: rgba(255, 255, 255, 0.9); - padding: 8px 12px; - border-radius: 20px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - - .location-text { - font-size: 12px; - color: #333; - } - } - - .sdk-status { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: rgba(0, 0, 0, 0.7); - color: white; - padding: 12px 20px; - border-radius: 20px; - font-size: 14px; - z-index: 20; - - .sdk-status-text { - color: white; - } - } - } - } - - .search-section { - background-color: #fff; - padding: 16px; - border-bottom: 1px solid #eee; - - .search-wrapper { - display: flex; - align-items: center; - background-color: #f8f8f8; - border-radius: 24px; - padding: 0 16px; - position: relative; - - .search-icon { - font-size: 16px; - color: #999; - margin-right: 8px; - } - - .search-input { - flex: 1; - height: 40px; - font-size: 14px; - color: #333; - background: transparent; - border: none; - outline: none; - - &::placeholder { - color: #999; - } - - &:disabled { - color: #ccc; - } - } - - .clear-btn { - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - background-color: #e0e0e0; - border-radius: 50%; - cursor: pointer; - font-size: 12px; - color: #666; - transition: all 0.2s; - - &:hover { - background-color: #d0d0d0; - color: #333; - } - } - } - } - - .search-results { - background-color: #fff; - flex: 1; - overflow: hidden; - - .results-header { - padding: 16px; - border-bottom: 1px solid #eee; - display: flex; - align-items: center; - - .results-title { - font-size: 16px; - font-weight: 500; - color: #333; - } - - .results-count { - font-size: 14px; - color: #999; - margin-left: 8px; - } - } - - .results-list { - max-height: 300px; - - .result-item { - display: flex; - align-items: center; - padding: 16px; - border-bottom: 1px solid #f5f5f5; - cursor: pointer; - transition: background-color 0.2s; - - &:hover { - background-color: #f8f8f8; - } - - .result-content { - flex: 1; - - .result-title { - font-size: 14px; - font-weight: 500; - color: #333; - margin-bottom: 4px; - display: block; - } - - .result-address { - font-size: 12px; - color: #999; - display: block; - } - } - - .result-arrow { - font-size: 16px; - color: #ccc; - margin-left: 12px; - } - } - } - } - - .searching-indicator { - padding: 20px; - text-align: center; - background-color: #fff; - - .searching-text { - font-size: 14px; - color: #999; - } - } - - .no-results { - padding: 40px 20px; - text-align: center; - background-color: #fff; - - .no-results-text { - font-size: 14px; - color: #999; - } - } - - .sdk-status-full { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: rgba(0, 0, 0, 0.8); - color: white; - padding: 20px 30px; - border-radius: 12px; - font-size: 16px; - z-index: 1000; - - .sdk-status-text { - color: white; - } - } -} \ No newline at end of file diff --git a/src/components/MapDisplay/index.tsx b/src/components/MapDisplay/index.tsx deleted file mode 100644 index 5f54d56..0000000 --- a/src/components/MapDisplay/index.tsx +++ /dev/null @@ -1,505 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react' -import { View, Text, Input, ScrollView, Map } from '@tarojs/components' -import Taro from '@tarojs/taro' -import { mapService, SearchResult, LocationInfo } from './mapService' -import './index.scss' - -const MapDisplay: React.FC = () => { - const [currentLocation, setCurrentLocation] = useState(null) - const [searchValue, setSearchValue] = useState('') - const [searchResults, setSearchResults] = useState([]) - const [isSearching, setIsSearching] = useState(false) - const [mapContext, setMapContext] = useState(null) - const [isSDKReady, setIsSDKReady] = useState(false) - const [mapMarkers, setMapMarkers] = useState([]) - // 地图中心点状态 - const [mapCenter, setMapCenter] = useState<{lat: number, lng: number} | null>(null) - // 用户点击的中心点标记 - const [centerMarker, setCenterMarker] = useState(null) - // 是否正在移动地图 - const [isMapMoving, setIsMapMoving] = useState(false) - // 地图移动的动画帧ID - const animationFrameRef = useRef(null) - // 地图移动的目标位置 - const [targetCenter, setTargetCenter] = useState<{lat: number, lng: number} | null>(null) - // 实时移动的定时器 - const moveTimerRef = useRef(null) - // 地图移动状态 - const [mapMoveState, setMapMoveState] = useState({ - isMoving: false, - startTime: 0, - startCenter: null as {lat: number, lng: number} | null, - lastUpdateTime: 0 - }) - - useEffect(() => { - initializeMapService() - return () => { - // 清理动画帧和定时器 - if (animationFrameRef.current) { - cancelAnimationFrame(animationFrameRef.current) - } - if (moveTimerRef.current) { - clearInterval(moveTimerRef.current) - } - } - }, []) - - // 初始化地图服务 - const initializeMapService = async () => { - try { - const success = await mapService.initSDK() - if (success) { - setIsSDKReady(true) - console.log('地图服务初始化成功') - getCurrentLocation() - } else { - console.error('地图服务初始化失败') - Taro.showToast({ - title: '地图服务初始化失败', - icon: 'none' - }) - } - } catch (error) { - console.error('初始化地图服务异常:', error) - Taro.showToast({ - title: '地图服务初始化异常', - icon: 'none' - }) - } - } - - // 获取当前位置 - const getCurrentLocation = async () => { - try { - const location = await mapService.getLocation() - if (location) { - setCurrentLocation(location) - // 设置地图中心为当前位置,但不显示标记 - setMapCenter({ lat: location.lat, lng: location.lng }) - // 清空所有标记 - setMapMarkers([]) - console.log('当前位置:', location) - } - } catch (error) { - console.error('获取位置失败:', error) - Taro.showToast({ - title: '获取位置失败', - icon: 'none' - }) - } - } - - // 地图加载完成 - const handleMapLoad = (e: any) => { - console.log('地图加载完成:', e) - setMapContext(e.detail) - } - - // 地图标记点击 - const handleMarkerTap = (e: any) => { - const markerId = e.detail.markerId - console.log('点击标记:', markerId) - - if (markerId === 'center') { - Taro.showToast({ - title: '中心点标记', - icon: 'success' - }) - } - } - - // 地图区域点击 - 设置中心点和标记 - const handleMapTap = (e: any) => { - const { latitude, longitude } = e.detail - console.log('地图点击:', { latitude, longitude }) - - // 设置新的地图中心点 - setMapCenter({ lat: latitude, lng: longitude }) - - // 设置中心点标记 - const newCenterMarker = { - id: 'center', - latitude: latitude, - longitude: longitude, - title: '中心点', - iconPath: '/assets/center-marker.png', // 可以添加自定义中心点图标 - width: 40, - height: 40 - } - setCenterMarker(newCenterMarker) - - // 更新地图标记,只显示中心点标记 - setMapMarkers([newCenterMarker]) - - Taro.showToast({ - title: '已设置中心点', - icon: 'success' - }) - } - - // 地图开始移动 - const handleMapMoveStart = () => { - console.log('地图开始移动') - setIsMapMoving(true) - setMapMoveState(prev => ({ - ...prev, - isMoving: true, - startTime: Date.now(), - startCenter: mapCenter, - lastUpdateTime: Date.now() - })) - - // 启动实时移动更新 - startRealTimeMoveUpdate() - } - - // 启动实时移动更新 - const startRealTimeMoveUpdate = () => { - if (moveTimerRef.current) { - clearInterval(moveTimerRef.current) - } - - // 每16ms更新一次(约60fps) - moveTimerRef.current = setInterval(() => { - if (mapMoveState.isMoving && centerMarker) { - // 模拟地图移动过程中的位置更新 - // 这里我们基于时间计算一个平滑的移动轨迹 - const currentTime = Date.now() - const elapsed = currentTime - mapMoveState.startTime - const moveDuration = 300 // 假设移动持续300ms - - if (elapsed < moveDuration) { - // 计算移动进度 - const progress = elapsed / moveDuration - const easeProgress = 1 - Math.pow(1 - progress, 3) // 缓动函数 - - // 如果有目标位置,进行插值计算 - if (targetCenter && mapMoveState.startCenter) { - const newLat = mapMoveState.startCenter.lat + (targetCenter.lat - mapMoveState.startCenter.lat) * easeProgress - const newLng = mapMoveState.startCenter.lng + (targetCenter.lng - mapMoveState.startCenter.lng) * easeProgress - - // 更新中心点标记位置 - const updatedCenterMarker = { - ...centerMarker, - latitude: newLat, - longitude: newLng - } - setCenterMarker(updatedCenterMarker) - - // 更新地图标记 - const searchMarkers = mapMarkers.filter(marker => marker.id.startsWith('search_')) - setMapMarkers([updatedCenterMarker, ...searchMarkers]) - } - } - } - }, 16) - } - - // 地图区域变化 - 更新目标位置 - const handleRegionChange = (e: any) => { - console.log('地图区域变化:', e.detail) - - // 获取地图当前的中心点坐标 - if (e.detail && e.detail.centerLocation) { - const { latitude, longitude } = e.detail.centerLocation - const newCenter = { lat: latitude, lng: longitude } - - // 设置目标位置 - setTargetCenter(newCenter) - - // 更新地图中心点状态 - setMapCenter(newCenter) - - // 如果有中心点标记,立即更新标记位置到新的地图中心 - if (centerMarker) { - const updatedCenterMarker = { - ...centerMarker, - latitude: latitude, - longitude: longitude - } - setCenterMarker(updatedCenterMarker) - - // 更新地图标记,保持搜索结果标记 - const searchMarkers = mapMarkers.filter(marker => marker.id.startsWith('search_')) - setMapMarkers([updatedCenterMarker, ...searchMarkers]) - } - } - } - - // 地图移动结束 - const handleMapMoveEnd = () => { - console.log('地图移动结束') - setIsMapMoving(false) - setMapMoveState(prev => ({ - ...prev, - isMoving: false - })) - - // 停止实时移动更新 - if (moveTimerRef.current) { - clearInterval(moveTimerRef.current) - moveTimerRef.current = null - } - - // 清理动画帧 - if (animationFrameRef.current) { - cancelAnimationFrame(animationFrameRef.current) - animationFrameRef.current = null - } - } - - // 处理搜索输入 - const handleSearchInput = (e: any) => { - const value = e.detail.value - setSearchValue(value) - - // 如果输入内容为空,清空搜索结果 - if (!value.trim()) { - setSearchResults([]) - return - } - - // 防抖搜索 - clearTimeout((window as any).searchTimer) - ;(window as any).searchTimer = setTimeout(() => { - performSearch(value) - }, 500) - } - - // 执行搜索 - const performSearch = async (keyword: string) => { - if (!keyword.trim() || !isSDKReady) return - - setIsSearching(true) - - try { - const results = await mapService.search({ - keyword, - location: currentLocation ? `${currentLocation.lat},${currentLocation.lng}` : undefined - }) - setSearchResults(results) - - // 在地图上添加搜索结果标记 - if (results.length > 0) { - const newMarkers = results.map((result, index) => ({ - id: `search_${index}`, - latitude: result.location.lat, - longitude: result.location.lng, - title: result.title, - iconPath: '/assets/search-marker.png', // 可以添加自定义图标 - width: 24, - height: 24 - })) - - // 合并中心点标记和搜索结果标记 - const allMarkers = centerMarker ? [centerMarker, ...newMarkers] : newMarkers - setMapMarkers(allMarkers) - } - - console.log('搜索结果:', results) - } catch (error) { - console.error('搜索异常:', error) - Taro.showToast({ - title: '搜索失败', - icon: 'none' - }) - setSearchResults([]) - } finally { - setIsSearching(false) - } - } - - // 处理搜索结果点击 - 切换地图中心到对应地点 - const handleResultClick = (result: SearchResult) => { - console.log('选择地点:', result) - Taro.showToast({ - title: `已切换到: ${result.title}`, - icon: 'success' - }) - - // 点击搜索结果时,将地图中心移动到该位置 - const newCenter = { lat: result.location.lat, lng: result.location.lng } - setMapCenter(newCenter) - - // 更新中心点标记 - const newCenterMarker = { - id: 'center', - latitude: result.location.lat, - longitude: result.location.lng, - title: '中心点', - iconPath: '/assets/center-marker.png', - width: 40, - height: 40 - } - setCenterMarker(newCenterMarker) - - // 更新地图标记,保留搜索结果标记 - const searchMarkers = mapMarkers.filter(marker => marker.id.startsWith('search_')) - setMapMarkers([newCenterMarker, ...searchMarkers]) - - // 如果地图上下文可用,也可以调用地图API移动 - if (mapContext && mapContext.moveToLocation) { - mapContext.moveToLocation({ - latitude: result.location.lat, - longitude: result.location.lng, - success: () => { - console.log('地图移动到搜索结果位置') - }, - fail: (err: any) => { - console.error('地图移动失败:', err) - } - }) - } - } - - // 处理搜索框清空 - const handleSearchClear = () => { - setSearchValue('') - setSearchResults([]) - // 清空搜索结果标记,只保留中心点标记 - setMapMarkers(centerMarker ? [centerMarker] : []) - } - - // 刷新位置 - const handleRefreshLocation = () => { - getCurrentLocation() - Taro.showToast({ - title: '正在刷新位置...', - icon: 'loading' - }) - } - - return ( - - {/* 地图区域 */} - - - {currentLocation ? ( - console.error('地图加载错误:', e)} - /> - ) : ( - - 地图加载中... - - )} - - {/* 位置信息悬浮层 */} - {currentLocation && ( - - - - {currentLocation.address || `当前位置: ${currentLocation.lat.toFixed(6)}, ${currentLocation.lng.toFixed(6)}`} - - - 🔄 - - - - )} - - {/* 中心点信息悬浮层 */} - {centerMarker && ( - - - - 中心点: {centerMarker.latitude.toFixed(6)}, {centerMarker.longitude.toFixed(6)} - - {isMapMoving && ( - - 移动中... - - )} - - - )} - - {!isSDKReady && ( - - 地图服务初始化中... - - )} - - - - {/* 搜索区域 */} - - - 🔍 - - {searchValue && ( - - ✕ - - )} - - - - {/* 搜索结果列表 */} - {searchResults.length > 0 && ( - - - 搜索结果 - ({searchResults.length}) - - - {searchResults.map((result) => ( - handleResultClick(result)} - > - - {result.title} - {result.address} - - - - ))} - - - )} - - {/* 搜索状态提示 */} - {isSearching && ( - - 搜索中... - - )} - - {/* 无搜索结果提示 */} - {searchValue && !isSearching && searchResults.length === 0 && isSDKReady && ( - - 未找到相关地点 - - )} - - {/* SDK状态提示 */} - {!isSDKReady && ( - - 正在初始化地图服务,请稍候... - - )} - - ) -} - -export default MapDisplay \ No newline at end of file diff --git a/src/components/MapDisplay/mapPlugin.tsx b/src/components/MapDisplay/mapPlugin.tsx deleted file mode 100644 index 7c334a1..0000000 --- a/src/components/MapDisplay/mapPlugin.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import Taro from '@tarojs/taro'; -import { Button } from '@tarojs/components'; -import { mapService, SearchResult, LocationInfo } from './mapService' -import { useEffect, useState } from 'react'; - -export default function MapPlugin() { - const key = 'AZNBZ-VCSC4-MLVUF-KBASD-6GZ6H-KBFTX'; //使用在腾讯位置服务申请的key -const referer = '八瓜一月'; //调用插件的app的名称 -const [currentLocation, setCurrentLocation] = useState(null) - -const category = ''; - -const chooseLocation = () => { - Taro.navigateTo({ - url: 'plugin://chooseLocation/index?key=' + key + '&referer=' + referer + '&latitude=' + currentLocation?.lat + '&longitude=' + currentLocation?.lng - }); -} -useEffect(() => { - initializeMapService() - }, []) - - // 初始化地图服务 - const initializeMapService = async () => { - try { - const success = await mapService.initSDK() - if (success) { - console.log('地图服务初始化成功') - getCurrentLocation() - } else { - console.error('地图服务初始化失败') - Taro.showToast({ - title: '地图服务初始化失败', - icon: 'none' - }) - } - } catch (error) { - console.error('初始化地图服务异常:', error) - Taro.showToast({ - title: '地图服务初始化异常', - icon: 'none' - }) - } - } - // 获取当前位置 - const getCurrentLocation = async () => { - try { - const location = await mapService.getLocation() - if (location) { - setCurrentLocation(location) - console.log('当前位置:', location) - } - } catch (error) { - console.error('获取位置失败:', error) - Taro.showToast({ - title: '获取位置失败', - icon: 'none' - }) - } - } - return ( - - ) -} \ No newline at end of file diff --git a/src/components/MapDisplay/mapService.ts b/src/components/MapDisplay/mapService.ts deleted file mode 100644 index 04cb68a..0000000 --- a/src/components/MapDisplay/mapService.ts +++ /dev/null @@ -1,190 +0,0 @@ -// 腾讯地图SDK服务 -import QQMapWX from "qqmap-wx-jssdk"; -import Taro from '@tarojs/taro'; - -// 扩展Window接口,添加qqmapsdk属性 -declare global { - interface Window { - qqmapsdk?: any; - } -} - -export interface LocationInfo { - lat: number - lng: number - address?: string -} - -export interface SearchResult { - id: string - title: string - address: string - location: { - lat: number - lng: number - } -} - -export interface SearchOptions { - keyword: string - location?: string - page_size?: number - page_index?: number -} - -class MapService { - private qqmapsdk: any = null - private isInitialized = false - - // 初始化腾讯地图SDK - async initSDK(): Promise { - if (this.isInitialized) { - return true - } - - try { - // 直接使用QQMapWX,不需要通过window对象 - this.qqmapsdk = new QQMapWX({ - key: 'AZNBZ-VCSC4-MLVUF-KBASD-6GZ6H-KBFTX' - }); - - this.isInitialized = true - console.log('腾讯地图SDK初始化成功') - return true - } catch (error) { - console.error('初始化腾讯地图SDK失败:', error) - return false - } - } - - // 搜索地点 - async search(options: SearchOptions): Promise { - if (!this.isInitialized) { - await this.initSDK() - } - - try { - console.log(this.qqmapsdk,11) - if (this.qqmapsdk && this.qqmapsdk.search) { - return new Promise((resolve, reject) => { - this.qqmapsdk.getSuggestion({ - keyword: options.keyword, - location: options.location || '39.908802,116.397502', // 默认北京 - page_size: options.page_size || 20, - page_index: options.page_index || 1, - success: (res: any) => { - console.log('搜索成功:', res) - if (res.data && res.data.length > 0) { - const results: SearchResult[] = res.data.map((item: any, index: number) => ({ - id: `search_${index}`, - title: item.title || item.name || '未知地点', - address: item.address || item.location || '地址未知', - location: { - lat: item.location?.lat || 0, - lng: item.location?.lng || 0 - } - })) - resolve(results) - } else { - resolve([]) - } - }, - fail: (err: any) => { - console.error('搜索失败:', err) - reject(err) - } - }) - }) - } else { - // 使用模拟数据 - console.log('使用模拟搜索数据') - return this.getMockSearchResults(options.keyword) - } - } catch (error) { - console.error('搜索异常:', error) - return this.getMockSearchResults(options.keyword) - } - } - - // 获取模拟搜索结果 - private getMockSearchResults(keyword: string): SearchResult[] { - const mockResults: SearchResult[] = [ - { - id: 'mock_1', - title: `${keyword}相关地点1`, - address: '模拟地址1 - 这是一个示例地址', - location: { lat: 39.908802, lng: 116.397502 } - }, - { - id: 'mock_2', - title: `${keyword}相关地点2`, - address: '模拟地址2 - 这是另一个示例地址', - location: { lat: 39.918802, lng: 116.407502 } - }, - { - id: 'mock_3', - title: `${keyword}相关地点3`, - address: '模拟地址3 - 第三个示例地址', - location: { lat: 39.898802, lng: 116.387502 } - } - ] - return mockResults - } - - // 获取当前位置 - async getCurrentLocation(): Promise<{ lat: number; lng: number } | null> { - try { - // 这里可以集成实际的定位服务 - // 暂时返回模拟位置 - const res = await Taro.getLocation({ - type: 'gcj02', - isHighAccuracy: true - }) - return { - lat: res.latitude, - lng: res.longitude - } - } catch (error) { - console.error('获取位置失败:', error) - return null - } - } - async getAddress(lat: number, lng: number): Promise { - try { - const addressRes: any = await new Promise((resolve, reject) => { - this.qqmapsdk.reverseGeocoder({ - location: { - latitude: lat, - longitude: lng - }, - success: resolve, - fail: reject - }) - }) - return addressRes?.results?.address - } catch (error) { - console.error('获取地址失败:', error) - } - } - async getLocation(): Promise<{ lat: number; lng: number; address: string } | null | undefined> { - try { - const currentInfo: any = {}; - const location = await this.getCurrentLocation(); - const { lat, lng } = location || {}; - - if (lat && lng) { - currentInfo.lat = lat; - currentInfo.lng = lng; - const addressRes = await this.getAddress(lat, lng) - if (addressRes) { - currentInfo.address = addressRes; - } - } - return currentInfo; - } catch (error) { - console.error('获取位置失败:', error) - } - } -} - -export const mapService = new MapService() \ No newline at end of file diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index fd7b4db..53961f0 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -6,7 +6,7 @@ import { MenuFilterProps } from "../../../types/list/types"; import styles from "./index.module.scss"; const MenuComponent = (props: MenuFilterProps) => { - const { options, value, onChange, wrapperClassName, itemClassName, name } = + const { options, value, onChange, wrapperClassName, itemClassName, name, onOpen, onClose,open } = props; const [isChange, setIsChange] = useState(false); const [isOpen, setIsOpen] = useState(false); @@ -18,10 +18,12 @@ const MenuComponent = (props: MenuFilterProps) => { const handleOpen = () => { setIsOpen(true); + onOpen && typeof onOpen === "function" && onOpen(); }; const handleClose = () => { setIsOpen(false); + onClose && typeof onClose === "function" && onClose(); }; return ( diff --git a/src/components/NumberInterval/NumberInterval.tsx b/src/components/NumberInterval/NumberInterval.tsx index 0a4ef3b..d3b8ed6 100644 --- a/src/components/NumberInterval/NumberInterval.tsx +++ b/src/components/NumberInterval/NumberInterval.tsx @@ -6,16 +6,30 @@ import { InputNumber } from '@nutui/nutui-react-taro' interface NumberIntervalProps { value: [number, number] onChange: (value: [number, number]) => void + min: number + max: number } const NumberInterval: React.FC = ({ value, - onChange + onChange, + min, + max }) => { - const [minParticipants, maxParticipants] = value || [1, 4] + const [minParticipants, maxParticipants] = value || [1, 1] + const handleChange = (value: [number | string, number | string]) => { - onChange([Number(value[0]), Number(value[1])]) + const newMin = Number(value[0]) + const newMax = Number(value[1]) + + // 确保最少人数不能大于最多人数 + if (newMin > newMax) { + return + } + + onChange([newMin, newMax]) } + return ( @@ -24,7 +38,7 @@ const NumberInterval: React.FC = ({ handleChange([value, maxParticipants])} formatter={(value) => `${value}人`} @@ -37,9 +51,9 @@ const NumberInterval: React.FC = ({ handleChange([value, maxParticipants])} + onChange={(value) => handleChange([minParticipants, value])} min={minParticipants} - max={maxParticipants} + max={max} formatter={(value) => `${value}人`} /> diff --git a/src/components/PublishMenu/PublishMenu.tsx b/src/components/PublishMenu/PublishMenu.tsx new file mode 100644 index 0000000..869dabf --- /dev/null +++ b/src/components/PublishMenu/PublishMenu.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react' +import { View, Text, Image } from '@tarojs/components' +import Taro from '@tarojs/taro' +import styles from './index.module.scss' +import images from '@/config/images' + +export interface PublishMenuProps { + onPersonalPublish?: () => void + onActivityPublish?: () => void +} + +const PublishMenu: React.FC = () => { + const [isVisible, setIsVisible] = useState(false) + + const handleIconClick = () => { + setIsVisible(!isVisible) + } + + const handleMenuItemClick = (type: 'individual' | 'group') => { + // 跳转到publishBall页面并传递type参数 + console.log(type, 'type'); + Taro.navigateTo({ + url: `/pages/publishBall/index?type=${type}` + }) + setIsVisible(false) + } + + + + return ( + + + {/* 菜单选项 */} + {isVisible && ( + + handleMenuItemClick('individual')} + > + + + + + 发布个人约球 + 已订场,找球友;未订场,找搭子 + + + + + + + handleMenuItemClick('group')} + > + + + + + 发布畅打活动 + 认证球场官方组织 + + + + + + + )} + + {/* 绿色圆形按钮 */} + + + + + ) +} + +export default PublishMenu diff --git a/src/components/PublishMenu/index.module.scss b/src/components/PublishMenu/index.module.scss new file mode 100644 index 0000000..f09ec51 --- /dev/null +++ b/src/components/PublishMenu/index.module.scss @@ -0,0 +1,195 @@ +.publishMenu { + position: relative; + z-index: 1000; +} + + +.menuCard { + position: absolute; + bottom: 80px; + right: 0; + width: 302px; + background: white; + border-radius: 16px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + padding: 12px; + animation: slideIn 0.3s ease-out; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 8px; + /* 小三角指示器 */ + &::after { + content: ''; + position: absolute; + bottom: -8px; + right: 20px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid white; + /* 移除阴影,避免连接处的黑色 */ + } + + /* 为小三角添加单独的阴影效果 */ + &::before { + content: ''; + position: absolute; + bottom: -9px; + right: 20px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid rgba(0, 0, 0, 0.1); + z-index: -1; + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.menuItem { + display: flex; + align-items: center; + padding: 0 10px; + cursor: pointer; + transition: background-color 0.2s ease; + border-radius: 20px; + border: 0.5px solid rgba(0, 0, 0, 0.08); + background: var(--Backgrounds-Primary, #FFF); + height: 68px; +} + +.menuIcon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; +} + +.ballIcon { + width: 24px; + height: 24px; + border: 2px solid #333; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 8px; + height: 8px; + border: 1px solid #333; + border-radius: 50%; + } + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 4px; + height: 4px; + background: #333; + border-radius: 50%; + } +} + +.activityIcon { + width: 24px; + height: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 16px; + height: 16px; + border: 2px solid #333; + border-radius: 50% 50% 0 50%; + transform: rotate(-45deg); + } + + &::after { + content: '+'; + position: absolute; + top: -2px; + right: 0; + font-size: 12px; + font-weight: bold; + color: #333; + } +} + +.menuContent { + flex: 1; + display: flex; + flex-direction: column; +} + +.menuTitle { + font-size: 16px; + font-weight: 600; + color: #000; + margin-bottom: 2px; + line-height: 24px; /* 150% */ +} + +.menuDesc { + font-size: 12px; + color: rgba(60, 60, 67, 0.60); + line-height: 18px; +} + +.menuArrow { + font-size: 16px; + color: #ccc; + margin-left: 8px; + .img{ + width: 16px; + height: 16px; + } +} + +.greenButton { + border-radius: 50%; + display: flex; + width: 60px; + height: 60px; + justify-content: space-between; + align-items: center; + box-sizing: border-box; + justify-content: center; + flex-shrink: 0; + overflow: hidden; + &.rotated { + transform: rotate(-90deg); + } +} + +.closeIcon { + color: white; + font-size: 24px; + width: 60px; + height: 60px; + font-weight: bold; + line-height: 1; +} diff --git a/src/components/PublishMenu/index.ts b/src/components/PublishMenu/index.ts new file mode 100644 index 0000000..a2a3890 --- /dev/null +++ b/src/components/PublishMenu/index.ts @@ -0,0 +1,2 @@ +export { default } from './PublishMenu' +export type { PublishMenuProps } from './PublishMenu' diff --git a/src/components/SearchBar/index.module.scss b/src/components/SearchBar/index.module.scss index fc822f1..493e867 100644 --- a/src/components/SearchBar/index.module.scss +++ b/src/components/SearchBar/index.module.scss @@ -5,9 +5,17 @@ --nutui-searchbar-input-text-color: #000000; --nutui-searchbar-input-padding: 0 0 0 10px; --nutui-searchbar-padding: 10px 0 0 0; + background-color: unset; + :global(.nut-searchbar-content) { box-shadow: 0 4px 48px #00000014; } + + .searchBarLeft { + display: flex; + align-items: center; + } + .searchBarRight { position: relative; width: 44px; @@ -18,14 +26,17 @@ display: flex; align-items: center; justify-content: center; + &.active { background-color: #000000; } } + .filterIcon { width: 20px; height: 20px; } + .filterCount { background-color: #000000; position: absolute; @@ -41,8 +52,9 @@ justify-content: center; font-size: 11px; } + .searchIcon { width: 20px; height: 20px; } -} +} \ No newline at end of file diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx index e4f1fab..f0aa118 100644 --- a/src/components/SearchBar/index.tsx +++ b/src/components/SearchBar/index.tsx @@ -8,20 +8,24 @@ interface IProps { isSelect: boolean; filterCount: number; onChange: (value: string) => void; + value: string; + onInputClick: () => void; } const SearchBarComponent = (props: IProps) => { - const { handleFilterIcon, isSelect, filterCount, onChange } = props; + const { handleFilterIcon, isSelect, filterCount, onChange, value, onInputClick } = props; + const handleChange = (value: string) => { onChange && onChange(value); }; return ( <> + - + } right={ { className={styles.searchBar} placeholder="搜索上海的球局和场地" onChange={handleChange} + value={value} + onInputClick={onInputClick} /> ); diff --git a/src/components/TextareaTag/TextareaTag.tsx b/src/components/TextareaTag/TextareaTag.tsx index 4aa5e91..96c2343 100644 --- a/src/components/TextareaTag/TextareaTag.tsx +++ b/src/components/TextareaTag/TextareaTag.tsx @@ -19,7 +19,7 @@ const TextareaTag: React.FC = ({ value, onChange, placeholder = '请输入', - maxLength = 500, + maxLength = 1000, options = [] }) => { // 处理文本输入变化 diff --git a/src/components/TimePicker/README.md b/src/components/TimePicker/README.md new file mode 100644 index 0000000..bf70b24 --- /dev/null +++ b/src/components/TimePicker/README.md @@ -0,0 +1,77 @@ +# TimePicker 时间选择器组件 + +## 功能特性 + +- 使用自定义样式重写PickerViewColumn功能 +- 完全还原原生PickerView的样式和动画效果 +- 支持年份和月份选择 +- 平滑的滚动动画和切换效果 +- 响应式设计,支持触摸滚动 +- 渐变遮罩效果增强视觉层次 + +## 技术实现 + +### 核心特性 +- 使用ScrollView替代PickerViewColumn +- 自定义滚动逻辑实现选项对齐 +- CSS动画和过渡效果还原原生体验 +- 智能滚动位置计算和自动对齐 + +### 样式还原 +- 选中项指示器(高亮背景) +- 渐变遮罩效果(顶部和底部) +- 平滑的过渡动画 +- 精确的尺寸和间距 + +## 使用方法 + +```tsx +import { TimePicker } from '@/components/TimePicker' + +const [visible, setVisible] = useState(false) + + setVisible(false)} + onConfirm={(year, month) => { + console.log('选择的时间:', year, month) + setVisible(false) + }} + defaultYear={2024} + defaultMonth={6} + minYear={2020} + maxYear={2030} +/> +``` + +## Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| visible | boolean | - | 是否显示选择器 | +| visible | boolean | - | 是否显示选择器 | +| onClose | () => void | - | 关闭回调 | +| onConfirm | (year: number, month: number) => void | - | 确认选择回调 | +| defaultYear | number | 当前年份 | 默认选中的年份 | +| defaultMonth | number | 当前月份 | 默认选中的月份 | +| minYear | number | 2020 | 最小年份 | +| maxYear | number | 2030 | 最大年份 | + +## 样式定制 + +组件使用CSS Modules,可以通过修改`index.module.scss`文件来自定义样式: + +- `.time-picker-popup`: 弹出层容器 +- `.picker-container`: 选择器容器 +- `.custom-picker`: 自定义选择器 +- `.picker-indicator`: 选中项指示器 +- `.picker-column`: 选择列 +- `.picker-item`: 选择项 +- `.picker-item-active`: 激活状态的选择项 + +## 测试 + +运行测试页面: +```tsx +import TimePickerTest from '@/components/TimePicker/test' +``` \ No newline at end of file diff --git a/src/components/TimePicker/TimePicker.tsx b/src/components/TimePicker/TimePicker.tsx new file mode 100644 index 0000000..b18ff41 --- /dev/null +++ b/src/components/TimePicker/TimePicker.tsx @@ -0,0 +1,233 @@ +import React, { useState, useEffect, useRef } from 'react' +import { View, Text, ScrollView } from '@tarojs/components' +import { CommonPopup } from '../index' +import styles from './index.module.scss' + +export interface TimePickerProps { + visible: boolean + onClose: () => void + onConfirm: (year: number, month: number) => void + defaultYear?: number + defaultMonth?: number + minYear?: number + maxYear?: number +} + +const TimePicker: React.FC = ({ + visible, + onClose, + onConfirm, + defaultYear = new Date().getFullYear(), + defaultMonth = new Date().getMonth() + 1, + minYear = 2020, + maxYear = 2030 +}) => { + const [selectedYear, setSelectedYear] = useState(defaultYear) + const [selectedMonth, setSelectedMonth] = useState(defaultMonth) + const [yearScrollTop, setYearScrollTop] = useState(0) + const [monthScrollTop, setMonthScrollTop] = useState(0) + + const yearScrollRef = useRef(null) + const monthScrollRef = useRef(null) + + // 计算当前选项在数组中的索引 + const getYearIndex = (year: number) => year - minYear + const getMonthIndex = (month: number) => month - 1 + + // 生成选择器的选项数据 + const yearOptions = Array.from({ length: maxYear - minYear + 1 }, (_, index) => ({ + text: `${minYear + index}年`, + value: minYear + index + })) + + const monthOptions = Array.from({ length: 12 }, (_, index) => ({ + text: `${index + 1}月`, + value: index + 1 + })) + + // 计算滚动位置 - 确保每次只显示一个选项 + const calculateScrollTop = (index: number) => { + const itemHeight = 48 // 每个选项的高度 + const containerHeight = 216 // 容器高度 + const centerOffset = (containerHeight - itemHeight) / 2 + return index * itemHeight - centerOffset + } + + // 获取当前可见的选项数量 + const getVisibleItemCount = () => { + const containerHeight = 216 + const itemHeight = 48 + return Math.floor(containerHeight / itemHeight) + } + + useEffect(() => { + if (visible) { + setSelectedYear(defaultYear) + setSelectedMonth(defaultMonth) + + // 设置初始滚动位置 + const yearScrollTop = calculateScrollTop(getYearIndex(defaultYear)) + const monthScrollTop = calculateScrollTop(getMonthIndex(defaultMonth)) + setYearScrollTop(yearScrollTop) + setMonthScrollTop(monthScrollTop) + } + }, [visible, defaultYear, defaultMonth]) + + // 处理年份滚动 + const handleYearScroll = (event: any) => { + const scrollTop = event.detail.scrollTop + const itemHeight = 48 + const containerHeight = 216 + const centerOffset = (containerHeight - itemHeight) / 2 + + // 计算当前选中的年份索引 + const currentIndex = Math.round((scrollTop + centerOffset) / itemHeight) + const clampedIndex = Math.max(0, Math.min(currentIndex, yearOptions.length - 1)) + const newYear = minYear + clampedIndex + + if (newYear !== selectedYear) { + setSelectedYear(newYear) + } + } + + // 处理年份滚动结束,自动对齐 + const handleYearScrollEnd = () => { + const yearIndex = getYearIndex(selectedYear) + const alignedScrollTop = calculateScrollTop(yearIndex) + // 使用setTimeout确保滚动动画完成后再对齐 + setTimeout(() => { + setYearScrollTop(alignedScrollTop) + }, 100) + } + + // 处理月份滚动 + const handleMonthScroll = (event: any) => { + const scrollTop = event.detail.scrollTop + const itemHeight = 48 + const containerHeight = 216 + const centerOffset = (containerHeight - itemHeight) / 2 + + // 计算当前选中的月份索引 + const currentIndex = Math.round((scrollTop + centerOffset) / itemHeight) + const clampedIndex = Math.max(0, Math.min(currentIndex, monthOptions.length - 1)) + const newMonth = clampedIndex + 1 + + if (newMonth !== selectedMonth) { + setSelectedMonth(newMonth) + } + } + + // 处理月份滚动结束,自动对齐 + const handleMonthScrollEnd = () => { + const monthIndex = getMonthIndex(selectedMonth) + const alignedScrollTop = calculateScrollTop(monthIndex) + // 使用setTimeout确保滚动动画完成后再对齐 + setTimeout(() => { + setMonthScrollTop(alignedScrollTop) + }, 100) + } + + const handleConfirm = () => { + onConfirm(selectedYear, selectedMonth) + onClose() + } + + if (!visible) return null + + return ( + + {/* 拖拽手柄 */} + + + {/* 时间选择器 */} + + {/* 自定义多列选择器 */} + + + {/* 选中项指示器 */} + + + {/* 年份列 */} + + + + {yearOptions.map((option, index) => ( + + {option.text} + + ))} + + + + + {/* 月份列 */} + + + + {monthOptions.map((option, index) => ( + + {option.text} + + ))} + + + + + + + + ) +} + +export default TimePicker diff --git a/src/components/TimePicker/demo.module.scss b/src/components/TimePicker/demo.module.scss new file mode 100644 index 0000000..fa78e7f --- /dev/null +++ b/src/components/TimePicker/demo.module.scss @@ -0,0 +1,81 @@ +.demoContainer { + padding: 20px; + text-align: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + color: white; +} + +.title { + font-size: 24px; + font-weight: 700; + margin-bottom: 10px; + display: block; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.subtitle { + font-size: 16px; + margin-bottom: 30px; + display: block; + opacity: 0.9; +} + +.demoButton { + margin: 20px 0; + width: 250px; + height: 50px; + border-radius: 25px; + font-size: 18px; + font-weight: 600; + background: rgba(255, 255, 255, 0.2); + border: 2px solid rgba(255, 255, 255, 0.3); + color: white; + + &:active { + background: rgba(255, 255, 255, 0.3); + transform: scale(0.98); + } +} + +.demoResult { + margin: 30px 0; + padding: 20px; + background: rgba(255, 255, 255, 0.1); + border-radius: 16px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + + text { + font-size: 18px; + font-weight: 600; + color: white; + } +} + +.demoFeatures { + margin-top: 40px; + padding: 20px; + background: rgba(255, 255, 255, 0.1); + opacity: 0.9; + border-radius: 16px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + text-align: left; +} + +.featureTitle { + font-size: 18px; + font-weight: 600; + margin-bottom: 15px; + display: block; + color: white; +} + +.featureItem { + font-size: 14px; + margin: 8px 0; + display: block; + color: rgba(255, 255, 255, 0.9); + line-height: 1.5; +} \ No newline at end of file diff --git a/src/components/TimePicker/demo.tsx b/src/components/TimePicker/demo.tsx new file mode 100644 index 0000000..ca0a2a7 --- /dev/null +++ b/src/components/TimePicker/demo.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react' +import { View, Button, Text } from '@tarojs/components' +import TimePicker from './TimePicker' +import styles from './demo.module.scss' + +const TimePickerDemo: React.FC = () => { + const [visible, setVisible] = useState(false) + const [selectedTime, setSelectedTime] = useState('') + + const handleConfirm = (year: number, month: number) => { + setSelectedTime(`${year}年${month}月`) + setVisible(false) + } + + return ( + + TimePicker 演示 + 体验"一个一个往下翻"的效果 + + + + {selectedTime && ( + + 已选择: {selectedTime} + + )} + + + 特性说明: + • 每次只显示一个选项 + • 完美居中对齐 + • 平滑滚动动画 + • 触摸结束后自动对齐 + + + setVisible(false)} + onConfirm={handleConfirm} + defaultYear={2024} + defaultMonth={6} + minYear={2020} + maxYear={2030} + /> + + ) +} + +export default TimePickerDemo \ No newline at end of file diff --git a/src/components/TimePicker/index.module.scss b/src/components/TimePicker/index.module.scss new file mode 100644 index 0000000..a2cc061 --- /dev/null +++ b/src/components/TimePicker/index.module.scss @@ -0,0 +1,187 @@ +/* 时间选择器弹出层样式 */ +.time-picker-popup { + .common-popup-content { + padding: 0; + } +} + +.popup-handle { + width: 32px; + height: 4px; + background: #e0e0e0; + border-radius: 2px; + margin: 12px auto; +} + +.picker-container { + padding: 26px 16px 0 16px; + background: #fff; +} + +.picker-wrapper { + position: relative; +} + +.custom-picker { + position: relative; + width: 100%; + height: 216px; + background: #fff; + border-radius: 8px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + /* 确保只显示一个选项 */ + perspective: 1000px; + /* 水平布局 */ + flex-direction: row; + /* 确保列之间有适当间距 */ + gap: 0; +} + +/* 选中项指示器 */ +.picker-indicator { + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 48px; + background: rgba(22, 24, 35, 0.05); + transform: translateY(-50%); + pointer-events: none; + z-index: 1; + border-radius: 4px; + box-shadow: inset 0 0 0 1px rgba(22, 24, 35, 0.1); + /* 确保指示器完美覆盖选中项 */ + margin: 0 20px; + width: calc(100% - 40px); +} + +.picker-column { + flex: 1; + height: 100%; + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + /* 水平居中布局 */ + min-width: 0; + /* 确保列之间有适当间距 */ + padding: 0 8px; + + &:first-child { + border-right: 1px solid rgba(0, 0, 0, 0.1); + } + + /* 确保滚动容器正确显示 */ + &::before, + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 84px; + pointer-events: none; + z-index: 2; + } + + &::before { + top: 0; + background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%); + } + + &::after { + bottom: 0; + background: linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%); + } +} + +.picker-scroll { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + will-change: scroll-position; + -webkit-overflow-scrolling: touch; + /* 确保滚动行为 */ + scroll-snap-type: y mandatory; + /* 优化滚动性能 */ + overscroll-behavior: contain; +} + +.picker-padding { + height: 84px; /* (216 - 48) / 2 = 84px,用于居中对齐 */ + /* 确保padding区域不可见 */ + opacity: 0; + pointer-events: none; +} + +.picker-item { + display: flex; + align-items: center; + justify-content: center; + height: 48px; + width: 100%; + font-size: 16px; + color: #161823; + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + position: relative; + will-change: transform, color; + /* 确保每个选项都能正确对齐 */ + scroll-snap-align: center; + /* 水平居中 */ + text-align: center; + + &.picker-item-active { + color: #161823; + font-weight: 600; + transform: scale(1.02); + + .picker-item-text { + color: #161823; + font-weight: 600; + } + } + + &:not(.picker-item-active) { + color: rgba(22, 24, 35, 0.6); + + .picker-item-text { + color: rgba(22, 24, 35, 0.6); + } + } +} + +.picker-item-text { + font-size: 16px; + color: inherit; + text-align: center; + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + user-select: none; + width: 100%; + line-height: 48px; + white-space: nowrap; + /* 确保文字完美居中 */ + display: block; + overflow: hidden; + text-overflow: ellipsis; + /* 强制居中对齐 */ + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +/* 滚动条隐藏 */ +.picker-scroll { + ::-webkit-scrollbar { + width: 0; + background: transparent; + } +} + +/* 移除重复的渐变遮罩代码,已在.picker-column中定义 */ \ No newline at end of file diff --git a/src/components/TimePicker/index.ts b/src/components/TimePicker/index.ts new file mode 100644 index 0000000..0febefb --- /dev/null +++ b/src/components/TimePicker/index.ts @@ -0,0 +1,2 @@ +export { default } from './TimePicker' +export type { TimePickerProps } from './TimePicker' \ No newline at end of file diff --git a/src/components/TimePicker/layout-test.module.scss b/src/components/TimePicker/layout-test.module.scss new file mode 100644 index 0000000..f6e0ea0 --- /dev/null +++ b/src/components/TimePicker/layout-test.module.scss @@ -0,0 +1,59 @@ +.testContainer { + padding: 20px; + text-align: center; + background: #f8f9fa; + min-height: 100vh; +} + +.testTitle { + font-size: 22px; + font-weight: 700; + margin-bottom: 10px; + display: block; + color: #333; +} + +.testSubtitle { + font-size: 16px; + margin-bottom: 30px; + display: block; + color: #666; +} + +.testInfo { + margin: 20px 0; + padding: 20px; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + text-align: left; + + text { + font-size: 14px; + margin: 8px 0; + display: block; + color: #555; + line-height: 1.5; + } +} + +.testButton { + margin: 20px 0; + width: 200px; + height: 44px; + border-radius: 22px; + font-size: 16px; + background: #007bff; + border: none; +} + +.testResult { + margin: 20px 0; + color: white; + border-radius: 8px; + + text { + font-size: 16px; + font-weight: 600; + } +} \ No newline at end of file diff --git a/src/components/TimePicker/layout-test.tsx b/src/components/TimePicker/layout-test.tsx new file mode 100644 index 0000000..80075d9 --- /dev/null +++ b/src/components/TimePicker/layout-test.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react' +import { View, Text, Button } from '@tarojs/components' +import TimePicker from './TimePicker' +import styles from './layout-test.module.scss' + +const LayoutTest: React.FC = () => { + const [visible, setVisible] = useState(false) + const [selectedTime, setSelectedTime] = useState('') + + const handleConfirm = (year: number, month: number) => { + setSelectedTime(`${year}年${month}月`) + setVisible(false) + } + + return ( + + 布局测试 + 验证年份和月份的水平居中对齐 + + + • 年份和月份应该在同一行显示 + • 两个列应该水平居中对齐 + • 选中项指示器应该完美覆盖两个列 + + + + + {selectedTime && ( + + 选择结果: {selectedTime} + + )} + + setVisible(false)} + onConfirm={handleConfirm} + defaultYear={2024} + defaultMonth={6} + minYear={2020} + maxYear={2030} + /> + + ) +} \ No newline at end of file diff --git a/src/components/TimePicker/test.module.scss b/src/components/TimePicker/test.module.scss new file mode 100644 index 0000000..594819e --- /dev/null +++ b/src/components/TimePicker/test.module.scss @@ -0,0 +1,36 @@ +.container { + padding: 20px; + text-align: center; + background: #f5f5f5; + min-height: 100vh; +} + +.title { + font-size: 20px; + font-weight: 600; + color: #333; + margin-bottom: 30px; + display: block; +} + +.button { + margin: 20px 0; + width: 200px; + height: 44px; + border-radius: 22px; + font-size: 16px; +} + +.result { + margin-top: 30px; + padding: 20px; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + text { + font-size: 16px; + color: #333; + font-weight: 500; + } +} \ No newline at end of file diff --git a/src/components/TimePicker/test.tsx b/src/components/TimePicker/test.tsx new file mode 100644 index 0000000..fe8ef7a --- /dev/null +++ b/src/components/TimePicker/test.tsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react' +import { View, Button, Text } from '@tarojs/components' +import TimePicker from './TimePicker' +import styles from './test.module.scss' + +const TimePickerTest: React.FC = () => { + const [visible, setVisible] = useState(false) + const [selectedTime, setSelectedTime] = useState('') + + const handleConfirm = (year: number, month: number) => { + setSelectedTime(`${year}年${month}月`) + setVisible(false) + } + + return ( + + TimePicker 组件测试 + + + + {selectedTime && ( + + 已选择: {selectedTime} + + )} + + setVisible(false)} + onConfirm={handleConfirm} + defaultYear={2024} + defaultMonth={6} + minYear={2020} + maxYear={2030} + /> + + ) +} + +export default TimePickerTest \ No newline at end of file diff --git a/src/components/TimeSelector/TimeSelector.tsx b/src/components/TimeSelector/TimeSelector.tsx index 45d956d..1390d66 100644 --- a/src/components/TimeSelector/TimeSelector.tsx +++ b/src/components/TimeSelector/TimeSelector.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { View, Text, } from '@tarojs/components' -import { getDate, getTime } from '@/utils/timeUtils' -import DateTimePicker from '@/components/DateTimePicker' +import { getDate, getTime, getDateStr, getEndTime } from '@/utils/timeUtils' +import DialogCalendarCard from '@/components/CalendarCard/DialogCalendarCard' import './TimeSelector.scss' export interface TimeRange { @@ -23,8 +23,11 @@ const TimeSelector: React.FC = ({ }) => { // 格式化日期显示 const [visible, setVisible] = useState(false) - const handleConfirm = (year: number, month: number) => { - console.log('选择的日期:', year, month) + const handleConfirm = (date: Date) => { + console.log('选择的日期:', date) + const start_time = getDateStr(date) + const end_time = getEndTime(start_time) + if (onChange) onChange({start_time, end_time}) } return ( @@ -56,14 +59,10 @@ const TimeSelector: React.FC = ({ - setVisible(false)} - onConfirm={handleConfirm} - defaultYear={2025} - defaultMonth={11} - minYear={2020} - maxYear={2030} + setVisible(false)} /> ) diff --git a/src/components/UploadCover/index.tsx b/src/components/UploadCover/index.tsx index 08927d0..6da8f37 100644 --- a/src/components/UploadCover/index.tsx +++ b/src/components/UploadCover/index.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, useState } from 'react' +import React, { useCallback, useRef, useState } from 'react' import { Image, View, Text } from '@tarojs/components' import img from '../../config/images' -import UploadSourcePopup from './upload-source-popup' +import UploadSourcePopup, { sourceMap } from './upload-source-popup' import UploadFromWx from './upload-from-wx' import { CommonPopup } from '../' @@ -58,6 +58,9 @@ export default function UploadCover(props: UploadCoverProps) { } = props const [visible, setVisible] = useState(false) + const uploadSourcePopupRef = useRef<{ + show: (sourceType: sourceType, maxCount: number) => void + }>(null) const onAdd = useCallback((images: CoverImageValue[]) => { // FIXME: prev is not latest value @@ -86,6 +89,7 @@ export default function UploadCover(props: UploadCoverProps) { round position="bottom" hideFooter + zIndex={1000} > { @@ -96,7 +100,9 @@ export default function UploadCover(props: UploadCoverProps) { item === 'album' ? ( ) : ( - + uploadSourcePopupRef.current?.show(item, maxCount - value.length)}> + {sourceMap.get(item)} + ) } @@ -105,6 +111,7 @@ export default function UploadCover(props: UploadCoverProps) { } +
{value.length < maxCount && (
setVisible(true)}> diff --git a/src/components/UploadCover/upload-source-popup.tsx b/src/components/UploadCover/upload-source-popup.tsx index ccb156f..dc72e44 100644 --- a/src/components/UploadCover/upload-source-popup.tsx +++ b/src/components/UploadCover/upload-source-popup.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react' import { Image, View, Text, ScrollView, Button } from '@tarojs/components' import Taro from '@tarojs/taro' import img from '../../config/images' @@ -17,12 +17,10 @@ type ImageItem = { } interface UploadImageProps { - sourceType: SourceType onAdd: (images: ImageItem[]) => void - maxCount: number } -const sourceMap = new Map([ +export const sourceMap = new Map([ ['history', '历史图库'], ['preset', '预设图库'] ]) @@ -31,13 +29,13 @@ const checkImageSelected = (images: ImageItem[], image: ImageItem) => { return images.some(item => item.id === image.id) } -export default function UploadImage(props: UploadImageProps) { +export default forwardRef(function UploadImage(props: UploadImageProps, ref) { const { - sourceType = 'history', onAdd = () => void 0, - maxCount = 9, } = props const [visible, setVisible] = useState(false) + const [sourceType, setSourceType] = useState('history') + const [maxCount, setMaxCount] = useState(9) const [images, setImages] = useState([]) const [selectedImages, setSelectedImages] = useState([]) @@ -54,36 +52,50 @@ export default function UploadImage(props: UploadImageProps) { } } - useEffect(() => { - if (visible) { - publishService.getPictures({ - pageOption: { - page: 1, - pageSize: 100, - }, - seachOption: { - tag: '', - resource_type: 'image', - dateRange: [], - }, - }).then(res => { - if (res.success) { - setImages(res.data.data.rows.map(item => ({ - id: Date.now().toString(), - url: item.thumbnail_url, - }))) - } else { - // TODO: 显示错误信息 - Taro.showToast({ - title: res.message, - icon: 'none' - }) - } - }) - } else { - setSelectedImages([]) + useImperativeHandle(ref, () => ({ + show: (sourceType: SourceType, maxCount: number) => { + setVisible(true) + setSourceType(sourceType) + setMaxCount(maxCount) + fetchImages() } - }, [visible]) + })) + + function fetchImages() { + publishService.getPictures({ + pageOption: { + page: 1, + pageSize: 100, + }, + seachOption: { + tag: '', + resource_type: 'image', + dateRange: [], + }, + }).then(res => { + if (res.success) { + let start = 0 + setImages(res.data.data.rows.map(item => ({ + id: (Date.now() + start++).toString(), + url: item.thumbnail_url, + }))) + } else { + // TODO: 显示错误信息 + Taro.showToast({ + title: res.message, + icon: 'none' + }) + } + }) + } + + function onClose() { + setVisible(false) + setSelectedImages([]) + setImages([]) + setSourceType('history') + setMaxCount(9) + } const handleConfirm = () => { if (selectedImages.length > 0) { @@ -103,10 +115,11 @@ export default function UploadImage(props: UploadImageProps) { <> setVisible(false)} + onClose={onClose} round hideFooter position='bottom' + zIndex={1001} > {sourceMap.get(sourceType)} @@ -154,8 +167,7 @@ export default function UploadImage(props: UploadImageProps) { )} - setVisible(true)}>{sourceMap.get(sourceType)}选取 + {/* setVisible(true)}>{sourceMap.get(sourceType)}选取 */} ); -}; - +}); diff --git a/src/components/index.ts b/src/components/index.ts index 16b7a31..665421f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -9,19 +9,28 @@ import TimeSelector from './TimeSelector' import TitleTextarea from './TitleTextarea' import CommonPopup from './CommonPopup' import DateTimePicker from './DateTimePicker/DateTimePicker' +import TimePicker from './TimePicker/TimePicker' +import CalendarCard, { DialogCalendarCard } from './CalendarCard' +import CommonDialog from './CommonDialog' +import PublishMenu from './PublishMenu/PublishMenu' import UploadCover from './UploadCover' -export { - ActivityTypeSwitch, - TextareaTag, - FormSwitch, - ImageUpload, - Range, - NumberInterval, - TimeSelector, - TitleTextarea, - CommonPopup, - DateTimePicker, - UploadCover, -} + export { + ActivityTypeSwitch, + TextareaTag, + FormSwitch, + ImageUpload, + Range, + NumberInterval, + TimeSelector, + TitleTextarea, + CommonPopup, + DateTimePicker, + TimePicker, + CalendarCard, + DialogCalendarCard, + CommonDialog, + PublishMenu, + UploadCover + } diff --git a/src/config/formSchema/publishBallFormSchema.ts b/src/config/formSchema/publishBallFormSchema.ts index 563a4d3..92bc56b 100644 --- a/src/config/formSchema/publishBallFormSchema.ts +++ b/src/config/formSchema/publishBallFormSchema.ts @@ -50,7 +50,8 @@ export const publishBallFormSchema: FormFieldConfig[] = [ placeholder: '请选择活动类型', required: true, props: { - maxCount: 9 + maxCount: 9, + source: ['album', 'history', 'preset'] } }, { @@ -121,7 +122,8 @@ export const publishBallFormSchema: FormFieldConfig[] = [ defaultValue: 1, props: { showSummary: true, - summary: '最少1人,最多4人', + min: 1, + max: 20, } }, @@ -146,6 +148,9 @@ export const publishBallFormSchema: FormFieldConfig[] = [ type: FieldType.TEXTAREATAG, placeholder: '补充性别偏好、特殊要求和注意事项等信息', required: true, + props: { + maxLength: 1000, + }, options:[ { label: '仅限男生', value: '仅限男生' }, { label: '仅限女生', value: '仅限女生' }, diff --git a/src/config/images.js b/src/config/images.js index 13c69c9..8009e5f 100644 --- a/src/config/images.js +++ b/src/config/images.js @@ -19,7 +19,6 @@ export default { ICON_LIST_RIGHT_ARROW: require('@/static/list/icon-list-right-arrow.svg'), ICON_ARROW_LEFT: require('@/static/detail/icon-arrow-left.svg'), ICON_LOGO_GO: require('@/static/detail/icon-logo-go.svg'), - ICON_SEARCH: require('@/static/publishBall/icon-search.svg'), ICON_MAP: require('@/static/publishBall/icon-map.svg'), ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'), ICON_ARRORW_SMALL: require('@/static/publishBall/icon-arrow-small.svg'), @@ -28,6 +27,11 @@ export default { ICON_ADD: require('@/static/publishBall/icon-add.svg'), ICON_COPY: require('@/static/publishBall/icon-arrow-right.svg'), ICON_DELETE: require('@/static/publishBall/icon-delete.svg'), + ICON_RIGHT_MAX: require('@/static/publishBall/icon-right-max.svg'), + ICON_PLUS: require('@/static/publishBall/icon-plus.svg'), + ICON_GROUP: require('@/static/publishBall/icon-group.svg'), + ICON_PERSON: require('@/static/publishBall/icon-person.svg'), + ICON_PUBLISH: require('@/static/publishBall/icon-publish.png'), ICON_CIRCLE_UNSELECT: require('@/static/publishBall/icon-circle-unselect.svg'), ICON_CIRCLE_SELECT: require('@/static/publishBall/icon-circle-select-ring.svg'), ICON_CIRCLE_SELECT_ARROW: require('@/static/publishBall/icon-circle-select-arrow.svg'), @@ -41,4 +45,15 @@ export default { ICON_DETAIL_COMMENT: require('@/static/detail/icon-comment.svg'), ICON_DETAIL_COMMENT_DARK: require('@/static/detail/icon-comment-dark.svg'), ICON_DETAIL_SHARE: require('@/static/detail/icon-share-dark.svg'), + ICON_GUIDE_BAR_PUBLISH: require('@/static/common/guide-bar-publish.svg'), + ICON_NAVIGATOR_BACK: require('@/static/common/navigator-back.svg'), + ICON_LIST_PLAYING_GAME: require('@/static/list/icon-paying-game.svg'), + ICON_LIST_LOAD_ERROR: require('@/static/list/icon-load-error.svg'), + ICON_LIST_RELOAD: require('@/static/list/icon-reload.svg'), + ICON_LIST_SEARCH_SEARCH: require('@/static/search/icon-search.svg'), + ICON_LIST_SEARCH_BACK: require('@/static/search/icon-back.svg'), + ICON_LIST_SEARCH_CLEAR: require('@/static/search/icon-search-clear.svg'), + ICON_LIST_SEARCH_CLEAR_HISTORY: require('@/static/search/icon-clear-history.svg'), + ICON_LIST_SEARCH_SUGGESTION: require('@/static/search/icon-search-suggestion.svg'), + ICON_LIST_INPUT_LOGO: require('@/static/list/icon-input-logo.svg'), } \ No newline at end of file diff --git a/src/container/inputCustomerNavbar/index.scss b/src/container/inputCustomerNavbar/index.scss new file mode 100644 index 0000000..a92d9cd --- /dev/null +++ b/src/container/inputCustomerNavbar/index.scss @@ -0,0 +1,47 @@ + .inputCustomerNavbarContainer { + padding-left: 17px; + display: flex; + gap: 8px; + + + .logo { + width: 28px; + height: 16px; + } + + .icon16 { + width: 16px; + height: 16px; + } + + .navContent { + display: flex; + align-items: center; + gap: 4px; + width: 73%; + height: max-content; + padding-top: 5px; + } + + .searchContainer { + width: 100%; + display: flex; + align-items: center; + gap: 5.85px; + padding: 7.8px; + border-radius: 30px; + border: 0.488px solid rgba(0, 0, 0, 0.06); + box-shadow: 0 3.902px 46.829px 0 rgba(0, 0, 0, 0.08); + height: 30px; + box-sizing: border-box; + font-size: 13.659px; + font-style: normal; + font-weight: 400; + line-height: 17.561px; + flex: 1; + + .nut-input { + padding: 0; + } + } + } \ No newline at end of file diff --git a/src/container/inputCustomerNavbar/index.tsx b/src/container/inputCustomerNavbar/index.tsx new file mode 100644 index 0000000..3414715 --- /dev/null +++ b/src/container/inputCustomerNavbar/index.tsx @@ -0,0 +1,74 @@ +import { View, Image } from "@tarojs/components"; +import img from "@/config/images"; +import { getCurrentLocation } from "@/utils/locationUtils"; +import "./index.scss"; +import { useEffect } from "react"; +import { useGlobalState } from "@/store/global"; +import { useListState } from "@/store/listStore"; +import CustomNavbar from "@/components/CustomNavbar"; +import { Input } from "@nutui/nutui-react-taro"; +import Taro from "@tarojs/taro"; + +interface IProps { + icon: string; +} + +const ListHeader = (props: IProps) => { + const { icon } = props; + const { updateState, statusNavbarHeightInfo } = useGlobalState(); + const { searchValue } = useListState(); + const { statusBarHeight, navbarHeight } = statusNavbarHeightInfo; + + // 获取位置信息 + const getCurrentLocal = () => { + updateState({ + getLocationLoading: true, + }); + getCurrentLocation().then((res) => { + updateState({ + getLocationLoading: false, + location: res || {}, + }); + }); + }; + useEffect(() => { + getCurrentLocal(); + }, []); + + const handleInputClick = () => { + Taro.navigateTo({ + url: "/pages/search/index", + }); + } + + return ( + + + + {/* logo */} + + {/* 搜索框 */} + + + + + + + + ); +}; +export default ListHeader; diff --git a/src/container/listContainer/index.scss b/src/container/listContainer/index.scss new file mode 100644 index 0000000..d466de1 --- /dev/null +++ b/src/container/listContainer/index.scss @@ -0,0 +1,22 @@ +.listContentWrapper { + padding: 0 5px; + background: #fafafa; + display: flex; + flex-direction: column; + gap: 5px; + // background-color: red; + + .recommendTextWrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 22px 10px; + } + + .recommendText { + color: rgba(0, 0, 0, 0.85); + font-size: 14px; + font-weight: 500; + line-height: 24px; + } +} \ No newline at end of file diff --git a/src/container/listContainer/index.tsx b/src/container/listContainer/index.tsx new file mode 100644 index 0000000..6543a98 --- /dev/null +++ b/src/container/listContainer/index.tsx @@ -0,0 +1,45 @@ +import { View, Text } from "@tarojs/components"; +import ListCard from "@/components/ListCard"; +import ListLoadError from "@/components/ListLoadError"; +import ListCardSkeleton from "@/components/ListCardSkeleton"; +import "./index.scss"; + +const ListContainer = (props) => { + const { loading, data = [], error, reload, recommendList } = props; + + if (error) { + return ; + } + + const renderList = (list) => { + if (loading && list.length === 0) { + return ( + <> + {new Array(10).fill(0).map(() => { + return ; + })} + + ); + } + + return ( + <> + {list?.map((match, index) => ( + + ))} + + ); + }; + + return ( + + {renderList(data)} + + 搜索结果较少,已为你推荐其他内容 + + {renderList(recommendList)} + + ); +}; + +export default ListContainer; diff --git a/src/container/listCustomNavbar/index.module.scss b/src/container/listCustomNavbar/index.module.scss new file mode 100644 index 0000000..b31514b --- /dev/null +++ b/src/container/listCustomNavbar/index.module.scss @@ -0,0 +1,43 @@ + .container { + padding-left: 17px; + display: flex; + align-items: center; + gap: 8px; + + .line { + width: 1px; + height: 25px; + background-color: #0000000F; + } + + .logo { + width: 60px; + height: 34px; + } + + .change { + width: 12px; + height: 12px; + } + + .cityWrapper { + line-height: 20px; + } + + .city { + font-weight: 600; + font-size: 13px; + line-height: 20px; + } + + .infoWrapper { + line-height: 12px; + } + + .info { + font-weight: 400; + font-size: 10px; + line-height: 12px; + color: #3C3C4399; + } + } \ No newline at end of file diff --git a/src/container/listCustomNavbar/index.tsx b/src/container/listCustomNavbar/index.tsx new file mode 100644 index 0000000..b9484eb --- /dev/null +++ b/src/container/listCustomNavbar/index.tsx @@ -0,0 +1,69 @@ +import { View, Text, Image } from "@tarojs/components"; +import img from "@/config/images"; +import { getCurrentLocation } from "@/utils/locationUtils"; +import styles from "./index.module.scss"; +import { useEffect } from "react"; +import { useGlobalState } from "@/store/global"; +import { useListState } from "@/store/listStore"; +import CustomNavbar from '@/components/CustomNavbar' + +const ListHeader = () => { + const { + updateState, + location, + getLocationText, + getLocationLoading, + statusNavbarHeightInfo, + } = useGlobalState(); + const { gamesNum } = useListState(); + const { statusBarHeight, navbarHeight } = statusNavbarHeightInfo; + + // 获取位置信息 + const getCurrentLocal = () => { + updateState({ + getLocationLoading: true, + }); + getCurrentLocation().then((res) => { + updateState({ + getLocationLoading: false, + location: res || {}, + }); + }); + }; + useEffect(() => { + getCurrentLocal(); + }, []); + + const currentAddress = getLocationLoading + ? getLocationText + : location?.address; + + return ( + + + {/* logo */} + + + + + {/* 位置 */} + {currentAddress} + {!getLocationLoading && ( + + )} + + + 附近${gamesNum}场球局 + + + + + ); +}; +export default ListHeader; diff --git a/src/nutui-theme.scss b/src/nutui-theme.scss index 9ebc0cb..f6cd5b2 100644 --- a/src/nutui-theme.scss +++ b/src/nutui-theme.scss @@ -3,7 +3,7 @@ // ========================================== // 引入NutUI原始样式(如果需要) -// @import '@nutui/nutui-react-taro/dist/style.css'; +@import '@nutui/nutui-react-taro/dist/style.css'; // 全局主题变量覆盖 $nut-primary-color: #000000 !important; diff --git a/src/package/qqmap-wx-jssdk.js b/src/package/qqmap-wx-jssdk.js deleted file mode 100644 index e5c3454..0000000 --- a/src/package/qqmap-wx-jssdk.js +++ /dev/null @@ -1,1122 +0,0 @@ -/** - * 微信小程序JavaScriptSDK - * - * @version 1.2 - * @date 2019-03-06 - */ - -var ERROR_CONF = { - KEY_ERR: 311, - KEY_ERR_MSG: 'key格式错误', - PARAM_ERR: 310, - PARAM_ERR_MSG: '请求参数信息有误', - SYSTEM_ERR: 600, - SYSTEM_ERR_MSG: '系统错误', - WX_ERR_CODE: 1000, - WX_OK_CODE: 200 -}; -var BASE_URL = 'https://apis.map.qq.com/ws/'; -var URL_SEARCH = BASE_URL + 'place/v1/search'; -var URL_SUGGESTION = BASE_URL + 'place/v1/suggestion'; -var URL_GET_GEOCODER = BASE_URL + 'geocoder/v1/'; -var URL_CITY_LIST = BASE_URL + 'district/v1/list'; -var URL_AREA_LIST = BASE_URL + 'district/v1/getchildren'; -var URL_DISTANCE = BASE_URL + 'distance/v1/'; -var URL_DIRECTION = BASE_URL + 'direction/v1/'; -var MODE = { - driving: 'driving', - transit: 'transit' -}; -var EARTH_RADIUS = 6378136.49; -var Utils = { - /** - * md5加密方法 - * 版权所有©2011 Sebastian Tschan,https://blueimp.net - */ - safeAdd(x, y) { - var lsw = (x & 0xffff) + (y & 0xffff); - var msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xffff); - }, - bitRotateLeft(num, cnt) { - return (num << cnt) | (num >>> (32 - cnt)); - }, - md5cmn(q, a, b, x, s, t) { - return this.safeAdd(this.bitRotateLeft(this.safeAdd(this.safeAdd(a, q), this.safeAdd(x, t)), s), b); - }, - md5ff(a, b, c, d, x, s, t) { - return this.md5cmn((b & c) | (~b & d), a, b, x, s, t); - }, - md5gg(a, b, c, d, x, s, t) { - return this.md5cmn((b & d) | (c & ~d), a, b, x, s, t); - }, - md5hh(a, b, c, d, x, s, t) { - return this.md5cmn(b ^ c ^ d, a, b, x, s, t); - }, - md5ii(a, b, c, d, x, s, t) { - return this.md5cmn(c ^ (b | ~d), a, b, x, s, t); - }, - binlMD5(x, len) { - /* append padding */ - x[len >> 5] |= 0x80 << (len % 32); - x[((len + 64) >>> 9 << 4) + 14] = len; - - var i; - var olda; - var oldb; - var oldc; - var oldd; - var a = 1732584193; - var b = -271733879; - var c = -1732584194; - var d = 271733878; - - for (i = 0; i < x.length; i += 16) { - olda = a; - oldb = b; - oldc = c; - oldd = d; - - a = this.md5ff(a, b, c, d, x[i], 7, -680876936); - d = this.md5ff(d, a, b, c, x[i + 1], 12, -389564586); - c = this.md5ff(c, d, a, b, x[i + 2], 17, 606105819); - b = this.md5ff(b, c, d, a, x[i + 3], 22, -1044525330); - a = this.md5ff(a, b, c, d, x[i + 4], 7, -176418897); - d = this.md5ff(d, a, b, c, x[i + 5], 12, 1200080426); - c = this.md5ff(c, d, a, b, x[i + 6], 17, -1473231341); - b = this.md5ff(b, c, d, a, x[i + 7], 22, -45705983); - a = this.md5ff(a, b, c, d, x[i + 8], 7, 1770035416); - d = this.md5ff(d, a, b, c, x[i + 9], 12, -1958414417); - c = this.md5ff(c, d, a, b, x[i + 10], 17, -42063); - b = this.md5ff(b, c, d, a, x[i + 11], 22, -1990404162); - a = this.md5ff(a, b, c, d, x[i + 12], 7, 1804603682); - d = this.md5ff(d, a, b, c, x[i + 13], 12, -40341101); - c = this.md5ff(c, d, a, b, x[i + 14], 17, -1502002290); - b = this.md5ff(b, c, d, a, x[i + 15], 22, 1236535329); - - a = this.md5gg(a, b, c, d, x[i + 1], 5, -165796510); - d = this.md5gg(d, a, b, c, x[i + 6], 9, -1069501632); - c = this.md5gg(c, d, a, b, x[i + 11], 14, 643717713); - b = this.md5gg(b, c, d, a, x[i], 20, -373897302); - a = this.md5gg(a, b, c, d, x[i + 5], 5, -701558691); - d = this.md5gg(d, a, b, c, x[i + 10], 9, 38016083); - c = this.md5gg(c, d, a, b, x[i + 15], 14, -660478335); - b = this.md5gg(b, c, d, a, x[i + 4], 20, -405537848); - a = this.md5gg(a, b, c, d, x[i + 9], 5, 568446438); - d = this.md5gg(d, a, b, c, x[i + 14], 9, -1019803690); - c = this.md5gg(c, d, a, b, x[i + 3], 14, -187363961); - b = this.md5gg(b, c, d, a, x[i + 8], 20, 1163531501); - a = this.md5gg(a, b, c, d, x[i + 13], 5, -1444681467); - d = this.md5gg(d, a, b, c, x[i + 2], 9, -51403784); - c = this.md5gg(c, d, a, b, x[i + 7], 14, 1735328473); - b = this.md5gg(b, c, d, a, x[i + 12], 20, -1926607734); - - a = this.md5hh(a, b, c, d, x[i + 5], 4, -378558); - d = this.md5hh(d, a, b, c, x[i + 8], 11, -2022574463); - c = this.md5hh(c, d, a, b, x[i + 11], 16, 1839030562); - b = this.md5hh(b, c, d, a, x[i + 14], 23, -35309556); - a = this.md5hh(a, b, c, d, x[i + 1], 4, -1530992060); - d = this.md5hh(d, a, b, c, x[i + 4], 11, 1272893353); - c = this.md5hh(c, d, a, b, x[i + 7], 16, -155497632); - b = this.md5hh(b, c, d, a, x[i + 10], 23, -1094730640); - a = this.md5hh(a, b, c, d, x[i + 13], 4, 681279174); - d = this.md5hh(d, a, b, c, x[i], 11, -358537222); - c = this.md5hh(c, d, a, b, x[i + 3], 16, -722521979); - b = this.md5hh(b, c, d, a, x[i + 6], 23, 76029189); - a = this.md5hh(a, b, c, d, x[i + 9], 4, -640364487); - d = this.md5hh(d, a, b, c, x[i + 12], 11, -421815835); - c = this.md5hh(c, d, a, b, x[i + 15], 16, 530742520); - b = this.md5hh(b, c, d, a, x[i + 2], 23, -995338651); - - a = this.md5ii(a, b, c, d, x[i], 6, -198630844); - d = this.md5ii(d, a, b, c, x[i + 7], 10, 1126891415); - c = this.md5ii(c, d, a, b, x[i + 14], 15, -1416354905); - b = this.md5ii(b, c, d, a, x[i + 5], 21, -57434055); - a = this.md5ii(a, b, c, d, x[i + 12], 6, 1700485571); - d = this.md5ii(d, a, b, c, x[i + 3], 10, -1894986606); - c = this.md5ii(c, d, a, b, x[i + 10], 15, -1051523); - b = this.md5ii(b, c, d, a, x[i + 1], 21, -2054922799); - a = this.md5ii(a, b, c, d, x[i + 8], 6, 1873313359); - d = this.md5ii(d, a, b, c, x[i + 15], 10, -30611744); - c = this.md5ii(c, d, a, b, x[i + 6], 15, -1560198380); - b = this.md5ii(b, c, d, a, x[i + 13], 21, 1309151649); - a = this.md5ii(a, b, c, d, x[i + 4], 6, -145523070); - d = this.md5ii(d, a, b, c, x[i + 11], 10, -1120210379); - c = this.md5ii(c, d, a, b, x[i + 2], 15, 718787259); - b = this.md5ii(b, c, d, a, x[i + 9], 21, -343485551); - - a = this.safeAdd(a, olda); - b = this.safeAdd(b, oldb); - c = this.safeAdd(c, oldc); - d = this.safeAdd(d, oldd); - } - return [a, b, c, d]; - }, - binl2rstr(input) { - var i; - var output = ''; - var length32 = input.length * 32; - for (i = 0; i < length32; i += 8) { - output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff); - } - return output; - }, - rstr2binl(input) { - var i; - var output = []; - output[(input.length >> 2) - 1] = undefined; - for (i = 0; i < output.length; i += 1) { - output[i] = 0; - } - var length8 = input.length * 8; - for (i = 0; i < length8; i += 8) { - output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32); - } - return output; - }, - rstrMD5(s) { - return this.binl2rstr(this.binlMD5(this.rstr2binl(s), s.length * 8)); - }, - rstrHMACMD5(key, data) { - var i; - var bkey = this.rstr2binl(key); - var ipad = []; - var opad = []; - var hash; - ipad[15] = opad[15] = undefined; - if (bkey.length > 16) { - bkey = this.binlMD5(bkey, key.length * 8); - } - for (i = 0; i < 16; i += 1) { - ipad[i] = bkey[i] ^ 0x36363636; - opad[i] = bkey[i] ^ 0x5c5c5c5c; - } - hash = this.binlMD5(ipad.concat(this.rstr2binl(data)), 512 + data.length * 8); - return this.binl2rstr(this.binlMD5(opad.concat(hash), 512 + 128)); - }, - rstr2hex(input) { - var hexTab = '0123456789abcdef'; - var output = ''; - var x; - var i; - for (i = 0; i < input.length; i += 1) { - x = input.charCodeAt(i); - output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f); - } - return output; - }, - str2rstrUTF8(input) { - return unescape(encodeURIComponent(input)); - }, - rawMD5(s) { - return this.rstrMD5(this.str2rstrUTF8(s)); - }, - hexMD5(s) { - return this.rstr2hex(this.rawMD5(s)); - }, - rawHMACMD5(k, d) { - return this.rstrHMACMD5(this.str2rstrUTF8(k), str2rstrUTF8(d)); - }, - hexHMACMD5(k, d) { - return this.rstr2hex(this.rawHMACMD5(k, d)); - }, - - md5(string, key, raw) { - if (!key) { - if (!raw) { - return this.hexMD5(string); - } - return this.rawMD5(string); - } - if (!raw) { - return this.hexHMACMD5(key, string); - } - return this.rawHMACMD5(key, string); - }, - /** - * 得到md5加密后的sig参数 - * @param {Object} requestParam 接口参数 - * @param {String} sk签名字符串 - * @param {String} featrue 方法名 - * @return 返回加密后的sig参数 - */ - getSig(requestParam, sk, feature, mode) { - var sig = null; - var requestArr = []; - Object.keys(requestParam).sort().forEach(function(key){ - requestArr.push(key + '=' + requestParam[key]); - }); - if (feature == 'search') { - sig = '/ws/place/v1/search?' + requestArr.join('&') + sk; - } - if (feature == 'suggest') { - sig = '/ws/place/v1/suggestion?' + requestArr.join('&') + sk; - } - if (feature == 'reverseGeocoder') { - sig = '/ws/geocoder/v1/?' + requestArr.join('&') + sk; - } - if (feature == 'geocoder') { - sig = '/ws/geocoder/v1/?' + requestArr.join('&') + sk; - } - if (feature == 'getCityList') { - sig = '/ws/district/v1/list?' + requestArr.join('&') + sk; - } - if (feature == 'getDistrictByCityId') { - sig = '/ws/district/v1/getchildren?' + requestArr.join('&') + sk; - } - if (feature == 'calculateDistance') { - sig = '/ws/distance/v1/?' + requestArr.join('&') + sk; - } - if (feature == 'direction') { - sig = '/ws/direction/v1/' + mode + '?' + requestArr.join('&') + sk; - } - sig = this.md5(sig); - return sig; - }, - /** - * 得到终点query字符串 - * @param {Array|String} 检索数据 - */ - location2query(data) { - if (typeof data == 'string') { - return data; - } - var query = ''; - for (var i = 0; i < data.length; i++) { - var d = data[i]; - if (!!query) { - query += ';'; - } - if (d.location) { - query = query + d.location.lat + ',' + d.location.lng; - } - if (d.latitude && d.longitude) { - query = query + d.latitude + ',' + d.longitude; - } - } - return query; - }, - - /** - * 计算角度 - */ - rad(d) { - return d * Math.PI / 180.0; - }, - /** - * 处理终点location数组 - * @return 返回终点数组 - */ - getEndLocation(location){ - var to = location.split(';'); - var endLocation = []; - for (var i = 0; i < to.length; i++) { - endLocation.push({ - lat: parseFloat(to[i].split(',')[0]), - lng: parseFloat(to[i].split(',')[1]) - }) - } - return endLocation; - }, - - /** - * 计算两点间直线距离 - * @param a 表示纬度差 - * @param b 表示经度差 - * @return 返回的是距离,单位m - */ - getDistance(latFrom, lngFrom, latTo, lngTo) { - var radLatFrom = this.rad(latFrom); - var radLatTo = this.rad(latTo); - var a = radLatFrom - radLatTo; - var b = this.rad(lngFrom) - this.rad(lngTo); - var distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLatFrom) * Math.cos(radLatTo) * Math.pow(Math.sin(b / 2), 2))); - distance = distance * EARTH_RADIUS; - distance = Math.round(distance * 10000) / 10000; - return parseFloat(distance.toFixed(0)); - }, - /** - * 使用微信接口进行定位 - */ - getWXLocation(success, fail, complete) { - wx.getLocation({ - type: 'gcj02', - success: success, - fail: fail, - complete: complete - }); - }, - - /** - * 获取location参数 - */ - getLocationParam(location) { - if (typeof location == 'string') { - var locationArr = location.split(','); - if (locationArr.length === 2) { - location = { - latitude: location.split(',')[0], - longitude: location.split(',')[1] - }; - } else { - location = {}; - } - } - return location; - }, - - /** - * 回调函数默认处理 - */ - polyfillParam(param) { - param.success = param.success || function () { }; - param.fail = param.fail || function () { }; - param.complete = param.complete || function () { }; - }, - - /** - * 验证param对应的key值是否为空 - * - * @param {Object} param 接口参数 - * @param {String} key 对应参数的key - */ - checkParamKeyEmpty(param, key) { - if (!param[key]) { - var errconf = this.buildErrorConfig(ERROR_CONF.PARAM_ERR, ERROR_CONF.PARAM_ERR_MSG + key +'参数格式有误'); - param.fail(errconf); - param.complete(errconf); - return true; - } - return false; - }, - - /** - * 验证参数中是否存在检索词keyword - * - * @param {Object} param 接口参数 - */ - checkKeyword(param){ - return !this.checkParamKeyEmpty(param, 'keyword'); - }, - - /** - * 验证location值 - * - * @param {Object} param 接口参数 - */ - checkLocation(param) { - var location = this.getLocationParam(param.location); - if (!location || !location.latitude || !location.longitude) { - var errconf = this.buildErrorConfig(ERROR_CONF.PARAM_ERR, ERROR_CONF.PARAM_ERR_MSG + ' location参数格式有误'); - param.fail(errconf); - param.complete(errconf); - return false; - } - return true; - }, - - /** - * 构造错误数据结构 - * @param {Number} errCode 错误码 - * @param {Number} errMsg 错误描述 - */ - buildErrorConfig(errCode, errMsg) { - return { - status: errCode, - message: errMsg - }; - }, - - /** - * - * 数据处理函数 - * 根据传入参数不同处理不同数据 - * @param {String} feature 功能名称 - * search 地点搜索 - * suggest关键词提示 - * reverseGeocoder逆地址解析 - * geocoder地址解析 - * getCityList获取城市列表:父集 - * getDistrictByCityId获取区县列表:子集 - * calculateDistance距离计算 - * @param {Object} param 接口参数 - * @param {Object} data 数据 - */ - handleData(param,data,feature){ - if (feature == 'search') { - var searchResult = data.data; - var searchSimplify = []; - for (var i = 0; i < searchResult.length; i++) { - searchSimplify.push({ - id: searchResult[i].id || null, - title: searchResult[i].title || null, - latitude: searchResult[i].location && searchResult[i].location.lat || null, - longitude: searchResult[i].location && searchResult[i].location.lng || null, - address: searchResult[i].address || null, - category: searchResult[i].category || null, - tel: searchResult[i].tel || null, - adcode: searchResult[i].ad_info && searchResult[i].ad_info.adcode || null, - city: searchResult[i].ad_info && searchResult[i].ad_info.city || null, - district: searchResult[i].ad_info && searchResult[i].ad_info.district || null, - province: searchResult[i].ad_info && searchResult[i].ad_info.province || null - }) - } - param.success(data, { - searchResult: searchResult, - searchSimplify: searchSimplify - }) - } else if (feature == 'suggest') { - var suggestResult = data.data; - var suggestSimplify = []; - for (var i = 0; i < suggestResult.length; i++) { - suggestSimplify.push({ - adcode: suggestResult[i].adcode || null, - address: suggestResult[i].address || null, - category: suggestResult[i].category || null, - city: suggestResult[i].city || null, - district: suggestResult[i].district || null, - id: suggestResult[i].id || null, - latitude: suggestResult[i].location && suggestResult[i].location.lat || null, - longitude: suggestResult[i].location && suggestResult[i].location.lng || null, - province: suggestResult[i].province || null, - title: suggestResult[i].title || null, - type: suggestResult[i].type || null - }) - } - param.success(data, { - suggestResult: suggestResult, - suggestSimplify: suggestSimplify - }) - } else if (feature == 'reverseGeocoder') { - var reverseGeocoderResult = data.result; - var reverseGeocoderSimplify = { - address: reverseGeocoderResult.address || null, - latitude: reverseGeocoderResult.location && reverseGeocoderResult.location.lat || null, - longitude: reverseGeocoderResult.location && reverseGeocoderResult.location.lng || null, - adcode: reverseGeocoderResult.ad_info && reverseGeocoderResult.ad_info.adcode || null, - city: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.city || null, - district: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.district || null, - nation: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.nation || null, - province: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.province || null, - street: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.street || null, - street_number: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.street_number || null, - recommend: reverseGeocoderResult.formatted_addresses && reverseGeocoderResult.formatted_addresses.recommend || null, - rough: reverseGeocoderResult.formatted_addresses && reverseGeocoderResult.formatted_addresses.rough || null - }; - if (reverseGeocoderResult.pois) {//判断是否返回周边poi - var pois = reverseGeocoderResult.pois; - var poisSimplify = []; - for (var i = 0;i < pois.length;i++) { - poisSimplify.push({ - id: pois[i].id || null, - title: pois[i].title || null, - latitude: pois[i].location && pois[i].location.lat || null, - longitude: pois[i].location && pois[i].location.lng || null, - address: pois[i].address || null, - category: pois[i].category || null, - adcode: pois[i].ad_info && pois[i].ad_info.adcode || null, - city: pois[i].ad_info && pois[i].ad_info.city || null, - district: pois[i].ad_info && pois[i].ad_info.district || null, - province: pois[i].ad_info && pois[i].ad_info.province || null - }) - } - param.success(data,{ - reverseGeocoderResult: reverseGeocoderResult, - reverseGeocoderSimplify: reverseGeocoderSimplify, - pois: pois, - poisSimplify: poisSimplify - }) - } else { - param.success(data, { - reverseGeocoderResult: reverseGeocoderResult, - reverseGeocoderSimplify: reverseGeocoderSimplify - }) - } - } else if (feature == 'geocoder') { - var geocoderResult = data.result; - var geocoderSimplify = { - title: geocoderResult.title || null, - latitude: geocoderResult.location && geocoderResult.location.lat || null, - longitude: geocoderResult.location && geocoderResult.location.lng || null, - adcode: geocoderResult.ad_info && geocoderResult.ad_info.adcode || null, - province: geocoderResult.address_components && geocoderResult.address_components.province || null, - city: geocoderResult.address_components && geocoderResult.address_components.city || null, - district: geocoderResult.address_components && geocoderResult.address_components.district || null, - street: geocoderResult.address_components && geocoderResult.address_components.street || null, - street_number: geocoderResult.address_components && geocoderResult.address_components.street_number || null, - level: geocoderResult.level || null - }; - param.success(data,{ - geocoderResult: geocoderResult, - geocoderSimplify: geocoderSimplify - }); - } else if (feature == 'getCityList') { - var provinceResult = data.result[0]; - var cityResult = data.result[1]; - var districtResult = data.result[2]; - param.success(data,{ - provinceResult: provinceResult, - cityResult: cityResult, - districtResult: districtResult - }); - } else if (feature == 'getDistrictByCityId') { - var districtByCity = data.result[0]; - param.success(data, districtByCity); - } else if (feature == 'calculateDistance') { - var calculateDistanceResult = data.result.elements; - var distance = []; - for (var i = 0; i < calculateDistanceResult.length; i++){ - distance.push(calculateDistanceResult[i].distance); - } - param.success(data, { - calculateDistanceResult: calculateDistanceResult, - distance: distance - }); - } else if (feature == 'direction') { - var direction = data.result.routes; - param.success(data,direction); - } else { - param.success(data); - } - }, - - /** - * 构造微信请求参数,公共属性处理 - * - * @param {Object} param 接口参数 - * @param {Object} param 配置项 - * @param {String} feature 方法名 - */ - buildWxRequestConfig(param, options, feature) { - var that = this; - options.header = { "content-type": "application/json" }; - options.method = 'GET'; - options.success = function (res) { - var data = res.data; - if (data.status === 0) { - that.handleData(param, data, feature); - } else { - param.fail(data); - } - }; - options.fail = function (res) { - res.statusCode = ERROR_CONF.WX_ERR_CODE; - param.fail(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)); - }; - options.complete = function (res) { - var statusCode = +res.statusCode; - switch(statusCode) { - case ERROR_CONF.WX_ERR_CODE: { - param.complete(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)); - break; - } - case ERROR_CONF.WX_OK_CODE: { - var data = res.data; - if (data.status === 0) { - param.complete(data); - } else { - param.complete(that.buildErrorConfig(data.status, data.message)); - } - break; - } - default:{ - param.complete(that.buildErrorConfig(ERROR_CONF.SYSTEM_ERR, ERROR_CONF.SYSTEM_ERR_MSG)); - } - - } - }; - return options; - }, - - /** - * 处理用户参数是否传入坐标进行不同的处理 - */ - locationProcess(param, locationsuccess, locationfail, locationcomplete) { - var that = this; - locationfail = locationfail || function (res) { - res.statusCode = ERROR_CONF.WX_ERR_CODE; - param.fail(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)); - }; - locationcomplete = locationcomplete || function (res) { - if (res.statusCode == ERROR_CONF.WX_ERR_CODE) { - param.complete(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)); - } - }; - if (!param.location) { - that.getWXLocation(locationsuccess, locationfail, locationcomplete); - } else if (that.checkLocation(param)) { - var location = Utils.getLocationParam(param.location); - locationsuccess(location); - } - } -}; - - -class QQMapWX { - - /** - * 构造函数 - * - * @param {Object} options 接口参数,key 为必选参数 - */ - constructor(options) { - if (!options.key) { - throw Error('key值不能为空'); - } - this.key = options.key; - }; - - /** - * POI周边检索 - * - * @param {Object} options 接口参数对象 - * - * 参数对象结构可以参考 - * @see http://lbs.qq.com/webservice_v1/guide-search.html - */ - search(options) { - var that = this; - options = options || {}; - - Utils.polyfillParam(options); - - if (!Utils.checkKeyword(options)) { - return; - } - - var requestParam = { - keyword: options.keyword, - orderby: options.orderby || '_distance', - page_size: options.page_size || 10, - page_index: options.page_index || 1, - output: 'json', - key: that.key - }; - - if (options.address_format) { - requestParam.address_format = options.address_format; - } - - if (options.filter) { - requestParam.filter = options.filter; - } - - var distance = options.distance || "1000"; - var auto_extend = options.auto_extend || 1; - var region = null; - var rectangle = null; - - //判断城市限定参数 - if (options.region) { - region = options.region; - } - - //矩形限定坐标(暂时只支持字符串格式) - if (options.rectangle) { - rectangle = options.rectangle; - } - - var locationsuccess = function (result) { - if (region && !rectangle) { - //城市限定参数拼接 - requestParam.boundary = "region(" + region + "," + auto_extend + "," + result.latitude + "," + result.longitude + ")"; - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'search'); - } - } else if (rectangle && !region) { - //矩形搜索 - requestParam.boundary = "rectangle(" + rectangle + ")"; - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'search'); - } - } else { - requestParam.boundary = "nearby(" + result.latitude + "," + result.longitude + "," + distance + "," + auto_extend + ")"; - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'search'); - } - } - wx.request(Utils.buildWxRequestConfig(options, { - url: URL_SEARCH, - data: requestParam - }, 'search')); - }; - Utils.locationProcess(options, locationsuccess); - }; - - /** - * sug模糊检索 - * - * @param {Object} options 接口参数对象 - * - * 参数对象结构可以参考 - * http://lbs.qq.com/webservice_v1/guide-suggestion.html - */ - getSuggestion(options) { - var that = this; - options = options || {}; - Utils.polyfillParam(options); - - if (!Utils.checkKeyword(options)) { - return; - } - - var requestParam = { - keyword: options.keyword, - region: options.region || '全国', - region_fix: options.region_fix || 0, - policy: options.policy || 0, - page_size: options.page_size || 10,//控制显示条数 - page_index: options.page_index || 1,//控制页数 - get_subpois : options.get_subpois || 0,//返回子地点 - output: 'json', - key: that.key - }; - //长地址 - if (options.address_format) { - requestParam.address_format = options.address_format; - } - //过滤 - if (options.filter) { - requestParam.filter = options.filter; - } - //排序 - if (options.location) { - var locationsuccess = function (result) { - requestParam.location = result.latitude + ',' + result.longitude; - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'suggest'); - } - wx.request(Utils.buildWxRequestConfig(options, { - url: URL_SUGGESTION, - data: requestParam - }, "suggest")); - }; - Utils.locationProcess(options, locationsuccess); - } else { - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'suggest'); - } - wx.request(Utils.buildWxRequestConfig(options, { - url: URL_SUGGESTION, - data: requestParam - }, "suggest")); - } - }; - - /** - * 逆地址解析 - * - * @param {Object} options 接口参数对象 - * - * 请求参数结构可以参考 - * http://lbs.qq.com/webservice_v1/guide-gcoder.html - */ - reverseGeocoder(options) { - var that = this; - options = options || {}; - Utils.polyfillParam(options); - var requestParam = { - coord_type: options.coord_type || 5, - get_poi: options.get_poi || 0, - output: 'json', - key: that.key - }; - if (options.poi_options) { - requestParam.poi_options = options.poi_options - } - - var locationsuccess = function (result) { - requestParam.location = result.latitude + ',' + result.longitude; - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'reverseGeocoder'); - } - wx.request(Utils.buildWxRequestConfig(options, { - url: URL_GET_GEOCODER, - data: requestParam - }, 'reverseGeocoder')); - }; - Utils.locationProcess(options, locationsuccess); - }; - - /** - * 地址解析 - * - * @param {Object} options 接口参数对象 - * - * 请求参数结构可以参考 - * http://lbs.qq.com/webservice_v1/guide-geocoder.html - */ - geocoder(options) { - var that = this; - options = options || {}; - Utils.polyfillParam(options); - - if (Utils.checkParamKeyEmpty(options, 'address')) { - return; - } - - var requestParam = { - address: options.address, - output: 'json', - key: that.key - }; - - //城市限定 - if (options.region) { - requestParam.region = options.region; - } - - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'geocoder'); - } - - wx.request(Utils.buildWxRequestConfig(options, { - url: URL_GET_GEOCODER, - data: requestParam - },'geocoder')); - }; - - - /** - * 获取城市列表 - * - * @param {Object} options 接口参数对象 - * - * 请求参数结构可以参考 - * http://lbs.qq.com/webservice_v1/guide-region.html - */ - getCityList(options) { - var that = this; - options = options || {}; - Utils.polyfillParam(options); - var requestParam = { - output: 'json', - key: that.key - }; - - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'getCityList'); - } - - wx.request(Utils.buildWxRequestConfig(options, { - url: URL_CITY_LIST, - data: requestParam - },'getCityList')); - }; - - /** - * 获取对应城市ID的区县列表 - * - * @param {Object} options 接口参数对象 - * - * 请求参数结构可以参考 - * http://lbs.qq.com/webservice_v1/guide-region.html - */ - getDistrictByCityId(options) { - var that = this; - options = options || {}; - Utils.polyfillParam(options); - - if (Utils.checkParamKeyEmpty(options, 'id')) { - return; - } - - var requestParam = { - id: options.id || '', - output: 'json', - key: that.key - }; - - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'getDistrictByCityId'); - } - - wx.request(Utils.buildWxRequestConfig(options, { - url: URL_AREA_LIST, - data: requestParam - },'getDistrictByCityId')); - }; - - /** - * 用于单起点到多终点的路线距离(非直线距离)计算: - * 支持两种距离计算方式:步行和驾车。 - * 起点到终点最大限制直线距离10公里。 - * - * 新增直线距离计算。 - * - * @param {Object} options 接口参数对象 - * - * 请求参数结构可以参考 - * http://lbs.qq.com/webservice_v1/guide-distance.html - */ - calculateDistance(options) { - var that = this; - options = options || {}; - Utils.polyfillParam(options); - - if (Utils.checkParamKeyEmpty(options, 'to')) { - return; - } - - var requestParam = { - mode: options.mode || 'walking', - to: Utils.location2query(options.to), - output: 'json', - key: that.key - }; - - if (options.from) { - options.location = options.from; - } - - //计算直线距离 - if(requestParam.mode == 'straight'){ - var locationsuccess = function (result) { - var locationTo = Utils.getEndLocation(requestParam.to);//处理终点坐标 - var data = { - message:"query ok", - result:{ - elements:[] - }, - status:0 - }; - for (var i = 0; i < locationTo.length; i++) { - data.result.elements.push({//将坐标存入 - distance: Utils.getDistance(result.latitude, result.longitude, locationTo[i].lat, locationTo[i].lng), - duration:0, - from:{ - lat: result.latitude, - lng:result.longitude - }, - to:{ - lat: locationTo[i].lat, - lng: locationTo[i].lng - } - }); - } - var calculateResult = data.result.elements; - var distanceResult = []; - for (var i = 0; i < calculateResult.length; i++) { - distanceResult.push(calculateResult[i].distance); - } - return options.success(data,{ - calculateResult: calculateResult, - distanceResult: distanceResult - }); - }; - - Utils.locationProcess(options, locationsuccess); - } else { - var locationsuccess = function (result) { - requestParam.from = result.latitude + ',' + result.longitude; - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'calculateDistance'); - } - wx.request(Utils.buildWxRequestConfig(options, { - url: URL_DISTANCE, - data: requestParam - },'calculateDistance')); - }; - - Utils.locationProcess(options, locationsuccess); - } - }; - - /** - * 路线规划: - * - * @param {Object} options 接口参数对象 - * - * 请求参数结构可以参考 - * https://lbs.qq.com/webservice_v1/guide-road.html - */ - direction(options) { - var that = this; - options = options || {}; - Utils.polyfillParam(options); - - if (Utils.checkParamKeyEmpty(options, 'to')) { - return; - } - - var requestParam = { - output: 'json', - key: that.key - }; - - //to格式处理 - if (typeof options.to == 'string') { - requestParam.to = options.to; - } else { - requestParam.to = options.to.latitude + ',' + options.to.longitude; - } - //初始化局部请求域名 - var SET_URL_DIRECTION = null; - //设置默认mode属性 - options.mode = options.mode || MODE.driving; - - //设置请求域名 - SET_URL_DIRECTION = URL_DIRECTION + options.mode; - - if (options.from) { - options.location = options.from; - } - - if (options.mode == MODE.driving) { - if (options.from_poi) { - requestParam.from_poi = options.from_poi; - } - if (options.heading) { - requestParam.heading = options.heading; - } - if (options.speed) { - requestParam.speed = options.speed; - } - if (options.accuracy) { - requestParam.accuracy = options.accuracy; - } - if (options.road_type) { - requestParam.road_type = options.road_type; - } - if (options.to_poi) { - requestParam.to_poi = options.to_poi; - } - if (options.from_track) { - requestParam.from_track = options.from_track; - } - if (options.waypoints) { - requestParam.waypoints = options.waypoints; - } - if (options.policy) { - requestParam.policy = options.policy; - } - if (options.plate_number) { - requestParam.plate_number = options.plate_number; - } - } - - if (options.mode == MODE.transit) { - if (options.departure_time) { - requestParam.departure_time = options.departure_time; - } - if (options.policy) { - requestParam.policy = options.policy; - } - } - - var locationsuccess = function (result) { - requestParam.from = result.latitude + ',' + result.longitude; - if (options.sig) { - requestParam.sig = Utils.getSig(requestParam, options.sig, 'direction',options.mode); - } - wx.request(Utils.buildWxRequestConfig(options, { - url: SET_URL_DIRECTION, - data: requestParam - }, 'direction')); - }; - - Utils.locationProcess(options, locationsuccess); - } -}; - -module.exports = QQMapWX; \ No newline at end of file diff --git a/src/package/qqmap-wx-jssdk.min.js b/src/package/qqmap-wx-jssdk.min.js deleted file mode 100644 index 8fa1477..0000000 --- a/src/package/qqmap-wx-jssdk.min.js +++ /dev/null @@ -1 +0,0 @@ -var ERROR_CONF = { KEY_ERR: 311, KEY_ERR_MSG: 'key格式错误', PARAM_ERR: 310, PARAM_ERR_MSG: '请求参数信息有误', SYSTEM_ERR: 600, SYSTEM_ERR_MSG: '系统错误', WX_ERR_CODE: 1000, WX_OK_CODE: 200 }; var BASE_URL = 'https://apis.map.qq.com/ws/'; var URL_SEARCH = BASE_URL + 'place/v1/search'; var URL_SUGGESTION = BASE_URL + 'place/v1/suggestion'; var URL_GET_GEOCODER = BASE_URL + 'geocoder/v1/'; var URL_CITY_LIST = BASE_URL + 'district/v1/list'; var URL_AREA_LIST = BASE_URL + 'district/v1/getchildren'; var URL_DISTANCE = BASE_URL + 'distance/v1/'; var URL_DIRECTION = BASE_URL + 'direction/v1/'; var MODE = { driving: 'driving', transit: 'transit' }; var EARTH_RADIUS = 6378136.49; var Utils = { safeAdd(x, y) { var lsw = (x & 0xffff) + (y & 0xffff); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xffff) }, bitRotateLeft(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)) }, md5cmn(q, a, b, x, s, t) { return this.safeAdd(this.bitRotateLeft(this.safeAdd(this.safeAdd(a, q), this.safeAdd(x, t)), s), b) }, md5ff(a, b, c, d, x, s, t) { return this.md5cmn((b & c) | (~b & d), a, b, x, s, t) }, md5gg(a, b, c, d, x, s, t) { return this.md5cmn((b & d) | (c & ~d), a, b, x, s, t) }, md5hh(a, b, c, d, x, s, t) { return this.md5cmn(b ^ c ^ d, a, b, x, s, t) }, md5ii(a, b, c, d, x, s, t) { return this.md5cmn(c ^ (b | ~d), a, b, x, s, t) }, binlMD5(x, len) { x[len >> 5] |= 0x80 << (len % 32); x[((len + 64) >>> 9 << 4) + 14] = len; var i; var olda; var oldb; var oldc; var oldd; var a = 1732584193; var b = -271733879; var c = -1732584194; var d = 271733878; for (i = 0; i < x.length; i += 16) { olda = a; oldb = b; oldc = c; oldd = d; a = this.md5ff(a, b, c, d, x[i], 7, -680876936); d = this.md5ff(d, a, b, c, x[i + 1], 12, -389564586); c = this.md5ff(c, d, a, b, x[i + 2], 17, 606105819); b = this.md5ff(b, c, d, a, x[i + 3], 22, -1044525330); a = this.md5ff(a, b, c, d, x[i + 4], 7, -176418897); d = this.md5ff(d, a, b, c, x[i + 5], 12, 1200080426); c = this.md5ff(c, d, a, b, x[i + 6], 17, -1473231341); b = this.md5ff(b, c, d, a, x[i + 7], 22, -45705983); a = this.md5ff(a, b, c, d, x[i + 8], 7, 1770035416); d = this.md5ff(d, a, b, c, x[i + 9], 12, -1958414417); c = this.md5ff(c, d, a, b, x[i + 10], 17, -42063); b = this.md5ff(b, c, d, a, x[i + 11], 22, -1990404162); a = this.md5ff(a, b, c, d, x[i + 12], 7, 1804603682); d = this.md5ff(d, a, b, c, x[i + 13], 12, -40341101); c = this.md5ff(c, d, a, b, x[i + 14], 17, -1502002290); b = this.md5ff(b, c, d, a, x[i + 15], 22, 1236535329); a = this.md5gg(a, b, c, d, x[i + 1], 5, -165796510); d = this.md5gg(d, a, b, c, x[i + 6], 9, -1069501632); c = this.md5gg(c, d, a, b, x[i + 11], 14, 643717713); b = this.md5gg(b, c, d, a, x[i], 20, -373897302); a = this.md5gg(a, b, c, d, x[i + 5], 5, -701558691); d = this.md5gg(d, a, b, c, x[i + 10], 9, 38016083); c = this.md5gg(c, d, a, b, x[i + 15], 14, -660478335); b = this.md5gg(b, c, d, a, x[i + 4], 20, -405537848); a = this.md5gg(a, b, c, d, x[i + 9], 5, 568446438); d = this.md5gg(d, a, b, c, x[i + 14], 9, -1019803690); c = this.md5gg(c, d, a, b, x[i + 3], 14, -187363961); b = this.md5gg(b, c, d, a, x[i + 8], 20, 1163531501); a = this.md5gg(a, b, c, d, x[i + 13], 5, -1444681467); d = this.md5gg(d, a, b, c, x[i + 2], 9, -51403784); c = this.md5gg(c, d, a, b, x[i + 7], 14, 1735328473); b = this.md5gg(b, c, d, a, x[i + 12], 20, -1926607734); a = this.md5hh(a, b, c, d, x[i + 5], 4, -378558); d = this.md5hh(d, a, b, c, x[i + 8], 11, -2022574463); c = this.md5hh(c, d, a, b, x[i + 11], 16, 1839030562); b = this.md5hh(b, c, d, a, x[i + 14], 23, -35309556); a = this.md5hh(a, b, c, d, x[i + 1], 4, -1530992060); d = this.md5hh(d, a, b, c, x[i + 4], 11, 1272893353); c = this.md5hh(c, d, a, b, x[i + 7], 16, -155497632); b = this.md5hh(b, c, d, a, x[i + 10], 23, -1094730640); a = this.md5hh(a, b, c, d, x[i + 13], 4, 681279174); d = this.md5hh(d, a, b, c, x[i], 11, -358537222); c = this.md5hh(c, d, a, b, x[i + 3], 16, -722521979); b = this.md5hh(b, c, d, a, x[i + 6], 23, 76029189); a = this.md5hh(a, b, c, d, x[i + 9], 4, -640364487); d = this.md5hh(d, a, b, c, x[i + 12], 11, -421815835); c = this.md5hh(c, d, a, b, x[i + 15], 16, 530742520); b = this.md5hh(b, c, d, a, x[i + 2], 23, -995338651); a = this.md5ii(a, b, c, d, x[i], 6, -198630844); d = this.md5ii(d, a, b, c, x[i + 7], 10, 1126891415); c = this.md5ii(c, d, a, b, x[i + 14], 15, -1416354905); b = this.md5ii(b, c, d, a, x[i + 5], 21, -57434055); a = this.md5ii(a, b, c, d, x[i + 12], 6, 1700485571); d = this.md5ii(d, a, b, c, x[i + 3], 10, -1894986606); c = this.md5ii(c, d, a, b, x[i + 10], 15, -1051523); b = this.md5ii(b, c, d, a, x[i + 1], 21, -2054922799); a = this.md5ii(a, b, c, d, x[i + 8], 6, 1873313359); d = this.md5ii(d, a, b, c, x[i + 15], 10, -30611744); c = this.md5ii(c, d, a, b, x[i + 6], 15, -1560198380); b = this.md5ii(b, c, d, a, x[i + 13], 21, 1309151649); a = this.md5ii(a, b, c, d, x[i + 4], 6, -145523070); d = this.md5ii(d, a, b, c, x[i + 11], 10, -1120210379); c = this.md5ii(c, d, a, b, x[i + 2], 15, 718787259); b = this.md5ii(b, c, d, a, x[i + 9], 21, -343485551); a = this.safeAdd(a, olda); b = this.safeAdd(b, oldb); c = this.safeAdd(c, oldc); d = this.safeAdd(d, oldd) } return [a, b, c, d] }, binl2rstr(input) { var i; var output = ''; var length32 = input.length * 32; for (i = 0; i < length32; i += 8) { output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff) } return output }, rstr2binl(input) { var i; var output = []; output[(input.length >> 2) - 1] = undefined; for (i = 0; i < output.length; i += 1) { output[i] = 0 } var length8 = input.length * 8; for (i = 0; i < length8; i += 8) { output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32) } return output }, rstrMD5(s) { return this.binl2rstr(this.binlMD5(this.rstr2binl(s), s.length * 8)) }, rstrHMACMD5(key, data) { var i; var bkey = this.rstr2binl(key); var ipad = []; var opad = []; var hash; ipad[15] = opad[15] = undefined; if (bkey.length > 16) { bkey = this.binlMD5(bkey, key.length * 8) } for (i = 0; i < 16; i += 1) { ipad[i] = bkey[i] ^ 0x36363636; opad[i] = bkey[i] ^ 0x5c5c5c5c } hash = this.binlMD5(ipad.concat(this.rstr2binl(data)), 512 + data.length * 8); return this.binl2rstr(this.binlMD5(opad.concat(hash), 512 + 128)) }, rstr2hex(input) { var hexTab = '0123456789abcdef'; var output = ''; var x; var i; for (i = 0; i < input.length; i += 1) { x = input.charCodeAt(i); output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f) } return output }, str2rstrUTF8(input) { return unescape(encodeURIComponent(input)) }, rawMD5(s) { return this.rstrMD5(this.str2rstrUTF8(s)) }, hexMD5(s) { return this.rstr2hex(this.rawMD5(s)) }, rawHMACMD5(k, d) { return this.rstrHMACMD5(this.str2rstrUTF8(k), str2rstrUTF8(d)) }, hexHMACMD5(k, d) { return this.rstr2hex(this.rawHMACMD5(k, d)) }, md5(string, key, raw) { if (!key) { if (!raw) { return this.hexMD5(string) } return this.rawMD5(string) } if (!raw) { return this.hexHMACMD5(key, string) } return this.rawHMACMD5(key, string) }, getSig(requestParam, sk, feature, mode) { var sig = null; var requestArr = []; Object.keys(requestParam).sort().forEach(function (key) { requestArr.push(key + '=' + requestParam[key]) }); if (feature == 'search') { sig = '/ws/place/v1/search?' + requestArr.join('&') + sk } if (feature == 'suggest') { sig = '/ws/place/v1/suggestion?' + requestArr.join('&') + sk } if (feature == 'reverseGeocoder') { sig = '/ws/geocoder/v1/?' + requestArr.join('&') + sk } if (feature == 'geocoder') { sig = '/ws/geocoder/v1/?' + requestArr.join('&') + sk } if (feature == 'getCityList') { sig = '/ws/district/v1/list?' + requestArr.join('&') + sk } if (feature == 'getDistrictByCityId') { sig = '/ws/district/v1/getchildren?' + requestArr.join('&') + sk } if (feature == 'calculateDistance') { sig = '/ws/distance/v1/?' + requestArr.join('&') + sk } if (feature == 'direction') { sig = '/ws/direction/v1/' + mode + '?' + requestArr.join('&') + sk } sig = this.md5(sig); return sig }, location2query(data) { if (typeof data == 'string') { return data } var query = ''; for (var i = 0; i < data.length; i++) { var d = data[i]; if (!!query) { query += ';' } if (d.location) { query = query + d.location.lat + ',' + d.location.lng } if (d.latitude && d.longitude) { query = query + d.latitude + ',' + d.longitude } } return query }, rad(d) { return d * Math.PI / 180.0 }, getEndLocation(location) { var to = location.split(';'); var endLocation = []; for (var i = 0; i < to.length; i++) { endLocation.push({ lat: parseFloat(to[i].split(',')[0]), lng: parseFloat(to[i].split(',')[1]) }) } return endLocation }, getDistance(latFrom, lngFrom, latTo, lngTo) { var radLatFrom = this.rad(latFrom); var radLatTo = this.rad(latTo); var a = radLatFrom - radLatTo; var b = this.rad(lngFrom) - this.rad(lngTo); var distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLatFrom) * Math.cos(radLatTo) * Math.pow(Math.sin(b / 2), 2))); distance = distance * EARTH_RADIUS; distance = Math.round(distance * 10000) / 10000; return parseFloat(distance.toFixed(0)) }, getWXLocation(success, fail, complete) { wx.getLocation({ type: 'gcj02', success: success, fail: fail, complete: complete }) }, getLocationParam(location) { if (typeof location == 'string') { var locationArr = location.split(','); if (locationArr.length === 2) { location = { latitude: location.split(',')[0], longitude: location.split(',')[1] } } else { location = {} } } return location }, polyfillParam(param) { param.success = param.success || function () { }; param.fail = param.fail || function () { }; param.complete = param.complete || function () { } }, checkParamKeyEmpty(param, key) { if (!param[key]) { var errconf = this.buildErrorConfig(ERROR_CONF.PARAM_ERR, ERROR_CONF.PARAM_ERR_MSG + key + '参数格式有误'); param.fail(errconf); param.complete(errconf); return true } return false }, checkKeyword(param) { return !this.checkParamKeyEmpty(param, 'keyword') }, checkLocation(param) { var location = this.getLocationParam(param.location); if (!location || !location.latitude || !location.longitude) { var errconf = this.buildErrorConfig(ERROR_CONF.PARAM_ERR, ERROR_CONF.PARAM_ERR_MSG + ' location参数格式有误'); param.fail(errconf); param.complete(errconf); return false } return true }, buildErrorConfig(errCode, errMsg) { return { status: errCode, message: errMsg } }, handleData(param, data, feature) { if (feature == 'search') { var searchResult = data.data; var searchSimplify = []; for (var i = 0; i < searchResult.length; i++) { searchSimplify.push({ id: searchResult[i].id || null, title: searchResult[i].title || null, latitude: searchResult[i].location && searchResult[i].location.lat || null, longitude: searchResult[i].location && searchResult[i].location.lng || null, address: searchResult[i].address || null, category: searchResult[i].category || null, tel: searchResult[i].tel || null, adcode: searchResult[i].ad_info && searchResult[i].ad_info.adcode || null, city: searchResult[i].ad_info && searchResult[i].ad_info.city || null, district: searchResult[i].ad_info && searchResult[i].ad_info.district || null, province: searchResult[i].ad_info && searchResult[i].ad_info.province || null }) } param.success(data, { searchResult: searchResult, searchSimplify: searchSimplify }) } else if (feature == 'suggest') { var suggestResult = data.data; var suggestSimplify = []; for (var i = 0; i < suggestResult.length; i++) { suggestSimplify.push({ adcode: suggestResult[i].adcode || null, address: suggestResult[i].address || null, category: suggestResult[i].category || null, city: suggestResult[i].city || null, district: suggestResult[i].district || null, id: suggestResult[i].id || null, latitude: suggestResult[i].location && suggestResult[i].location.lat || null, longitude: suggestResult[i].location && suggestResult[i].location.lng || null, province: suggestResult[i].province || null, title: suggestResult[i].title || null, type: suggestResult[i].type || null }) } param.success(data, { suggestResult: suggestResult, suggestSimplify: suggestSimplify }) } else if (feature == 'reverseGeocoder') { var reverseGeocoderResult = data.result; var reverseGeocoderSimplify = { address: reverseGeocoderResult.address || null, latitude: reverseGeocoderResult.location && reverseGeocoderResult.location.lat || null, longitude: reverseGeocoderResult.location && reverseGeocoderResult.location.lng || null, adcode: reverseGeocoderResult.ad_info && reverseGeocoderResult.ad_info.adcode || null, city: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.city || null, district: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.district || null, nation: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.nation || null, province: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.province || null, street: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.street || null, street_number: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.street_number || null, recommend: reverseGeocoderResult.formatted_addresses && reverseGeocoderResult.formatted_addresses.recommend || null, rough: reverseGeocoderResult.formatted_addresses && reverseGeocoderResult.formatted_addresses.rough || null }; if (reverseGeocoderResult.pois) { var pois = reverseGeocoderResult.pois; var poisSimplify = []; for (var i = 0; i < pois.length; i++) { poisSimplify.push({ id: pois[i].id || null, title: pois[i].title || null, latitude: pois[i].location && pois[i].location.lat || null, longitude: pois[i].location && pois[i].location.lng || null, address: pois[i].address || null, category: pois[i].category || null, adcode: pois[i].ad_info && pois[i].ad_info.adcode || null, city: pois[i].ad_info && pois[i].ad_info.city || null, district: pois[i].ad_info && pois[i].ad_info.district || null, province: pois[i].ad_info && pois[i].ad_info.province || null }) } param.success(data, { reverseGeocoderResult: reverseGeocoderResult, reverseGeocoderSimplify: reverseGeocoderSimplify, pois: pois, poisSimplify: poisSimplify }) } else { param.success(data, { reverseGeocoderResult: reverseGeocoderResult, reverseGeocoderSimplify: reverseGeocoderSimplify }) } } else if (feature == 'geocoder') { var geocoderResult = data.result; var geocoderSimplify = { title: geocoderResult.title || null, latitude: geocoderResult.location && geocoderResult.location.lat || null, longitude: geocoderResult.location && geocoderResult.location.lng || null, adcode: geocoderResult.ad_info && geocoderResult.ad_info.adcode || null, province: geocoderResult.address_components && geocoderResult.address_components.province || null, city: geocoderResult.address_components && geocoderResult.address_components.city || null, district: geocoderResult.address_components && geocoderResult.address_components.district || null, street: geocoderResult.address_components && geocoderResult.address_components.street || null, street_number: geocoderResult.address_components && geocoderResult.address_components.street_number || null, level: geocoderResult.level || null }; param.success(data, { geocoderResult: geocoderResult, geocoderSimplify: geocoderSimplify }) } else if (feature == 'getCityList') { var provinceResult = data.result[0]; var cityResult = data.result[1]; var districtResult = data.result[2]; param.success(data, { provinceResult: provinceResult, cityResult: cityResult, districtResult: districtResult }) } else if (feature == 'getDistrictByCityId') { var districtByCity = data.result[0]; param.success(data, districtByCity) } else if (feature == 'calculateDistance') { var calculateDistanceResult = data.result.elements; var distance = []; for (var i = 0; i < calculateDistanceResult.length; i++) { distance.push(calculateDistanceResult[i].distance) } param.success(data, { calculateDistanceResult: calculateDistanceResult, distance: distance }) } else if (feature == 'direction') { var direction = data.result.routes; param.success(data, direction) } else { param.success(data) } }, buildWxRequestConfig(param, options, feature) { var that = this; options.header = { "content-type": "application/json" }; options.method = 'GET'; options.success = function (res) { var data = res.data; if (data.status === 0) { that.handleData(param, data, feature) } else { param.fail(data) } }; options.fail = function (res) { res.statusCode = ERROR_CONF.WX_ERR_CODE; param.fail(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)) }; options.complete = function (res) { var statusCode = +res.statusCode; switch (statusCode) { case ERROR_CONF.WX_ERR_CODE: { param.complete(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)); break } case ERROR_CONF.WX_OK_CODE: { var data = res.data; if (data.status === 0) { param.complete(data) } else { param.complete(that.buildErrorConfig(data.status, data.message)) } break } default: { param.complete(that.buildErrorConfig(ERROR_CONF.SYSTEM_ERR, ERROR_CONF.SYSTEM_ERR_MSG)) } } }; return options }, locationProcess(param, locationsuccess, locationfail, locationcomplete) { var that = this; locationfail = locationfail || function (res) { res.statusCode = ERROR_CONF.WX_ERR_CODE; param.fail(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)) }; locationcomplete = locationcomplete || function (res) { if (res.statusCode == ERROR_CONF.WX_ERR_CODE) { param.complete(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)) } }; if (!param.location) { that.getWXLocation(locationsuccess, locationfail, locationcomplete) } else if (that.checkLocation(param)) { var location = Utils.getLocationParam(param.location); locationsuccess(location) } } }; class QQMapWX { constructor(options) { if (!options.key) { throw Error('key值不能为空') } this.key = options.key }; search(options) { var that = this; options = options || {}; Utils.polyfillParam(options); if (!Utils.checkKeyword(options)) { return } var requestParam = { keyword: options.keyword, orderby: options.orderby || '_distance', page_size: options.page_size || 10, page_index: options.page_index || 1, output: 'json', key: that.key }; if (options.address_format) { requestParam.address_format = options.address_format } if (options.filter) { requestParam.filter = options.filter } var distance = options.distance || "1000"; var auto_extend = options.auto_extend || 1; var region = null; var rectangle = null; if (options.region) { region = options.region } if (options.rectangle) { rectangle = options.rectangle } var locationsuccess = function (result) { if (region && !rectangle) { requestParam.boundary = "region(" + region + "," + auto_extend + "," + result.latitude + "," + result.longitude + ")"; if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'search') } } else if (rectangle && !region) { requestParam.boundary = "rectangle(" + rectangle + ")"; if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'search') } } else { requestParam.boundary = "nearby(" + result.latitude + "," + result.longitude + "," + distance + "," + auto_extend + ")"; if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'search') } } wx.request(Utils.buildWxRequestConfig(options, { url: URL_SEARCH, data: requestParam }, 'search')) }; Utils.locationProcess(options, locationsuccess) }; getSuggestion(options) { var that = this; options = options || {}; Utils.polyfillParam(options); if (!Utils.checkKeyword(options)) { return } var requestParam = { keyword: options.keyword, region: options.region || '全国', region_fix: options.region_fix || 0, policy: options.policy || 0, page_size: options.page_size || 10, page_index: options.page_index || 1, get_subpois: options.get_subpois || 0, output: 'json', key: that.key }; if (options.address_format) { requestParam.address_format = options.address_format } if (options.filter) { requestParam.filter = options.filter } if (options.location) { var locationsuccess = function (result) { requestParam.location = result.latitude + ',' + result.longitude; if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'suggest') } wx.request(Utils.buildWxRequestConfig(options, { url: URL_SUGGESTION, data: requestParam }, "suggest")) }; Utils.locationProcess(options, locationsuccess) } else { if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'suggest') } wx.request(Utils.buildWxRequestConfig(options, { url: URL_SUGGESTION, data: requestParam }, "suggest")) } }; reverseGeocoder(options) { var that = this; options = options || {}; Utils.polyfillParam(options); var requestParam = { coord_type: options.coord_type || 5, get_poi: options.get_poi || 0, output: 'json', key: that.key }; if (options.poi_options) { requestParam.poi_options = options.poi_options } var locationsuccess = function (result) { requestParam.location = result.latitude + ',' + result.longitude; if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'reverseGeocoder') } wx.request(Utils.buildWxRequestConfig(options, { url: URL_GET_GEOCODER, data: requestParam }, 'reverseGeocoder')) }; Utils.locationProcess(options, locationsuccess) }; geocoder(options) { var that = this; options = options || {}; Utils.polyfillParam(options); if (Utils.checkParamKeyEmpty(options, 'address')) { return } var requestParam = { address: options.address, output: 'json', key: that.key }; if (options.region) { requestParam.region = options.region } if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'geocoder') } wx.request(Utils.buildWxRequestConfig(options, { url: URL_GET_GEOCODER, data: requestParam }, 'geocoder')) }; getCityList(options) { var that = this; options = options || {}; Utils.polyfillParam(options); var requestParam = { output: 'json', key: that.key }; if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'getCityList') } wx.request(Utils.buildWxRequestConfig(options, { url: URL_CITY_LIST, data: requestParam }, 'getCityList')) }; getDistrictByCityId(options) { var that = this; options = options || {}; Utils.polyfillParam(options); if (Utils.checkParamKeyEmpty(options, 'id')) { return } var requestParam = { id: options.id || '', output: 'json', key: that.key }; if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'getDistrictByCityId') } wx.request(Utils.buildWxRequestConfig(options, { url: URL_AREA_LIST, data: requestParam }, 'getDistrictByCityId')) }; calculateDistance(options) { var that = this; options = options || {}; Utils.polyfillParam(options); if (Utils.checkParamKeyEmpty(options, 'to')) { return } var requestParam = { mode: options.mode || 'walking', to: Utils.location2query(options.to), output: 'json', key: that.key }; if (options.from) { options.location = options.from } if (requestParam.mode == 'straight') { var locationsuccess = function (result) { var locationTo = Utils.getEndLocation(requestParam.to); var data = { message: "query ok", result: { elements: [] }, status: 0 }; for (var i = 0; i < locationTo.length; i++) { data.result.elements.push({ distance: Utils.getDistance(result.latitude, result.longitude, locationTo[i].lat, locationTo[i].lng), duration: 0, from: { lat: result.latitude, lng: result.longitude }, to: { lat: locationTo[i].lat, lng: locationTo[i].lng } }) } var calculateResult = data.result.elements; var distanceResult = []; for (var i = 0; i < calculateResult.length; i++) { distanceResult.push(calculateResult[i].distance) } return options.success(data, { calculateResult: calculateResult, distanceResult: distanceResult }) }; Utils.locationProcess(options, locationsuccess) } else { var locationsuccess = function (result) { requestParam.from = result.latitude + ',' + result.longitude; if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'calculateDistance') } wx.request(Utils.buildWxRequestConfig(options, { url: URL_DISTANCE, data: requestParam }, 'calculateDistance')) }; Utils.locationProcess(options, locationsuccess) } }; direction(options) { var that = this; options = options || {}; Utils.polyfillParam(options); if (Utils.checkParamKeyEmpty(options, 'to')) { return } var requestParam = { output: 'json', key: that.key }; if (typeof options.to == 'string') { requestParam.to = options.to } else { requestParam.to = options.to.latitude + ',' + options.to.longitude } var SET_URL_DIRECTION = null; options.mode = options.mode || MODE.driving; SET_URL_DIRECTION = URL_DIRECTION + options.mode; if (options.from) { options.location = options.from } if (options.mode == MODE.driving) { if (options.from_poi) { requestParam.from_poi = options.from_poi } if (options.heading) { requestParam.heading = options.heading } if (options.speed) { requestParam.speed = options.speed } if (options.accuracy) { requestParam.accuracy = options.accuracy } if (options.road_type) { requestParam.road_type = options.road_type } if (options.to_poi) { requestParam.to_poi = options.to_poi } if (options.from_track) { requestParam.from_track = options.from_track } if (options.waypoints) { requestParam.waypoints = options.waypoints } if (options.policy) { requestParam.policy = options.policy } if (options.plate_number) { requestParam.plate_number = options.plate_number } } if (options.mode == MODE.transit) { if (options.departure_time) { requestParam.departure_time = options.departure_time } if (options.policy) { requestParam.policy = options.policy } } var locationsuccess = function (result) { requestParam.from = result.latitude + ',' + result.longitude; if (options.sig) { requestParam.sig = Utils.getSig(requestParam, options.sig, 'direction', options.mode) } wx.request(Utils.buildWxRequestConfig(options, { url: SET_URL_DIRECTION, data: requestParam }, 'direction')) }; Utils.locationProcess(options, locationsuccess) } }; module.exports = QQMapWX; \ No newline at end of file diff --git a/src/pages/detail/index.scss b/src/pages/detail/index.scss index 4f7fa63..fa88e84 100644 --- a/src/pages/detail/index.scss +++ b/src/pages/detail/index.scss @@ -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); + } + } +} + diff --git a/src/pages/detail/index.tsx b/src/pages/detail/index.tsx index ee52196..5565e69 100644 --- a/src/pages/detail/index.tsx +++ b/src/pages/detail/index.tsx @@ -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 ( + { setVisible(false) }} + hideFooter + style={{ minHeight: '100px' }} + > + + + + 分享到微信 + + + + 分享朋友圈 + + + + 保存到本地 + + + + ) +}) + function Index() { // 使用Zustand store // const userStats = useUserStats() @@ -31,16 +97,24 @@ function Index() { const [colors, setColors] = useState([]) const [detail, setDetail] = useState(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(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 */} - + { Taro.navigateBack() }}> @@ -234,7 +328,7 @@ function Index() { {/* location message */} {/* venue name and distance */} - + 上海体育场 · 1.2km @@ -325,7 +419,7 @@ function Index() { {/* participants list */} - + {participants.map((participant) => ( {/* */} @@ -420,7 +514,7 @@ function Index() { - 报名人数 {game.checkedApplications} / {game.applications} + 报名人数 {game.checkedApplications}/{game.applications} {game.levelRequirements} @@ -439,10 +533,17 @@ function Index() { {/* sticky bottom action bar */} - - + + + 分享 + + + { Taro.showToast({ title: 'To be continued', icon: 'none' }) }}> + + 32 + - + 🎾 立即加入 @@ -450,6 +551,8 @@ function Index() { + {/* share popup */} + ) diff --git a/src/pages/index/index.scss b/src/pages/index/index.scss deleted file mode 100644 index 282231f..0000000 --- a/src/pages/index/index.scss +++ /dev/null @@ -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; - } - } -} diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 7c4e67b..792170d 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -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(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() { {/* 用户信息卡片 */} - {userProfile?.nickname?.charAt(0) || 'U'} @@ -205,17 +206,17 @@ function Index() { {/* 统计数据 */} 📊 API 请求统计 - + - {interests.length > 0 && ( - )} @@ -224,10 +225,10 @@ function Index() { {/* API 请求按钮区域 */} 🚀 API 请求功能 - + - -
- ); - } - const toggleShowPopup = () => { updateState({ isShowFilterPopup: !isShowFilterPopup }); }; @@ -174,21 +117,40 @@ const ListPage = () => { }); }; + const handleSearchClick = () => { + Taro.navigateTo({ + url: "/pages/search/index", + }); + }; + return ( <> - + {!isShowInputCustomerNavBar ? ( + + ) : ( + + )} - + 0} filterCount={filterCount} onChange={handleSearchChange} + value={searchValue} + onInputClick={handleSearchClick} /> {/* 综合筛选 */} {isShowFilterPopup && ( -
+ { onClose={toggleShowPopup} statusNavbarHeigh={statusNavbarHeightInfo?.totalHeight} /> -
+
)} - {/* 筛选 */} -
- {/* 全城筛选 */} - - {/* 智能排序 */} - -
+
+ {/* 筛选 */} + + - - {/* 列表内容 */} - - {!loading && - matches.length > 0 && - matches.map((match, index) => ( - - ))} - - - {/* 空状态 */} - {loading && - matches.length === 0 && - new Array(10).fill(0).map(() => { - return ; - })} - + {/* 列表内容 */} +
+ + ); }; diff --git a/src/pages/login/index/index.tsx b/src/pages/login/index/index.tsx index 29facb3..ac0cbef 100644 --- a/src/pages/login/index/index.tsx +++ b/src/pages/login/index/index.tsx @@ -110,7 +110,7 @@ const LoginPage: React.FC = () => { @@ -229,7 +229,7 @@ const LoginPage: React.FC = () => { {agree_terms ? '已同意' : '同意并继续'} - + )} diff --git a/src/pages/login/verification/index.tsx b/src/pages/login/verification/index.tsx index 9fe0d70..3d977ea 100644 --- a/src/pages/login/verification/index.tsx +++ b/src/pages/login/verification/index.tsx @@ -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; \ No newline at end of file +export default VerificationPage; \ No newline at end of file diff --git a/src/pages/mapDisplay/index.tsx b/src/pages/mapDisplay/index.tsx deleted file mode 100644 index be7d211..0000000 --- a/src/pages/mapDisplay/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -// import MapPlugin from "src/components/MapDisplay/mapPlugin"; -import MapDisplay from "src/components/MapDisplay"; -export default function MapDisplayPage() { - return -} \ No newline at end of file diff --git a/src/pages/message/index.config.ts b/src/pages/message/index.config.ts new file mode 100644 index 0000000..42d7ad1 --- /dev/null +++ b/src/pages/message/index.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '消息', + // navigationBarBackgroundColor: '#FAFAFA', + navigationStyle: 'custom', +}) \ No newline at end of file diff --git a/src/pages/message/index.scss b/src/pages/message/index.scss new file mode 100644 index 0000000..092c41b --- /dev/null +++ b/src/pages/message/index.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/src/pages/message/index.tsx b/src/pages/message/index.tsx new file mode 100644 index 0000000..18f4130 --- /dev/null +++ b/src/pages/message/index.tsx @@ -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 ( + + + + + 消息 + + + + + {messageList.map((item) => ( + + + {item.title} + + + {item.content} + + + ))} + + + + + ) +} + +export default Personal \ No newline at end of file diff --git a/src/pages/orderCheck/index.config.ts b/src/pages/orderCheck/index.config.ts new file mode 100644 index 0000000..e8419ed --- /dev/null +++ b/src/pages/orderCheck/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '订单确认', + navigationBarBackgroundColor: '#FAFAFA' +}) \ No newline at end of file diff --git a/src/pages/orderCheck/index.scss b/src/pages/orderCheck/index.scss new file mode 100644 index 0000000..e9e7c6b --- /dev/null +++ b/src/pages/orderCheck/index.scss @@ -0,0 +1 @@ +@use '~@/scss/images.scss' as img; \ No newline at end of file diff --git a/src/pages/orderCheck/index.tsx b/src/pages/orderCheck/index.tsx new file mode 100644 index 0000000..7a32e62 --- /dev/null +++ b/src/pages/orderCheck/index.tsx @@ -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 ( + + OrderCheck + + + ) +} + +export default OrderCheck \ No newline at end of file diff --git a/src/pages/personal/index.config.ts b/src/pages/personal/index.config.ts new file mode 100644 index 0000000..ca1f786 --- /dev/null +++ b/src/pages/personal/index.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '个人中心', + // navigationBarBackgroundColor: '#FAFAFA', + navigationStyle: 'custom', +}) \ No newline at end of file diff --git a/src/pages/personal/index.scss b/src/pages/personal/index.scss new file mode 100644 index 0000000..c2475fe --- /dev/null +++ b/src/pages/personal/index.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/pages/personal/index.tsx b/src/pages/personal/index.tsx new file mode 100644 index 0000000..042bc8e --- /dev/null +++ b/src/pages/personal/index.tsx @@ -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 ( + + {id && ( + + + + )} + + Personal + + + + ) +} + +export default Personal \ No newline at end of file diff --git a/src/pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx b/src/pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx index 8ff4de1..3a2e375 100644 --- a/src/pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx +++ b/src/pages/publishBall/components/FormBasicInfo/FormBasicInfo.tsx @@ -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 = ({ }) setShowStadiumSelector(false) } + + const handleChange = useCallback((key: string, costValue: any) => { + // 价格输入限制:¥0.00–9999.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 = ({ setPlayGame(options) } }, [children]) + + useEffect(() => { + console.log(value, 'valuevalue'); + }, [value]) const renderChildren = () => { return children.map((child: any, index: number) => { return @@ -91,6 +137,7 @@ const FormBasicInfo: React.FC = ({ placeholder='请输入' placeholderClass='title-placeholder' type='digit' + maxlength={7} value={value[child.prop]} onInput={(e) => handleChange(child.prop, e.detail.value)} /> diff --git a/src/pages/publishBall/components/SelectStadium/SelectStadium.tsx b/src/pages/publishBall/components/SelectStadium/SelectStadium.tsx index 90c474b..db4ed2c 100644 --- a/src/pages/publishBall/components/SelectStadium/SelectStadium.tsx +++ b/src/pages/publishBall/components/SelectStadium/SelectStadium.tsx @@ -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 = ({ 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 = ({ handleItemLocation(stadium) }} > - {stadium.istance} · + {calculateDistance(stadium)} · (({ 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(({ 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(({ 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(({ {formData.name} - {formData.istance} · + {calculateDistance(formData.istance || null)} · {formData.address} diff --git a/src/pages/publishBall/components/WechatSwitch/WechatSwitch.tsx b/src/pages/publishBall/components/WechatSwitch/WechatSwitch.tsx index be90c67..1088f68 100644 --- a/src/pages/publishBall/components/WechatSwitch/WechatSwitch.tsx +++ b/src/pages/publishBall/components/WechatSwitch/WechatSwitch.tsx @@ -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 = ({ value, onChange, subTitle, wechatId }) => { - + const [editWechat, setEditWechat] = useState(false) + const editWechatId = () => { + + } + const setWechatId = useCallback((e: any) => { + const value = e.target.value + onChange(value) + }, []) return ( <> @@ -28,7 +35,14 @@ const FormSwitch: React.FC = ({ value, onChange, subTitle, wech wechatId && ( 微信号: {wechatId.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')} - 修改 + 修改 + + ) + } + { + editWechat && ( + + ) } diff --git a/src/pages/publishBall/index.module.scss b/src/pages/publishBall/index.module.scss index bf496b5..3e69fad 100644 --- a/src/pages/publishBall/index.module.scss +++ b/src/pages/publishBall/index.module.scss @@ -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; - } - } - } - } } // 旋转动画 diff --git a/src/pages/publishBall/index.tsx b/src/pages/publishBall/index.tsx index 71f92e3..f4c2bd8 100644 --- a/src/pages/publishBall/index.tsx +++ b/src/pages/publishBall/index.tsx @@ -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('individual') + const [isSubmitDisabled, setIsSubmitDisabled] = useState(false) + // 获取页面参数并设置导航标题 + const [optionsConfig, setOptionsConfig] = useState(publishBallFormSchema) const [formData, setFormData] = useState([ 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 ( {/* 活动类型切换 */} - + /> */} - + { formData.map((item, index) => ( @@ -223,19 +387,19 @@ const PublishBall: React.FC = () => { 第{index + 1}场 - showDeleteConfirm(index)} > - + - + {index > 0 && ( - handleCopyPrevious(index)} > 复制上一场 @@ -244,10 +408,10 @@ const PublishBall: React.FC = () => { )} - updateFormData(key, value, index)} - optionsConfig={publishBallFormSchema} + updateFormData(key, value, index)} + optionsConfig={optionsConfig} /> )) @@ -263,38 +427,40 @@ const PublishBall: React.FC = () => { {/* 删除确认弹窗 */} - {deleteConfirm.visible && ( - - - 确认移除该场次? - 该操作不可恢复 - - - - - - - )} - + {/* 完成按钮 */} - - - 点击确定发布约球,即表示已经同意条款 - 《约球规则》 - + { + activityType === 'individual' && ( + + 点击确定发布约球,即表示已经同意条款 + 《约球规则》 + + ) + } + { + activityType === 'group' && ( + + + 已认证 徐汇爱打球官方球场,请严格遵守签约协议 + + ) + } ) diff --git a/src/pages/publishBall/publishForm.tsx b/src/pages/publishBall/publishForm.tsx index f3a70c9..eb7b8b6 100644 --- a/src/pages/publishBall/publishForm.tsx +++ b/src/pages/publishBall/publishForm.tsx @@ -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 {item.props?.summary} + if (item.prop === 'skill_level') { + return {getNTRPText(formData.skill_level)} + } + if (item.prop === 'players') { + return {getPlayersText(formData.players)} + } } return null } diff --git a/src/pages/index/index.config.ts b/src/pages/search/index.config.ts similarity index 51% rename from src/pages/index/index.config.ts rename to src/pages/search/index.config.ts index 12abc5f..0adc673 100644 --- a/src/pages/index/index.config.ts +++ b/src/pages/search/index.config.ts @@ -1,3 +1,3 @@ export default definePageConfig({ - navigationBarTitleText: '首页' + navigationBarTitleText: '' }) diff --git a/src/pages/search/index.scss b/src/pages/search/index.scss new file mode 100644 index 0000000..5dc03cb --- /dev/null +++ b/src/pages/search/index.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/src/pages/search/index.tsx b/src/pages/search/index.tsx new file mode 100644 index 0000000..66b4299 --- /dev/null +++ b/src/pages/search/index.tsx @@ -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(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 ( + <> + + {/* 搜索 */} + + + + + {isShowClearIcon && ( + + )} + + + 搜索 + + + + {/* 联想词 */} + {isShowSuggestion && ( + + {(suggestionList || [])?.map((item) => { + // 替换匹配文本为高亮版本 + const highlightedText = item.replace(regex, (match) => { + // 如果匹配不到,则返回原文本 + if (!match) return match; + return `${match}`; + }); + return ( + handleSuggestionSearch(item)} + > + + + + + + + ); + })} + + )} + {/* 历史搜索 */} + {!isShowClearIcon && ( + + + 历史搜索 + + 清空 + + + + + {isShowHistory && ( + + {(searchHistory || [])?.map((item) => { + return ( + handleHistoryClick(item)} + > + {item} + + ); + })} + + )} + + )} + + + ); +}; +export default ListSearch; diff --git a/src/pages/searchResult/index.config.ts b/src/pages/searchResult/index.config.ts new file mode 100644 index 0000000..65e057f --- /dev/null +++ b/src/pages/searchResult/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '搜索结果', + // navigationStyle: 'custom', +}) diff --git a/src/pages/searchResult/index.scss b/src/pages/searchResult/index.scss new file mode 100644 index 0000000..8812cee --- /dev/null +++ b/src/pages/searchResult/index.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/pages/searchResult/index.tsx b/src/pages/searchResult/index.tsx new file mode 100644 index 0000000..cc47c16 --- /dev/null +++ b/src/pages/searchResult/index.tsx @@ -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 ( + + {/* 筛选 */} + + + + + {/* 列表内容 */} + + + ); +}; + +export default SearchResult; \ No newline at end of file diff --git a/src/scss/images.scss b/src/scss/images.scss index 3527d5a..28b8db1 100644 --- a/src/scss/images.scss +++ b/src/scss/images.scss @@ -38,6 +38,8 @@ $-images: ( 'icon-detail-comment': '/detail/icon-comment.svg', 'icon-detail-comment-dark': '/detail/icon-comment-dark.svg', 'icon-detail-share': '/detail/icon-share-dark.svg', + 'icon-guide-bar-publish': '/common/guide-bar-publish.svg', + 'icon-navigator-back': '/common/navigator-back.svg', ) !default; // 图片获取函数 diff --git a/src/scss/themeColor.scss b/src/scss/themeColor.scss index 03a92a0..5283ec2 100644 --- a/src/scss/themeColor.scss +++ b/src/scss/themeColor.scss @@ -5,4 +5,5 @@ $primary-shallow-bg: rgb(245, 245, 245); $input-placeholder-color: rgba(60, 60, 67, 0.6); $textarea-placeholder-color: rgba(60, 60, 67, 0.3); $primary-background-color: rgba(0, 0, 0, 0.06); -$primary-border-color: rgba(0, 0, 0, 0.16); \ No newline at end of file +$primary-border-color: rgba(0, 0, 0, 0.16); +$primary-border-light-color: rgba(22, 24, 35, 0.12); \ No newline at end of file diff --git a/src/services/listApi.ts b/src/services/listApi.ts index 77ebd65..414efbb 100644 --- a/src/services/listApi.ts +++ b/src/services/listApi.ts @@ -1,4 +1,4 @@ -import { TennisMatch } from "../store/listStore"; +import httpService from "./httpService"; // 模拟网络延迟 const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -11,76 +11,6 @@ interface ApiResponse { timestamp: number; } -// 模拟网球比赛数据 -const mockTennisMatches: TennisMatch[] = [ - { - id: "1", - title: "周一晚场浦东新区单打约球", - dateTime: "明天(周五)下午5点 2小时", - location: "仁恒河滨花园网球场", - distance: "3.5km", - shinei: "室内", - registeredCount: 3, - maxCount: 4, - skillLevel: "2.0 至 2.5", - matchType: "双打", - images: [ - "https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center", - "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=200&h=200&fit=crop&crop=center", - "https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center", - ], - }, - { - id: "2", - title: "浦东新区单打约球", - dateTime: "明天(周五)下午5点 2小时", - location: "仁恒河滨花园网球场", - distance: "3.5km", - shinei: "室外", - registeredCount: 2, - maxCount: 4, - skillLevel: "2.0 至 2.5", - matchType: "双打", - images: [ - "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=200&h=200&fit=crop&crop=center", - "https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center", - ], - }, - { - id: "3", - title: "黄浦区双打约球", - dateTime: "7月20日(周日)下午6点 2小时", - location: "仁恒河滨花园网球场", - distance: "3.5km", - registeredCount: 3, - maxCount: 4, - skillLevel: "2.0 至 2.5", - matchType: "双打", - images: [ - "https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center", - ], - }, -]; - -// 模拟数据变化 -const generateDynamicData = (): TennisMatch[] => { - Promise.resolve((res) => { - setTimeout(res, 3000); - }); - return mockTennisMatches.map((match) => ({ - ...match, - // 随机更新注册人数 - registeredCount: Math.min( - match.maxCount, - Math.max(0, match.registeredCount + Math.floor(Math.random() * 3) - 1) - ), - // 随机更新距离 - distance: `${(Math.random() * 5 + 1).toFixed(1)}km`, - // 随机更新时间 - dateTime: Math.random() > 0.5 ? match.dateTime : "今天下午3点 2小时", - })); -}; - /** * 获取网球比赛列表 * @param params 查询参数 @@ -91,14 +21,11 @@ export const getTennisMatches = async (params?: { pageSize?: number; location?: string; skillLevel?: string; -}): Promise => { +}) => { try { - // 生成动态数据 - const matches = generateDynamicData(); - - return matches; + return httpService.post('/venues/list', params, { showLoading: false }) } catch (error) { - console.error("API调用失败:", error); + console.error("列表数据获取失败:", error); throw error; } }; @@ -107,10 +34,10 @@ export const getTennisMatches = async (params?: { * 刷新网球比赛数据 * @returns Promise */ -export const refreshTennisMatches = async (): Promise => { +export const refreshTennisMatches = async (params) => { try { // 生成新的动态数据 - const matches = generateDynamicData(); + const matches = generateDynamicData(params); return matches; } catch (error) { console.error("API刷新失败:", error); @@ -119,46 +46,51 @@ export const refreshTennisMatches = async (): Promise => { }; /** - * 获取比赛详情 - * @param id 比赛ID - * @returns Promise + * 获取搜索历史记录的异步函数 + * @param {Object} params - 查询参数对象 + * @returns {Promise} - 返回一个Promise对象,包含获取到的搜索历史数据 */ -export const getTennisMatchDetail = async ( - id: string -): Promise => { +export const getSearchHistory = async (params) => { try { - console.log("API调用: getTennisMatchDetail", id); - - // 模拟网络延迟 - await delay(600 + Math.random() * 400); - - // 模拟网络错误 - if (simulateNetworkError()) { - throw new Error("获取详情失败,请稍后重试"); - } - - const match = mockTennisMatches.find((m) => m.id === id); - - if (!match) { - throw new Error("比赛不存在"); - } - - console.log("API获取详情成功:", match.title); - return match; + // 调用HTTP服务获取搜索历史记录 + return httpService.get('/games/search_history', params) } catch (error) { - console.error("API获取详情失败:", error); + // 捕获并打印错误信息 + console.error("历史记录获取失败:", error); + // 抛出错误以便上层处理 throw error; } -}; +} /** - * 模拟API统计信息 + * @description 清除搜索历史 + * @returns */ -export const getApiStats = () => { - return { - totalCalls: 0, - successRate: 0.95, - averageResponseTime: 800, - lastCallTime: new Date().toISOString(), - }; -}; +export const clearHistory = async () => { + try { + // 调用HTTP服务清除搜索历史记录 + return httpService.post('/games/clear_history') + } catch (error) { + // 捕获并打印错误信息 + console.error("清除历史记录失败:", error); + // 抛出错误以便上层处理 + throw error; + } +} + +/** + * @description 获取联想 + * @param params 查询参数 + * @returns + */ +export const searchSuggestion = async (params) => { + try { + // 调用HTTP服务获取搜索建议 + return httpService.get('/games/search_suggestion', params) + } catch (error) { + // 捕获并打印错误信息 + console.error("搜索建议获取失败:", error); + // 抛出错误以便上层处理 + throw error; + } +} diff --git a/src/services/publishService.ts b/src/services/publishService.ts index 778ca2d..24d55d9 100644 --- a/src/services/publishService.ts +++ b/src/services/publishService.ts @@ -4,7 +4,7 @@ import type { ApiResponse } from './httpService' // 用户接口 export interface PublishBallData { title: string // 球局标题 - image_list: Array[] // 球局封面 + image_list: string[] // 球局封面 start_time: string, end_time: string play_type: string // 玩法类型 @@ -16,17 +16,17 @@ export interface PublishBallData { longitude?: string // 经度 court_type?: string // 场地类型 1: 室内 2: 室外 court_surface?: string // 场地表面 1: 硬地 2: 红土 3: 草地 - venue_description_tag?: Array[] // 场地描述标签 + venue_description_tag?: string[] // 场地描述标签 venue_description?: string // 场地描述 - venue_image_list?: Array[] // 场地图片 + venue_image_list?: string[] // 场地图片 max_players: number // 人数要求 current_players: number // 人数要求 skill_level_min: number // 水平要求(NTRP) skill_level_max: number // 水平要求(NTRP) description: string // 备注 - description_tag: Array[] // 备注标签 - is_substitute_supported: boolean // 是否支持替补 - is_wechat_contact: boolean // 是否需要微信联系 + description_tag: string[] // 备注标签 + is_substitute_supported?: boolean // 是否支持替补 + is_wechat_contact?: boolean // 是否需要微信联系 wechat_contact?: string // 微信联系 } @@ -122,6 +122,13 @@ class PublishService { return httpService.post('/venues/list', data, { showLoading: false }) } + // 畅打发布 + async create_play_pmoothlys(data: {rows: PublishBallData[]}): Promise> { + return httpService.post('/games/create_play_pmoothlys', data, { + showLoading: true, + loadingText: '发布中...' + }) + } async getPictures(req: getPicturesReq): Promise> { return httpService.post('/gallery/sys_img_list', req, { showLoading: false, diff --git a/src/static/asserts/fonts/PoetsenOne-Regular.ttf b/src/static/asserts/fonts/PoetsenOne-Regular.ttf new file mode 100644 index 0000000..1a89422 Binary files /dev/null and b/src/static/asserts/fonts/PoetsenOne-Regular.ttf differ diff --git a/src/static/common/guide-bar-publish.svg b/src/static/common/guide-bar-publish.svg new file mode 100644 index 0000000..133858f --- /dev/null +++ b/src/static/common/guide-bar-publish.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/common/navigator-back.svg b/src/static/common/navigator-back.svg new file mode 100644 index 0000000..2ac5245 --- /dev/null +++ b/src/static/common/navigator-back.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/list/icon-input-logo.svg b/src/static/list/icon-input-logo.svg new file mode 100644 index 0000000..286df7f --- /dev/null +++ b/src/static/list/icon-input-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/list/icon-load-error.svg b/src/static/list/icon-load-error.svg new file mode 100644 index 0000000..3b7ad03 --- /dev/null +++ b/src/static/list/icon-load-error.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/list/icon-paying-game.svg b/src/static/list/icon-paying-game.svg new file mode 100644 index 0000000..a312921 --- /dev/null +++ b/src/static/list/icon-paying-game.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/list/icon-reload.svg b/src/static/list/icon-reload.svg new file mode 100644 index 0000000..121dcc5 --- /dev/null +++ b/src/static/list/icon-reload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/publishBall/icon-group.svg b/src/static/publishBall/icon-group.svg new file mode 100644 index 0000000..5ad138c --- /dev/null +++ b/src/static/publishBall/icon-group.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/static/publishBall/icon-person.svg b/src/static/publishBall/icon-person.svg new file mode 100644 index 0000000..e93635c --- /dev/null +++ b/src/static/publishBall/icon-person.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/static/publishBall/icon-plus.svg b/src/static/publishBall/icon-plus.svg new file mode 100644 index 0000000..d9282ea --- /dev/null +++ b/src/static/publishBall/icon-plus.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/static/publishBall/icon-publish.png b/src/static/publishBall/icon-publish.png new file mode 100644 index 0000000..50e9dbd Binary files /dev/null and b/src/static/publishBall/icon-publish.png differ diff --git a/src/static/publishBall/icon-right-max.svg b/src/static/publishBall/icon-right-max.svg new file mode 100644 index 0000000..f629e11 --- /dev/null +++ b/src/static/publishBall/icon-right-max.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/search/icon-back.svg b/src/static/search/icon-back.svg new file mode 100644 index 0000000..2ac5245 --- /dev/null +++ b/src/static/search/icon-back.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/search/icon-clear-history.svg b/src/static/search/icon-clear-history.svg new file mode 100644 index 0000000..9ab1407 --- /dev/null +++ b/src/static/search/icon-clear-history.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/static/search/icon-search-clear.svg b/src/static/search/icon-search-clear.svg new file mode 100644 index 0000000..3a7f384 --- /dev/null +++ b/src/static/search/icon-search-clear.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/static/search/icon-search-suggestion.svg b/src/static/search/icon-search-suggestion.svg new file mode 100644 index 0000000..996d104 --- /dev/null +++ b/src/static/search/icon-search-suggestion.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/search/icon-search.svg b/src/static/search/icon-search.svg new file mode 100644 index 0000000..ada172b --- /dev/null +++ b/src/static/search/icon-search.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/store/listStore.ts b/src/store/listStore.ts index b3fe98e..bf7b901 100644 --- a/src/store/listStore.ts +++ b/src/store/listStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' -import { getTennisMatches } from '../services/listApi' -import {ListActions, IFilterOptions, ListState } from '../../types/list/types' +import { getTennisMatches, getSearchHistory, clearHistory, searchSuggestion } from '../services/listApi' +import { ListActions, IFilterOptions, ListState } from '../../types/list/types' // 完整的 Store 类型 type TennisStore = ListState & ListActions @@ -19,9 +19,13 @@ const defaultDistance = 'all'; // 默认距离 export const useListStore = create()((set, get) => ({ // 初始状态 matches: [], + // 推荐列表 + recommendList: [], + // 是否加载中 loading: false, error: null, - lastRefreshTime: null, + // 搜索的value + searchValue: '', // 是否展示综合筛选弹窗 isShowFilterPopup: false, // 综合筛选项 @@ -40,7 +44,7 @@ export const useListStore = create()((set, get) => ({ { id: 3, label: "10km", value: "10km" }, ], // 快捷筛选数据 - quickFilterData:[ + quickFilterData: [ { text: "默认排序", value: "0" }, { text: "好评排序", value: "1" }, { text: "销量排序", value: "2" }, @@ -75,21 +79,73 @@ export const useListStore = create()((set, get) => ({ ], // 球局数量 gamesNum: 124, + // 页面滚动距离顶部距离 是否大于0 + isScrollTop: false, + // 搜索历史数据 + searchHistory: ['上海', '黄浦', '上海', '静安', '徐汇', '黄浦', '普陀', '黄浦', '长宁', '黄浦'], + // 搜索历史数据默认 Top 15 + searchHistoryParams: { + page: 1, + pageSize: 15, + }, + // 联想词 + suggestionList: [], + // 是否显示联想词 + isShowSuggestion: false, + // 列表页是否显示搜索框自定义导航 + isShowInputCustomerNavBar: false, + // 结果页是否显示搜索框自定义导航 + isShowResultInputCustomerNavBar: false, + // 打开距离筛选框 + isOpenDistancePopup: false, + // 打开快捷筛选框 + isOpenQuickFilterPopup: false, // 获取比赛数据 fetchMatches: async (params) => { set({ loading: true, error: null }) try { - const matches = await getTennisMatches(params) + const resData = await getTennisMatches(params) || {}; + const { data = {}, code } = resData; + if (code !== 0) { + set({ + error: '1', + matches: [], + loading: false, + }) + } + const { count, rows } = data; + const list = (rows || []).map(() => { + return { + id: "3", + title: "黄浦区双打约球", + dateTime: "7月20日(周日)下午6点 2小时", + location: "仁恒河滨花园网球场", + distance: "3.5km", + registeredCount: 3, + maxCount: 4, + skillLevel: "2.0 至 2.5", + matchType: "双打", + images: [ + "https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center", + ], + } + }) set({ - matches, + matches: list || rows || [], + recommendList: list || rows || [], + error: null, loading: false, - lastRefreshTime: new Date().toISOString() + gamesNum: count, }) } catch (error) { - + set({ + error, + matches: [], + loading: false, + }) } }, @@ -98,17 +154,57 @@ export const useListStore = create()((set, get) => ({ set({ loading: true, error: null }) try { - const matches = await getTennisMatches() + const resData = await getTennisMatches() || {}; + const { data = {}, code } = resData; + const { count, rows } = data; set({ - matches, + matches: rows, loading: false, - lastRefreshTime: new Date().toISOString() }) - console.log('Store: 成功刷新网球比赛数据:', matches.length, '条') } catch (error) { } }, + // 获取历史搜索数据 + getSearchHistory: async () => { + try { + const params = get()?.searchHistoryParams || {} + const resData = await getSearchHistory(params) || {}; + console.log('===resData', resData) + } catch (error) { + + } + }, + + // 清空历史记录 + clearHistory: async () => { + try { + const resData = await clearHistory() || {}; + } catch (error) { + + } + set({ + searchHistory: [], + }) + }, + + // 获取联想 + searchSuggestion: async (val: string) => { + try { + const resData = await searchSuggestion({ val }) || {}; + console.log('===获取联想', resData) + // set({ + // suggestionList: ['上海球局', '黄浦球局', '上海球局', '静安球局', '徐汇球局', '黄浦球局', '普陀球局', '黄浦球局', '长宁球局', '黄浦球局'], + // isShowSuggestion: true, + // }) + } catch (error) { + set({ + suggestionList: ['上海球局', '黄浦球局', '上海球局', '静安球局', '徐汇球局', '黄浦球局', '普陀球局', '黄浦球局', '长宁球局', '黄浦球局'], + isShowSuggestion: true, + }) + } + }, + // 清除错误信息 clearError: () => { set({ error: null }) diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..1192161 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,9 @@ +export * from './getNavbarHeight' +export * from './locationUtils' +export * from './processImage' +export * from './timeUtils' +export * from './tokenManager' + +export function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} \ No newline at end of file diff --git a/src/utils/timeUtils.ts b/src/utils/timeUtils.ts index afdeca0..74d65d0 100644 --- a/src/utils/timeUtils.ts +++ b/src/utils/timeUtils.ts @@ -21,6 +21,9 @@ export const getEndTime = (startTime: string): string => { return endDateTime.format('YYYY-MM-DD HH:mm') } +export const getDateStr = (date: Date): string => { + return dayjs(date).format('YYYY-MM-DD HH:mm') +} export const getDate = (date: string): string => { return dayjs(date).format('YYYY年MM月DD日') @@ -32,10 +35,10 @@ export const getTime = (time: string): string => { const minute = timeObj.minute() // 判断是上午还是下午 - const period = hour < 12 ? 'AM' : 'PM' + const period = hour <= 12 ? 'AM' : 'PM' // 转换为12小时制 - const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour + const hour12 = hour === 0 ? 0 : hour > 12 ? hour - 12 : hour // 格式化分钟,保证两位数 const minuteStr = minute.toString().padStart(2, '0') diff --git a/types/list/types.ts b/types/list/types.ts index 6d71c50..144697a 100644 --- a/types/list/types.ts +++ b/types/list/types.ts @@ -1,6 +1,6 @@ // 网球比赛数据接口 export interface TennisMatch { - id: string + id: number title: string dateTime: string location: string @@ -21,9 +21,10 @@ export interface IFilterOptions { } export interface ListState { matches: TennisMatch[] + recommendList: TennisMatch[] loading: boolean error: string | null - lastRefreshTime: string | null + searchValue: string isShowFilterPopup: boolean filterOptions: IFilterOptions filterCount: number @@ -39,24 +40,15 @@ export interface ListState { locationOptions: BubbleOption[] gamePlayOptions: BubbleOption[] gamesNum: number -} - -export interface ListState { - matches: TennisMatch[] - loading: boolean - error: string | null - lastRefreshTime: string | null - isShowFilterPopup: boolean - filterOptions: IFilterOptions - filterCount: number - distance: string | number - quickFilter: string | number - distanceData: any[] - quickFilterData: any[] - distanceQuickFilter: { - distance: string - quick: string - } + isScrollTop: boolean + searchHistoryParams: Record + searchHistory: string[] + suggestionList: string[] + isShowSuggestion: boolean + isShowInputCustomerNavBar: boolean + isShowResultInputCustomerNavBar: boolean + isOpenDistancePopup: boolean, + isOpenQuickFilterPopup: boolean, } export interface ListActions { @@ -71,6 +63,9 @@ export interface ListActions { updateState: (payload: Record) => void updateFilterOptions: (payload: Record) => void clearFilterOptions: () => void + getSearchHistory: () => Promise + clearHistory: () => void + searchSuggestion: (val: string) => Promise } // 快捷筛选 @@ -81,6 +76,8 @@ export interface MenuFilterProps { wrapperClassName?: string; itemClassName?: string; name: string; + onOpen?: () => void; + onClose?: () => void; } // 距离筛选 @@ -91,6 +88,8 @@ export interface DistanceFilterProps { wrapperClassName?: string; itemClassName?: string; name: string; + onOpen?: () => void; + onClose?: () => void; } // bubble 组件 @@ -146,6 +145,7 @@ export interface FilterPopupProps { // 列表卡片 export interface ListCardProps { + id: number; title: string; dateTime: string; location: string; diff --git a/types/publishBall.ts b/types/publishBall.ts index c40a734..198215c 100644 --- a/types/publishBall.ts +++ b/types/publishBall.ts @@ -1,7 +1,7 @@ export interface PublishBallFormData { title: string // 球局标题 - image_list: Array[] // 球局封面 + image_list: string[] // 球局封面 timeRange: { start_time: string, end_time: string @@ -16,15 +16,15 @@ export interface PublishBallFormData { longitude?: string // 经度 court_type?: string // 场地类型 1: 室内 2: 室外 court_surface?: string // 场地表面 1: 硬地 2: 红土 3: 草地 - venue_description_tag?: Array[] // 场地描述标签 + venue_description_tag?: string[] // 场地描述标签 venue_description?: string // 场地描述 - venue_image_list?: Array[] // 场地图片 + venue_image_list?: string[] // 场地图片 } players: [number, number] // 人数要求 skill_level: [number, number] // 水平要求(NTRP) descriptionInfo: { description: string // 备注 - description_tag: Array[] // 备注标签 + description_tag: string[] // 备注标签 } is_substitute_supported: boolean // 是否支持替补 is_wechat_contact: boolean // 是否需要微信联系 diff --git a/yarn.lock b/yarn.lock index 4dfd709..b558aa5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8293,11 +8293,6 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -qqmap-wx-jssdk@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/qqmap-wx-jssdk/-/qqmap-wx-jssdk-1.0.0.tgz#8ab4b0d3aa900458217d6fbe52af832bb6c63c73" - integrity sha512-wuaNetsA9/OKEQGgK1CNPsX6pppWpY10cQwQu1OHJplGMyMIMzK2bliMkNXjtry99qXYCsvDAWPqw2DI+/foJg== - qs@6.13.0: version "6.13.0" resolved "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"