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 0000000..50e9dbd Binary files /dev/null and b/src/static/publishBall/icon-publish.png differ diff --git a/src/static/publishBall/icon-right-max.svg b/src/static/publishBall/icon-right-max.svg new file mode 100644 index 0000000..f629e11 --- /dev/null +++ b/src/static/publishBall/icon-right-max.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/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"