From fe14e01267503ec5bf8635f242e9be0267d372a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AD=B1=E9=87=8E?= Date: Sat, 30 Aug 2025 22:25:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=8F=91=E5=B8=83=E6=97=A5?= =?UTF-8?q?=E5=8E=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/index.ts | 15 +- package.json | 1 - postcss.config.js | 1 + src/app.config.ts | 3 +- src/components/CalendarCard/CalendarCard.tsx | 117 ++ .../CalendarCard/DialogCalendarCard.tsx | 130 ++ src/components/CalendarCard/index.module.scss | 105 ++ src/components/CalendarCard/index.ts | 2 + src/components/CommonDialog/CommonDialog.tsx | 62 + src/components/CommonDialog/index.module.scss | 86 ++ src/components/CommonDialog/index.ts | 2 + .../DateTimePicker/DateTimePicker.tsx | 153 ++- src/components/DateTimePicker/README.md | 67 - src/components/DateTimePicker/example.tsx | 45 - .../DateTimePicker/index.module.scss | 147 +-- .../HourMinutePicker/HourMinutePicker.tsx | 97 ++ .../HourMinutePicker/index.module.scss | 52 + src/components/HourMinutePicker/index.ts | 2 + src/components/MapDisplay/README.md | 215 ---- src/components/MapDisplay/index.scss | 382 ------ src/components/MapDisplay/index.tsx | 505 -------- src/components/MapDisplay/mapPlugin.tsx | 63 - src/components/MapDisplay/mapService.ts | 190 --- src/components/PublishMenu/PublishMenu.tsx | 81 ++ src/components/PublishMenu/index.module.scss | 206 +++ src/components/PublishMenu/index.ts | 2 + src/components/TimePicker/README.md | 77 ++ src/components/TimePicker/TimePicker.tsx | 233 ++++ src/components/TimePicker/demo.module.scss | 81 ++ src/components/TimePicker/demo.tsx | 55 + src/components/TimePicker/index.module.scss | 187 +++ src/components/TimePicker/index.ts | 2 + .../TimePicker/layout-test.module.scss | 59 + src/components/TimePicker/layout-test.tsx | 51 + src/components/TimePicker/test.module.scss | 36 + src/components/TimePicker/test.tsx | 46 + src/components/TimeSelector/TimeSelector.tsx | 23 +- src/components/index.ts | 35 +- src/config/images.js | 7 +- src/nutui-theme.scss | 2 +- src/package/qqmap-wx-jssdk.js | 1122 ----------------- src/package/qqmap-wx-jssdk.min.js | 1 - src/pages/index/index.tsx | 8 + src/pages/mapDisplay/index.tsx | 5 - .../SelectStadium/SelectStadium.tsx | 13 +- .../SelectStadium/StadiumDetail.tsx | 16 +- src/pages/publishBall/index.module.scss | 70 +- src/pages/publishBall/index.tsx | 115 +- src/pages/publishBall/publishForm.tsx | 28 + src/scss/themeColor.scss | 3 +- src/services/publishService.ts | 15 +- src/static/publishBall/icon-group.svg | 6 + src/static/publishBall/icon-person.svg | 5 + src/static/publishBall/icon-plus.svg | 17 + src/static/publishBall/icon-publish.png | Bin 0 -> 31955 bytes src/static/publishBall/icon-right-max.svg | 3 + src/utils/timeUtils.ts | 7 +- types/publishBall.ts | 8 +- yarn.lock | 5 - 59 files changed, 2151 insertions(+), 2921 deletions(-) create mode 100644 postcss.config.js create mode 100644 src/components/CalendarCard/CalendarCard.tsx create mode 100644 src/components/CalendarCard/DialogCalendarCard.tsx create mode 100644 src/components/CalendarCard/index.module.scss create mode 100644 src/components/CalendarCard/index.ts create mode 100644 src/components/CommonDialog/CommonDialog.tsx create mode 100644 src/components/CommonDialog/index.module.scss create mode 100644 src/components/CommonDialog/index.ts delete mode 100644 src/components/DateTimePicker/README.md delete mode 100644 src/components/DateTimePicker/example.tsx create mode 100644 src/components/HourMinutePicker/HourMinutePicker.tsx create mode 100644 src/components/HourMinutePicker/index.module.scss create mode 100644 src/components/HourMinutePicker/index.ts delete mode 100644 src/components/MapDisplay/README.md delete mode 100644 src/components/MapDisplay/index.scss delete mode 100644 src/components/MapDisplay/index.tsx delete mode 100644 src/components/MapDisplay/mapPlugin.tsx delete mode 100644 src/components/MapDisplay/mapService.ts create mode 100644 src/components/PublishMenu/PublishMenu.tsx create mode 100644 src/components/PublishMenu/index.module.scss create mode 100644 src/components/PublishMenu/index.ts create mode 100644 src/components/TimePicker/README.md create mode 100644 src/components/TimePicker/TimePicker.tsx create mode 100644 src/components/TimePicker/demo.module.scss create mode 100644 src/components/TimePicker/demo.tsx create mode 100644 src/components/TimePicker/index.module.scss create mode 100644 src/components/TimePicker/index.ts create mode 100644 src/components/TimePicker/layout-test.module.scss create mode 100644 src/components/TimePicker/layout-test.tsx create mode 100644 src/components/TimePicker/test.module.scss create mode 100644 src/components/TimePicker/test.tsx delete mode 100644 src/package/qqmap-wx-jssdk.js delete mode 100644 src/package/qqmap-wx-jssdk.min.js delete mode 100644 src/pages/mapDisplay/index.tsx create mode 100644 src/static/publishBall/icon-group.svg create mode 100644 src/static/publishBall/icon-person.svg create mode 100644 src/static/publishBall/icon-plus.svg create mode 100644 src/static/publishBall/icon-publish.png create mode 100644 src/static/publishBall/icon-right-max.svg diff --git a/config/index.ts b/config/index.ts index c43eeef..69d7626 100644 --- a/config/index.ts +++ b/config/index.ts @@ -12,11 +12,11 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { date: '2025-8-9', designWidth: 390, // 改为 390 deviceRatio: { - 640: 2.34 / 2 * (390 / 640), // 原值重新计算 - 750: 1 * (390 / 750), // 原值重新计算 - 375: 2 * (390 / 375), // 原值重新计算 - 828: 1.81 / 2 * (390 / 828), // 原值重新计算 - 390: 2 // 新增基准设备 + 640: 2.34 / 2, + 750: 1, + 375: 2, + 828: 1.81 / 2, + 390: 1.92 }, sourceRoot: 'src', outputRoot: 'dist', @@ -55,11 +55,6 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { pxtransform: { enable: true, config: { - platform: 'weapp', - designWidth: 390, // 这里也要同步修改 - deviceRatio: { - 390: 2 // 这里只需要基准比例 - }, selectorBlackList: ['nut-'] } }, 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/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/postcss.config.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app.config.ts b/src/app.config.ts index 9d80db6..bc81743 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,8 +1,7 @@ export default defineAppConfig({ pages: [ + 'pages/index/index', 'pages/publishBall/index', - 'pages/mapDisplay/index', - 'pages/index/index' ], window: { backgroundTextStyle: 'light', 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/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/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/HourMinutePicker/HourMinutePicker.tsx b/src/components/HourMinutePicker/HourMinutePicker.tsx new file mode 100644 index 0000000..fb95772 --- /dev/null +++ b/src/components/HourMinutePicker/HourMinutePicker.tsx @@ -0,0 +1,97 @@ +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..e513b57 --- /dev/null +++ b/src/components/HourMinutePicker/index.module.scss @@ -0,0 +1,52 @@ +.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; +} + +.drag-handle { + width: 40px; + height: 4px; + background-color: #e0e0e0; + border-radius: 2px; + margin: 0 auto 20px; +} + +.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/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/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..8e5f484 --- /dev/null +++ b/src/components/PublishMenu/index.module.scss @@ -0,0 +1,206 @@ +.publishMenu { + position: fixed; + bottom: 40px; + right: 40px; + z-index: 1000; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: transparent; + z-index: 999; +} + +.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(45deg); + } +} + +.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/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/index.ts b/src/components/index.ts index a61fa13..e2e019e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -9,17 +9,26 @@ import TimeSelector from './TimeSelector' import TitleTextarea from './TitleTextarea' import CommonPopup from './CommonPopup' import DateTimePicker from './DateTimePicker/DateTimePicker' - -export { - ActivityTypeSwitch, - TextareaTag, - FormSwitch, - ImageUpload, - Range, - NumberInterval, - TimeSelector, - TitleTextarea, - CommonPopup, - DateTimePicker -} +import TimePicker from './TimePicker/TimePicker' +import CalendarCard, { DialogCalendarCard } from './CalendarCard' +import CommonDialog from './CommonDialog' +import PublishMenu from './PublishMenu/PublishMenu' + + export { + ActivityTypeSwitch, + TextareaTag, + FormSwitch, + ImageUpload, + Range, + NumberInterval, + TimeSelector, + TitleTextarea, + CommonPopup, + DateTimePicker, + TimePicker, + CalendarCard, + DialogCalendarCard, + CommonDialog, + PublishMenu + } diff --git a/src/config/images.js b/src/config/images.js index c2ac918..c5d30b2 100644 --- a/src/config/images.js +++ b/src/config/images.js @@ -16,5 +16,10 @@ export default { ICON_HEART_CIRCLE: require('@/static/publishBall/icon-heartcircle.png'), 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_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'), } \ No newline at end of file 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/index/index.tsx b/src/pages/index/index.tsx index bf32306..792170d 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -5,6 +5,7 @@ import Taro from '@tarojs/taro' // 导入API服务 import demoApi from '../../services/demoApi' import commonApi from '../../services/commonApi' +import PublishMenu from '../../components/PublishMenu' import { useUserStats, useUserActions @@ -286,6 +287,13 @@ function Index() { • 请求失败时会自动使用模拟数据 + { + Taro.navigateTo({ + url: '/pages/publishBall/index' + }) + }} + /> ) } 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/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: '', @@ -116,6 +116,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 = () => { @@ -127,7 +134,8 @@ const StadiumDetail = forwardRef(({ name: res.name, address: res.address, latitude: res.longitude, - longitude: res.latitude + longitude: res.latitude, + istance: null }) }, fail: (err) => { @@ -165,7 +173,7 @@ const StadiumDetail = forwardRef(({ {formData.name} - {formData.istance} · + {calculateDistance(formData.istance || null)} · {formData.address} diff --git a/src/pages/publishBall/index.module.scss b/src/pages/publishBall/index.module.scss index bf496b5..ccecff6 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; @@ -230,74 +231,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 dbd7b46..2b2f69b 100644 --- a/src/pages/publishBall/index.tsx +++ b/src/pages/publishBall/index.tsx @@ -1,7 +1,9 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { View, Text, Button, Image } from '@tarojs/components' import Taro from '@tarojs/taro' -import ActivityTypeSwitch, { type ActivityType } from '../../components/ActivityTypeSwitch' +import { type ActivityType } from '../../components/ActivityTypeSwitch' +// import ActivityTypeSwitch, { type ActivityType } from '../../components/ActivityTypeSwitch' +import CommonDialog from '../../components/CommonDialog' import PublishForm from './publishForm' import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema'; import { PublishBallFormData } from '../../../types/publishBall'; @@ -44,6 +46,29 @@ const defaultFormData: PublishBallFormData = { const PublishBall: React.FC = () => { const [activityType, setActivityType] = useState('individual') + + // 获取页面参数并设置导航标题 + useEffect(() => { + 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) + // 根据type设置导航标题 + if (type === 'group') { + Taro.setNavigationBarTitle({ + title: '发布畅打活动' + }) + } else { + Taro.setNavigationBarTitle({ + title: '发布' + }) + } + } + handleActivityTypeChange(type) + } + }, []) const [formData, setFormData] = useState([ defaultFormData ]) @@ -69,10 +94,13 @@ const PublishBall: React.FC = () => { } - // 处理活动类型变化 const handleActivityTypeChange = (type: ActivityType) => { - setActivityType(type) + if (type === 'group') { + setFormData([defaultFormData]) + } else { + setFormData([defaultFormData]) + } } const handleAdd = () => { @@ -80,8 +108,10 @@ const PublishBall: React.FC = () => { setFormData(prev => [...prev, { ...defaultFormData, title: '', - start_time: newStartTime, - end_time: getEndTime(newStartTime) + timeRange: { + start_time: newStartTime, + end_time: getEndTime(newStartTime) + } }]) } @@ -131,7 +161,7 @@ const PublishBall: React.FC = () => { const validateFormData = (formData: PublishBallFormData) => { const { activityInfo, image_list, title } = formData; const { play_type, price, location_name } = activityInfo; - if (!image_list.length) { + if (!image_list?.length) { Taro.showToast({ title: `请上传活动封面`, icon: 'none' @@ -145,21 +175,21 @@ const PublishBall: React.FC = () => { }) return false } - if (!price) { + if (!price || (typeof price === 'number' && price <= 0) || (typeof price === 'string' && !price.trim())) { Taro.showToast({ title: `请输入费用`, icon: 'none' }) return false } - if (!play_type) { + if (!play_type || !play_type.trim()) { Taro.showToast({ title: `请选择玩法类型`, icon: 'none' }) return false } - if (!location_name) { + if (!location_name || !location_name.trim()) { Taro.showToast({ title: `请选择场地`, icon: 'none' @@ -202,16 +232,46 @@ const PublishBall: React.FC = () => { }) } } + if (activityType === 'group') { + const isValid = formData.every(item => validateFormData(item)) + if (!isValid) { + return + } + formData.forEach(async (item) => { + const { activityInfo, descriptionInfo, timeRange, players, skill_level, ...rest } = item; + const options = { + ...rest, + ...activityInfo, + ...descriptionInfo, + ...timeRange, + max_players: players[1], + current_players: players[0], + skill_level_min: skill_level[0], + skill_level_max: skill_level[1] + } + const res = await PublishService.create_play_pmoothly(options); + if (res.code === 0 && res.data) { + Taro.showToast({ + title: '发布成功', + icon: 'success' + }) + } + }) + } } + useEffect(() => { + console.log(formData, 'formData'); + }, []) + return ( {/* 活动类型切换 */} - + /> */} @@ -263,28 +323,15 @@ const PublishBall: React.FC = () => { {/* 删除确认弹窗 */} - {deleteConfirm.visible && ( - - - 确认移除该场次? - 该操作不可恢复 - - - - - - - )} + {/* 完成按钮 */} diff --git a/src/pages/publishBall/publishForm.tsx b/src/pages/publishBall/publishForm.tsx index b0c6843..0c97aed 100644 --- a/src/pages/publishBall/publishForm.tsx +++ b/src/pages/publishBall/publishForm.tsx @@ -84,8 +84,36 @@ 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 renderSummary = (item: FormFieldConfig) => { if (item.props?.showSummary) { + if (item.prop === 'skill_level') { + return {getNTRPText(formData.skill_level)} + } return {item.props?.summary} } return null 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/publishService.ts b/src/services/publishService.ts index b9ec637..50e395d 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,15 +16,15 @@ 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[] // 备注标签 + description_tag: string[] // 备注标签 is_substitute_supported: boolean // 是否支持替补 is_wechat_contact: boolean // 是否需要微信联系 wechat_contact?: string // 微信联系 @@ -66,6 +66,13 @@ class PublishService { return httpService.post('/venues/list', data, { showLoading: false }) } + // 畅打发布 + async create_play_pmoothly(data: PublishBallData): Promise> { + return httpService.post('/games/create_play_pmoothly', data, { + showLoading: true, + loadingText: '发布中...' + }) + } } // 导出认证服务实例 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 0000000000000000000000000000000000000000..50e9dbd30be6a9a8e58209bcc635d91f50b923cb GIT binary patch literal 31955 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 N002ovPDHLkV1m7y($D|^ literal 0 HcmV?d00001 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/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/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 b4fcd75..5b1fef6 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"