diff --git a/analyze.js b/analyze.js new file mode 100644 index 0000000..3b3453e --- /dev/null +++ b/analyze.js @@ -0,0 +1,50 @@ +const fs = require('fs'); +const path = require('path'); + +// dist 目录路径,根据你项目实际情况修改 +const DIST_DIR = path.join(__dirname, 'dist'); + +// 递归统计文件大小 +function getFiles(dir) { + let results = []; + const list = fs.readdirSync(dir); + list.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + if (stat && stat.isDirectory()) { + results = results.concat(getFiles(filePath)); + } else { + results.push({ path: filePath, size: stat.size }); + } + }); + return results; +} + +function formatSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; +} + +function analyze() { + if (!fs.existsSync(DIST_DIR)) { + console.error('dist 目录不存在,请先执行 taro build --type weapp'); + return; + } + + const files = getFiles(DIST_DIR); + const total = files.reduce((sum, f) => sum + f.size, 0); + + console.log('文件大小分析(按从大到小排序):'); + files + .sort((a, b) => b.size - a.size) + .forEach(f => { + console.log( + `${formatSize(f.size)} | ${(f.size / total * 100).toFixed(2)}% | ${path.relative(DIST_DIR, f.path)}` + ); + }); + + console.log(`\n总大小: ${formatSize(total)}`); +} + +analyze(); diff --git a/config/index.ts b/config/index.ts index c77a458..56b2b5f 100644 --- a/config/index.ts +++ b/config/index.ts @@ -2,7 +2,7 @@ import { defineConfig, type UserConfigExport } from '@tarojs/cli' import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin' import devConfig from './dev' import prodConfig from './prod' -import vitePluginImp from 'vite-plugin-imp' +// import vitePluginImp from 'vite-plugin-imp' import path from 'path' // https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数 @@ -16,7 +16,7 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { 750: 1, 375: 2, 828: 1.81 / 2, - 390: 1.92 + 390: 1.92 }, sourceRoot: 'src', outputRoot: 'dist', @@ -66,9 +66,16 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { } } }, + miniCssExtractPluginOption: { + ignoreOrder: true, + // filename: 'css/[name].[hash].css', + // chunkFilename: 'css/[name].[chunkhash].css' + }, webpackChain(chain) { chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin) - } + }, + // @ts-expect-error: Taro 类型定义缺少 mini.hot + hot: true, }, h5: { publicPath: '/', diff --git a/project.private.config.json b/project.private.config.json new file mode 100644 index 0000000..2c19aa8 --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,23 @@ +{ + "libVersion": "3.9.0", + "projectname": "playBallTogether", + "condition": {}, + "setting": { + "urlCheck": true, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": false, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "compileHotReLoad": true, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true, + "bigPackageSizeSupport": false + } +} \ No newline at end of file diff --git a/src/app.config.ts b/src/app.config.ts index 7c74bbe..e12e2bd 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,12 +1,16 @@ export default defineAppConfig({ pages: [ + 'pages/list/index', + // 'pages/userInfo/myself/index', 'pages/login/index/index', 'pages/login/verification/index', 'pages/login/terms/index', - // 'pages/publishBall/index', + 'pages/publishBall/index', // 'pages/mapDisplay/index', - // 'pages/list/index', - 'pages/index/index' + 'pages/detail/index', + 'pages/message/index', + 'pages/personal/index', + 'pages/orderCheck/index', ], window: { backgroundTextStyle: 'light', diff --git a/src/app.scss b/src/app.scss index e69de29..3228f0b 100644 --- a/src/app.scss +++ b/src/app.scss @@ -0,0 +1,4 @@ +@font-face { + font-family: 'PoetsenOne'; + src: url('./static/asserts/fonts/PoetsenOne-Regular.ttf') format('truetype'); +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 6ef979c..7ce6cd4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,10 @@ import { Component, ReactNode } from 'react' -import './app.scss' import './nutui-theme.scss' +import './app.scss' import { useDictionaryStore } from './store/dictionaryStore' +import { useGlobalStore } from './store/global' + +// import { getNavbarHeight } from "@/utils/getNavbarHeight"; interface AppProps { children: ReactNode @@ -11,11 +14,12 @@ class App extends Component { componentDidMount() { // 初始化字典数据 this.initDictionaryData() + this.getNavBarHeight() } - componentDidShow() {} + componentDidShow() { } - componentDidHide() {} + componentDidHide() { } // 初始化字典数据 private async initDictionaryData() { @@ -27,6 +31,13 @@ class App extends Component { } } + // 获取导航高度 + getNavBarHeight = () => { + const { getNavbarHeightInfo } = useGlobalStore.getState() + getNavbarHeightInfo() + + } + render() { // this.props.children 是将要会渲染的页面 return this.props.children diff --git a/src/components/Bubble/BubbleItem.tsx b/src/components/Bubble/BubbleItem.tsx index 0ede586..62ba0c8 100644 --- a/src/components/Bubble/BubbleItem.tsx +++ b/src/components/Bubble/BubbleItem.tsx @@ -1,16 +1,7 @@ import React from 'react'; -import { BubbleOption } from './index'; +import { BubbleItemProps } from '../../../types/list/types'; import styles from './bubbleItem.module.scss'; -export interface BubbleItemProps { - option: BubbleOption; - isSelected: boolean; - size: 'small' | 'medium' | 'large'; - disabled: boolean; - onClick: (option: BubbleOption) => void; - itemClassName?: string; -} - const BubbleItem: React.FC = ({ option, isSelected, diff --git a/src/components/Bubble/README.md b/src/components/Bubble/README.md deleted file mode 100644 index 90512d3..0000000 --- a/src/components/Bubble/README.md +++ /dev/null @@ -1,208 +0,0 @@ -# Bubble 通用气泡组件 - -一个高度可配置的气泡选择器组件,支持任何内容的选择,包括但不限于时间、地点、标签、分类等。 - -## 特性 - -- 🎯 支持单选和多选模式 -- 📱 三种布局方式:水平、垂直、网格 -- 🎨 三种尺寸:小、中、大 -- ♿ 支持禁用状态和图标描述 -- 🔄 支持受控和非受控模式 -- 📱 响应式设计,自动适应不同屏幕 -- 🎨 可自定义样式和类名 - -## 基本用法 - -### 室内外选择示例(如UI图所示) - -```tsx -import React, { useState } from 'react'; -import Bubble, { BubbleOption } from './index'; - -const LocationSelector: React.FC = () => { - const [selectedLocation, setSelectedLocation] = useState(''); - - const locationOptions: BubbleOption[] = [ - { id: 1, label: '室内', value: 'indoor' }, - { id: 2, label: '室外', value: 'outdoor' }, - { id: 3, label: '半室外', value: 'semi-outdoor' } - ]; - - return ( - setSelectedLocation(value as string)} - layout="horizontal" - size="medium" - /> - ); -}; -``` - -### 时间选择器示例 - -```tsx -const TimeSelector: React.FC = () => { - const [selectedTime, setSelectedTime] = useState(''); - - const timeOptions: BubbleOption[] = [ - { id: 1, label: '晨间 6:00-10:00', value: 'morning' }, - { id: 2, label: '上午 10:00-12:00', value: 'forenoon' }, - { id: 3, label: '中午 12:00-14:00', value: 'noon' }, - { id: 4, label: '下午 14:00-18:00', value: 'afternoon' }, - { id: 5, label: '晚上 18:00-22:00', value: 'evening' }, - { id: 6, label: '夜间 22:00-24:00', value: 'night' } - ]; - - return ( - setSelectedTime(value as string)} - layout="grid" - columns={3} - size="medium" - /> - ); -}; -``` - -### 多选模式 - -```tsx -const MultiSelectExample: React.FC = () => { - const [selectedValues, setSelectedValues] = useState([]); - - const options: BubbleOption[] = [ - { id: 1, label: '运动', value: 'sports' }, - { id: 2, label: '音乐', value: 'music' }, - { id: 3, label: '阅读', value: 'reading' }, - { id: 4, label: '旅行', value: 'travel' } - ]; - - return ( - setSelectedValues(value as string[])} - multiple={true} - layout="grid" - columns={2} - size="medium" - /> - ); -}; -``` - -### 带图标和描述 - -```tsx -const IconExample: React.FC = () => { - const [selectedValue, setSelectedValue] = useState(''); - - const options: BubbleOption[] = [ - { - id: 1, - label: '网球', - value: 'tennis', - icon: '🎾', - description: '室内外均可' - }, - { - id: 2, - label: '篮球', - value: 'basketball', - icon: '🏀', - description: '室内场地' - } - ]; - - return ( - setSelectedValue(value as string)} - layout="vertical" - size="large" - /> - ); -}; -``` - -## API - -### BubbleProps - -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| options | `BubbleOption[]` | - | 选项数组 | -| value | `string \| number \| (string \| number)[]` | - | 当前选中的值 | -| onChange | `(value, option) => void` | - | 选择变化时的回调 | -| multiple | `boolean` | `false` | 是否支持多选 | -| layout | `'horizontal' \| 'vertical' \| 'grid'` | `'horizontal'` | 布局方式 | -| columns | `number` | `3` | 网格布局的列数 | -| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 按钮尺寸 | -| className | `string` | `''` | 自定义类名 | -| style | `React.CSSProperties` | `{}` | 自定义样式 | -| disabled | `boolean` | `false` | 是否禁用整个组件 | - -### BubbleOption - -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| id | `string \| number` | - | 选项的唯一标识 | -| label | `string` | - | 显示的文本 | -| value | `string \| number` | - | 选项的值 | -| disabled | `boolean` | `false` | 是否禁用 | -| icon | `React.ReactNode` | - | 可选的图标 | -| description | `string` | - | 可选的描述文本 | - -## 布局说明 - -### 水平布局 (horizontal) -- 适合选项较少的情况 -- 自动换行,响应式设计 -- 适合顶部导航、标签选择等 - -### 垂直布局 (vertical) -- 适合选项较多的情况 -- 每个选项占满一行 -- 适合侧边栏、设置页面等 - -### 网格布局 (grid) -- 适合选项较多且需要整齐排列的情况 -- 可自定义列数 -- 适合分类选择、时间选择等 - -## 尺寸说明 - -### 小尺寸 (small) -- 适合紧凑的界面 -- 适合移动端或空间受限的场景 - -### 中尺寸 (medium) -- 默认尺寸,适合大多数场景 -- 平衡了可用性和美观性 - -### 大尺寸 (large) -- 适合需要突出显示的场景 -- 适合触摸设备或重要操作 - -## 样式定制 - -组件使用 SCSS 编写,可以通过以下方式自定义样式: - -1. 覆盖 CSS 变量 -2. 使用 `className` 和 `style` 属性 -3. 修改 SCSS 源文件 - -## 注意事项 - -- 在网格布局中,`columns` 属性控制列数,行数会根据选项数量自动计算 -- 多选模式下,`value` 应该是数组类型 -- 单选模式下,`value` 可以是字符串或数字类型 -- 组件会自动处理选中状态的样式变化 -- 支持图标和描述,让选项更加丰富 -- 响应式设计,自动适应不同屏幕尺寸 diff --git a/src/components/Bubble/USAGE.md b/src/components/Bubble/USAGE.md deleted file mode 100644 index afd455f..0000000 --- a/src/components/Bubble/USAGE.md +++ /dev/null @@ -1,211 +0,0 @@ -# 如何使用 Bubble 通用气泡组件 - -## 在其他组件中导入 - -```tsx -import Bubble, { BubbleOption } from '@/components/Bubble'; -``` - -## 基本使用示例 - -### 1. 室内外选择(如UI图所示) - -```tsx -import React, { useState } from 'react'; -import Bubble, { BubbleOption } from '@/components/Bubble'; - -const MyPage: React.FC = () => { - const [selectedLocation, setSelectedLocation] = useState(''); - - const locationOptions: BubbleOption[] = [ - { id: 1, label: '室内', value: 'indoor' }, - { id: 2, label: '室外', value: 'outdoor' }, - { id: 3, label: '半室外', value: 'semi-outdoor' } - ]; - - return ( -
-

选择场地类型

- setSelectedLocation(value as string)} - layout="horizontal" - size="medium" - /> -

您选择的场地类型: {selectedLocation}

-
- ); -}; -``` - -### 2. 时间选择器 - -```tsx -const [selectedTime, setSelectedTime] = useState(''); - -const timeOptions: BubbleOption[] = [ - { id: 1, label: '晨间 6:00-10:00', value: 'morning' }, - { id: 2, label: '上午 10:00-12:00', value: 'forenoon' }, - { id: 3, label: '中午 12:00-14:00', value: 'noon' }, - { id: 4, label: '下午 14:00-18:00', value: 'afternoon' }, - { id: 5, label: '晚上 18:00-22:00', value: 'evening' }, - { id: 6, label: '夜间 22:00-24:00', value: 'night' } -]; - - setSelectedTime(value as string)} - layout="grid" - columns={3} - size="medium" -/> -``` - -### 3. 多选模式 - -```tsx -const [selectedValues, setSelectedValues] = useState([]); - -const options: BubbleOption[] = [ - { id: 1, label: '运动', value: 'sports' }, - { id: 2, label: '音乐', value: 'music' }, - { id: 3, label: '阅读', value: 'reading' }, - { id: 4, label: '旅行', value: 'travel' } -]; - - setSelectedValues(value as string[])} - multiple={true} - layout="grid" - columns={2} - size="medium" -/> -``` - -### 4. 带图标和描述 - -```tsx -const [selectedSport, setSelectedSport] = useState(''); - -const sportOptions: BubbleOption[] = [ - { - id: 1, - label: '网球', - value: 'tennis', - icon: '🎾', - description: '室内外均可' - }, - { - id: 2, - label: '篮球', - value: 'basketball', - icon: '🏀', - description: '室内场地' - } -]; - - setSelectedSport(value as string)} - layout="vertical" - size="large" -/> -``` - -### 5. 不同布局方式 - -```tsx -// 水平布局 - 适合选项较少 - - -// 垂直布局 - 适合选项较多 - - -// 网格布局 - 适合需要整齐排列 - -``` - -### 6. 不同尺寸 - -```tsx -// 小尺寸 - 适合紧凑界面 - - -// 中尺寸 - 默认尺寸 - - -// 大尺寸 - 适合触摸设备 - -``` - -## 组件特性 - -- **通用性**: 不局限于特定功能,可用于任何选择场景 -- **灵活布局**: 支持水平、垂直、网格三种布局方式 -- **多尺寸支持**: 小、中、大三种尺寸适应不同场景 -- **丰富内容**: 支持图标和描述,让选项更加丰富 -- **响应式设计**: 自动适应不同屏幕尺寸 -- **状态管理**: 内置选中状态管理,支持单选和多选 -- **类型安全**: 完整的 TypeScript 类型定义 -- **可访问性**: 支持键盘导航和屏幕阅读器 - -## 常见使用场景 - -1. **场地选择**: 室内/室外/半室外 -2. **时间选择**: 时间段、日期范围 -3. **分类选择**: 兴趣爱好、技能标签 -4. **设置选项**: 主题、语言、通知设置 -5. **筛选条件**: 价格范围、评分、距离等 -6. **导航菜单**: 顶部导航、侧边栏菜单 - -## 注意事项 - -1. 确保传入的 `options` 数组不为空 -2. 多选模式下,`value` 应该是数组类型 -3. 单选模式下,`value` 可以是字符串或数字类型 -4. 组件会自动处理选中状态的样式变化 -5. 支持图标和描述,让选项更加丰富 -6. 响应式设计,自动适应不同屏幕尺寸 -7. 可以通过 `disabled` 属性禁用整个组件或单个选项 diff --git a/src/components/Bubble/example.tsx b/src/components/Bubble/example.tsx deleted file mode 100644 index 0ebcdf8..0000000 --- a/src/components/Bubble/example.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import React, { useState } from 'react'; -import Bubble, { BubbleOption } from './index'; - -// 室内外选择示例(如UI图所示) -export const LocationSelector: React.FC = () => { - const [selectedLocation, setSelectedLocation] = useState(''); - - const locationOptions: BubbleOption[] = [ - { id: 1, label: '室内', value: 'indoor' }, - { id: 2, label: '室外', value: 'outdoor' }, - { id: 3, label: '半室外', value: 'semi-outdoor' } - ]; - - return ( -
-

选择场地类型

- setSelectedLocation(value as string)} - layout="horizontal" - size="small" - /> -

当前选择: {selectedLocation || '未选择'}

-
- ); -}; - -// 时间选择器示例 -export const TimeSelector: React.FC = () => { - const [selectedTime, setSelectedTime] = useState(''); - - const timeOptions: BubbleOption[] = [ - { id: 1, label: '晨间 6:00-10:00', value: 'morning' }, - { id: 2, label: '上午 10:00-12:00', value: 'forenoon' }, - { id: 3, label: '中午 12:00-14:00', value: 'noon' }, - { id: 4, label: '下午 14:00-18:00', value: 'afternoon' }, - { id: 5, label: '晚上 18:00-22:00', value: 'evening' }, - { id: 6, label: '夜间 22:00-24:00', value: 'night' } - ]; - - return ( -
-

选择时间段

- setSelectedTime(value as string)} - layout="grid" - size="small" - columns={3} - /> -

当前选择: {selectedTime || '未选择'}

-
-

选择时间段

- setSelectedTime(value as string)} - layout="grid" - columns={2} - size="small" - /> -

当前选择: {selectedTime || '未选择'}

-
- ); -}; - -// 多选示例 -export const MultiSelectExample: React.FC = () => { - const [selectedValues, setSelectedValues] = useState([]); - - const options: BubbleOption[] = [ - { id: 1, label: '运动', value: 'sports' }, - { id: 2, label: '音乐', value: 'music' }, - { id: 3, label: '阅读', value: 'reading' }, - { id: 4, label: '旅行', value: 'travel' }, - { id: 5, label: '美食', value: 'food' }, - { id: 6, label: '摄影', value: 'photography' } - ]; - - return ( -
-

多选示例 - 兴趣爱好

- setSelectedValues(value as string[])} - multiple={true} - layout="grid" - columns={3} - size="medium" - /> -

当前选择: {selectedValues.join(', ') || '未选择'}

-
- ); -}; - -// 带图标和描述的示例 -export const IconExample: React.FC = () => { - const [selectedValue, setSelectedValue] = useState(''); - - const options: BubbleOption[] = [ - { - id: 1, - label: '网球', - value: 'tennis', - icon: '🎾', - description: '室内外均可' - }, - { - id: 2, - label: '篮球', - value: 'basketball', - icon: '🏀', - description: '室内场地' - }, - { - id: 3, - label: '足球', - value: 'football', - icon: '⚽', - description: '室外场地' - } - ]; - - return ( -
-

带图标和描述的运动选择

- setSelectedValue(value as string)} - layout="vertical" - size="large" - /> -

当前选择: {selectedValue || '未选择'}

-
- ); -}; - -// 带禁用状态的示例 -export const DisabledExample: React.FC = () => { - const [selectedValue, setSelectedValue] = useState(''); - - const options: BubbleOption[] = [ - { id: 1, label: '可用选项1', value: 'option1' }, - { id: 2, label: '禁用选项2', value: 'option2', disabled: true }, - { id: 3, label: '可用选项3', value: 'option3' }, - { id: 4, label: '禁用选项4', value: 'option4', disabled: true } - ]; - - return ( -
-

带禁用状态的示例

- setSelectedValue(value as string)} - layout="grid" - columns={2} - size="medium" - /> -

当前选择: {selectedValue || '未选择'}

-
- ); -}; - -// 不同尺寸的示例 -export const SizeExample: React.FC = () => { - const [selectedSize, setSelectedSize] = useState(''); - - const sizeOptions: BubbleOption[] = [ - { id: 1, label: '小尺寸', value: 'small' }, - { id: 2, label: '中尺寸', value: 'medium' }, - { id: 3, label: '大尺寸', value: 'large' } - ]; - - return ( -
-

不同尺寸的示例

- -

小尺寸

- setSelectedSize(value as string)} - layout="horizontal" - size="small" - /> - -

中尺寸

- setSelectedSize(value as string)} - layout="horizontal" - size="medium" - /> - -

大尺寸

- setSelectedSize(value as string)} - layout="horizontal" - size="large" - /> - -

当前选择: {selectedSize || '未选择'}

-
- ); -}; - -// 主示例组件 -export const BubbleExamples: React.FC = () => { - return ( -
-

Bubble 通用气泡组件示例

- -
- -
- -
- -
- -
- -
- ); -}; - -export default BubbleExamples; diff --git a/src/components/Bubble/index.tsx b/src/components/Bubble/index.tsx index 264de4e..30dee30 100644 --- a/src/components/Bubble/index.tsx +++ b/src/components/Bubble/index.tsx @@ -1,32 +1,7 @@ import React, { useState, useEffect } from "react"; import styles from "./index.module.scss"; import BubbleItem from "./BubbleItem"; - -export interface BubbleOption { - id: string | number; - label: string; - value: string | number; - disabled?: boolean; - icon?: React.ReactNode; - description?: string; -} - -export interface BubbleProps { - options: BubbleOption[]; - value?: string | number | (string | number)[]; - onChange?: ( - value: string | number | (string | number)[], - option: BubbleOption | BubbleOption[] - ) => void; - multiple?: boolean; - layout?: "horizontal" | "vertical" | "grid"; - columns?: number; - size?: "small" | "medium" | "large"; - className?: string; - itemClassName?: string; - style?: React.CSSProperties; - disabled?: boolean; -} +import {BubbleOption, BubbleProps} from '../../../types/list/types' const Bubble: React.FC = ({ options, @@ -40,6 +15,7 @@ const Bubble: React.FC = ({ itemClassName = "", style = {}, disabled = false, + name, }) => { const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]); @@ -74,9 +50,10 @@ const Bubble: React.FC = ({ const selectedOptions = options.filter((opt) => newSelectedValues.includes(opt.value) ); - onChange(newSelectedValues, selectedOptions); + onChange(name, newSelectedValues, selectedOptions); } else { - onChange(option.value, option); + console.log('===111', name, option.value) + onChange(name, option.value, option); } } }; diff --git a/src/components/CityFilter/example.tsx b/src/components/CityFilter/example.tsx deleted file mode 100644 index 022297a..0000000 --- a/src/components/CityFilter/example.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useState } from "react"; -import MenuComponent from "./index"; - -export default function Example() { - const [value, setValue] = useState("a"); - const options = [ - { text: "默认排序", value: "a" }, - { text: "好评排序", value: "b" }, - { text: "销量排序", value: "c" }, - ]; - return ( - setValue(val)} - /> - ); -} diff --git a/src/components/CityFilter/index.module.scss b/src/components/CityFilter/index.module.scss index aa8442f..dca674d 100644 --- a/src/components/CityFilter/index.module.scss +++ b/src/components/CityFilter/index.module.scss @@ -1,35 +1,53 @@ .menuWrap { padding: 5px 20px 10px; + $height: 26px; + + .menuIcon { + width: 16px; + height: 16px; + + &.rotate { + transform: rotate(180deg); + } + } + .menuItem { + width: 100vw; left: 0; border-bottom-left-radius: 30px; border-bottom-right-radius: 30px; } + + .menuItem { + position: fixed; + } + &.active { - .nut-menu-bar { + :global(.nut-menu-bar) { background-color: #000000; color: #ffffff; } } + :global(.nut-menu-bar) { color: #000000; - line-height: 1; + width: 66px; + height: $height; + border-radius: $height; + line-height: $height; box-shadow: unset; - min-height: 28px; - min-width: 80px; - border-radius: 28px; border: 1px solid #e5e5e5; - line-height: 28px; font-size: 14px; - width: max-content; - .nut-menu-title { - color: inherit !important; - font-weight: 600; - } + font-weight: 600; + } - .nut-menu-title-text { - padding-left: 0; - } + :global(.nut-menu-title) { + color: inherit !important; + font-weight: 600; + } + + :global(.nut-menu-title-text) { + --nutui-menu-title-padding: 0 6px 0 0; } .positionWrap { @@ -48,13 +66,15 @@ .cityName { font-size: 13px; font-weight: 400; - color: #3C3C43; + color: #3c3c43; } + .distanceWrap { margin-bottom: 16px; width: 100%; } + .distanceBubbleItem { width: auto; } -} +} \ No newline at end of file diff --git a/src/components/CityFilter/index.tsx b/src/components/CityFilter/index.tsx index db40f86..9784915 100644 --- a/src/components/CityFilter/index.tsx +++ b/src/components/CityFilter/index.tsx @@ -1,33 +1,33 @@ import { Menu } from "@nutui/nutui-react-taro"; import styles from "./index.module.scss"; import { useState, useRef } from "react"; -import Bubble, { BubbleOption } from "../Bubble"; +import Bubble from "../Bubble"; +import { Image } from "@tarojs/components"; +import img from "../../config/images"; +import {DistanceFilterProps} from '../../../types/list/types' -interface IProps { - options: BubbleOption[]; - value: string; - onChange: (value: string) => void; - wrapperClassName?: string; - itemClassName?: string; -} -const MenuComponent = (props: IProps) => { - const { value, onChange, wrapperClassName, itemClassName } = props; + +const MenuComponent = (props: DistanceFilterProps) => { + const { value, onChange, wrapperClassName, itemClassName, options, name } = + props; const [isChange, setIsChange] = useState(false); + const [iOpen, setIsOpen] = useState(false); const itemRef = useRef(null); - const handleChange = (value: string) => { - console.log("===value", value); + const handleChange = (name: string, value: string) => { setIsChange(true); - onChange && onChange(value); + onChange && onChange(name, value); + (itemRef.current as any)?.toggle(false); }; - const options: BubbleOption[] = [ - { id: 0, label: "全城", value: "0" }, - { id: 1, label: "3km", value: "3" }, - { id: 2, label: "5km", value: "5" }, - { id: 3, label: "10km", value: "10" }, - ]; + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); + }; return ( { isChange ? styles.active : "" }`} activeColor="#000" + onOpen={handleOpen} + onClose={handleClose} + icon={ + + } > + } >

当前位置

@@ -54,6 +68,7 @@ const MenuComponent = (props: IProps) => { size="small" columns={4} itemClassName={styles.distanceBubbleItem} + name={name} />
diff --git a/src/components/CommonPopup/CommonPopup.tsx b/src/components/CommonPopup/CommonPopup.tsx index a8dbeef..26b0a03 100644 --- a/src/components/CommonPopup/CommonPopup.tsx +++ b/src/components/CommonPopup/CommonPopup.tsx @@ -18,6 +18,7 @@ export interface CommonPopupProps { zIndex?: number children?: React.ReactNode className?: string + style?: React.CSSProperties } const CommonPopup: React.FC = ({ @@ -34,6 +35,7 @@ const CommonPopup: React.FC = ({ position = 'bottom', round = true, zIndex, + style, children }) => { const handleCancel = () => { @@ -52,7 +54,7 @@ const CommonPopup: React.FC = ({ closeable={false} onClose={onClose} className={`${styles['common-popup']} ${className ? className : ''}`} - style={zIndex ? { zIndex } : undefined} + style={{ zIndex: zIndex ? zIndex : undefined, ...style }} > {showHeader && ( @@ -78,4 +80,4 @@ const CommonPopup: React.FC = ({ ) } -export default CommonPopup \ No newline at end of file +export default CommonPopup \ No newline at end of file diff --git a/src/components/CourtType/index.module.scss b/src/components/CourtType/index.module.scss new file mode 100644 index 0000000..f7a1246 --- /dev/null +++ b/src/components/CourtType/index.module.scss @@ -0,0 +1,3 @@ +.courtTypeWrapper { + margin-bottom: 18px; +} \ No newline at end of file diff --git a/src/components/CourtType/index.tsx b/src/components/CourtType/index.tsx new file mode 100644 index 0000000..7f8b163 --- /dev/null +++ b/src/components/CourtType/index.tsx @@ -0,0 +1,31 @@ +import { View, Image } from "@tarojs/components"; +import TitleComponent from "@/components/Title"; +import img from "@/config/images"; +import Bubble from "../Bubble"; +import { BubbleOption } from "types/list/types"; +import styles from './index.module.scss' + +interface IProps { + name: string; + options: BubbleOption[]; + value: string; + onChange: (name: string, value: string) => void; +} +const GamePlayType = (props: IProps) => { + const { name, onChange , options, value} = props; + return ( + + } /> + + + ); +}; +export default GamePlayType; diff --git a/src/components/CustomNavbar/index.module.scss b/src/components/CustomNavbar/index.module.scss new file mode 100644 index 0000000..9c921b2 --- /dev/null +++ b/src/components/CustomNavbar/index.module.scss @@ -0,0 +1,50 @@ +.customerNavbar { + position: sticky; + top: 0; + z-index: 999; + background-color: #ffffff; + + .container { + padding-left: 17px; + display: flex; + align-items: center; + gap: 8px; + } + + .line { + width: 1px; + height: 25px; + background-color: #0000000F; + } + + .logo { + width: 60px; + height: 34px; + } + + .change { + width: 12px; + height: 12px; + } + + .cityWrapper { + line-height: 20px; + } + + .city { + font-weight: 600; + font-size: 13px; + line-height: 20px; + } + + .infoWrapper { + line-height: 12px; + } + + .info { + font-weight: 400; + font-size: 10px; + line-height: 12px; + color: #3C3C4399; + } +} \ No newline at end of file diff --git a/src/components/CustomNavbar/index.tsx b/src/components/CustomNavbar/index.tsx new file mode 100644 index 0000000..45f96fb --- /dev/null +++ b/src/components/CustomNavbar/index.tsx @@ -0,0 +1,73 @@ +import { View, Text, Image } from "@tarojs/components"; +import img from "@/config/images"; +import { getCurrentLocation } from "@/utils/locationUtils"; +import styles from "./index.module.scss"; +import { useEffect } from "react"; +import { useGlobalState } from "@/store/global"; +import { useListState } from "@/store/listStore"; + +const ListHeader = () => { + const { + updateState, + location, + getLocationText, + getLocationLoading, + statusNavbarHeightInfo, + } = useGlobalState(); + const { gamesNum } = useListState(); + console.log("===statusNavbarHeightInfo", statusNavbarHeightInfo); + const { statusBarHeight, navbarHeight, totalHeight } = statusNavbarHeightInfo; + + // 获取位置信息 + const getCurrentLocal = () => { + updateState({ + getLocationLoading: true, + }); + getCurrentLocation().then((res) => { + updateState({ + getLocationLoading: false, + location: res || {}, + }); + }); + }; + useEffect(() => { + // getNavbarHeightInfo(); + getCurrentLocal(); + }, []); + + const currentAddress = getLocationLoading + ? getLocationText + : location?.address; + + return ( + + + {/* logo */} + + + + + {/* 位置 */} + {currentAddress} + {!getLocationLoading && ( + + )} + + + 附近${gamesNum}场球局 + + + + + ); +}; +export default ListHeader; diff --git a/src/components/GamePlayType/index.module.scss b/src/components/GamePlayType/index.module.scss new file mode 100644 index 0000000..2eebf1e --- /dev/null +++ b/src/components/GamePlayType/index.module.scss @@ -0,0 +1,3 @@ +.gamePlayWrapper { + margin-bottom: 18px; +} \ No newline at end of file diff --git a/src/components/GamePlayType/index.tsx b/src/components/GamePlayType/index.tsx new file mode 100644 index 0000000..7a70de0 --- /dev/null +++ b/src/components/GamePlayType/index.tsx @@ -0,0 +1,47 @@ +// import PopupGameplay from "../../pages/publishBall/components/PopupGameplay"; +import { View, Image } from "@tarojs/components"; +import TitleComponent from "@/components/Title"; +import img from "@/config/images"; +import Bubble from "../Bubble"; +import styles from "./index.module.scss"; +import { BubbleOption } from "types/list/types"; +interface IProps { + name: string; + value: string; + options: BubbleOption[]; + onChange: (name: string, value: string) => void; +} +const GamePlayType = (props: IProps) => { + const { name, onChange, value, options } = props; + return ( + + } /> + + {/* { + console.log("onClose"); + }} + onConfirm={() => { + console.log("onConfirm"); + }} + visible={false} + options={[ + { label: "不限", value: "不限" }, + { label: "单打", value: "单打" }, + { label: "双打", value: "双打" }, + { label: "娱乐", value: "娱乐" }, + { label: "拉球", value: "拉球" }, + ]} + /> */} + + ); +}; +export default GamePlayType; diff --git a/src/components/GuideBar/index.scss b/src/components/GuideBar/index.scss new file mode 100644 index 0000000..7cd40b1 --- /dev/null +++ b/src/components/GuideBar/index.scss @@ -0,0 +1,90 @@ +@use '~@/scss/images.scss' as img; + +.guide-bar-container { + padding-top: calc(60px + 20px + env(safe-area-inset-bottom)); +} + +.guide-bar { + position: fixed; + bottom: 0; + width: 100%; + height: calc(60px + 20px + env(safe-area-inset-bottom)); + box-sizing: border-box; + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 12px env(safe-area-inset-bottom); + z-index: 999; + + &-pages { + display: flex; + justify-content: space-between; + align-items: center; + display: inline-flex; + width: 240px; + height: 60px; + padding: 8px 6px; + box-sizing: border-box; + flex-shrink: 0; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.20); + background: rgba(255, 255, 255, 0.40); + box-shadow: 0 4px 64px 0 rgba(0, 0, 0, 0.16); + backdrop-filter: blur(16px); + + &-item { + display: flex; + width: 76px; + height: 48px; + // padding: 14px 0; + box-sizing: border-box; + justify-content: center; + align-items: center; + gap: 10px; + color: rgba(60, 60, 67, 0.60); + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 125% */ + } + + &-item-active { + display: flex; + width: 76px; + height: 48px; + // padding: 14px 22px; + box-sizing: border-box; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 999px; + border: 0.5px solid rgba(0, 0, 0, 0.06); + background: #FFF; + box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08); + color: #000; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 125% */ + } + } + + &-publish { + display: flex; + width: 60px; + height: 60px; + justify-content: center; + align-items: center; + flex-shrink: 0; + border-radius: 999px; + // border: 2px solid rgba(0, 0, 0, 0.06); + background: radial-gradient(75.92% 98.69% at 26.67% 8.33%, #BDFF4A 16.88%, #95F23E 54.19%, #32D838 100%); + box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08); + backdrop-filter: blur(16px); + + &-icon { + width: 36px; + height: 36px; + } + } +} \ No newline at end of file diff --git a/src/components/GuideBar/index.tsx b/src/components/GuideBar/index.tsx new file mode 100644 index 0000000..d616759 --- /dev/null +++ b/src/components/GuideBar/index.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react' +import { View, Text, Image } from '@tarojs/components' +import Taro from '@tarojs/taro' +import img from '@/config/images' +import './index.scss' + +export type currentPageType = 'games' | 'message' | 'personal' + +const GuideBar = (props) => { + const { currentPage } = props + + const guideItems = [ + { + code: 'list', + text: '球局', + }, + { + code: 'message', + text: '消息', + }, + { + code: 'personal', + text: '我的', + }, + ] + + const handlePublish = () => { + Taro.navigateTo({ + url: '/pages/publishBall/index', + }) + } + + const handlePageChange = (code: string) => { + if (code === currentPage) { + return + } + Taro.navigateTo({ + url: `/pages/${code}/index`, + }).then(() => { + Taro.pageScrollTo({ + scrollTop: 0, + duration: 300, + }) + }) + } + + return ( + + + {/* guide area on the left */} + + {guideItems.map((item) => ( + handlePageChange(item.code)} + > + {item.text} + + ))} + + {/* publish button on the right */} + + + + + + ) +} + +export default GuideBar \ No newline at end of file diff --git a/src/components/List/index.scss b/src/components/List/index.scss index 391a9aa..5742c9e 100644 --- a/src/components/List/index.scss +++ b/src/components/List/index.scss @@ -1,6 +1,5 @@ .list { background: #fafafa; - margin-top: 12px; display: flex; flex-direction: column; gap: 5px; diff --git a/src/components/ListCard/index.scss b/src/components/ListCard/index.scss new file mode 100644 index 0000000..bcb39aa --- /dev/null +++ b/src/components/ListCard/index.scss @@ -0,0 +1,295 @@ +.listCard { + background: linear-gradient(90deg, rgba(183, 248, 113, 0.5) 0%, rgba(183, 248, 113, 0.1) 100%); + border-radius: 20px; + border-width: 0.5px; +} + +.listItem { + display: flex; + padding: 12px 15px; + background: #ffffff; + border-radius: 20px; + border: 0.5px solid #f0f0f0; + justify-content: space-between; +} + +.content { + flex: 1; + display: flex; + flex-direction: column; + width: calc(100% - 122px); +} + +.titleWrapper { + display: flex; + align-items: center; +} + +.title { + font-size: 16px; + font-weight: 600; + color: #000000; + line-height: 24px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.title-right-arrow { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.location { + display: flex; + align-items: center; + font-weight: 400; + font-size: 12px; + line-height: 18px; + color: #3C3C4399; +} + +.location-position { + max-width: 66%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.location-text { + display: block; +} + +.date-time { + font-size: 12px; + color: #3C3C4399; + font-weight: 400; + line-height: 18px; + margin-top: 6px; + margin-bottom: 4px; +} + +.bottom-info { + display: flex; + align-items: center; + margin-top: 4px; + column-gap: 4px; +} + +.left-section { + display: flex; + align-items: center; + gap: 8px; +} + +.avatar-group { + display: flex; + align-items: center; +} + +.avatar { + width: 20px; + height: 20px; + border-radius: 50%; + background: #e0e0e0; + border: 2px solid #ffffff; + margin-left: -8px; + overflow: hidden; + box-sizing: border-box; + + .avatar-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + &:first-child { + margin-left: 0; + z-index: 3; + } + + &:nth-child(2) { + z-index: 2; + } + + &:nth-child(3) { + z-index: 1; + } +} + +.registration-text { + font-size: 12px; + color: #999999; +} + +.tags { + display: flex; + gap: 4px; +} + +.tag { + box-sizing: border-box; + padding: 0 6px; + border: 0.5px solid #00000029; + height: 20px; + border-radius: 20px; + min-width: 38px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: #000000; + font-size: 11px; +} + +.tag-text-max { + color: #666666; +} + +.image-section { + width: 100px; + height: 100px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-basis: 100px; + flex-grow: 0; + flex-shrink: 0; + + .image-container { + width: 100%; + height: 100%; + border: 1.5px solid #ffffff; + border-radius: 10px; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); + overflow: hidden; + position: absolute; + box-sizing: border-box; + + .image { + border-radius: 10px; + } + } +} + +.single-image { + position: relative; + width: 88px; + height: 88px; + + .image-container { + width: 88px; + height: 88px; + transform: rotate(-10deg); + } +} + +.double-image { + width: 100%; + height: 100%; + position: relative; + + .image-container { + width: 60%; + height: 60%; + position: absolute; + overflow: hidden; + top: 20%; + + &:first-child { + z-index: 2; + transform: translateX(4px) rotate(-10deg); + } + + &:last-child { + right: 0; + z-index: 1; + transform: translateX(-4px) rotate(10deg); + } + } +} + +.triple-image { + width: 100%; + height: 100%; + position: relative; + + .image-container { + position: absolute; + overflow: hidden; + + &:nth-child(1) { + bottom: 0; + left: 0; + width: 55px; + height: 55px; + z-index: 3; + transform: translateX(4px) rotate(-10deg); + } + + &:nth-child(2) { + bottom: 10px; + right: 0; + width: 55px; + height: 55px; + z-index: 2; + transform: rotate(3deg); + } + + &:nth-child(3) { + top: 5%; + left: 50%; + width: 100rpx; + height: 100rpx; + z-index: 1; + transform: translateX(-50%); + } + } +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; +} + +// 底部 +.smoothPlayingGame { + padding: 5px 12px; + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + + .smoothWrapper, + .localAreaWrapper { + line-height: 18px; + display: flex; + align-items: center; + gap: 5px; + } + + .smoothTitle { + font-size: 14px; + } + + .line { + height: 8px; + width: 1px; + background: #00000040; + border-radius: 99px; + } + + .iconListPlayingGame, + .localArea { + width: 14px; + height: 14px; + } + + .localArea { + border: 0.5px solid #FFFFFFA6; + border-radius: 50%; + } +} \ No newline at end of file diff --git a/src/components/ListCard/index.tsx b/src/components/ListCard/index.tsx new file mode 100644 index 0000000..df80400 --- /dev/null +++ b/src/components/ListCard/index.tsx @@ -0,0 +1,146 @@ +import { View, Text, Image } from "@tarojs/components"; +import Taro from "@tarojs/taro"; +import img from "../../config/images"; +import { ListCardProps } from "../../../types/list/types"; +import "./index.scss"; + +const ListCard: React.FC = ({ + id, + title, + dateTime, + location, + distance, + registeredCount, + maxCount, + skillLevel, + matchType, + images = [], + shinei, +}) => { + const renderItemImage = (src: string) => { + return ; + }; + + const handleViewDetail = () => { + Taro.navigateTo({ + url: `/pages/detail/index?id=${id || 1}&from=list&autoShare=0`, + }); + }; + + // 根据图片数量决定展示样式 + const renderImages = () => { + if (images?.length === 0) return null; + + if (images?.length === 1) { + return ( + + {renderItemImage(images[0])} + + ); + } + + if (images?.length === 2) { + return ( + + {renderItemImage(images[0])} + {renderItemImage(images[1])} + + ); + } + + // 3张或更多图片 + return ( + + {renderItemImage(images?.[0])} + {renderItemImage(images?.[1])} + {renderItemImage(images?.[2])} + + ); + }; + return ( + + + {/* 左侧内容区域 */} + + {/* 标题 */} + + {title} + + + + {/* 时间信息 */} + + + {dateTime} + + + {/* 地点,室内外,距离 */} + + + {location} + + {shinei && `・${shinei}`} + {distance && `・${distance}`} + + + + {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} + + + + {Array.from({ length: Math.min(registeredCount, 3) }).map( + (_, index) => ( + + + + ) + )} + + + + + + + 报名人数 {registeredCount}/ + {maxCount} + + + + {skillLevel} + + + {matchType} + + + + + + {/* 右侧图片区域 */} + {renderImages()} + + {/* 畅打球局 */} + + + + 畅打球局 + + + 场馆方: + + + 仁恒河滨花园网球场 + + + + ); +}; + +export default ListCard; diff --git a/src/components/ListItem/index.scss b/src/components/ListCardSkeleton/index.scss similarity index 65% rename from src/components/ListItem/index.scss rename to src/components/ListCardSkeleton/index.scss index ded1507..e642055 100644 --- a/src/components/ListItem/index.scss +++ b/src/components/ListCardSkeleton/index.scss @@ -1,35 +1,69 @@ .list-item { display: flex; - padding: 16px; + padding: 12px 15px; background: #ffffff; border-radius: 20px; border: 0.5px solid #f0f0f0; + justify-content: space-between; + --nutui-skeleton-line-height: 24px; + --nutui-skeleton-line-border-radius: 24px; + + .nut-skeleton-block { + margin: 0; + } } .content { flex: 1; display: flex; flex-direction: column; - gap: 8px; + width: calc(100% - 122px); +} + +.titleWrapper { + display: flex; + align-items: center; } .title { font-size: 16px; font-weight: 600; - color: #333333; - line-height: 1.4; + color: #000000; + line-height: 24px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.date-time { - font-size: 14px; - color: #666666; - line-height: 1.3; +.title-right-arrow { + width: 16px; + height: 16px; + flex-shrink: 0; } .location { - font-size: 14px; - color: #666666; - line-height: 1.3; + display: flex; + align-items: center; +} + +.location-position { + max-width: 66%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.location-text { + display: block; +} + +.date-time { + font-size: 12px; + color: #3C3C4399; + font-weight: 400; + line-height: 18px; + margin-top: 6px; + margin-bottom: 4px; } .bottom-info { @@ -58,6 +92,8 @@ border: 2px solid #ffffff; margin-left: -8px; overflow: hidden; + box-sizing: border-box; + .avatar-image { width: 100%; height: 100%; @@ -89,8 +125,10 @@ } .tag { - padding: 3px; - border: 1px solid #f5f5f5; + box-sizing: border-box; + padding: 0 6px; + border: 0.5px solid #00000029; + height: 20px; border-radius: 20px; min-width: 38px; display: flex; @@ -98,7 +136,7 @@ justify-content: center; flex-shrink: 0; color: #000000; - font-size: 12px; + font-size: 11px; } .tag-text-max { @@ -112,16 +150,32 @@ display: flex; align-items: center; justify-content: center; + flex-basis: 100px; + flex-grow: 0; + flex-shrink: 0; .image-container { width: 100%; height: 100%; - padding: 2px; - background: #ffffff; + border: 1.5px solid #ffffff; border-radius: 10px; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); overflow: hidden; position: absolute; + box-sizing: border-box; + + .nut-skeleton, + .nut-skeleton-content, + .nut-skeleton-block { + width: 100%; + height: 100%; + } + + .nut-skeleton-block { + margin: 0; + border-radius: unset; + } + .image { border-radius: 10px; } @@ -132,7 +186,10 @@ position: relative; width: 88px; height: 88px; + .image-container { + width: 88px; + height: 88px; transform: rotate(-10deg); } } @@ -151,13 +208,13 @@ &:first-child { z-index: 2; - transform: rotate(-10deg); + transform: translateX(4px) rotate(-10deg); } &:last-child { right: 0; z-index: 1; - transform: rotate(10deg); + transform: translateX(-4px) rotate(10deg); } } } @@ -177,7 +234,7 @@ width: 55px; height: 55px; z-index: 3; - transform: rotate(-10deg); + transform: translateX(4px) rotate(-10deg); } &:nth-child(2) { @@ -204,4 +261,4 @@ width: 100%; height: 100%; object-fit: cover; -} +} \ No newline at end of file diff --git a/src/components/ListCardSkeleton/index.tsx b/src/components/ListCardSkeleton/index.tsx new file mode 100644 index 0000000..77441a0 --- /dev/null +++ b/src/components/ListCardSkeleton/index.tsx @@ -0,0 +1,56 @@ +import { View } from "@tarojs/components"; +import { Skeleton } from "@nutui/nutui-react-taro"; +import "./index.scss"; + +const ListCard = () => { + return ( + + {/* 左侧内容区域 */} + + {/* 标题 */} + + + + + {/* 时间信息 */} + + + + + {/* 地点,室内外,距离 */} + + + + + + {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} + + + + {Array.from({ length: 3 }).map((_, index) => ( + + + + ))} + + + + + + + + + + {/* 右侧图片区域 */} + + + + + + + + + ); +}; + +export default ListCard; diff --git a/src/components/ListItem/index.tsx b/src/components/ListItem/index.tsx deleted file mode 100644 index cc0f498..0000000 --- a/src/components/ListItem/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { View, Text, Image } from '@tarojs/components' -import './index.scss' - -interface ListItemProps { - title: string - dateTime: string - location: string - distance: string - registeredCount: number - maxCount: number - skillLevel: string - matchType: string - images: string[] -} - -const ListItem: React.FC = ({ - title, - dateTime, - location, - distance, - registeredCount, - maxCount, - skillLevel, - matchType, - images -}) => { - // 根据图片数量决定展示样式 - const renderImages = () => { - if (images.length === 0) return null - - if (images.length === 1) { - return ( - - - - - - ) - } - - if (images.length === 2) { - return ( - - - - - - - - - ) - } - - // 3张或更多图片 - return ( - - - - - - - - - - - - ) - } - - return ( - - {/* 左侧内容区域 */} - - {/* 标题 */} - {title} - - {/* 时间信息 */} - {dateTime} - - {/* 地点和距离 */} - {location}・{distance} - - {/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */} - - - - {Array.from({ length: Math.min(registeredCount, 3) }).map((_, index) => ( - - - - ))} - - - - - - - 报名人数 {registeredCount}/{maxCount} - - - - {skillLevel} - - - {matchType} - - - - - - {/* 右侧图片区域 */} - - {renderImages()} - - - ) -} - -export default ListItem \ No newline at end of file diff --git a/src/components/ListLoadError/index.module.scss b/src/components/ListLoadError/index.module.scss new file mode 100644 index 0000000..1629275 --- /dev/null +++ b/src/components/ListLoadError/index.module.scss @@ -0,0 +1,44 @@ +.listLoadError { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + + .listLoadErrorImg { + width: 154px; + height: 154px; + } + + .listLoadErrorText { + margin-top: 35px; + margin-bottom: 12px; + font-weight: 500; + font-style: Medium; + font-size: 14px; + line-height: 24px; + letter-spacing: 0px; + } + + .listLoadErrorBtn { + display: flex; + align-items: center; + justify-content: center; + width: 76px; + background: #00000008; + border: 0.5px solid #0000001F; + border-radius: 12px; + padding: 12px 0; + font-weight: 500; + font-style: Medium; + font-size: 14px; + line-height: 24px; + letter-spacing: 0px; + + } + + .reloadIcon { + width: 16px; + height: 16px; + } +} \ No newline at end of file diff --git a/src/components/ListLoadError/index.tsx b/src/components/ListLoadError/index.tsx new file mode 100644 index 0000000..420393d --- /dev/null +++ b/src/components/ListLoadError/index.tsx @@ -0,0 +1,24 @@ +import { Image, View, Text, Button } from "@tarojs/components"; +import styles from "./index.module.scss"; +import img from "@/config/images"; + +const ListLoadError = ({ reload }: { reload: () => void }) => { + const handleReload = () => { + reload && typeof reload === "function" && reload(); + }; + + return ( + + + 加载失败 + + + ); +}; +export default ListLoadError; diff --git a/src/components/Menu/example.tsx b/src/components/Menu/example.tsx deleted file mode 100644 index 022297a..0000000 --- a/src/components/Menu/example.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useState } from "react"; -import MenuComponent from "./index"; - -export default function Example() { - const [value, setValue] = useState("a"); - const options = [ - { text: "默认排序", value: "a" }, - { text: "好评排序", value: "b" }, - { text: "销量排序", value: "c" }, - ]; - return ( - setValue(val)} - /> - ); -} diff --git a/src/components/Menu/index.module.scss b/src/components/Menu/index.module.scss index 0867087..cf1df49 100644 --- a/src/components/Menu/index.module.scss +++ b/src/components/Menu/index.module.scss @@ -1,31 +1,46 @@ .menuWrap { + position: static; padding: 5px 20px 10px; + --nutui-menu-title-padding: 0 6px 0 0; + $height: 26px; + .menuItem { + width: 100vw; left: 0; border-bottom-left-radius: 30px; border-bottom-right-radius: 30px; } + + .menuIcon { + width: 16px; + height: 16px; + + &.rotate { + transform: rotate(180deg); + } + } + + .itemIcon { + width: 20px; + height: 20px; + } + &.active { :global(.nut-menu-bar) { background-color: #000000; color: #ffffff; } } + :global(.nut-menu-bar) { color: #000000; - line-height: 1; box-shadow: unset; - min-height: 28px; - min-width: 94px; - border-radius: 28px; + border-radius: $height; + line-height: $height; + height: $height; border: 1px solid #e5e5e5; - line-height: 28px; font-size: 14px; - width: max-content; - - .nut-menu-title-text { - padding-left: 0; - } + width: 94px; } :global(.nut-menu-title) { @@ -33,13 +48,18 @@ font-weight: 600; } + :global(.nut-menu-title-text) { + --nutui-menu-title-padding: 0 6px 0 4px; + } + :global(.nut-menu-container-item) { color: #3c3c43; font-weight: 600; font-size: 14px; } + :global(.nut-menu-container-item.active) { flex-direction: row-reverse; justify-content: space-between; } -} +} \ No newline at end of file diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 0f3d435..fd7b4db 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -1,34 +1,55 @@ import { Menu } from "@nutui/nutui-react-taro"; -import styles from "./index.module.scss"; import { useState } from "react"; +import { Image } from "@tarojs/components"; +import img from "../../config/images"; +import { MenuFilterProps } from "../../../types/list/types"; +import styles from "./index.module.scss"; -interface IProps { - options: { text: string; value: string }[]; - value: string; - onChange: (value: string) => void; - wrapperClassName?: string; - itemClassName?: string; -} - -const MenuComponent = (props: IProps) => { - const { options, value, onChange, wrapperClassName, itemClassName } = props; +const MenuComponent = (props: MenuFilterProps) => { + const { options, value, onChange, wrapperClassName, itemClassName, name } = + props; const [isChange, setIsChange] = useState(false); + const [isOpen, setIsOpen] = useState(false); - const handleChange = (value: string) => { + const handleChange = (val: Record) => { setIsChange(true); - onChange && onChange(value); + onChange && onChange(name, val.value); + }; + + const handleOpen = () => { + setIsOpen(true); + }; + + const handleClose = () => { + setIsOpen(false); }; return ( + } > + } /> ); diff --git a/src/components/Range/README.md b/src/components/Range/README.md deleted file mode 100644 index 5da1a01..0000000 --- a/src/components/Range/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# NtrpRange 范围选择器组件 - -基于NutUI Range组件的双滑块范围选择器,通过CSS样式覆盖完全匹配设计稿,支持自定义范围、步长和回调函数。 - -## 功能特性 - -- 🎯 双滑块设计,支持选择范围区间 -- 🎨 精准还原设计稿的视觉效果 -- 📱 响应式设计,支持移动端 -- 🎮 流畅的拖拽交互体验 -- ⚙️ 可配置的最小值、最大值和步长 -- 🔒 支持禁用状态 -- 📊 实时值变化回调 - -## 基本用法 - -```tsx -import NtrpRange from '@/components/Range'; - -const MyComponent = () => { - const [value, setValue] = useState<[number, number]>([2.0, 4.0]); - - return ( - - ); -}; -``` - -## 属性说明 - -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `min` | `number` | `2.0` | 最小值 | -| `max` | `number` | `4.0` | 最大值 | -| `step` | `number` | `0.5` | 步长 | -| `value` | `[number, number]` | `[min, max]` | 当前选择的范围值 | -| `onChange` | `(value: [number, number]) => void` | - | 值变化时的回调函数 | -| `disabled` | `boolean` | `false` | 是否禁用 | - -## 技术实现 - -- 基于NutUI Range组件,确保拖拽功能的可靠性 -- 通过CSS样式覆盖,完全匹配设计稿视觉效果 -- TypeScript + React Hooks -- 响应式设计,支持移动端 - -## 设计规范 - -组件严格按照设计稿实现,包含以下视觉元素: - -- **网球图标**: 黑色轮廓的网球图标 -- **标题**: "NTRP水平区间" 文字 -- **标签**: 左右两端的范围标签(如"2.0及以下"、"4.0及以上") -- **滑块轨道**: 圆角矩形容器,带有浅灰色边框和阴影 -- **滑块手柄**: 两个白色圆形手柄,带有黑色边框和阴影 -- **轨道填充**: 黑色填充条,显示当前选择的范围 -- **标记点**: 四个浅灰色圆点,均匀分布在轨道上 - -## 样式定制 - -组件使用 BEM 命名规范,可以通过 CSS 变量或覆盖样式来自定义外观: - -```scss -.ntrp-range { - // 自定义样式 - &__track { - background: #f5f5f5; - border-color: #d0d0d0; - } - - &__handle { - background: #007bff; - border-color: #0056b3; - } -} -``` - -## 注意事项 - -1. 确保 `min < max`,否则组件可能无法正常工作 -2. `step` 值应该能够整除 `max - min` 的差值 -3. 组件内部会确保左右滑块不会重叠,最小间距为 `step` 值 -4. 拖拽时会自动吸附到最近的步长值 - -## 示例 - -查看 `example.tsx` 文件获取更多使用示例。 diff --git a/src/components/Range/example.tsx b/src/components/Range/example.tsx deleted file mode 100644 index a9b65df..0000000 --- a/src/components/Range/example.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * @Author: juguohong juguohong@flashhold.com - * @Date: 2025-08-16 17:59:28 - * @LastEditors: juguohong juguohong@flashhold.com - * @LastEditTime: 2025-08-16 23:48:25 - * @FilePath: /mini-programs/src/components/Range/example.tsx - * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE - */ -import React, { useState } from 'react'; -import NtrpRange from './index'; - -const RangeExample: React.FC = () => { - const [ntrpRange, setNtrpRange] = useState<[number, number]>([2.0, 4.0]); - const [customRange, setCustomRange] = useState<[number, number]>([0, 100]); - - const handleNtrpChange = (value: [number, number]) => { - console.log('NTRP range changed:', value); - setNtrpRange(value); - }; - - const handleCustomChange = (value: [number, number]) => { - console.log('Custom range changed:', value); - setCustomRange(value); - }; - - return ( -
-

Range 组件示例

- -
-

NTRP 水平区间选择器

- -
- 当前选择范围: {ntrpRange[0]} - {ntrpRange[1]} -
-
- -
-

自定义范围选择器

- -
- 当前选择范围: {customRange[0]} - {customRange[1]} -
-
- -
-

禁用状态

- -
- 此选择器已被禁用 -
-
- -
-

测试说明

-
-

1. 点击并拖拽左右滑块手柄

-

2. 查看控制台日志确认拖拽事件

-

3. 观察滑块位置和值的实时变化

-

4. 检查调试信息显示

-
-
-
- ); -}; - -export default RangeExample; diff --git a/src/components/Range/index.tsx b/src/components/Range/index.tsx index a4a9ff2..7a16109 100644 --- a/src/components/Range/index.tsx +++ b/src/components/Range/index.tsx @@ -1,16 +1,19 @@ import React, { useState, useEffect, useMemo } from "react"; +import { View, Text, Image } from "@tarojs/components"; import { Range } from "@nutui/nutui-react-taro"; import styles from "./index.module.scss"; import TitleComponent from "../Title"; +import img from "../../config/images"; interface RangeProps { min?: number; max?: number; step?: number; value?: [number, number]; - onChange?: (value: [number, number]) => void; + onChange?: (name: string, value: [number, number]) => void; disabled?: boolean; className?: string; + name: string; showTitle?: boolean; } @@ -22,6 +25,7 @@ const NtrpRange: React.FC = ({ onChange, disabled = false, className, + name, showTitle = true, }) => { const [currentValue, setCurrentValue] = useState<[number, number]>(value); @@ -32,7 +36,7 @@ const NtrpRange: React.FC = ({ const handleChange = (val: [number, number]) => { setCurrentValue(val); - onChange?.(val); + onChange?.(name, val); }; const marks = useMemo(() => { @@ -52,18 +56,16 @@ const NtrpRange: React.FC = ({ }, [JSON.stringify(currentValue || []), min, max]); return ( -
- { showTitle && ( +
+ {showTitle && (
- {/*
-
icon
-

NTRP水平区间

-
*/} - + } + />

{rangContent}

)} -
@@ -73,8 +75,9 @@ const NtrpRange: React.FC = ({ min={min} max={max} step={step} - // value={currentValue} + value={currentValue} onEnd={handleChange} + onChange={handleChange} disabled={disabled} defaultValue={[min, max]} className={styles.rangeHandle} diff --git a/src/components/Range/simple-test.tsx b/src/components/Range/simple-test.tsx deleted file mode 100644 index f874642..0000000 --- a/src/components/Range/simple-test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React, { useState } from 'react'; -import NtrpRange from './index'; - -const SimpleTest: React.FC = () => { - const [value, setValue] = useState<[number, number]>([2.0, 4.0]); - - return ( -
-

简单测试

- -

当前值: {value[0]} - {value[1]}

-
- ); -}; - -export default SimpleTest; diff --git a/src/components/Range/style-test.tsx b/src/components/Range/style-test.tsx deleted file mode 100644 index 60134d0..0000000 --- a/src/components/Range/style-test.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useState } from 'react'; -import NtrpRange from './index'; - -const StyleTest: React.FC = () => { - const [value, setValue] = useState<[number, number]>([2.0, 4.0]); - - return ( -
-

NtrpRange 样式测试

- -
-

NTRP 水平区间选择器

- -
- 当前选择范围: {value[0]} - {value[1]} -
-
- -
-

自定义范围选择器

- console.log('Custom range:', val)} - /> -
- 固定范围: 20 - 80 -
-
- -
-

禁用状态

- -
- 此选择器已被禁用 -
-
- -
-

样式说明

-
-

✅ 网球图标 + "NTRP水平区间"标题

-

✅ 左右范围标签("2.0及以下"、"4.0及以上")

-

✅ 圆角矩形轨道容器,带有边框和阴影

-

✅ 白色圆形滑块手柄,黑色边框和阴影

-

✅ 黑色轨道填充条

-

✅ 五个浅灰色标记点

-
-
-
- ); -}; - -export default StyleTest; diff --git a/src/components/Range/test.tsx b/src/components/Range/test.tsx deleted file mode 100644 index 624772c..0000000 --- a/src/components/Range/test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useState } from 'react'; -import NtrpRange from './index'; - -const TestPage: React.FC = () => { - const [value, setValue] = useState<[number, number]>([2.0, 4.0]); - - return ( -
-

NtrpRange 组件测试

- -
- -
- -
- 当前值: {value[0].toFixed(1)} - {value[1].toFixed(1)} -
- -
-

测试说明:

-

1. 拖拽左右滑块手柄

-

2. 观察值的变化

-

3. 检查样式是否正确

-
-
- ); -}; - -export default TestPage; diff --git a/src/components/SearchBar/index.module.scss b/src/components/SearchBar/index.module.scss index b376efc..cf5e343 100644 --- a/src/components/SearchBar/index.module.scss +++ b/src/components/SearchBar/index.module.scss @@ -1,23 +1,59 @@ .searchBar { - --nutui-searchbar-padding: 10px 15px; --nutui-searchbar-font-size: 16px; --nutui-searchbar-input-height: 44px; --nutui-searchbar-content-border-radius: 44px; --nutui-searchbar-input-text-color: #000000; --nutui-searchbar-input-padding: 0 0 0 10px; - --nutui-searchbar-padding:0 15px; - // --nutui-searchbar-background: #ffffff; + --nutui-searchbar-padding: 10px 0 0 0; + :global(.nut-searchbar-content) { box-shadow: 0 4px 48px #00000014; } + + .searchBarLeft { + display: flex; + align-items: center; + } + .searchBarRight { + position: relative; width: 44px; height: 44px; border-radius: 50%; - border: 1px solid #0000000F; + border: 1px solid #0000000f; background-color: #ffffff; display: flex; align-items: center; justify-content: center; + + &.active { + background-color: #000000; + } + } + + .filterIcon { + width: 20px; + height: 20px; + } + + .filterCount { + background-color: #000000; + position: absolute; + width: 18px; + height: 18px; + border: 2px solid #ffffff; + border-radius: 50%; + right: -5px; + bottom: -5px; + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + } + + .searchIcon { + width: 20px; + height: 20px; } } \ No newline at end of file diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx index 60fd06f..8181030 100644 --- a/src/components/SearchBar/index.tsx +++ b/src/components/SearchBar/index.tsx @@ -1,22 +1,51 @@ +import { SearchBar } from "@nutui/nutui-react-taro"; +import { View, Text, Image } from "@tarojs/components"; +import img from "../../config/images"; +import styles from "./index.module.scss"; -import { SearchBar } from '@nutui/nutui-react-taro' -import styles from './index.module.scss' - -const SearchBarComponent = () => { - return ( - <> - 123
- // } - right={ -
- } - className={styles.searchBar} - placeholder='搜索上海的球局和场地' - /> - - ) +interface IProps { + handleFilterIcon: () => void; + isSelect: boolean; + filterCount: number; + onChange: (value: string) => void; } -export default SearchBarComponent \ No newline at end of file +const SearchBarComponent = (props: IProps) => { + const { handleFilterIcon, isSelect, filterCount, onChange } = props; + + const handleChange = (value: string) => { + onChange && onChange(value); + }; + return ( + <> + + + + } + right={ + + + {isSelect && ( + {filterCount} + )} + + } + className={styles.searchBar} + placeholder="搜索上海的球局和场地" + onChange={handleChange} + /> + + ); +}; + +export default SearchBarComponent; diff --git a/src/components/Title/index.module.scss b/src/components/Title/index.module.scss index a731606..c31b552 100644 --- a/src/components/Title/index.module.scss +++ b/src/components/Title/index.module.scss @@ -4,8 +4,19 @@ margin-bottom: 10px; gap: 6px; } + +.icon { + width: 16px; + height: 16px; + + image { + width: 100%; + height: 100%; + } +} + .title { font-weight: 600; font-size: 16px; line-height: 20px; -} +} \ No newline at end of file diff --git a/src/components/Title/index.tsx b/src/components/Title/index.tsx index 2eca985..425c593 100644 --- a/src/components/Title/index.tsx +++ b/src/components/Title/index.tsx @@ -2,15 +2,16 @@ import styles from "./index.module.scss"; interface IProps { title: string; className?: string; + icon?: JSX.Element; } const TitleComponent = (props: IProps) => { - const { title, className } = props; + const { title, className, icon } = props; return ( <>
-
+
{icon}

{title}

diff --git a/src/components/UploadCover/index.scss b/src/components/UploadCover/index.scss new file mode 100644 index 0000000..ff76d06 --- /dev/null +++ b/src/components/UploadCover/index.scss @@ -0,0 +1,127 @@ +@use '~@/scss/images.scss' as img; +@use '~@/scss/themeColor.scss' as theme; + +.upload-cover-root { + display: flex; + width: 100%; + height: 112px; + margin-bottom: 8px; + position: relative; + align-items: flex-end; + + &.upload-cover-act-center { + justify-content: center; + } + + .upload-cover-act { + display: flex; + width: 108px; + height: 108px; + padding: 16px 12px 10px 12px; + margin-top: 4px; + box-sizing: border-box; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 20px; + border: 1px dashed rgba(0, 0, 0, 0.12); + background: theme.$page-background-color; + box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06); + z-index: 1; + + .upload-cover-act-icon { + width: 20px; + height: 20px; + } + + .upload-cover-text { + color: var(--Labels-Secondary, var(--Labels-Secondary, rgba(60, 60, 67, 0.60))); + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 166.667% */ + } + } + + .cover-image-list-container { + position: absolute; + left: 114px; + top: 0; + width: calc(100% - 114px); + overflow-x: scroll; + height: 112px; + + &.full { + left: 0; + width: 100%; + } + + .cover-image-list { + width: auto; + height: 112px; + display: flex; + gap: 6px; + justify-content: flex-start; + align-items: flex-end; + flex-wrap: nowrap; + flex-shrink: 0; + flex-grow: 0; + + .cover-image-item { + display: flex; + width: 108px; + height: 108px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 20px; + position: relative; + border: 1px solid rgba(0, 0, 0, 0.12); + box-sizing: border-box; + + .cover-image-item-image { + width: 100%; + height: 100%; + aspect-ratio: 1/1; + border-radius: 20px; + } + + .cover-image-item-delete { + position: absolute; + top: -4px; + right: -4px; + width: 16px; + height: 16px; + } + } + } + } +} + +.upload-source-popup-container { + width: 100%; + height: auto; + display: flex; + flex-direction: column; + padding: 26px 0; + box-sizing: border-box; +} + +.upload-source-popup-item { + display: flex; + width: 100%; + height: 56px; + padding: 16px 24px; + box-sizing: border-box; + justify-content: center; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + + &:last-child { + border-bottom: none; + } +} \ No newline at end of file diff --git a/src/components/UploadCover/index.tsx b/src/components/UploadCover/index.tsx new file mode 100644 index 0000000..6da8f37 --- /dev/null +++ b/src/components/UploadCover/index.tsx @@ -0,0 +1,140 @@ +import React, { useCallback, useRef, useState } from 'react' +import { Image, View, Text } from '@tarojs/components' +import img from '../../config/images' +import UploadSourcePopup, { sourceMap } from './upload-source-popup' +import UploadFromWx from './upload-from-wx' +import { CommonPopup } from '../' + +import './index.scss' +import { uploadFileResponseData } from '@/services/uploadFiles' + +export type sourceType = 'album' | 'history' | 'preset' + +export type source = sourceType[] + +export type CoverImageValue = { + id: string + url: string + tempFilePath?: string +} + +export interface UploadCoverProps { + value: CoverImageValue[] + onChange: (value: CoverImageValue[] | ((prev: CoverImageValue[]) => CoverImageValue[]) +) => void + source?: source + maxCount?: number + align?: 'center' | 'left' +} + +// const values = [ +// 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1a35ebbf-2361-44da-b338-7608561d0b31.png', +// 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/cf5a82ba-90af-4138-a1b3-9119adcde9e0.png', +// 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/49d7cdf0-b03c-4a0f-91c6-e7778080cfcd.png' +// ] + +const mergeCoverImages = (value: CoverImageValue[], images: CoverImageValue[]) => { + console.log(value, images, 11111) + // 根据id来更新url, 如果id不存在,则添加到value中 + const newImages = images + const updatedValue = value.map(item => { + const index = images.findIndex(image => image.id === item.id) + if (index !== -1) { + newImages.splice(index, 1) + return { ...item, url: images[index].url } + } + return item + }) + return [...updatedValue, ...newImages] +} + +export default function UploadCover(props: UploadCoverProps) { + const { + value = [], + onChange = () => void 0, + source = ['album', 'history', 'preset'] as source, + maxCount = 9, + align = 'center', + } = props + + const [visible, setVisible] = useState(false) + const uploadSourcePopupRef = useRef<{ + show: (sourceType: sourceType, maxCount: number) => void + }>(null) + + const onAdd = useCallback((images: CoverImageValue[]) => { + // FIXME: prev is not latest value + onChange(prev => mergeCoverImages(prev, images)) + setVisible(false) + }, [value]) + + const onWxAdd = useCallback((images: CoverImageValue[], onFileUploaded: Promise<{ id: string, data: uploadFileResponseData }[]>) => { + onAdd(images) + onFileUploaded.then(res => { + onAdd(res.map(item => ({ + id: item.id, + url: item.data.file_path, + }))) + }) + }, [onAdd]) + const onDelete = (image: CoverImageValue) => { + onChange(value.filter(item => item.id !== image.id)) + } + + return ( + <> + setVisible(false)} + round + position="bottom" + hideFooter + zIndex={1000} + > + + { + source.map((item) => { + return ( + + { + item === 'album' ? ( + + ) : ( + uploadSourcePopupRef.current?.show(item, maxCount - value.length)}> + {sourceMap.get(item)} + + ) + } + + ) + }) + } + + + +
+ {value.length < maxCount && ( +
setVisible(true)}> + +
添加活动封面
+
+ )} +
+
+ { + value.map((item) => { + return ( + + + onDelete(item)} /> + + ) + }) + } +
+
+
+ + ); +}; + diff --git a/src/components/UploadCover/upload-from-wx.scss b/src/components/UploadCover/upload-from-wx.scss new file mode 100644 index 0000000..195c5b6 --- /dev/null +++ b/src/components/UploadCover/upload-from-wx.scss @@ -0,0 +1,11 @@ +.upload-from-wx-text { + width: 100%; + height: 56px; + padding: 16px 24px; + box-sizing: border-box; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 24px; + text-align: center; +} \ No newline at end of file diff --git a/src/components/UploadCover/upload-from-wx.tsx b/src/components/UploadCover/upload-from-wx.tsx new file mode 100644 index 0000000..841c27b --- /dev/null +++ b/src/components/UploadCover/upload-from-wx.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { View, Text } from '@tarojs/components' +import Taro from '@tarojs/taro' +import uploadApi from '@/services/uploadFiles' +import './upload-from-wx.scss' +import { CoverImageValue } from '.' +import { uploadFileResponseData } from '@/services/uploadFiles' + +export interface UploadFromWxProps { + onAdd: (images: CoverImageValue[], onFileUploaded: Promise<{ id: string, data: uploadFileResponseData }[]>) => void + maxCount: number +} + +export default function UploadFromWx(props: UploadFromWxProps) { + const { + onAdd = () => void 0, + maxCount = 9, // calc from parent + } = props + const handleImportFromWx = () => { + Taro.chooseImage({ + count: maxCount, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + }).then(async (res) => { + // TODO: compress image + // TODO: cropping image to const size + let count = 0 + const files = res.tempFiles.map(item => ({ + filePath: item.path, + description: 'test', + tags: 'test', + is_public: 1 as unknown as 0 | 1, + id: (Date.now() + count++).toString(), + })) + const onFileUploaded = uploadApi.batchUpload(files) + onAdd(files.map(item => ({ + id: item.id, + url: item.filePath, + })), onFileUploaded) // TODO: add loading state + }) + } + return ( + + 从相册添加 + + ) +} \ No newline at end of file diff --git a/src/components/UploadCover/upload-source-popup.scss b/src/components/UploadCover/upload-source-popup.scss new file mode 100644 index 0000000..999c934 --- /dev/null +++ b/src/components/UploadCover/upload-source-popup.scss @@ -0,0 +1,156 @@ +@use '~@/scss/themeColor.scss' as theme; + +.upload-source-popup-text { + width: 100%; + height: 56px; + padding: 16px 24px; + box-sizing: border-box; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 24px; + text-align: center; +} + +.upload-popup { + width: 100%; + padding: 26px 0; + box-sizing: border-box; + + .upload-popup-title { + display: flex; + padding: 18px 20px 10px 20px; + align-items: center; + align-self: stretch; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 150% */ + letter-spacing: -0.23px; + } + + .upload-popup-scroll-view { + max-height: calc(100vh - 260px); + overflow-y: auto; + + .upload-popup-image-list { + width: 100%; + padding: 0 16px; + box-sizing: border-box; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px 10px; + + .upload-popup-image-item { + aspect-ratio: 1/1; + border-radius: 9px; + border: 1px solid rgba(0, 0, 0, 0.12); + box-sizing: border-box; + background: rgba(0, 0, 0, 0.06); + margin: 0; + position: relative; + + &:not(.selected) { + &.disabled { + opacity: 0.5; + pointer-events: none; + cursor: not-allowed; + } + } + + .upload-popup-image-item-image { + width: 100%; + height: 100%; + border-radius: 9px; + margin: 0; + } + + .upload-popup-image-item-select { + position: absolute; + top: 5px; + right: 5px; + width: 14px; + height: 14px; + color: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + + &.selected { + background: rgba(0, 0, 0, 0.5); + + .select-image-icon { + width: 7px; + height: 7px; + } + } + } + + .select-image-icon { + width: 14px; + height: 14px; + } + } + } + + .upload-popup-image-list-empty { + width: 100%; + height: 60vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + } + + .upload-popup-image-list-empty-image { + width: 80%; + aspect-ratio: 4/3; + height: auto; + } + + .upload-popup-image-list-empty-text { + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 150% */ + letter-spacing: -0.23px; + } + } + + .upload-popup-footer { + display: flex; + width: 100%; + height: 62px; + padding: 8px 10px 10px 10px; + box-sizing: border-box; + justify-content: center; + align-items: flex-start; + gap: 8px; + + .upload-popup-footer-cancel, .upload-popup-footer-confirm { + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + box-sizing: border-box; + height: 44px; + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.12); + flex: 1; + } + + .upload-popup-footer-cancel { + background: theme.$page-background-color; + } + + .upload-popup-footer-confirm { + background: theme.$primary-color; + color: rgba(255, 255, 255, 0.5); + + &.active { + color: #fff; + } + } + } +} \ No newline at end of file diff --git a/src/components/UploadCover/upload-source-popup.tsx b/src/components/UploadCover/upload-source-popup.tsx new file mode 100644 index 0000000..dc72e44 --- /dev/null +++ b/src/components/UploadCover/upload-source-popup.tsx @@ -0,0 +1,173 @@ +import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react' +import { Image, View, Text, ScrollView, Button } from '@tarojs/components' +import Taro from '@tarojs/taro' +import img from '../../config/images' +import publishService from '../../services/publishService' +import { CommonPopup } from '../' +import emptyStatus from '../../static/emptyStatus/publish-empty.png' + +import './upload-source-popup.scss' + +type SourceType = 'history' | 'preset' + +type ImageItem = { + id: string + url: string + tempFilePath?: string +} + +interface UploadImageProps { + onAdd: (images: ImageItem[]) => void +} + +export const sourceMap = new Map([ + ['history', '历史图库'], + ['preset', '预设图库'] +]) + +const checkImageSelected = (images: ImageItem[], image: ImageItem) => { + return images.some(item => item.id === image.id) +} + +export default forwardRef(function UploadImage(props: UploadImageProps, ref) { + const { + onAdd = () => void 0, + } = props + const [visible, setVisible] = useState(false) + const [sourceType, setSourceType] = useState('history') + const [maxCount, setMaxCount] = useState(9) + const [images, setImages] = useState([]) + const [selectedImages, setSelectedImages] = useState([]) + + const handleImageClick = (image: ImageItem) => { + if (checkImageSelected(selectedImages, image)) { + setSelectedImages(selectedImages.filter(item => item.id !== image.id)) + } else if (!outOfMax) { + setSelectedImages([...selectedImages, image]) + } else { + Taro.showToast({ + title: `最多选择${maxCount}张图片`, + icon: 'none' + }) + } + } + + useImperativeHandle(ref, () => ({ + show: (sourceType: SourceType, maxCount: number) => { + setVisible(true) + setSourceType(sourceType) + setMaxCount(maxCount) + fetchImages() + } + })) + + function fetchImages() { + publishService.getPictures({ + pageOption: { + page: 1, + pageSize: 100, + }, + seachOption: { + tag: '', + resource_type: 'image', + dateRange: [], + }, + }).then(res => { + if (res.success) { + let start = 0 + setImages(res.data.data.rows.map(item => ({ + id: (Date.now() + start++).toString(), + url: item.thumbnail_url, + }))) + } else { + // TODO: 显示错误信息 + Taro.showToast({ + title: res.message, + icon: 'none' + }) + } + }) + } + + function onClose() { + setVisible(false) + setSelectedImages([]) + setImages([]) + setSourceType('history') + setMaxCount(9) + } + + const handleConfirm = () => { + if (selectedImages.length > 0) { + onAdd(selectedImages) + setVisible(false) + } else { + Taro.showToast({ + title: '请选择图片', + icon: 'none' + }) + } + } + + const outOfMax = selectedImages.length >= maxCount + + return ( + <> + + + {sourceMap.get(sourceType)} + {/* TODO: 分页 加载更多 */} + {/* TODO: 图片加载失败 */} + {/* TODO: 图片加载中 */} + + {images.length > 0 ? ( + + {images.map(item => { + const isSelected = checkImageSelected(selectedImages, item) + return ( + handleImageClick(item)}> + + + {isSelected ? ( + + ) : ( + + )} + + + ) + })} + + ) : ( + + + 暂无内容 + + )} + + {images.length > 0 ? ( + + + + + ) : ( + + + + )} + + + {/* setVisible(true)}>{sourceMap.get(sourceType)}选取 */} + + ); +}); diff --git a/src/components/index.ts b/src/components/index.ts index e2e019e..6e67045 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -9,6 +9,7 @@ import TimeSelector from './TimeSelector' import TitleTextarea from './TitleTextarea' import CommonPopup from './CommonPopup' import DateTimePicker from './DateTimePicker/DateTimePicker' +<<<<<<< HEAD import TimePicker from './TimePicker/TimePicker' import CalendarCard, { DialogCalendarCard } from './CalendarCard' import CommonDialog from './CommonDialog' @@ -31,4 +32,21 @@ import PublishMenu from './PublishMenu/PublishMenu' CommonDialog, PublishMenu } +======= +import UploadCover from './UploadCover' + +export { + ActivityTypeSwitch, + TextareaTag, + FormSwitch, + ImageUpload, + Range, + NumberInterval, + TimeSelector, + TitleTextarea, + CommonPopup, + DateTimePicker, + UploadCover, +} +>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034 diff --git a/src/config/images.js b/src/config/images.js index c5d30b2..76a8f0a 100644 --- a/src/config/images.js +++ b/src/config/images.js @@ -8,6 +8,17 @@ export default { ICON_COST: require('@/static/publishBall/icon-cost.svg'), ICON_TIPS: require('@/static/publishBall/icon-tips.svg'), ICON_ARROW_RIGHT: require('@/static/publishBall/icon-arrow-right.svg'), + ICON_FILTER: require('@/static/list/icon-filter.svg'), + ICON_FILTER_SELECTED: require('@/static/list/icon-filter-selected.svg'), + ICON_SEARCH: require('@/static/list/icon-search.svg'), + ICON_PLAY: require('@/static/list/icon-play.svg'), + ICON_SITE: require('@/static/list/icon-site.svg'), + ICON_ARROW_DOWN: require('@/static/list/icon-arrow-down.svg'), + ICON_MENU_ITEM_SELECTED: require('@/static/list/icon-menu-item-selected.svg'), + ICON_ARROW_DOWN_WHITE: require('@/static/list/icon-arrow-down-white.svg'), + ICON_LIST_RIGHT_ARROW: require('@/static/list/icon-list-right-arrow.svg'), + ICON_ARROW_LEFT: require('@/static/detail/icon-arrow-left.svg'), + ICON_LOGO_GO: require('@/static/detail/icon-logo-go.svg'), ICON_SEARCH: require('@/static/publishBall/icon-search.svg'), ICON_MAP: require('@/static/publishBall/icon-map.svg'), ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'), @@ -17,9 +28,30 @@ export default { ICON_ADD: require('@/static/publishBall/icon-add.svg'), ICON_COPY: require('@/static/publishBall/icon-arrow-right.svg'), ICON_DELETE: require('@/static/publishBall/icon-delete.svg'), +<<<<<<< HEAD ICON_RIGHT_MAX: require('@/static/publishBall/icon-right-max.svg'), ICON_PLUS: require('@/static/publishBall/icon-plus.svg'), ICON_GROUP: require('@/static/publishBall/icon-group.svg'), ICON_PERSON: require('@/static/publishBall/icon-person.svg'), ICON_PUBLISH: require('@/static/publishBall/icon-publish.png'), +======= + ICON_CIRCLE_UNSELECT: require('@/static/publishBall/icon-circle-unselect.svg'), + ICON_CIRCLE_SELECT: require('@/static/publishBall/icon-circle-select-ring.svg'), + ICON_CIRCLE_SELECT_ARROW: require('@/static/publishBall/icon-circle-select-arrow.svg'), + ICON_LOGO: require('@/static/logo.svg'), + ICON_CHANGE: require('@/static/list/icon-change.svg'), + ICON_WEATHER_SUN: require('@/static/detail/icon-weather-sun.svg'), + ICON_DETAIL_MAP: require('@/static/detail/icon-map.svg'), + ICON_DETAIL_ARROW_RIGHT: require('@/static/detail/icon-arrow-right.svg'), + ICON_DETAIL_NOTICE: require('@/static/detail/icon-notice.svg'), + ICON_DETAIL_APPLICATION_ADD: require('@/static/detail/icon-application-add.svg'), + ICON_DETAIL_COMMENT: require('@/static/detail/icon-comment.svg'), + ICON_DETAIL_COMMENT_DARK: require('@/static/detail/icon-comment-dark.svg'), + ICON_DETAIL_SHARE: require('@/static/detail/icon-share-dark.svg'), + ICON_GUIDE_BAR_PUBLISH: require('@/static/common/guide-bar-publish.svg'), + ICON_NAVIGATOR_BACK: require('@/static/common/navigator-back.svg'), + ICON_LIST_PLAYING_GAME: require('@/static/list/icon-paying-game.svg'), + ICON_LIST_LOAD_ERROR: require('@/static/list/icon-load-error.svg'), + ICON_LIST_RELOAD: require('@/static/list/icon-reload.svg'), +>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034 } \ No newline at end of file diff --git a/src/container/listContainer/index.scss b/src/container/listContainer/index.scss new file mode 100644 index 0000000..89137be --- /dev/null +++ b/src/container/listContainer/index.scss @@ -0,0 +1,8 @@ +.listContentWrapper { + padding: 0 5px; + background: #fafafa; + display: flex; + flex-direction: column; + gap: 5px; + background-color: red; +} \ No newline at end of file diff --git a/src/container/listContainer/index.tsx b/src/container/listContainer/index.tsx new file mode 100644 index 0000000..5407c87 --- /dev/null +++ b/src/container/listContainer/index.tsx @@ -0,0 +1,52 @@ +import { View } from "@tarojs/components"; +import ListCard from "@/components/ListCard"; +import ListLoadError from "@/components/ListLoadError"; +import ListCardSkeleton from "@/components/ListCardSkeleton"; +// import { useGlobalState } from "@/store/global"; + +const ListContainer = (props) => { + const { loading, data = [], error, reload } = props; + // const { statusNavbarHeightInfo } = useGlobalState() || {}; + // const { totalHeight } = statusNavbarHeightInfo; + + const renderList = () => { + if (loading && data.length > 0) { + return ( + <> + {new Array(10).fill(0).map(() => { + return ; + })} + + ); + } + + if (error) { + return ; + } + + if (loading && data.length === 0) { + return null; + } + + return ( + <> + {data.map((match, index) => ( + + ))} + + ); + }; + + return ( + + {renderList()} + + ); +}; + +export default ListContainer; diff --git a/src/pages/detail/index.config.ts b/src/pages/detail/index.config.ts new file mode 100644 index 0000000..eaeff9d --- /dev/null +++ b/src/pages/detail/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '球局详情', + navigationStyle: 'custom', +}) diff --git a/src/pages/detail/index.scss b/src/pages/detail/index.scss new file mode 100644 index 0000000..fa88e84 --- /dev/null +++ b/src/pages/detail/index.scss @@ -0,0 +1,1034 @@ +@use '~@/scss/images.scss' as img; + +.detail-page { + width: 100%; + height: 100%; + // padding-bottom: env(safe-area-inset-bottom); + + .custom-navbar { + height: 56px; /* 通常与原生导航栏高度一致 */ + display: flex; + align-items: center; + justify-content: center; + // background-color: #fff; + color: #000; + padding-top: 44px; /* 适配状态栏 */ + position: sticky; + top: 0; + z-index: 100; + } + + .detail-navigator { + height: 30px; + width: 80px; + border-radius: 15px; + position: absolute; + left: 12px; + border: 1px solid #888; + box-sizing: border-box; + color: #fff; + display: flex; + align-items: center; + background: rgba(0, 0, 0, 0.10); + + .detail-navigator-back { + border-right: 1px solid #444; + } + + .detail-navigator-back, .detail-navigator-icon { + height: 20px; + width: 50%; + + display: flex; + justify-content: center; + + & > .detail-navigator-back-icon { + width: 20px; + height: 20px; + color: #fff; + } + + & > .detail-navigator-logo-icon { + width: 20px; + height: 20px; + color: #fff; + } + } + } + + .detail-page-bg { + width: 100%; + height: 100%; + position: fixed; + left: 0; + top: 0; + background-size: cover; + filter: blur(40px); + transform: scale(1.5); + z-index: -2; + width: calc(100% + 20px); + height: calc(100% + 20px); + margin: -10px; + } + + .detail-page-bg-text { + width: 100%; + height: 100%; + position: fixed; + left: 0; + top: 0; + z-index: -1; + background-color: rgba(0, 0, 0, 0.3); + } + + .detail-swiper { + height: 240px; + margin-top: 15px; + margin-left: 15px; + } + + .detail-swiper-item { + overflow: visible; + height: 100%; + + .detail-swiper-item-image { + width: 100%; + height: 100%; + border-radius: 12px; + transition: transform 0.5s; + } + } + + .detail-page-content { + + &-avatar-tags { + padding: 20px 20px 0; + box-sizing: border-box; + display: flex; + align-items: center; + gap: 6px; + + &-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + overflow: hidden; + + &-image { + width: 28px; + height: 28px; + border-radius: 50%; + } + } + + &-tags { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + + &-tag { + display: flex; + height: 28px; + padding: 6px 12px; + box-sizing: border-box; + align-items: center; + gap: 4px; + border-radius: 999px; + border: 0.5px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.25); + color: #fff; + font-size: 13px; + font-weight: 500; + line-height: 20px; + letter-spacing: -0.23px; + text-align: center; + } + } + } + + &-title { + padding: 20px 20px 0; + box-sizing: border-box; + + &-text { + overflow: hidden; + color: #FFF; + font-feature-settings: 'liga' off, 'clig' off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 22px; + font-style: normal; + font-weight: 600; + line-height: 32px; /* 145.455% */ + } + } + + &-game-info { + &-date-weather { + padding: 20px 20px 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; + // gap: 12px; + + &-calendar-date { + width: 60%; + display: flex; + align-items: center; + gap: 16px; + + &-calendar { + display: flex; + width: 48px; + height: 48px; + padding-bottom: 6px; + box-sizing: border-box; + flex-direction: column; + align-items: center; + gap: 4px; + border-radius: 12px; + // border: 0.5px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.25); + overflow: hidden; + color: #FFF; + + .month { + width: 100%; + height: 18px; + font-size: 10px; + display: flex; + padding: 1px auto; + box-sizing: border-box; + justify-content: center; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + background: #7B828B; + } + + .day { + display: flex; + width: 48px; + padding-bottom: 6px; + box-sizing: border-box; + flex-direction: column; + align-items: center; + gap: 4px; + // border: 0.5px solid rgba(255, 255, 255, 0.08); + // background: rgba(255, 255, 255, 0.25); + } + } + + &-date { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-evenly; + gap: 4px; + align-self: stretch; + color: #FFF; + + .date { + color: #FFF; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + } + + .venue-time { + color: rgba(255, 255, 255, 0.80); + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + } + } + } + + &-weather { + display: flex; + align-items: flex-end; + flex-direction: column; + gap: 4px; + + &-icon { + width: 20px; + height: 20px; + + .weather-icon { + width: 20px; + height: 20px; + } + } + &-text-temperature { + display: flex; + align-items: center; + gap: 12px; + color: rgba(255, 255, 255, 0.80); + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + } + } + } + + &-place { + .location-message { + padding: 20px 20px 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + + &-icon { + width: 48px; + height: 48px; + border-radius: 12px; + padding: 14px; + box-sizing: border-box; + background: #4D5865; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + // border: 0.5px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.25); + + &-image { + width: 20px; + height: 20px; + } + } + + &-text { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-evenly; + gap: 4px; + align-self: stretch; + + &-name-distance { + display: flex; + align-items: center; + gap: 4px; + color: #FFF; + text-align: center; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + + &-arrow { + width: 16px; + height: 16px; + } + } + + &-address { + color: rgba(255, 255, 255, 0.80); + text-align: center; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + } + } + } + + .location-map { + width: 100%; + padding: 20px 20px 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + + &-map { + width: 100%; + height: 95px; + } + } + } + } + + &-detail { + padding: 24px 15px 0; + box-sizing: border-box; + + .venue-detail-title { + display: flex; + height: 31px; + align-items: center; + justify-content: flex-start; + gap: 4px; + padding-bottom: 6px; + color: #FFF; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + &-notice-icon { + width: 14px; + height: 14px; + } + } + + .venue-detail-content { + padding: 16px 0 0; + box-sizing: border-box; + + &-tags { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + + &-tag { + overflow: hidden; + color: #FFF; + font-feature-settings: 'liga' off, 'clig' off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 160% */ + } + } + + &-remarks { + overflow: hidden; + color: rgba(255, 255, 255, 0.65); + // font-feature-settings: 'liga' off, 'clig' off; + // text-overflow: ellipsis; + // white-space: nowrap; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 160% */ + } + } + } + + &-gameplay-requirements { + padding: 24px 15px 0; + box-sizing: border-box; + + .gameplay-requirements-title { + overflow: hidden; + color: #FFF; + height: 24px; + padding-bottom: 6px; + font-feature-settings: 'liga' off, 'clig' off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + .gameplay-requirements { + padding: 12px 0 0; + display: flex; + flex-direction: column; + gap: 12px; + + &-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 2px; + align-self: stretch; + + &-title { + color: rgba(255, 255, 255, 0.80); + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + } + + &-desc { + color: #FFF; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 160% */ + } + } + } + } + + &-participants { + padding: 24px 15px 0; + box-sizing: border-box; + + .participants-title { + display: flex; + padding-bottom: 6px; + align-items: center; + overflow: hidden; + color: #FFF; + font-feature-settings: 'liga' off, 'clig' off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + .participants-list { + padding: 10px 0 0; + height: 162px; + display: flex; + flex-direction: row; + gap: 8px; + + &-application { + display: flex; + width: 108px; + height: 162px; + padding: 16px 12px 10px 12px; + box-sizing: border-box; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + align-self: stretch; + border-radius: 20px; + border: 1px dashed rgba(255, 255, 255, 0.20); + background: rgba(255, 255, 255, 0.16); + flex: 0 0 auto; + + &-icon { + width: 28px; + height: 28px; + } + + &-text { + color: rgba(255, 255, 255, 0.60); + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 166.667% */ + } + } + + &-scroll { + flex: 0 0 auto; + width: calc(100% - 116px); + + &-content { + display: flex; + flex-direction: row; + gap: 8px; + height: 162px; + flex-wrap: nowrap; + + .participants-list-item { + display: flex; + width: 108px; + padding: 16px 12px 10px 12px; + box-sizing: border-box; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 20px; + border: 0.5px solid rgba(255, 255, 255, 0.20); + background: rgba(255, 255, 255, 0.16); + flex: 0 0 auto; + + &-avatar { + width: 60px; + height: 60px; + } + + &-name { + color: rgba(255, 255, 255, 0.85); + text-align: center; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 184.615% */ + } + + &-level { + color: rgba(255, 255, 255, 0.45); + text-align: center; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + } + + &-role { + color: #FFF; + text-align: center; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 166.667% */ + } + } + } + } + } + } + + &-supplemental-notes { + padding: 24px 15px 0; + + .supplemental-notes-title { + overflow: hidden; + padding-bottom: 7px; + height: 24px; + color: #FFF; + font-feature-settings: 'liga' off, 'clig' off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + .supplemental-notes-content { + padding: 12px 0 0; + display: flex; + flex-direction: column; + gap: 6px; + + &-tags { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + + &-tag { + overflow: hidden; + color: #FFF; + font-feature-settings: 'liga' off, 'clig' off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 160% */ + } + } + + &-text { + overflow: hidden; + color: rgba(255, 255, 255, 0.65); + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 160% */ + } + } + } + + &-organizer-recommend-games { + padding: 24px 15px 0; + + .organizer-title { + overflow: hidden; + padding-bottom: 6px; + height: 24px; + color: #FFF; + font-feature-settings: 'liga' off, 'clig' off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + .organizer-avatar-name { + display: flex; + align-items: center; + padding: 16px 0 0; + align-items: center; + gap: 8px; + justify-content: flex-start; + + &-avatar { + width: 40px; + height: 40px; + } + + &-message { + display: flex; + flex-direction: column; + gap: 4px; + + &-name { + color: rgba(255, 255, 255, 0.85); + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 184.615% */ + } + + &-stats { + display: flex; + align-items: center; + gap: 5px; + color: rgba(255, 255, 255, 0.60); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 133.333% */ + letter-spacing: 0.06px; + + &-separator { + width: 1px; + height: 10px; + color: rgba(255, 255, 255, 0.20); + } + } + } + + .organizer-actions { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + + .organizer-actions-follow, .organizer-actions-comment { + display: flex; + height: 32px; + box-sizing: border-box; + align-items: center; + gap: 4px; + border-radius: 999px; + // border: 0.5px solid rgba(255, 255, 255, 0.10); + background: rgba(255, 255, 255, 0.16); + box-shadow: 0 4px 48px 0 rgba(0, 0, 0, 0.08); + + & > image { + width: 16px; + height: 16px; + } + } + + .organizer-actions-follow { + padding: 8px 12px 8px; + &-text { + color: #FFF; + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 153.846% */ + letter-spacing: -0.23px; + } + } + + .organizer-actions-comment { + padding: 8px 10px; + } + } + } + + .organizer-recommend-games { + padding-top: 20px; + + .organizer-recommend-games-title { + overflow: hidden; + color: rgba(255, 255, 255, 0.65); + font-family: "PingFang SC"; + font-size: 15px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 160% */ + display: flex; + align-items: center; + gap: 2px; + align-self: stretch; + + &-arrow { + width: 12px; + height: 12px; + } + } + + .recommend-games-list { + padding: 10px 0; + display: flex; + gap: 8px; + flex-wrap: nowrap; + + &-content { + display: flex; + gap: 8px; + flex-wrap: nowrap; + + .recommend-games-list-item { + width: 246px; + height: 122px; + display: flex; + flex-direction: column; + gap: 6px; + flex: 0 0 auto; + border-radius: 20px; + border: 1px solid rgba(33, 178, 0, 0.20); + background: rgba(255, 255, 255, 0.16); + padding: 12px 0 12px 15px; + box-sizing: border-box; + + &-title { + display: flex; + align-items: center; + height: 24px; + gap: 2px; + overflow: hidden; + color: rgba(255, 255, 255, 0.85); + text-overflow: ellipsis; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + + &-arrow { + width: 12px; + height: 12px; + } + } + + &-time-range { + overflow: hidden; + color: rgba(255, 255, 255, 0.45); + text-overflow: ellipsis; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + } + + &-location-venue-distance { + display: flex; + align-items: center; + gap: 2px; + overflow: hidden; + color: rgba(255, 255, 255, 0.45); + font-feature-settings: 'liga' off, 'clig' off; + text-overflow: ellipsis; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + } + + &-addon { + display: flex; + align-items: center; + gap: 4px; + + &-avatar { + width: 20px; + height: 20px; + } + + &-message { + display: flex; + gap: 4px; + + &-applications, &-level-requirements, &-play-type { + color: rgba(255, 255, 255, 0.85); + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 181.818% */ + letter-spacing: -0.23px; + display: flex; + height: 20px; + padding: 6px 8px; + box-sizing: border-box; + align-items: center; + gap: 4px; + border-radius: 999px; + // border: 0.5px solid rgba(0, 0, 0, 0.16); + background: rgba(255, 255, 255, 0.12); + } + } + } + } + } + } + } + } + + .sticky-bottom-bar { + position: sticky; + bottom: 0; + padding: 10px 10px env(safe-area-inset-bottom); + box-sizing: border-box; + width: 100%; + height: calc(52px + env(safe-area-inset-bottom)); + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + + &-share-and-comment { + display: flex; + align-items: center; + height: 52px; + width: 113px; + box-sizing: border-box; + padding: 2px 20px; + justify-content: center; + gap: 16px; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: #FFF; + + .sticky-bottom-bar-share { + display: flex; + align-items: center; + flex-direction: column; + gap: 4px; + + &-icon { + width: 16px; + height: 16px; + } + + &-text { + color: rgba(0, 0, 0, 0.85); + font-size: 10px; + font-style: normal; + font-weight: 500; + line-height: 16px; /* 160% */ + } + } + + &-separator { + width: 1px; + height: 24px; + background: rgba(0, 0, 0, 0.10); + } + + .sticky-bottom-bar-comment { + display: flex; + align-items: center; + flex-direction: column; + gap: 4px; + + &-icon { + width: 16px; + height: 16px; + } + + &-text { + color: rgba(0, 0, 0, 0.85); + font-size: 10px; + font-style: normal; + font-weight: 500; + line-height: 16px; /* 160% */ + } + } + } + + &-join-game { + display: flex; + align-items: center; + height: 52px; + width: auto; + padding: 2px 6px; + box-sizing: border-box; + justify-content: center; + gap: 12px; + flex: 1 0 0; + border-radius: 16px; + border: 1px solid rgba(0, 0, 0, 0.06); + background: #FFF; + + &-price { + font-family: "PoetsenOne"; + font-size: 28px; + font-weight: 400; + line-height: 24px; /* 114.286% */ + letter-spacing: -0.56px; + color: #000; + } + } + } + } +} + +.share-popup-content { + width: 100%; + height: 100%; + padding: 20px 16px env(safe-area-inset-bottom); + box-sizing: border-box; + // padding-bottom: env(safe-area-inset-bottom); + box-sizing: border-box; + display: flex; + justify-content: space-around; + align-items: center; + + & > view { + width: 100px; + height: 64px; + border-radius: 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + & > image { + width: 24px; + height: 24px; + } + + & > text { + color: rgba(0, 0, 0, 0.85); + } + } +} + diff --git a/src/pages/detail/index.tsx b/src/pages/detail/index.tsx new file mode 100644 index 0000000..b1e1626 --- /dev/null +++ b/src/pages/detail/index.tsx @@ -0,0 +1,556 @@ +import React, { useState, useEffect, useRef, useImperativeHandle, forwardRef } from 'react' +import { View, Text, Button, Swiper, SwiperItem, Image, Map, ScrollView } from '@tarojs/components' +import { Cell, Avatar, Progress, Popover } from '@nutui/nutui-react-taro' +import Taro, { useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro' +// 导入API服务 +import DetailService from '../../services/detailService' +import { + useUserStats, + useUserActions +} from '../../store/userStore' +import img from '../../config/images' +import { getTextColorOnImage } from '../../utils' +import './index.scss' +import { CommonPopup } from '@/components' + +const images = [ + 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1a35ebbf-2361-44da-b338-7608561d0b31.png', + 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/cf5a82ba-90af-4138-a1b3-9119adcde9e0.png', + 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/49d7cdf0-b03c-4a0f-91c6-e7778080cfcd.png' +] + +function insertDotInTags(tags: string[]) { + return tags.join('-·-').split('-') +} + +const SharePopup = forwardRef(({ id, from }: { id: string, from: string }, ref) => { + const [visible, setVisible] = useState(false) + + useImperativeHandle(ref, () => ({ + show: () => { + setVisible(true) + } + })) + + function handleShareToWechat() { + useShareAppMessage(() => { + return { + title: '分享', + path: `/pages/detail/index?id=${id}&from=${from}`, + } + }) + } + + function handleShareToWechatMoments() { + useShareTimeline(() => { + return { + title: '分享', + path: `/pages/detail/index?id=${id}&from=${from}`, + } + }) + } + + function handleSaveToLocal() { + Taro.saveImageToPhotosAlbum({ + filePath: images[0], + success: () => { + Taro.showToast({ title: '保存成功', icon: 'success' }) + }, + fail: () => { + Taro.showToast({ title: '保存失败', icon: 'none' }) + }, + }) + } + + return ( + { setVisible(false) }} + hideFooter + style={{ minHeight: '100px' }} + > + + + + 分享到微信 + + + + 分享朋友圈 + + + + 保存到本地 + + + + ) +}) + +function Index() { + // 使用Zustand store + // const userStats = useUserStats() + // const { incrementRequestCount, resetUserStats } = useUserActions() + + const [current, setCurrent] = useState(0) + const [colors, setColors] = useState([]) + const [detail, setDetail] = useState(null) + const { params } = useRouter() + const { id, autoShare, from } = params + + console.log('from', from) + + // 本地状态管理 + const [loading, setLoading] = useState(false) + const sharePopupRef = useRef(null) + + // 页面加载时获取数据 + useEffect(() => { + fetchDetail() + calcBgMainColors() + }, []) + + const fetchDetail = async () => { + const res = await DetailService.getDetail(Number(id)) + if (res.code === 0) { + console.log(res.data) + setDetail(res.data) + } + } + + const calcBgMainColors = async () => { + const textcolors: string[] = [] + for (const index in images) { + const { textColor } = await getTextColorOnImage(images[index]) + textcolors[index] = textColor + } + setColors(textcolors) + } + + function handleShare() { + sharePopupRef.current.show() + } + + const openMap = () => { + Taro.openLocation({ + latitude: detail?.latitude, // 纬度(必填) + longitude: detail?.longitude, // 经度(必填) + name: '上海体育场', // 位置名(可选) + address: '上海市徐汇区肇嘉浜路128号', // 地址详情(可选) + scale: 15, // 地图缩放级别(1-28) + }) + } + + const handleJoinGame = () => { + Taro.navigateTo({ + url: `/pages/orderCheck/index?id=${id}`, + }) + } + + const tags = [{ + name: '🕙 急招', + icon: '', + }, { + name: '🔥 本周热门', + icon: '', + }, { + name: '🎉 新活动', + icon: '', + }, { + name: '官方组织', + icon: '', + }] + + const detailTags = ['室内', '硬地', '2号场', '有停车场', '有淋浴间', '有更衣室'] + + const { title, longitude, latitude } = detail || {} + + const requirements = [{ + title: 'NTRP水平要求', + desc: '2.0 - 4.5 之间', + }, { + title: '活动玩法', + desc: '双打', + }, { + title: '人员构成', + desc: '个人球局 · 组织者参与活动', + }] + + const participants = detail?.participants || [] + + const supplementalNotesTags = ['仅限男生', '装备自备', '其他'] + + const recommendGames = [ + { + title: '黄浦日场对拉', + time: '2025-08-25 9:00', + timeLength: '2小时', + venue: '上海体育场', + veuneType: '室外', + distance: '1.2km', + avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', + applications: 10, + checkedApplications: 3, + levelRequirements: 'NTRP 3.5', + playType: '双打', + }, + { + title: '黄浦夜场对拉', + time: '2025-08-25 19:00', + timeLength: '2小时', + venue: '上海体育场', + veuneType: '室外', + distance: '1.2km', + avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', + applications: 10, + checkedApplications: 3, + levelRequirements: 'NTRP 3.5', + playType: '双打', + }, + { + title: '黄浦全天对拉', + time: '2025-08-25 9:00', + timeLength: '12小时', + venue: '上海体育场', + veuneType: '室外', + distance: '1.2km', + avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', + applications: 10, + checkedApplications: 3, + levelRequirements: 'NTRP 3.5', + playType: '双打', + }, + ] + + return ( + + {/* custom navbar */} + + + { Taro.navigateBack() }}> + + + + + + + + + + {/* swiper */} + { setCurrent(e.detail.current) }} + > + {images.map((imageUrl, index) => ( + + + + ))} + + {/* content */} + + {/* avatar and tags */} + + + {/* network image mock */} + + + + {tags.map((tag, index) => ( + + {tag.icon && } + {tag.name} + + ))} + + + {/* title */} + + {title} + + {/* Date and Place and weather */} + + {/* Date and Weather */} + + {/* Calendar and Date time */} + + {/* Calendar */} + + 3月 + 25 + + {/* Date time */} + + 3月25日 周一 + 19:00-21:00 (2小时) + + + {/* Weather */} + + {/* Weather icon */} + + + + {/* Weather text and temperature */} + + 28℃ - 32℃ + + + + {/* Place */} + + {/* venue location message */} + + {/* location icon */} + + + + {/* location message */} + + {/* venue name and distance */} + + 上海体育场 + · + 1.2km + + + {/* venue address */} + + 上海市徐汇区肇嘉浜路128号 + + + + {/* venue map */} + + {}} + // hide business msg + showLocation + theme='dark' + /> + + + + {/* detail */} + + {/* venue detail title and venue ordered status */} + + 场馆详情 + · + 已订场 + + + } + location='top' + visible={false} + > + + + + {/* venue detail content */} + + {/* venue detail tags */} + + {insertDotInTags(detailTags).map((tag, index) => ( + + {tag} + + ))} + + {/* venue remarks */} + + 其他这是用户在场地补充描述里自己写的东西啦啦啦啦啦啦啦啦啦啦啦啦 + + + + {/* gameplay requirements */} + + {/* title */} + + 玩法要求 + + {/* requirements */} + + {requirements.map((item, index) => ( + + {item.title} + {item.desc} + + ))} + + + {/* participants */} + + + 参与者 + · + 剩余空位 3 + + + {/* application */} + { Taro.showToast({ title: 'To be continued', icon: 'none' }) }}> + + 申请加入 + + {/* participants list */} + + + {participants.map((participant) => ( + + {/* */} + {/* network image mock random */} + + {participant.user.nickname || '未知'} + {participant.level || '未知'} + {participant.role || '参与者'} + + ))} + + + + + {/* supplemental notes */} + + + 补充说明 + + + {/* supplemental notes tags */} + + {insertDotInTags(supplementalNotesTags).map((tag, index) => ( + + {tag} + + ))} + + {/* supplemental notes content */} + + 其他这是用户在补充说明里自己写的东西啦啦啦啦啦啦啦啦啦啦啦啦其他这是用户在补充说明里自己写的东西啦啦啦啦啦啦啦啦啦啦啦啦 + + + + {/* organizer and recommend games by organizer */} + + {/* orgnizer title */} + + 组织者 + + {/* organizer avatar and name */} + + + + Light + + 已组织 8 次 + + NTRP 3.5 + + + + + + 关注 + + + + + + + {/* recommend games by organizer */} + + + TA的更多活动 + + + + + {recommendGames.map((game, index) => ( + + {/* game title */} + + {game.title} + + + {/* game time and range */} + + {game.time} + {game.timeLength} + + {/* game location、vunue、distance */} + + {game.venue} + · + {game.veuneType} + · + {game.distance} + + {/* organizer avatar、applications、level requirements、play type */} + + + + + 报名人数 {game.checkedApplications}/{game.applications} + + + {game.levelRequirements} + + + {game.playType} + + + + + ))} + + + + + {/* sticky bottom action bar */} + + + + + 分享 + + + { Taro.showToast({ title: 'To be continued', icon: 'none' }) }}> + + 32 + + + + 🎾 + 立即加入 + + ¥ 65 + + + + {/* share popup */} + + + + ) +} + +export default Index diff --git a/src/pages/index/index.config.ts b/src/pages/index/index.config.ts deleted file mode 100644 index 12abc5f..0000000 --- a/src/pages/index/index.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '首页' -}) diff --git a/src/pages/index/index.module.scss b/src/pages/index/index.module.scss deleted file mode 100644 index 0519ecb..0000000 --- a/src/pages/index/index.module.scss +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/pages/index/index.scss b/src/pages/index/index.scss deleted file mode 100644 index 282231f..0000000 --- a/src/pages/index/index.scss +++ /dev/null @@ -1,261 +0,0 @@ -.index-page { - padding: 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - min-height: 100vh; - - .page-header { - text-align: center; - margin-bottom: 24px; - - .page-title { - font-size: 28px; - font-weight: bold; - color: white; - display: block; - margin-bottom: 8px; - } - - .page-subtitle { - font-size: 16px; - color: rgba(255, 255, 255, 0.8); - display: block; - } - } - - .user-card { - background: white; - border-radius: 16px; - padding: 20px; - margin-bottom: 20px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - - .user-header { - display: flex; - align-items: center; - gap: 16px; - - .user-info { - flex: 1; - - .username { - font-size: 20px; - font-weight: bold; - color: #333; - display: block; - margin-bottom: 4px; - } - - .user-level { - font-size: 14px; - color: #666; - display: block; - margin-bottom: 4px; - } - - .join-date { - font-size: 12px; - color: #999; - display: block; - } - } - } - } - - .stats-section { - background: white; - border-radius: 16px; - padding: 20px; - margin-bottom: 20px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - - .section-title { - font-size: 18px; - font-weight: bold; - color: #333; - margin-bottom: 16px; - display: block; - } - - :global { - .nut-cell { - background: #f8f9fa; - border-radius: 12px; - margin-bottom: 8px; - border: none; - - &:last-child { - margin-bottom: 0; - } - - .nut-cell__title { - font-weight: 500; - color: #555; - } - - .nut-cell__value { - font-weight: bold; - color: #007bff; - } - } - } - } - - .action-section { - background: white; - border-radius: 16px; - padding: 20px; - margin-bottom: 20px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - - .section-title { - font-size: 18px; - font-weight: bold; - color: #333; - margin-bottom: 16px; - display: block; - } - - .button-group { - display: flex; - flex-direction: column; - gap: 12px; - - .custom-button { - border-radius: 12px; - font-weight: 500; - height: 48px; - border: none; - margin-bottom: 12px; - font-size: 16px; - color: white; - - &.primary-btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - } - - &.success-btn { - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); - } - - &.warning-btn { - background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); - } - - &:disabled { - opacity: 0.6; - } - } - - // 保留 NutUI 按钮样式(备用) - :global { - .nut-button { - border-radius: 12px; - font-weight: 500; - height: 48px; - border: none; - - &--primary { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - } - - &--success { - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); - } - - &--warning { - background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); - } - - &:disabled { - opacity: 0.6; - } - } - } - } - } - - .loading-section { - background: white; - border-radius: 16px; - padding: 20px; - margin-bottom: 20px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - text-align: center; - - .loading-text { - font-size: 16px; - color: #666; - margin-bottom: 12px; - display: block; - } - - :global { - .nut-progress { - .nut-progress-outer { - background: #f0f0f0; - border-radius: 10px; - } - - .nut-progress-inner { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 10px; - } - } - } - } - - .tips-section { - background: rgba(255, 255, 255, 0.95); - border-radius: 16px; - padding: 20px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - - .tips-title { - font-size: 18px; - font-weight: bold; - color: #333; - margin-bottom: 16px; - display: block; - } - - .tips-content { - .tip-item { - font-size: 14px; - color: #666; - line-height: 1.6; - margin-bottom: 8px; - display: block; - - &:last-child { - margin-bottom: 0; - } - } - } - } -} - -// 响应式设计 -@media (max-width: 768px) { - .index-page { - padding: 16px; - - .page-header { - .page-title { - font-size: 24px; - } - - .page-subtitle { - font-size: 14px; - } - } - - .user-card, - .stats-section, - .action-section, - .loading-section, - .tips-section { - padding: 16px; - margin-bottom: 16px; - } - } -} diff --git a/src/pages/list/FilterPopup.tsx b/src/pages/list/FilterPopup.tsx index 1e31276..05af903 100644 --- a/src/pages/list/FilterPopup.tsx +++ b/src/pages/list/FilterPopup.tsx @@ -1,46 +1,81 @@ import { Popup } from "@nutui/nutui-react-taro"; import Range from "../../components/Range"; -import Bubble, { BubbleOption } from "../../components/Bubble"; +import Bubble from "../../components/Bubble"; import styles from "./filterPopup.module.scss"; -import TitleComponent from "src/components/Title"; +import TitleComponent from "@/components/Title"; +import { Button } from "@nutui/nutui-react-taro"; +import { Image } from "@tarojs/components"; +import img from "../../config/images"; +import { useListStore } from "src/store/listStore"; +import { FilterPopupProps } from "../../../types/list/types"; +// 场地 +import CourtType from "@/components/CourtType"; +// 玩法 +import GamePlayType from "@/components/GamePlayType"; +import { useDictionaryActions } from "@/store/dictionaryStore"; +import { useMemo } from "react"; -const timeOptions: BubbleOption[] = [ - { id: 1, label: "晨间 6:00-10:00", value: "morning" }, - { id: 2, label: "上午 10:00-12:00", value: "forenoon" }, - { id: 3, label: "中午 12:00-14:00", value: "noon" }, - { id: 4, label: "下午 14:00-18:00", value: "afternoon" }, - { id: 5, label: "晚上 18:00-22:00", value: "evening" }, - { id: 6, label: "夜间 22:00-24:00", value: "night" }, -]; +const FilterPopup = (props: FilterPopupProps) => { + const { + loading, + onCancel, + onConfirm, + onChange, + filterOptions, + onClear, + visible, + onClose, + statusNavbarHeigh, + } = props; -const locationOptions: BubbleOption[] = [ - { id: 1, label: "室内", value: "1" }, - { id: 2, label: "室外", value: "2" }, - { id: 3, label: "半室外", value: "3" }, -]; + const store = useListStore() || {}; + const { getDictionaryValue } = useDictionaryActions() || {}; + const { timeBubbleData } = store; + + const handleOptions = (dictionaryValue: []) => { + return dictionaryValue?.map((item) => ({ label: item, value: item })) || []; + }; + + const courtType = getDictionaryValue("court_type") || []; + const locationOptions = useMemo(() => { + return courtType ? handleOptions(courtType) : []; + }, [courtType]); + + const gamePlay = getDictionaryValue("game_play") || []; + const gamePlayOptions = useMemo(() => { + return gamePlay ? handleOptions(gamePlay) : []; + }, [gamePlay]); + + const handleFilterChange = (name, value) => { + onChange({ [name]: value }); + }; + + const handleClearFilter = () => { + onClear(); + onCancel(); + }; -const FilterPopup = () => { return ( <> { - // setShowTop(false) - }} + visible={visible} + onClose={onClose} + style={{ marginTop: statusNavbarHeigh + "px" }} + overlayStyle={{ marginTop: statusNavbarHeigh + "px" }} >
{/* 时间气泡选项 */} {}} - onChange={(value) => {}} + options={timeBubbleData} + value={filterOptions?.time} + onChange={handleFilterChange} layout="grid" size="small" columns={3} + name="time" /> {/* 范围选择 */} @@ -49,19 +84,58 @@ const FilterPopup = () => { max={5.0} step={0.5} className={styles.filterPopupRange} + onChange={handleFilterChange} + value={filterOptions?.ntrp} + name="ntrp" /> {/* 场次气泡选项 */} -
- + {/*
+ } + /> {}} - onChange={(value) => {}} + value={filterOptions?.site} + onChange={handleFilterChange} layout="grid" size="small" columns={3} + name="site" /> +
*/} + {/* CourtType */} + + {/* 玩法 */} + + {/* 按钮 */} +
+ +
diff --git a/src/pages/list/filterPopup.module.scss b/src/pages/list/filterPopup.module.scss index c1e834a..9305f21 100644 --- a/src/pages/list/filterPopup.module.scss +++ b/src/pages/list/filterPopup.module.scss @@ -1,8 +1,35 @@ .filterPopupWrapper { + position: relative; $m18: 18px; padding: $m18; + .filterPopupRange { margin-top: $m18; margin-bottom: $m18; } + + .filterPopupBtnWrapper { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + position: sticky; + bottom: 0; + background-color: #ffffff; + padding: 8px 0; + + .btn { + flex: 1; + } + + --nutui-button-border-width: 0.5px; + --nutui-button-default-border-color: #0000000F; + --nutui-button-border-radius: 16px; + --nutui-button-default-height: 44px; + --nutui-button-default-color: #000000; + .confirm { + --nutui-button-default-color: #ffffff; + --nutui-button-default-background-color: #000000; + } + } } \ No newline at end of file diff --git a/src/pages/list/index.config.ts b/src/pages/list/index.config.ts index 17f6db0..d49b1ea 100644 --- a/src/pages/list/index.config.ts +++ b/src/pages/list/index.config.ts @@ -1,5 +1,6 @@ export default definePageConfig({ navigationBarTitleText: '', enablePullDownRefresh: true, - backgroundTextStyle: 'dark' + backgroundTextStyle: 'dark', + navigationStyle: 'custom', }) diff --git a/src/pages/list/index.module.scss b/src/pages/list/index.module.scss new file mode 100644 index 0000000..7a5f4ac --- /dev/null +++ b/src/pages/list/index.module.scss @@ -0,0 +1,26 @@ +.listPage { + background-color: #fafafa; + + .listTopSearchWrapper { + padding: 0 15px; + position: sticky; + background: #fefefe; + z-index: 999; + } + + .isScroll { + border-bottom: 0.5px solid #0000000F; + } + + .listTopFilterWrapper { + display: flex; + align-items: center; + padding-top: 10px; + padding-bottom: 10px; + gap: 5px; + } + + .menuFilter { + padding: 0; + } +} \ No newline at end of file diff --git a/src/pages/list/index.scss b/src/pages/list/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/list/index.tsx b/src/pages/list/index.tsx index 44d601d..8e7edb6 100644 --- a/src/pages/list/index.tsx +++ b/src/pages/list/index.tsx @@ -1,29 +1,51 @@ -import ListItem from "../../components/ListItem"; -import List from "../../components/List"; -import Bubble from "../../components/Bubble/example"; -import Range from "../../components/Range/example"; -import Menu from "../../components/Menu/example"; -import CityFilter from "../../components/CityFilter/example"; +import Menu from "../../components/Menu"; +import CityFilter from "../../components/CityFilter"; import SearchBar from "../../components/SearchBar"; import FilterPopup from "./FilterPopup"; +import styles from "./index.module.scss"; import { useEffect } from "react"; -import Taro from "@tarojs/taro"; -import { - useTennisMatches, - useTennisLoading, - useTennisError, - useTennisLastRefresh, - useTennisActions, -} from "../../store/listStore"; -import "./index.scss"; +import Taro, { usePageScroll, useReachBottom } from "@tarojs/taro"; +import { useListStore } from "@/store/listStore"; +import { useGlobalState } from "@/store/global"; +import { View } from "@tarojs/components"; +import CustomerNavBar from "@/components/CustomNavbar"; +import GuideBar from "@/components/GuideBar"; +import ListContainer from "@/container/listContainer"; const ListPage = () => { // 从 store 获取数据和方法 - const matches = useTennisMatches(); - const loading = useTennisLoading(); - const error = useTennisError(); - const lastRefreshTime = useTennisLastRefresh(); - const { fetchMatches, refreshMatches, clearError } = useTennisActions(); + const store = useListStore() || {}; + + const { statusNavbarHeightInfo } = useGlobalState() || {}; + const { + isShowFilterPopup, + error, + matches, + loading, + fetchMatches, + refreshMatches, + updateState, + filterCount, + updateFilterOptions, // 更新筛选条件 + filterOptions, + clearFilterOptions, + distanceData, + quickFilterData, + distanceQuickFilter, + isScrollTop, + } = store; + + usePageScroll((res) => { + if (res?.scrollTop > 0 && !isScrollTop) { + updateState({ isScrollTop: true }); + } + }); + + useReachBottom(() => { + console.log("触底了"); + // 调用 store 的加载更多方法 + // loadMoreMatches(); + }); useEffect(() => { // 页面加载时获取数据 @@ -59,147 +81,97 @@ const ListPage = () => { }); }); - // 错误处理 - useEffect(() => { - if (error) { - Taro.showToast({ - title: error, - icon: "error", - duration: 2000, - }); - // 3秒后自动清除错误 - setTimeout(() => { - clearError(); - }, 3000); - } - }, [error, clearError]); - - // 格式化时间显示 - const formatRefreshTime = (timeString: string | null) => { - if (!timeString) return ""; - const date = new Date(timeString); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const minutes = Math.floor(diff / 60000); - - if (minutes < 1) return "刚刚"; - if (minutes < 60) return `${minutes}分钟前`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}小时前`; - return date.toLocaleDateString(); + const toggleShowPopup = () => { + updateState({ isShowFilterPopup: !isShowFilterPopup }); }; - // 加载状态显示 - if (loading && matches.length === 0) { - return ( -
-
加载中...
-
- 正在获取网球比赛数据 -
-
- ); - } + /** + * @description 更新筛选条件 + * @param {Record} params 筛选项 + */ + const handleUpdateFilterOptions = (params: Record) => { + updateFilterOptions(params); + }; - // 错误状态显示 - if (error && matches.length === 0) { - return ( -
-
加载失败
-
- {error} -
- -
- ); - } + const handleSearchChange = () => {}; + + // 距离筛选 + const handleDistanceOrQuickChange = (name, value) => { + updateState({ + distanceQuickFilter: { + ...distanceQuickFilter, + [name]: value, + }, + }); + }; return ( -
- - {/* 综合筛选 */} -
- -
- {/* 筛选 */} -
- {/* 全城筛选 */} - - {/* 智能排序 */} - -
+ <> + - - - {/* 列表内容 */} - - {matches.map((match, index) => ( - - ))} - - - {/* 空状态 */} - {!loading && matches.length === 0 && ( -
+ -
暂无比赛数据
- -
- )} -
+ 0} + filterCount={filterCount} + onChange={handleSearchChange} + /> + {/* 综合筛选 */} + {isShowFilterPopup && ( +
+ +
+ )} + {/* 筛选 */} +
+ {/* 全城筛选 */} + + {/* 智能排序 */} + +
+ + + {/* 列表内容 */} + + + + + ); }; diff --git a/src/pages/login/index/index.scss b/src/pages/login/index/index.scss index 6436bd6..6f2f2f0 100644 --- a/src/pages/login/index/index.scss +++ b/src/pages/login/index/index.scss @@ -370,7 +370,7 @@ font-family: 'PingFang SC'; font-weight: 400; font-size: 16px; - color: #07C160; + color: #000000; text-decoration: underline; cursor: pointer; transition: all 0.3s ease; diff --git a/src/pages/login/index/index.tsx b/src/pages/login/index/index.tsx index 09951c0..ac0cbef 100644 --- a/src/pages/login/index/index.tsx +++ b/src/pages/login/index/index.tsx @@ -10,7 +10,7 @@ const LoginPage: React.FC = () => { const [show_terms_layer, set_show_terms_layer] = useState(false); // 微信授权登录 - const handle_wechat_login = async () => { + const handle_wechat_login = async (e: any) => { if (!agree_terms) { set_show_terms_layer(true); Taro.showToast({ @@ -21,9 +21,20 @@ const LoginPage: React.FC = () => { return; } + // 检查是否获取到手机号 + if (!e.detail || !e.detail.code) { + Taro.showToast({ + title: '获取手机号失败,请重试', + icon: 'none', + duration: 2000 + }); + return; + } + set_is_loading(true); try { - const response = await wechat_auth_login(); + // 传递手机号code给登录服务 + const response = await wechat_auth_login(e.detail.code); if (response.success) { save_login_state(response.token!, response.user_info!); @@ -99,7 +110,7 @@ const LoginPage: React.FC = () => { @@ -123,7 +134,8 @@ const LoginPage: React.FC = () => { {/* 微信快捷登录 */} - + )} diff --git a/src/pages/login/terms/index.scss b/src/pages/login/terms/index.scss index 61ac7c9..d163398 100644 --- a/src/pages/login/terms/index.scss +++ b/src/pages/login/terms/index.scss @@ -157,7 +157,7 @@ position: relative; z-index: 5; flex: 1; - padding: 0px 24px ; + box-sizing: border-box; overflow-y: auto; @@ -181,6 +181,7 @@ line-height: 1.43em; color: #000000; margin-bottom: 24px; + } // 条款详细内容 @@ -192,6 +193,14 @@ color: #000000; margin-bottom: 40px; white-space: pre-line; + padding: 0px 24px ; + + .terms_first_line, + span.terms_first_line { + font-weight: 500; + display: block; + margin-bottom: 16px; + } } // 底部按钮 diff --git a/src/pages/login/terms/index.tsx b/src/pages/login/terms/index.tsx index f5b94b5..591d437 100644 --- a/src/pages/login/terms/index.tsx +++ b/src/pages/login/terms/index.tsx @@ -24,7 +24,7 @@ const TermsPage: React.FC = () => { case 'terms': setPageTitle('条款和条件'); setTermsTitle('《开场的条款和条件》'); - setTermsContent(`欢迎使用本平台(以下简称"本平台")发布与参与网球活动。为保障您的权益,请您务必仔细阅读并理解以下服务条款。 + setTermsContent(`欢迎使用本平台(以下简称"本平台")发布与参与网球活动。为保障您的权益,请您务必仔细阅读并理解以下服务条款。 一、服务内容 1. 本平台为用户提供活动发布、报名、聊天室沟通、活动提醒等服务。 @@ -70,7 +70,7 @@ const TermsPage: React.FC = () => { case 'binding': setPageTitle('微信号绑定协议'); setTermsTitle('《开场与微信号绑定协议》'); - setTermsContent(`欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。 + setTermsContent(`欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。 一、绑定服务说明 1. 本平台提供微信账号绑定服务,用户可通过微信快捷登录方式使用平台功能。 @@ -115,7 +115,7 @@ const TermsPage: React.FC = () => { case 'privacy': setPageTitle('隐私权政策'); setTermsTitle('《隐私权政策》'); - setTermsContent(`本平台(以下简称"我们")非常重视用户的隐私保护,本隐私权政策说明了我们如何收集、使用、存储和保护您的个人信息。 + setTermsContent(`本平台(以下简称"我们")非常重视用户的隐私保护,本隐私权政策说明了我们如何收集、使用、存储和保护您的个人信息。 一、信息收集 1. 注册信息:包括手机号码、微信账号、昵称、头像等基本信息。 @@ -190,13 +190,14 @@ const TermsPage: React.FC = () => { {/* 条款详细内容 */} - - {termsContent} - + - + ); }; diff --git a/src/pages/message/index.config.ts b/src/pages/message/index.config.ts new file mode 100644 index 0000000..42d7ad1 --- /dev/null +++ b/src/pages/message/index.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '消息', + // navigationBarBackgroundColor: '#FAFAFA', + navigationStyle: 'custom', +}) \ No newline at end of file diff --git a/src/pages/message/index.scss b/src/pages/message/index.scss new file mode 100644 index 0000000..092c41b --- /dev/null +++ b/src/pages/message/index.scss @@ -0,0 +1,80 @@ +@use '~@/scss/images.scss' as img; + +$--Backgrounds-Primary: '#fff'; + +.message-container { + width: 100%; + height: 100vh; + background: radial-gradient(227.15% 100% at 50% 0%, #EEFFDC 0%, #FFF 36.58%), var(--Backgrounds-Primary, #FFF); + // padding-top: 100px; + box-sizing: border-box; + + .custom-navbar { + height: 56px; /* 通常与原生导航栏高度一致 */ + display: flex; + align-items: center; + justify-content: flex-start; + // background-color: #fff; + color: #000; + padding-top: 44px; /* 适配状态栏 */ + position: sticky; + top: 0; + z-index: 100; + + .message-navigator { + position: relative; + left: 15px; + top: -2px; + width: 80px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + + .message-navigator-avatar { + width: 32px; + height: 32px; + } + + .message-navigator-title { + font-size: 16px; + font-weight: 500; + color: #000; + } + } + } + + .message-content { + + .message-content-list { + display: flex; + flex-direction: column; + padding: 10px 15px; + box-sizing: border-box; + gap: 12px; + + .message-item { + padding: 10px; + // border: 1px solid rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 10px; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); + background-color: rgba(255, 255, 255, 0.5); + border-radius: 10px; + } + + .message-item-title { + font-size: 16px; + font-weight: 500; + color: #000; + } + + .message-item-content { + font-size: 14px; + color: #666; + } + } + } +} \ No newline at end of file diff --git a/src/pages/message/index.tsx b/src/pages/message/index.tsx new file mode 100644 index 0000000..18f4130 --- /dev/null +++ b/src/pages/message/index.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { View, Text, ScrollView } from '@tarojs/components' +import { Avatar } from '@nutui/nutui-react-taro' +import Taro from '@tarojs/taro' +import GuideBar from '@/components/GuideBar' +// import img from '@/config/images' +import './index.scss' + +const Personal = () => { + const messageList = Array(10).fill(0).map((_, index) => ({ + id: index + 1, + title: `消息${index + 1}消息${index + 1}消息${index + 1}消息${index + 1}`, + content: Array(Math.round(Math.random() * 40)).fill(0).map((_, index) => `消息${index + 1}`).join(''), + })) + + return ( + + + + + 消息 + + + + + {messageList.map((item) => ( + + + {item.title} + + + {item.content} + + + ))} + + + + + ) +} + +export default Personal \ No newline at end of file diff --git a/src/pages/orderCheck/index.config.ts b/src/pages/orderCheck/index.config.ts new file mode 100644 index 0000000..e8419ed --- /dev/null +++ b/src/pages/orderCheck/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '订单确认', + navigationBarBackgroundColor: '#FAFAFA' +}) \ No newline at end of file diff --git a/src/pages/orderCheck/index.scss b/src/pages/orderCheck/index.scss new file mode 100644 index 0000000..e9e7c6b --- /dev/null +++ b/src/pages/orderCheck/index.scss @@ -0,0 +1 @@ +@use '~@/scss/images.scss' as img; \ No newline at end of file diff --git a/src/pages/orderCheck/index.tsx b/src/pages/orderCheck/index.tsx new file mode 100644 index 0000000..8165946 --- /dev/null +++ b/src/pages/orderCheck/index.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { View, Text } from '@tarojs/components' + +const OrderCheck = () => { + return ( + + OrderCheck + + ) +} + +export default OrderCheck \ No newline at end of file diff --git a/src/pages/personal/index.config.ts b/src/pages/personal/index.config.ts new file mode 100644 index 0000000..ca1f786 --- /dev/null +++ b/src/pages/personal/index.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '个人中心', + // navigationBarBackgroundColor: '#FAFAFA', + navigationStyle: 'custom', +}) \ No newline at end of file diff --git a/src/pages/personal/index.scss b/src/pages/personal/index.scss new file mode 100644 index 0000000..c2475fe --- /dev/null +++ b/src/pages/personal/index.scss @@ -0,0 +1,35 @@ +@use '~@/scss/images.scss' as img; + +$--Backgrounds-Primary: '#fff'; + +.personal-container { + width: 100%; + height: 100vh; + background: radial-gradient(227.15% 100% at 50% 0%, #EEFFDC 0%, #FFF 36.58%), var(--Backgrounds-Primary, #FFF); + padding-top: 100px; + box-sizing: border-box; + + .personal-navigator { + position: fixed; + left: 10px; + top: 54px; + width: 32px; + height: 32px; + + .personal-navigator-back { + width: 100%; + height: 100%; + } + } + + .personal-content { + width: 100%; + height: calc(100vh - 300px); + display: flex; + justify-content: center; + align-items: center; + font-size: 32px; + font-weight: 500; + color: #000; + } +} \ No newline at end of file diff --git a/src/pages/personal/index.tsx b/src/pages/personal/index.tsx new file mode 100644 index 0000000..042bc8e --- /dev/null +++ b/src/pages/personal/index.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { View, Text, Image } from '@tarojs/components' +import Taro, { useRouter } from '@tarojs/taro' +import GuideBar from '@/components/GuideBar' +import img from '@/config/images' +import './index.scss' + +const Personal = () => { + const { params } = useRouter() + const { id } = params + + const handleBack = () => { + Taro.navigateBack() + } + + return ( + + {id && ( + + + + )} + + Personal + + + + ) +} + +export default Personal \ No newline at end of file diff --git a/src/pages/publishBall/components/SelectStadium/StadiumDetail.tsx b/src/pages/publishBall/components/SelectStadium/StadiumDetail.tsx index 068ef45..00ec828 100644 --- a/src/pages/publishBall/components/SelectStadium/StadiumDetail.tsx +++ b/src/pages/publishBall/components/SelectStadium/StadiumDetail.tsx @@ -3,7 +3,8 @@ import Taro from '@tarojs/taro' import { View, Text, Image } from '@tarojs/components' import images from '@/config/images' import TextareaTag from '@/components/TextareaTag' -import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload' +// import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload' +import UploadCover, { type CoverImageValue } from '@/components/UploadCover' import { useDictionaryActions } from '@/store/dictionaryStore' import './StadiumDetail.scss' @@ -18,7 +19,7 @@ export interface Stadium { court_surface?: string description?: string description_tag?: string[] - venue_image_list?: CoverImage[] + venue_image_list?: CoverImageValue[] } interface StadiumDetailProps { @@ -103,7 +104,7 @@ const StadiumDetail = forwardRef(({ court_type: court_type[0] || '', court_surface: court_surface[0] || '', additionalInfo: '', - venue_image_list: [] as CoverImage[], + venue_image_list: [] as CoverImageValue[], description:{ description: '', description_tag: [] @@ -216,9 +217,21 @@ const StadiumDetail = forwardRef(({ if (item.type === 'image') { return ( - updateFormData(item.prop, images)} + { + console.log(value, 'value') + if (value instanceof Function) { + const newValue = value(formData[item.prop]) + console.log(newValue, 'newValue') + updateFormData(item.prop, newValue) + } else { + updateFormData(item.prop, value) + } + }} + maxCount={9} + source={['album', 'history', 'preset']} + align='left' /> ) @@ -230,4 +243,4 @@ const StadiumDetail = forwardRef(({ ) }) -export default StadiumDetail \ No newline at end of file +export default StadiumDetail \ No newline at end of file diff --git a/src/pages/publishBall/index.tsx b/src/pages/publishBall/index.tsx index 1a10887..97f8f69 100644 --- a/src/pages/publishBall/index.tsx +++ b/src/pages/publishBall/index.tsx @@ -8,40 +8,40 @@ import PublishForm from './publishForm' import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema'; import { PublishBallFormData } from '../../../types/publishBall'; import PublishService from '@/services/publishService'; -import { getNextHourTime, getEndTime } from '@/utils/timeUtils'; +import { getNextHourTime, getEndTime, delay } from '@/utils'; import images from '@/config/images' import styles from './index.module.scss' const defaultFormData: PublishBallFormData = { - title: '', - image_list: ['https://static-o.oss-cn-shenzhen.aliyuncs.com/images/tpbj/tpss10.jpg'], + title: '', + image_list: ['https://static-o.oss-cn-shenzhen.aliyuncs.com/images/tpbj/tpss10.jpg'], timeRange: { - start_time: getNextHourTime(), + start_time: getNextHourTime(), end_time: getEndTime(getNextHourTime()) }, - activityInfo: { + activityInfo: { play_type: '不限', price: '', - venue_id: null, + venue_id: null, location_name: '', - location: '', - latitude: '', + location: '', + latitude: '', longitude: '', - court_type: '', - court_surface: '', - venue_description_tag: [], - venue_description: '', - venue_image_list: [], + court_type: '', + court_surface: '', + venue_description_tag: [], + venue_description: '', + venue_image_list: [], }, players: [1, 4], skill_level: [1.0, 5.0], descriptionInfo: { - description: '', - description_tag: [], + description: '', + description_tag: [], }, - is_substitute_supported: true, - is_wechat_contact: true, - wechat_contact: '14223332214' + is_substitute_supported: true, + is_wechat_contact: true, + wechat_contact: '14223332214' } const PublishBall: React.FC = () => { @@ -72,7 +72,7 @@ const PublishBall: React.FC = () => { const [formData, setFormData] = useState([ defaultFormData ]) - + // 删除确认弹窗状态 const [deleteConfirm, setDeleteConfirm] = useState<{ visible: boolean; @@ -225,6 +225,13 @@ const PublishBall: React.FC = () => { title: '发布成功', icon: 'success' }) + delay(1000) + // 如果是个人球局,则跳转到详情页,并自动分享 + // 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰 + Taro.navigateTo({ + // @ts-expect-error: id + url: `/pages/detail/index?id=${res.data.id || 1}&from=publish&autoShare=1` + }) } else { Taro.showToast({ title: res.message, @@ -268,12 +275,16 @@ const PublishBall: React.FC = () => { {/* 活动类型切换 */} +<<<<<<< HEAD {/* >>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034 value={activityType} onChange={handleActivityTypeChange} /> */} - + { formData.map((item, index) => ( @@ -283,19 +294,19 @@ const PublishBall: React.FC = () => { 第{index + 1}场 - showDeleteConfirm(index)} > - + - + {index > 0 && ( - handleCopyPrevious(index)} > 复制上一场 @@ -304,10 +315,10 @@ const PublishBall: React.FC = () => { )} - updateFormData(key, value, index)} - optionsConfig={publishBallFormSchema} + updateFormData(key, value, index)} + optionsConfig={publishBallFormSchema} /> )) @@ -323,6 +334,7 @@ const PublishBall: React.FC = () => { {/* 删除确认弹窗 */} +<<<<<<< HEAD { contentTitle="确认移除该场次?" contentDesc="该操作不可恢复" /> +======= + {deleteConfirm.visible && ( + + + 确认移除该场次? + 该操作不可恢复 + + + + + + + )} +>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034 {/* 完成按钮 */} diff --git a/src/pages/publishBall/publishForm.tsx b/src/pages/publishBall/publishForm.tsx index 0c97aed..2205326 100644 --- a/src/pages/publishBall/publishForm.tsx +++ b/src/pages/publishBall/publishForm.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react' import { View, Text } from '@tarojs/components' -import { ImageUpload, Range, TimeSelector, TextareaTag, NumberInterval, TitleTextarea, FormSwitch } from '../../components' +import { ImageUpload, Range, TimeSelector, TextareaTag, NumberInterval, TitleTextarea, FormSwitch, UploadCover } from '../../components' import FormBasicInfo from './components/FormBasicInfo' import { type CoverImage } from '../../components/index.types' import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema' @@ -22,18 +22,22 @@ const componentMap = { [FieldType.WECHATCONTACT]: WechatSwitch, } -const PublishForm: React.FC<{ - formData: PublishBallFormData, - onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void, +const PublishForm: React.FC<{ + formData: PublishBallFormData, + onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void, optionsConfig: FormFieldConfig[] }> = ({ formData, onChange, optionsConfig }) => { const [coverImages, setCoverImages] = useState([]) - + // 字典数据相关 const { getDictionaryValue } = useDictionaryActions() // 处理封面图片变化 - const handleCoverImagesChange = (images: CoverImage[]) => { - setCoverImages(images) + const handleCoverImagesChange = (fn: (images: CoverImage[]) => CoverImage[]) => { + if (fn instanceof Function) { + setCoverImages(fn(coverImages)) + } else { + setCoverImages(fn) + } } // 更新表单数据 @@ -70,7 +74,7 @@ const PublishForm: React.FC<{ }) } } - + // 如果是补充要求,从字典获取选项 if (item.prop === 'descriptionInfo') { const descriptionOptions = getDictionaryOptions('publishing_requirements', []) @@ -79,7 +83,7 @@ const PublishForm: React.FC<{ options: descriptionOptions } } - + return item }) } @@ -121,7 +125,7 @@ const PublishForm: React.FC<{ - + // 获取动态表单配置 const dynamicConfig = getDynamicFormConfig() @@ -140,8 +144,8 @@ const PublishForm: React.FC<{ } if (item.type === FieldType.UPLOADIMAGE) { /* 活动封面 */ - return @@ -182,7 +186,7 @@ const PublishForm: React.FC<{ value={formData[item.prop]} onChange={(value) => updateFormData(item.prop as keyof PublishBallFormData, value)} {...optionProps} - placeholder={item.placeholder} + placeholder={item.placeholder} /> diff --git a/src/pages/userInfo/myself/index.config.ts b/src/pages/userInfo/myself/index.config.ts new file mode 100644 index 0000000..eb44728 --- /dev/null +++ b/src/pages/userInfo/myself/index.config.ts @@ -0,0 +1,9 @@ +export default { + navigationBarTitleText: '个人主页', + navigationBarBackgroundColor: '#FFFFFF', + navigationBarTextStyle: 'black', + backgroundColor: '#FAFAFA', + enablePullDownRefresh: false, + disableScroll: false, + navigationStyle: 'custom' +} \ No newline at end of file diff --git a/src/pages/userInfo/myself/index.scss b/src/pages/userInfo/myself/index.scss new file mode 100644 index 0000000..2aa9ace --- /dev/null +++ b/src/pages/userInfo/myself/index.scss @@ -0,0 +1,600 @@ +// 个人页面样式 +.myself_page { + min-height: 100vh; + background: radial-gradient(circle at 50% 0%, rgba(238, 255, 220, 1) 0%, rgba(255, 255, 255, 1) 37%); + position: relative; + overflow: hidden; + box-sizing: border-box; +} + +// 主要内容区域 +.main_content { + position: relative; + z-index: 5; + flex: 1; + margin-top: 0; + box-sizing: border-box; + overflow-y: auto; + padding: 15px 15px 15px; + + // 用户信息区域 + .user_info_section { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 16px; + + // 基本信息 + .basic_info { + display: flex; + align-items: center; + gap: 16px; + + .avatar_container { + width: 64px; + height: 64px; + border-radius: 50%; + overflow: hidden; + box-shadow: 0px 8px 20px 0px rgba(0, 0, 0, 0.12), 0px 0px 1px 0px rgba(0, 0, 0, 0.2); + + .avatar { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .info_container { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + + .nickname { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 20px; + line-height: 1.4em; + letter-spacing: 1.9%; + color: #000000; + } + + .join_date { + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 14px; + line-height: 1.4em; + letter-spacing: 2.7%; + color: rgba(0, 0, 0, 0.35); + } + } + } + + // 统计数据 + .stats_section { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; + + .stats_container { + display: flex; + align-items: center; + gap: 20px; + + .stat_item { + display: flex; + flex-direction: column; + align-items: center; + + .stat_number { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 18px; + line-height: 1.4em; + letter-spacing: 2.1%; + color: rgba(0, 0, 0, 0.85); + } + + .stat_label { + font-family: 'PingFang SC'; + font-weight: 500; + font-size: 12px; + line-height: 1.4em; + letter-spacing: 3.2%; + color: rgba(0, 0, 0, 0.35); + } + } + } + + .action_buttons { + display: flex; + align-items: center; + gap: 12px; + + + + .follow_button { + display: flex; + align-items: center; + gap: 4px; + padding: 12px 16px 12px 12px; + height: 40px; + background: #000000; + border: 0.5px solid rgba(0, 0, 0, 0.06); + border-radius: 999px; + cursor: pointer; + transition: all 0.3s ease; + + &.following { + background: #FFFFFF; + color: #000000; + } + + .button_icon { + width: 20px; + height: 20px; + } + + .button_text { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 14px; + line-height: 1.4em; + color: #FFFFFF; + + .following & { + color: #000000; + } + } + } + + .message_button { + width: 40px; + height: 40px; + background: #FFFFFF; + border: 0.5px solid rgba(0, 0, 0, 0.12); + border-radius: 999px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + + .button_icon { + width: 18px; + height: 18px; + } + } + + .edit_button { + min-width: 60px; + height: 40px; + background: #FFFFFF; + border: 0.5px solid rgba(0, 0, 0, 0.12); + border-radius: 999px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + padding: 0 12px; + + .button_text { + font-family: 'PingFang SC'; + font-weight: 500; + font-size: 14px; + line-height: 1.4em; + color: #000000; + } + } + + .share_button { + min-width: 60px; + height: 40px; + background: #FFFFFF; + border: 0.5px solid rgba(0, 0, 0, 0.12); + border-radius: 999px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + padding: 0 12px; + margin: 0px !important; + + .button_text { + font-family: 'PingFang SC'; + font-weight: 500; + font-size: 14px; + line-height: 1.4em; + color: #000000; + } + } + } + } + + // 标签和简介 + .tags_bio_section { + display: flex; + flex-direction: column; + gap: 10px; + + .tags_container { + display: flex; + gap: 8px; + flex-wrap: wrap; + + .tag_item { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 8px; + height: 20px; + background: #FFFFFF; + border: 0.5px solid rgba(0, 0, 0, 0.16); + border-radius: 999px; + + .tag_icon { + width: 12px; + height: 12px; + } + + .tag_text { + font-family: 'PingFang SC'; + font-weight: 500; + font-size: 11px; + line-height: 1.8em; + letter-spacing: -2.1%; + color: #000000; + } + } + } + + .bio_text { + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 14px; + line-height: 1.571em; + color: rgba(0, 0, 0, 0.65); + white-space: pre-line; + } + } + + // 球局订单和收藏功能 + .quick_actions_section { + margin-bottom: 16px; + + .action_card { + display: flex; + align-items: center; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 12px; + box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06); + overflow: hidden; + + .action_content { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 20px 0; + cursor: pointer; + transition: background-color 0.3s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.02); + } + + .action_icon { + width: 20px; + height: 20px; + } + + .action_text { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 15px; + line-height: 1.4em; + color: #000000; + } + } + + .action_divider { + width: 1px; + height: 16px; + background: rgba(0, 0, 0, 0.06); + } + } + } + } + + // 球局类型标签页 + .game_tabs_section { + margin-bottom: 16px; + + .tab_container { + display: flex; + gap: 16px; + padding: 12px 15px; + + .tab_item { + padding: 12px 0; + cursor: pointer; + transition: all 0.3s ease; + + .tab_text { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 20px; + line-height: 1.4em; + letter-spacing: 1.9%; + color: rgba(0, 0, 0, 0.85); + transition: color 0.3s ease; + } + + &.active { + .tab_text { + color: #000000; + } + } + + &:not(.active) { + .tab_text { + color: rgba(0, 0, 0, 0.2); + } + } + } + } + } + + // 球局列表区域 + .game_list_section { + .date_header { + display: flex; + align-items: center; + gap: 4px; + padding: 10px 15px; + margin-bottom: 16px; + + .date_text { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 14px; + line-height: 1.4em; + letter-spacing: 2.71%; + color: rgba(0, 0, 0, 0.85); + } + + .separator { + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 18px; + line-height: 1.4em; + letter-spacing: 2.11%; + color: rgba(0, 0, 0, 0.35); + } + + .weekday_text { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 14px; + line-height: 1.4em; + letter-spacing: 2.71%; + color: rgba(0, 0, 0, 0.85); + } + } + + // 球局卡片 + .game_cards { + display: flex; + flex-direction: column; + gap: 5px; + padding: 0 5px 15px; + + .game_card { + background: #FFFFFF; + border: 0.5px solid rgba(0, 0, 0, 0.08); + border-radius: 20px; + padding: 0 0 12px; + box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06); + cursor: pointer; + transition: all 0.3s ease; + position: relative; + + &:active { + transform: scale(0.98); + } + + // 球局标题和类型 + .game_header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 15px 0; + + .game_title { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 16px; + line-height: 1.5em; + color: #000000; + } + + .game_type_icon { + width: 16px; + height: 16px; + + .type_icon { + width: 100%; + height: 100%; + } + } + } + + // 球局时间 + .game_time { + padding: 6px 15px 0; + + .time_text { + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 12px; + line-height: 1.5em; + color: rgba(60, 60, 67, 0.6); + } + } + + // 球局地点和类型 + .game_location { + display: flex; + align-items: center; + gap: 2px; + padding: 4px 15px 0; + + .location_text, + .type_text, + .distance_text { + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 12px; + line-height: 1.5em; + color: rgba(60, 60, 67, 0.6); + } + + .separator { + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 14px; + line-height: 1.3em; + color: rgba(60, 60, 67, 0.3); + } + } + + // 球局图片 + .game_images { + position: absolute; + top: 11px; + right: 5px; + width: 100px; + height: 100px; + box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.2); + + .game_image { + position: absolute; + width: 56.44px; + height: 56.44px; + border-radius: 9px; + border: 1.5px solid #FFFFFF; + + &:nth-child(1) { + top: 4.18px; + left: 19.18px; + } + + &:nth-child(2) { + top: 26.5px; + left: 38px; + width: 61.86px; + height: 61.86px; + } + + &:nth-child(3) { + top: 32.5px; + left: 0; + width: 62.04px; + height: 62.04px; + } + } + } + + // 球局信息标签 + .game_tags { + display: flex; + flex-direction: row; + gap: 6px; + padding: 8px 15px 0; + + .participants_info { + display: flex; + gap: 4px; + + .avatars { + display: flex; + align-items: center; + gap: -8px; + + .participant_avatar { + width: 20px; + height: 20px; + border-radius: 50%; + border: 1px solid #FFFFFF; + } + } + + .participants_count { + background: #FFFFFF; + border: 0.5px solid rgba(0, 0, 0, 0.16); + border-radius: 999px; + padding: 6px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + .count_text { + font-family: 'PingFang SC'; + font-weight: 500; + font-size: 11px; + line-height: 1.8em; + letter-spacing: -2.1%; + color: #000000; + } + } + } + + .game_info_tags { + display: flex; + gap: 4px; + + .info_tag { + background: #FFFFFF; + border: 0.5px solid rgba(0, 0, 0, 0.16); + border-radius: 999px; + padding: 6px 8px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + .tag_text { + font-family: 'PingFang SC'; + font-weight: 500; + font-size: 11px; + line-height: 1.8em; + letter-spacing: -2.1%; + color: #000000; + } + } + } + } + } + } + } +} + +// 底部指示器 +.home_indicator { + position: absolute; + bottom: 21px; + left: 50%; + transform: translateX(-50%); + width: 140px; + height: 5px; + background: #000000; + border-radius: 2.5px; + z-index: 10; +} \ No newline at end of file diff --git a/src/pages/userInfo/myself/index.tsx b/src/pages/userInfo/myself/index.tsx new file mode 100644 index 0000000..d4ca486 --- /dev/null +++ b/src/pages/userInfo/myself/index.tsx @@ -0,0 +1,377 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text, Image, ScrollView, Button } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import './index.scss'; + +// 用户信息接口 +interface UserInfo { + id: string; + nickname: string; + avatar: string; + join_date: string; + stats: { + following: number; + friends: number; + hosted: number; + participated: number; + }; + tags: string[]; + bio: string; + location: string; + occupation: string; + ntrp_level: string; +} + +// 球局记录接口 +interface GameRecord { + id: string; + title: string; + date: string; + time: string; + duration: string; + location: string; + type: string; + distance: string; + participants: { + avatar: string; + nickname: string; + }[]; + max_participants: number; + current_participants: number; + level_range: string; + game_type: string; + images: string[]; +} + +const MyselfPage: React.FC = () => { + // 获取页面参数 + const instance = Taro.getCurrentInstance(); + const user_id = instance.router?.params?.userid; + + // 判断是否为当前用户 + const is_current_user = !user_id; + + // 模拟用户数据 + const [user_info] = useState({ + id: '1', + nickname: '188的王晨', + avatar: require('../../../static/userInfo/default_avatar.svg'), + join_date: '2025年9月加入', + stats: { + following: 124, + friends: 24, + hosted: 7, + participated: 24 + }, + tags: ['上海黄浦', '互联网从业者', 'NTRP 4.0'], + bio: '网球入坑两年,偏好双打,正手进攻型选手\n平时在张江、世纪公园附近活动,欢迎约球!\n不卷分数,但认真对待每一拍,每一场球都想打得开心。有时候也会带相机来拍点照片📸', + location: '上海黄浦', + occupation: '互联网从业者', + ntrp_level: 'NTRP 4.0' + }); + + // 模拟球局数据 + const [game_records] = useState([ + { + id: '1', + title: '女生轻松双打', + date: '明天(周五)', + time: '下午5点', + duration: '2小时', + location: '仁恒河滨花园网球场', + type: '室外', + distance: '3.5km', + participants: [ + { avatar: require('../../../static/userInfo/user1.svg'), nickname: '用户1' }, + { avatar: require('../../../static/userInfo/user2.svg'), nickname: '用户2' } + ], + max_participants: 4, + current_participants: 2, + level_range: '2.0 至 2.5', + game_type: '双打', + images: [ + require('../../../static/userInfo/game1.svg'), + require('../../../static/userInfo/game2.svg'), + require('../../../static/userInfo/game3.svg') + ] + } + ]); + + // 关注状态 + const [is_following, setIsFollowing] = useState(false); + + // 当前激活的标签页 + const [active_tab, setActiveTab] = useState<'hosted' | 'participated'>('hosted'); + + // 处理关注/取消关注 + const handle_follow = () => { + setIsFollowing(!is_following); + Taro.showToast({ + title: is_following ? '已取消关注' : '关注成功', + icon: 'success', + duration: 1500 + }); + }; + + // 处理分享 + const handle_share = () => { + Taro.showShareMenu({ + withShareTicket: true + }); + }; + + // 处理返回 + const handle_back = () => { + Taro.navigateBack(); + }; + + // 处理编辑资料 + const handle_edit_profile = () => { + Taro.navigateTo({ + url: '/pages/userInfo/edit/index' + }); + }; + + // 处理球局详情 + const handle_game_detail = (game_id: string) => { + Taro.navigateTo({ + url: `/pages/game/detail/index?id=${game_id}` + }); + }; + + // 处理球局订单 + const handle_game_orders = () => { + Taro.navigateTo({ + url: '/pages/game/orders/index' + }); + }; + + // 处理收藏 + const handle_favorites = () => { + Taro.navigateTo({ + url: '/pages/game/favorites/index' + }); + }; + + return ( + + {/* 主要内容 */} + + {/* 用户信息区域 */} + + {/* 头像和基本信息 */} + + + + + + {user_info.nickname} + {user_info.join_date} + + + + {/* 统计数据 */} + + + + {user_info.stats.following} + 关注 + + + {user_info.stats.friends} + 球友 + + + {user_info.stats.hosted} + 主办 + + + {user_info.stats.participated} + 参加 + + + + {/* 只有非当前用户才显示关注按钮 */} + {!is_current_user && ( + + )} + {/* 只有非当前用户才显示消息按钮 */} + {!is_current_user && ( + + )} + {/* 只有当前用户才显示编辑按钮 */} + {is_current_user && ( + + )} + {/* 只有当前用户才显示分享按钮 */} + {is_current_user && ( + + )} + + + + {/* 标签和简介 */} + + + + + {user_info.location} + + + {user_info.occupation} + + + {user_info.ntrp_level} + + + {user_info.bio} + + + {/* 球局订单和收藏功能 */} + + + + + 球局订单 + + + + + 收藏 + + + + + + {/* 球局类型标签页 */} + + + setActiveTab('hosted')}> + 我主办的 + + setActiveTab('participated')}> + 我参与的 + + + + + {/* 球局列表 */} + + + 5月28日 + / + 星期三 + + + {/* 球局卡片 */} + + {game_records.map((game) => ( + handle_game_detail(game.id)} + > + {/* 球局标题和类型 */} + + {game.title} + + + + + + {/* 球局时间 */} + + + {game.date} {game.time} {game.duration} + + + + {/* 球局地点和类型 */} + + {game.location} + · + {game.type} + · + {game.distance} + + + {/* 球局图片 */} + + {game.images.map((image, index) => ( + + ))} + + + {/* 球局信息标签 */} + + + + {game.participants.map((participant, index) => ( + + ))} + + + + 报名人数 {game.current_participants}/{game.max_participants} + + + + + + {game.level_range} + + + {game.game_type} + + + + + ))} + + + + + ); +}; + +export default MyselfPage; diff --git a/src/scss/images.scss b/src/scss/images.scss index 32d40b4..28b8db1 100644 --- a/src/scss/images.scss +++ b/src/scss/images.scss @@ -16,7 +16,30 @@ $-images: ( 'icon-personal': '/publishBall/icon-personal.svg', 'icon-changda': '/publishBall/icon-changda.svg', 'icon-cost': '/publishBall/icon-cost.svg', - 'icon-remove': '/publishBall/icon-remove.svg' + 'icon-remove': '/publishBall/icon-remove.svg', + 'icon-arrow-left': '/detail/icon-arrow-left.svg', + 'icon-logo-go': '/detail/icon-logo-go.svg', + 'icon-search': '/publishBall/icon-search.svg', + 'icon-map': '/publishBall/icon-map.svg', + 'icon-stadium': '/publishBall/icon-stadium.svg', + 'icon-arrow-small': '/publishBall/icon-arrow-small.svg', + 'icon-map-search': '/publishBall/icon-map-search.svg', + 'icon-heartcircle': '/publishBall/icon-heartcircle.png', + 'icon-copy': '/publishBall/icon-arrow-right.svg', + 'icon-delete': '/publishBall/icon-delete.svg', + 'icon-circle-unselect': '/publishBall/icon-circle-unselect.svg', + 'icon-circle-select-ring': '/publishBall/icon-circle-select-ring.svg', + 'icon-circle-select-arrow': '/publishBall/icon-circle-select-arrow.svg', + 'icon-weather-sun': '/detail/icon-weather-sun.svg', + 'icon-detail-map': '/detail/icon-map.svg', + 'icon-detail-arrow-right': '/detail/icon-arrow-right.svg', + 'icon-detail-notice': '/detail/icon-notice.svg', + 'icon-detail-application-add': '/detail/icon-application-add.svg', + 'icon-detail-comment': '/detail/icon-comment.svg', + 'icon-detail-comment-dark': '/detail/icon-comment-dark.svg', + 'icon-detail-share': '/detail/icon-share-dark.svg', + 'icon-guide-bar-publish': '/common/guide-bar-publish.svg', + 'icon-navigator-back': '/common/navigator-back.svg', ) !default; // 图片获取函数 diff --git a/src/services/detailService.ts b/src/services/detailService.ts new file mode 100644 index 0000000..e971bf0 --- /dev/null +++ b/src/services/detailService.ts @@ -0,0 +1,41 @@ +import httpService from './httpService' +import type { ApiResponse } from './httpService' + +// 用户接口 +export interface GameDetail { + id: number, + title: string, + venue_id: number, + creator_id: number, + game_date: string, + start_time: string, + end_time: string, + max_participants: number, + current_participants: number, + ntrp_level: string, + play_style: string, + description: string, + status: string, + created_at: string, + updated_at: string, +} + +// 响应接口 +export interface Response { + code: string + message: string + data: GameDetail +} + +// 发布球局类 +class GameDetailService { + // 用户登录 + async getDetail(id: number): Promise> { + return httpService.post('/games/detail', { id }, { + showLoading: true, + }) + } +} + +// 导出认证服务实例 +export default new GameDetailService() \ No newline at end of file diff --git a/src/services/httpService.ts b/src/services/httpService.ts index 9b7a5fd..fcee8f3 100644 --- a/src/services/httpService.ts +++ b/src/services/httpService.ts @@ -15,6 +15,7 @@ export interface RequestConfig { needAuth?: boolean // 是否需要token认证 showLoading?: boolean // 是否显示加载提示 loadingText?: string // 加载提示文本 + showToast?: boolean // 是否显示toast } // 响应数据接口 @@ -58,7 +59,7 @@ class HttpService { // 构建完整URL private buildUrl(url: string, params?: Record): string { const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}` - + if (params) { const searchParams = new URLSearchParams() Object.entries(params).forEach(([key, value]) => { @@ -69,7 +70,7 @@ class HttpService { const queryString = searchParams.toString() return queryString ? `${fullUrl}?${queryString}` : fullUrl } - + return fullUrl } @@ -95,7 +96,7 @@ class HttpService { const logMethod = console[level] || console.log const timestamp = new Date().toLocaleTimeString() - + if (data) { logMethod(`[${timestamp}] HTTP ${level.toUpperCase()}: ${message}`, data) } else { @@ -165,9 +166,9 @@ class HttpService { // 处理业务错误 private handleBusinessError(data: any): void { const message = data.message || '操作失败' - + this.log('error', `业务错误: ${message}`, data) - + Taro.showToast({ title: message, icon: 'none', @@ -187,7 +188,7 @@ class HttpService { } = config const fullUrl = this.buildUrl(url, method === 'GET' ? params : undefined) - + this.log('info', `发起请求: ${method} ${fullUrl}`, { data: method !== 'GET' ? data : undefined, params: method === 'GET' ? params : undefined @@ -223,18 +224,18 @@ class HttpService { return this.handleResponse(response) } catch (error) { this.log('error', '请求失败', error) - + // 在模拟模式下返回模拟数据 if (envConfig.enableMock && isDevelopment()) { this.log('info', '使用模拟数据') return this.getMockResponse(url, method) } - + Taro.showToast({ title: '网络连接失败', icon: 'none' }) - + throw error } finally { // 隐藏加载提示 @@ -247,7 +248,7 @@ class HttpService { // 获取模拟数据 private getMockResponse(url: string, method: string): ApiResponse { this.log('info', `返回模拟数据: ${method} ${url}`) - + return { code: 200, success: true, @@ -323,4 +324,4 @@ class HttpService { } // 导出HTTP服务实例 -export default new HttpService() \ No newline at end of file +export default new HttpService() \ No newline at end of file diff --git a/src/services/listApi.ts b/src/services/listApi.ts index a937558..de2be2e 100644 --- a/src/services/listApi.ts +++ b/src/services/listApi.ts @@ -1,93 +1,67 @@ -import { TennisMatch } from '../store/listStore' +import { TennisMatch } from "../store/listStore"; +import httpService from "./httpService"; // 模拟网络延迟 -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // 模拟API响应格式 interface ApiResponse { - code: number - message: string - data: T - timestamp: number + code: number; + message: string; + data: T; + timestamp: number; } - // 模拟网球比赛数据 const mockTennisMatches: TennisMatch[] = [ { - id: '1', - title: '周一晚场浦东新区单打约球', - dateTime: '明天(周五)下午5点 2小时', - location: '仁恒河滨花园网球场・室外', - distance: '3.5km', + id: "1", + title: "周一晚场浦东新区单打约球", + dateTime: "明天(周五)下午5点 2小时", + location: "仁恒河滨花园网球场", + distance: "3.5km", + shinei: "室内", registeredCount: 3, maxCount: 4, - skillLevel: '2.0 至 2.5', - matchType: '双打', + skillLevel: "2.0 至 2.5", + matchType: "双打", images: [ - 'https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center', - 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=200&h=200&fit=crop&crop=center', - 'https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center' - ] + "https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center", + "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=200&h=200&fit=crop&crop=center", + "https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center", + ], }, { - id: '2', - title: '浦东新区单打约球', - dateTime: '明天(周五)下午5点 2小时', - location: '仁恒河滨花园网球场・室外', - distance: '3.5km', + id: "2", + title: "浦东新区单打约球", + dateTime: "明天(周五)下午5点 2小时", + location: "仁恒河滨花园网球场", + distance: "3.5km", + shinei: "室外", registeredCount: 2, maxCount: 4, - skillLevel: '2.0 至 2.5', - matchType: '双打', + skillLevel: "2.0 至 2.5", + matchType: "双打", images: [ - 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=200&h=200&fit=crop&crop=center', - 'https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center' - ] + "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=200&h=200&fit=crop&crop=center", + "https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center", + ], }, { - id: '3', - title: '黄浦区双打约球', - dateTime: '7月20日(周日)下午6点 2小时', - location: '仁恒河滨花园网球场・室外', - distance: '3.5km', + id: "3", + title: "黄浦区双打约球", + dateTime: "7月20日(周日)下午6点 2小时", + location: "仁恒河滨花园网球场", + distance: "3.5km", registeredCount: 3, maxCount: 4, - skillLevel: '2.0 至 2.5', - matchType: '双打', + skillLevel: "2.0 至 2.5", + matchType: "双打", images: [ - 'https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center' - ] - } -] - -// 模拟数据变化 -const generateDynamicData = (): TennisMatch[] => { - return mockTennisMatches.map(match => ({ - ...match, - // 随机更新注册人数 - registeredCount: Math.min( - match.maxCount, - Math.max(0, match.registeredCount + Math.floor(Math.random() * 3) - 1) - ), - // 随机更新距离 - distance: `${(Math.random() * 5 + 1).toFixed(1)}km`, - // 随机更新时间 - dateTime: Math.random() > 0.5 ? match.dateTime : '今天下午3点 2小时' - })) -} - -// 模拟网络错误 -const simulateNetworkError = (): boolean => { - // 10% 概率模拟网络错误 - return Math.random() < 0.1 -} - -// 模拟网络超时 -const simulateTimeout = (): boolean => { - // 5% 概率模拟超时 - return Math.random() < 0.05 -} + "https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center", + ], + }, +]; /** * 获取网球比赛列表 @@ -95,59 +69,18 @@ const simulateTimeout = (): boolean => { * @returns Promise */ export const getTennisMatches = async (params?: { - page?: number - pageSize?: number - location?: string - skillLevel?: string -}): Promise => { + page?: number; + pageSize?: number; + location?: string; + skillLevel?: string; +}) => { try { - console.log('API调用: getTennisMatches', params) - - // 模拟网络延迟 (800-1500ms) - const delayTime = 800 + Math.random() * 700 - await delay(delayTime) - - // 模拟网络错误 - if (simulateNetworkError()) { - throw new Error('网络连接失败,请检查网络设置') - } - - // 模拟超时 - if (simulateTimeout()) { - throw new Error('请求超时,请稍后重试') - } - - // 生成动态数据 - const matches = generateDynamicData() - - // 模拟分页 - if (params?.page && params?.pageSize) { - const start = (params.page - 1) * params.pageSize - const end = start + params.pageSize - return matches.slice(start, end) - } - - // 模拟筛选 - if (params?.location) { - return matches.filter(match => - match.location.includes(params.location!) - ) - } - - if (params?.skillLevel) { - return matches.filter(match => - match.skillLevel.includes(params.skillLevel!) - ) - } - - console.log('API响应成功:', matches.length, '条数据') - return matches - + return httpService.post('/venues/list', params, { showLoading: false }) } catch (error) { - console.error('API调用失败:', error) - throw error + console.error("列表数据获取失败:", error); + throw error; } -} +}; /** * 刷新网球比赛数据 @@ -155,69 +88,12 @@ export const getTennisMatches = async (params?: { */ export const refreshTennisMatches = async (): Promise => { try { - console.log('API调用: refreshTennisMatches') - - // 模拟刷新延迟 (500-1000ms) - const delayTime = 500 + Math.random() * 500 - await delay(delayTime) - - // 模拟网络错误 - if (simulateNetworkError()) { - throw new Error('刷新失败,请稍后重试') - } - // 生成新的动态数据 - const matches = generateDynamicData() - - console.log('API刷新成功:', matches.length, '条数据') - return matches - + const matches = generateDynamicData(); + return matches; } catch (error) { - console.error('API刷新失败:', error) - throw error + console.error("API刷新失败:", error); + throw error; } -} +}; -/** - * 获取比赛详情 - * @param id 比赛ID - * @returns Promise - */ -export const getTennisMatchDetail = async (id: string): Promise => { - try { - console.log('API调用: getTennisMatchDetail', id) - - // 模拟网络延迟 - await delay(600 + Math.random() * 400) - - // 模拟网络错误 - if (simulateNetworkError()) { - throw new Error('获取详情失败,请稍后重试') - } - - const match = mockTennisMatches.find(m => m.id === id) - - if (!match) { - throw new Error('比赛不存在') - } - - console.log('API获取详情成功:', match.title) - return match - - } catch (error) { - console.error('API获取详情失败:', error) - throw error - } -} - -/** - * 模拟API统计信息 - */ -export const getApiStats = () => { - return { - totalCalls: 0, - successRate: 0.95, - averageResponseTime: 800, - lastCallTime: new Date().toISOString() - } -} diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 54036ff..a95ca37 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -36,7 +36,7 @@ export interface VerifyCodeResponse { } // 微信授权登录 -export const wechat_auth_login = async (): Promise => { +export const wechat_auth_login = async (phone_code?: string): Promise => { try { // 先进行微信登录获取code const login_result = await Taro.login(); @@ -48,15 +48,12 @@ export const wechat_auth_login = async (): Promise => { }; } - - - // 使用 httpService 调用微信授权接口 - const auth_response = await httpService.post('/user/wx_auth', { - code: login_result.code + // 使用 httpService 调用微信授权接口,传递手机号code + const auth_response = await httpService.post('user/wx_auth', { + code: login_result.code, + phone_code: phone_code // 传递手机号加密code }); - - if (auth_response.code === 0) { return { success: true, @@ -89,7 +86,7 @@ export interface PhoneLoginParams { export const phone_auth_login = async (params: PhoneLoginParams): Promise => { try { // 使用 httpService 调用验证验证码接口 - const verify_response = await httpService.post('/user/sms/verify', { + const verify_response = await httpService.post('user/sms/verify', { phone: params.phone, code: params.verification_code }); @@ -127,7 +124,7 @@ export const phone_auth_login = async (params: PhoneLoginParams): Promise => { try { - const response = await httpService.post('/user/sms/send', { + const response = await httpService.post('user/sms/send', { phone: phone }); @@ -155,7 +152,7 @@ export const send_sms_code = async (phone: string): Promise => { // 验证短信验证码 export const verify_sms_code = async (phone: string, code: string): Promise => { try { - const response = await httpService.post('/user/sms/verify', { + const response = await httpService.post('user/sms/verify', { phone: phone, code: code }); diff --git a/src/services/publishService.ts b/src/services/publishService.ts index 50e395d..c6466e7 100644 --- a/src/services/publishService.ts +++ b/src/services/publishService.ts @@ -30,6 +30,12 @@ export interface PublishBallData { wechat_contact?: string // 微信联系 } +export interface createGameData extends PublishBallData { + status: string, + created_at: string, + updated_at: string, +} + // 响应接口 export interface Response { code: string @@ -37,7 +43,6 @@ export interface Response { data: any } -// 响应接口 export interface StadiumListResponse { rows: Stadium[] } @@ -51,10 +56,61 @@ export interface Stadium { latitude?: number } +// export type SourceType = 'history' | 'preset' + +export interface getPicturesReq { + pageOption: { + page: number, + pageSize: number, + }, + seachOption: { + tag: string, + resource_type: string, + dateRange: string[], + }, +} + +export interface getPicturesRes { + code: number, + message: string, + data: { + rows: [ + { + user_id: string, + resource_type: string, + file_name: string, + original_name: string, + file_path: string, + file_url: string, + file_size: number, + mime_type: string, + width: number, + height: number, + duration: number, + thumbnail_url: string, + is_public: string, + tags: string[], + description: string, + view_count: number, + download_count: number, + status: string, + last_modify_time: string, + } + ], + count: number, + page: number, + pageSize: number, + totalPages: number, + } +} + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} // 发布球局类 class PublishService { - // 发布 - async createPersonal(data: PublishBallData): Promise> { + // 用户登录 + async createPersonal(data: PublishBallData): Promise> { return httpService.post('/games/create', data, { showLoading: true, loadingText: '发布中...' @@ -66,14 +122,21 @@ class PublishService { return httpService.post('/venues/list', data, { showLoading: false }) } +<<<<<<< HEAD // 畅打发布 async create_play_pmoothly(data: PublishBallData): Promise> { return httpService.post('/games/create_play_pmoothly', data, { showLoading: true, loadingText: '发布中...' +======= + async getPictures(req: getPicturesReq): Promise> { + return httpService.post('/gallery/sys_img_list', req, { + showLoading: false, + showToast: false, +>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034 }) } } // 导出认证服务实例 -export default new PublishService() \ No newline at end of file +export default new PublishService() \ No newline at end of file diff --git a/src/services/uploadFiles.ts b/src/services/uploadFiles.ts new file mode 100644 index 0000000..ba5a2eb --- /dev/null +++ b/src/services/uploadFiles.ts @@ -0,0 +1,75 @@ +import httpService from './httpService' +import type { ApiResponse } from './httpService' +import Taro from '@tarojs/taro' +import envConfig from '@/config/env' + +// 用户接口 +export interface UploadFilesData { + id: string, + filePath: string, + description?: string, + tags?: string, + is_public?: 0 | 1, +} + +// {"code":0,"message":"请求成功!","data":{"tags":["test"],"create_time":"2025-08-24 22:51:03","last_modify_time":"2025-08-24 22:51:03","duration":"0","thumbnail_url":"","view_count":"0","download_count":"0","id":16,"user_id":1,"resource_type":"image","file_name":"front/ball/images/63f56978-ffe9-4397-b897-8aca6f4fdcd8.png","original_name":"QyoUvEsLG6ci57c7e25cca0845dafed3ee1fde07876d.png","file_path":"http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f56978-ffe9-4397-b897-8aca6f4fdcd8.png","file_url":"http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f56978-ffe9-4397-b897-8aca6f4fdcd8.png","file_size":17756,"mime_type":"image/png","description":"test","is_public":"1","status":"active","width":0,"height":0,"uploadInfo":{"success":true,"name":"front/ball/images/63f56978-ffe9-4397-b897-8aca6f4fdcd8.png","path":"http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f56978-ffe9-4397-b897-8aca6f4fdcd8.png","ossPath":"http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f56978-ffe9-4397-b897-8aca6f4fdcd8.png","fileType":"image/png","fileSize":17756,"originalName":"QyoUvEsLG6ci57c7e25cca0845dafed3ee1fde07876d.png","suffix":"png","storagePath":"front/ball/images/63f56978-ffe9-4397-b897-8aca6f4fdcd8.png"}}} + +export interface uploadFileResponse { + code: number, + message: string, + data: uploadFileResponseData, +} + +export interface uploadFileResponseData { + id: number, + user_id: number, + file_name: string, + original_name: string, + file_path: string, + file_url: string, + file_size: number, + resource_type: string, + mime_type: string, + description: string, + tags: string[], + is_public: string, + view_count: number, + download_count: number, + created_at: string, + updated_at: string, +} + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} +// 发布球局类 +class UploadApi { + async upload(req: UploadFilesData): Promise<{ id: string, data: uploadFileResponseData }> { + // return httpService.post('/files/upload', req, { + // showLoading: true, + // }) + const { id, ...rest } = req + return Taro.uploadFile({ + url: `${envConfig.apiBaseURL}/api/gallery/upload`, + filePath: rest.filePath, + name: 'file', + formData: { + description: rest.description, + tags: rest.tags, + is_public: rest.is_public, + } + }).then(res => { + return { + id, + data: JSON.parse(res.data).data, + } + }) + } + + async batchUpload(req: UploadFilesData[]): Promise<{ id: string, data: uploadFileResponseData }[]> { + return Promise.all(req.map(item => this.upload(item))) + } +} + +// 导出认证服务实例 +export default new UploadApi() \ No newline at end of file diff --git a/src/static/asserts/fonts/PoetsenOne-Regular.ttf b/src/static/asserts/fonts/PoetsenOne-Regular.ttf new file mode 100644 index 0000000..1a89422 Binary files /dev/null and b/src/static/asserts/fonts/PoetsenOne-Regular.ttf differ diff --git a/src/static/common/guide-bar-publish.svg b/src/static/common/guide-bar-publish.svg new file mode 100644 index 0000000..133858f --- /dev/null +++ b/src/static/common/guide-bar-publish.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/common/navigator-back.svg b/src/static/common/navigator-back.svg new file mode 100644 index 0000000..2ac5245 --- /dev/null +++ b/src/static/common/navigator-back.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/detail/icon-application-add.svg b/src/static/detail/icon-application-add.svg new file mode 100644 index 0000000..be4ac42 --- /dev/null +++ b/src/static/detail/icon-application-add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/detail/icon-arrow-left.svg b/src/static/detail/icon-arrow-left.svg new file mode 100644 index 0000000..fca111b --- /dev/null +++ b/src/static/detail/icon-arrow-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/detail/icon-arrow-right.svg b/src/static/detail/icon-arrow-right.svg new file mode 100644 index 0000000..3d6ef55 --- /dev/null +++ b/src/static/detail/icon-arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/detail/icon-comment-dark.svg b/src/static/detail/icon-comment-dark.svg new file mode 100644 index 0000000..1787969 --- /dev/null +++ b/src/static/detail/icon-comment-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/static/detail/icon-comment.svg b/src/static/detail/icon-comment.svg new file mode 100644 index 0000000..d6b5f7d --- /dev/null +++ b/src/static/detail/icon-comment.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/static/detail/icon-logo-go.svg b/src/static/detail/icon-logo-go.svg new file mode 100644 index 0000000..468b5ff --- /dev/null +++ b/src/static/detail/icon-logo-go.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/static/detail/icon-map.svg b/src/static/detail/icon-map.svg new file mode 100644 index 0000000..9787f48 --- /dev/null +++ b/src/static/detail/icon-map.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/static/detail/icon-notice.svg b/src/static/detail/icon-notice.svg new file mode 100644 index 0000000..31119b7 --- /dev/null +++ b/src/static/detail/icon-notice.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/static/detail/icon-share-dark.svg b/src/static/detail/icon-share-dark.svg new file mode 100644 index 0000000..f2f0cac --- /dev/null +++ b/src/static/detail/icon-share-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/detail/icon-weather-sun.svg b/src/static/detail/icon-weather-sun.svg new file mode 100644 index 0000000..3851486 --- /dev/null +++ b/src/static/detail/icon-weather-sun.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/static/emptyStatus/comment-empty.png b/src/static/emptyStatus/comment-empty.png new file mode 100644 index 0000000..ebe1e82 Binary files /dev/null and b/src/static/emptyStatus/comment-empty.png differ diff --git a/src/static/emptyStatus/comment-failed.png b/src/static/emptyStatus/comment-failed.png new file mode 100644 index 0000000..4c6ff52 Binary files /dev/null and b/src/static/emptyStatus/comment-failed.png differ diff --git a/src/static/emptyStatus/publish-empty.png b/src/static/emptyStatus/publish-empty.png new file mode 100644 index 0000000..8b41a26 Binary files /dev/null and b/src/static/emptyStatus/publish-empty.png differ diff --git a/src/static/emptyStatus/publish-failed.png b/src/static/emptyStatus/publish-failed.png new file mode 100644 index 0000000..09b8777 Binary files /dev/null and b/src/static/emptyStatus/publish-failed.png differ diff --git a/src/static/list/icon-arrow-down-white.svg b/src/static/list/icon-arrow-down-white.svg new file mode 100644 index 0000000..31becd1 --- /dev/null +++ b/src/static/list/icon-arrow-down-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/list/icon-arrow-down.svg b/src/static/list/icon-arrow-down.svg new file mode 100644 index 0000000..1c6953d --- /dev/null +++ b/src/static/list/icon-arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/list/icon-change.svg b/src/static/list/icon-change.svg new file mode 100644 index 0000000..dcd21d3 --- /dev/null +++ b/src/static/list/icon-change.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/static/list/icon-filter-selected.svg b/src/static/list/icon-filter-selected.svg new file mode 100644 index 0000000..4b51f02 --- /dev/null +++ b/src/static/list/icon-filter-selected.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/static/list/icon-filter.svg b/src/static/list/icon-filter.svg new file mode 100644 index 0000000..dd5b39a --- /dev/null +++ b/src/static/list/icon-filter.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/static/list/icon-list-right-arrow.svg b/src/static/list/icon-list-right-arrow.svg new file mode 100644 index 0000000..018fb2e --- /dev/null +++ b/src/static/list/icon-list-right-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/list/icon-load-error.svg b/src/static/list/icon-load-error.svg new file mode 100644 index 0000000..3b7ad03 --- /dev/null +++ b/src/static/list/icon-load-error.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/list/icon-menu-item-selected.svg b/src/static/list/icon-menu-item-selected.svg new file mode 100644 index 0000000..c79702f --- /dev/null +++ b/src/static/list/icon-menu-item-selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/list/icon-paying-game.svg b/src/static/list/icon-paying-game.svg new file mode 100644 index 0000000..a312921 --- /dev/null +++ b/src/static/list/icon-paying-game.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/list/icon-play.svg b/src/static/list/icon-play.svg new file mode 100644 index 0000000..715cd6b --- /dev/null +++ b/src/static/list/icon-play.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/static/list/icon-reload.svg b/src/static/list/icon-reload.svg new file mode 100644 index 0000000..121dcc5 --- /dev/null +++ b/src/static/list/icon-reload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/list/icon-search.svg b/src/static/list/icon-search.svg new file mode 100644 index 0000000..3ca4401 --- /dev/null +++ b/src/static/list/icon-search.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/static/list/icon-site.svg b/src/static/list/icon-site.svg new file mode 100644 index 0000000..8e28d73 --- /dev/null +++ b/src/static/list/icon-site.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/static/logo.svg b/src/static/logo.svg new file mode 100644 index 0000000..63be4ce --- /dev/null +++ b/src/static/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/publishBall/icon-circle-select-arrow.svg b/src/static/publishBall/icon-circle-select-arrow.svg new file mode 100644 index 0000000..196e4ed --- /dev/null +++ b/src/static/publishBall/icon-circle-select-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/publishBall/icon-circle-select-ring.svg b/src/static/publishBall/icon-circle-select-ring.svg new file mode 100644 index 0000000..61d19c5 --- /dev/null +++ b/src/static/publishBall/icon-circle-select-ring.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/publishBall/icon-circle-unselect.svg b/src/static/publishBall/icon-circle-unselect.svg new file mode 100644 index 0000000..aae86ec --- /dev/null +++ b/src/static/publishBall/icon-circle-unselect.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/userInfo/default_avatar.svg b/src/static/userInfo/default_avatar.svg new file mode 100644 index 0000000..5cf5917 --- /dev/null +++ b/src/static/userInfo/default_avatar.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/static/userInfo/game1.svg b/src/static/userInfo/game1.svg new file mode 100644 index 0000000..ca4f985 --- /dev/null +++ b/src/static/userInfo/game1.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/static/userInfo/game2.svg b/src/static/userInfo/game2.svg new file mode 100644 index 0000000..ab7fe76 --- /dev/null +++ b/src/static/userInfo/game2.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/static/userInfo/game3.svg b/src/static/userInfo/game3.svg new file mode 100644 index 0000000..875bdaa --- /dev/null +++ b/src/static/userInfo/game3.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/static/userInfo/location.svg b/src/static/userInfo/location.svg new file mode 100644 index 0000000..a1c89f9 --- /dev/null +++ b/src/static/userInfo/location.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/static/userInfo/message.svg b/src/static/userInfo/message.svg new file mode 100644 index 0000000..f22ed89 --- /dev/null +++ b/src/static/userInfo/message.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/static/userInfo/plus.svg b/src/static/userInfo/plus.svg new file mode 100644 index 0000000..a4e5876 --- /dev/null +++ b/src/static/userInfo/plus.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/static/userInfo/tennis.svg b/src/static/userInfo/tennis.svg new file mode 100644 index 0000000..2405eb1 --- /dev/null +++ b/src/static/userInfo/tennis.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/static/userInfo/user1.svg b/src/static/userInfo/user1.svg new file mode 100644 index 0000000..605867e --- /dev/null +++ b/src/static/userInfo/user1.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/static/userInfo/user2.svg b/src/static/userInfo/user2.svg new file mode 100644 index 0000000..86e660b --- /dev/null +++ b/src/static/userInfo/user2.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/store/global.ts b/src/store/global.ts new file mode 100644 index 0000000..40d07b2 --- /dev/null +++ b/src/store/global.ts @@ -0,0 +1,61 @@ +import { create } from 'zustand' +import { getNavbarHeight } from '@/utils/getNavbarHeight' + +interface GlobalState { + location: Record + getLocationLoading: boolean + getLocationText: string + statusNavbarHeightInfo: { + statusBarHeight: number + navbarHeight: number + totalHeight: number + } +} + +interface GlobalActions { + updateState: (payload: Record) => void + getNavbarHeightInfo: () => void +} +// 完整的 Store 类型 +type GlobalStore = GlobalState & GlobalActions + +// 创建 store +export const useGlobalStore = create()((set, get) => ({ + // 位置信息 + location: {}, + // 正在获取位置信息 + getLocationLoading: false, + // 获取位置信息文本 + getLocationText: '定位中...', + + // 状态栏和导航栏高度信息 + statusNavbarHeightInfo: { + statusBarHeight: 0, + navbarHeight: 0, + totalHeight: 0 + }, + + // 获取导航栏高度信息 + getNavbarHeightInfo: () => { + const { statusBarHeight, navbarHeight } = getNavbarHeight(); + set({ + statusNavbarHeightInfo: { + statusBarHeight, + navbarHeight, + totalHeight: statusBarHeight + navbarHeight + } + }) + }, + + // 更新store数据 + updateState: (payload: Record) => { + const state = get(); + set({ + ...state, + ...(payload || {}) + }) + } +})) + +// 导出便捷的 hooks +export const useGlobalState = () => useGlobalStore((state) => state) diff --git a/src/store/listStore.ts b/src/store/listStore.ts index d64d49e..2b767bd 100644 --- a/src/store/listStore.ts +++ b/src/store/listStore.ts @@ -1,108 +1,180 @@ import { create } from 'zustand' import { getTennisMatches } from '../services/listApi' - -// 网球比赛数据接口 -export interface TennisMatch { - id: string - title: string - dateTime: string - location: string - distance: string - registeredCount: number - maxCount: number - skillLevel: string - matchType: string - images: string[] -} - -// Store 状态接口 -interface TennisState { - matches: TennisMatch[] - loading: boolean - error: string | null - lastRefreshTime: string | null -} - -// Store Actions 接口 -interface TennisActions { - fetchMatches: (params?: { - page?: number - pageSize?: number - location?: string - skillLevel?: string - }) => Promise - refreshMatches: () => Promise - clearError: () => void -} +import { ListActions, IFilterOptions, ListState } from '../../types/list/types' // 完整的 Store 类型 -type TennisStore = TennisState & TennisActions +type TennisStore = ListState & ListActions + +const defaultFilterOptions: IFilterOptions = { + location: '', // 位置 + time: '', // 时间 + ntrp: [1.0, 5.0], // NTRP 水平区间 + court_type: '', // 场地类型 + game_play: '', // 玩法 +}; + +const defaultDistance = 'all'; // 默认距离 // 创建 store -export const useTennisStore = create()((set, get) => ({ +export const useListStore = create()((set, get) => ({ // 初始状态 matches: [], loading: false, error: null, lastRefreshTime: null, + // 是否展示综合筛选弹窗 + isShowFilterPopup: false, + // 综合筛选项 + filterOptions: defaultFilterOptions, + // 综合筛选 选择的筛选数量 + filterCount: 0, + // 距离筛选 + distance: defaultDistance, + // 快捷筛选 + quickFilter: 1, // 1: 默认 2: 好评 3: 销量 + // 距离筛选数据 + distanceData: [ + { id: 0, label: "全城", value: "全城" }, + { id: 1, label: "3km", value: "3km" }, + { id: 2, label: "5km", value: "5km" }, + { id: 3, label: "10km", value: "10km" }, + ], + // 快捷筛选数据 + quickFilterData: [ + { text: "默认排序", value: "0" }, + { text: "好评排序", value: "1" }, + { text: "销量排序", value: "2" }, + ], + // 距离筛选和快捷筛选 + distanceQuickFilter: { + distance: '全城', + quick: '0', + }, + // 时间气泡数据 + timeBubbleData: [ + { id: 1, label: "晨间 6:00-10:00", value: "1" }, + { id: 2, label: "上午 10:00-12:00", value: "2" }, + { id: 3, label: "中午 12:00-14:00", value: "3" }, + { id: 4, label: "下午 14:00-18:00", value: "4" }, + { id: 5, label: "晚上 18:00-22:00", value: "5" }, + { id: 6, label: "夜间 22:00-24:00", value: "6" }, + ], + // 场地类型数据 + locationOptions: [ + { id: 1, label: "室内", value: "1" }, + { id: 2, label: "室外", value: "2" }, + { id: 3, label: "半室外", value: "3" }, + ], + // 玩法数据 + gamePlayOptions: [ + { id: 1, label: "不限", value: "不限" }, + { id: 2, label: "单打", value: "单打" }, + { id: 3, label: "双打", value: "双打" }, + { id: 4, label: "娱乐", value: "娱乐" }, + { id: 5, label: "拉球", value: "拉球" }, + ], + // 球局数量 + gamesNum: 124, + // 页面滚动距离顶部距离 是否大于0 + isScrollTop: false, // 获取比赛数据 fetchMatches: async (params) => { set({ loading: true, error: null }) - + try { - const matches = await getTennisMatches(params) - set({ - matches, - loading: false, - lastRefreshTime: new Date().toISOString() + const resData = await getTennisMatches(params) || {}; + const { data = {}, code } = resData; + if (code !== 0) { + set({ + error: '1', + matches: [], + loading: false, + }) + } + const { count, rows } = data; + const list = (rows || []).map(() => { + return { + id: "3", + title: "黄浦区双打约球", + dateTime: "7月20日(周日)下午6点 2小时", + location: "仁恒河滨花园网球场", + distance: "3.5km", + registeredCount: 3, + maxCount: 4, + skillLevel: "2.0 至 2.5", + matchType: "双打", + images: [ + "https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center", + ], + } }) - console.log('Store: 成功获取网球比赛数据:', matches.length, '条') + set({ + matches: list || rows || [], + loading: false, + gamesNum: count, + }) + } catch (error) { - const errorMessage = error instanceof Error ? error.message : '未知错误' - set({ - error: errorMessage, - loading: false + set({ + error, + matches: [], + loading: false, }) - console.error('Store: 获取网球比赛数据失败:', errorMessage) } }, // 刷新比赛数据 refreshMatches: async () => { set({ loading: true, error: null }) - + try { - const matches = await getTennisMatches() - set({ - matches, - loading: false, - lastRefreshTime: new Date().toISOString() + const resData = await getTennisMatches() || {}; + const { data = {}, code } = resData; + const { count, rows } = data; + set({ + matches: rows, + loading: false, + lastRefreshTime: new Date().toISOString() }) - console.log('Store: 成功刷新网球比赛数据:', matches.length, '条') } catch (error) { - const errorMessage = error instanceof Error ? error.message : '未知错误' - set({ - error: errorMessage, - loading: false - }) - console.error('Store: 刷新网球比赛数据失败:', errorMessage) } }, // 清除错误信息 clearError: () => { set({ error: null }) + }, + + // 更新综合筛选项 + updateFilterOptions: (payload: Record) => { + console.log('===更新综合筛选项', payload) + const preFilterOptions = get()?.filterOptions || {} + const filterOptions = { ...preFilterOptions, ...payload } + const filterCount = Object.values(filterOptions).filter(Boolean).length + set({ + filterOptions, + filterCount + }) + }, + + // 清空综合筛选选项 + clearFilterOptions: () => { + set({ + filterOptions: defaultFilterOptions, + filterCount: 0 + }) + }, + + // 更新store数据 + updateState: (payload: Record) => { + const state = get(); + console.log('Store: 更新数据:', state); + set({ + ...(payload || {}) + }) } })) // 导出便捷的 hooks -export const useTennisMatches = () => useTennisStore((state) => state.matches) -export const useTennisLoading = () => useTennisStore((state) => state.loading) -export const useTennisError = () => useTennisStore((state) => state.error) -export const useTennisLastRefresh = () => useTennisStore((state) => state.lastRefreshTime) -export const useTennisActions = () => useTennisStore((state) => ({ - fetchMatches: state.fetchMatches, - refreshMatches: state.refreshMatches, - clearError: state.clearError -})) +export const useListState = () => useListStore((state) => state) diff --git a/src/utils/getNavbarHeight.ts b/src/utils/getNavbarHeight.ts new file mode 100644 index 0000000..0332c5b --- /dev/null +++ b/src/utils/getNavbarHeight.ts @@ -0,0 +1,13 @@ +import Taro from "@tarojs/taro"; + +export const getNavbarHeight = (): { statusBarHeight: number; navbarHeight: number; totalHeight: number; } => { + const systemInfo = Taro.getSystemInfoSync(); + const statusBarHeight = systemInfo?.statusBarHeight || 0; + const isIOS = systemInfo.platform === "ios"; + const navbarHeight = isIOS ? 44 : 48; + return { + statusBarHeight, + navbarHeight, + totalHeight: statusBarHeight + navbarHeight, + }; +}; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..1192161 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,9 @@ +export * from './getNavbarHeight' +export * from './locationUtils' +export * from './processImage' +export * from './timeUtils' +export * from './tokenManager' + +export function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} \ No newline at end of file diff --git a/src/utils/processImage.ts b/src/utils/processImage.ts new file mode 100644 index 0000000..1143085 --- /dev/null +++ b/src/utils/processImage.ts @@ -0,0 +1,33 @@ +export async function getTextColorOnImage(url) { + let canvas + const width = 100 + const height = 50 + try { + // 1. 创建离屏Canvas + canvas = wx.createOffscreenCanvas({ type: '2d', width, height }) + const ctx = canvas.getContext('2d') + + // 2. 加载图片 + const img = canvas.createImage() + await new Promise((resolve) => { + img.onload = resolve + img.src = url + }) + + // 3. 绘制并分析图像 + ctx.drawImage(img, 0, 0, width, height) + + // TODO: 增加取样,提高精确性 + const pixelData = ctx.getImageData(10, 10, 1, 1).data + + // 4. 计算文字颜色 + const brightness = (pixelData[0] * 2126 + pixelData[1] * 7152 + pixelData[2] * 722) / 10000 + return { textColor: brightness > 128 ? 'black' : 'white' } + } finally { + // 释放资源 + if (canvas) { + canvas.width = 0; + canvas.height = 0; + } + } +} diff --git a/types/list/types.ts b/types/list/types.ts new file mode 100644 index 0000000..e9aa406 --- /dev/null +++ b/types/list/types.ts @@ -0,0 +1,162 @@ +// 网球比赛数据接口 +export interface TennisMatch { + id: number + title: string + dateTime: string + location: string + distance: string + registeredCount: number + maxCount: number + skillLevel: string + matchType: string + images: string[] + shinei: string +} +export interface IFilterOptions { + location: string + time: string + ntrp: [number, number] + court_type: string + game_play: string +} +export interface ListState { + matches: TennisMatch[] + loading: boolean + error: string | null + lastRefreshTime: string | null + isShowFilterPopup: boolean + filterOptions: IFilterOptions + filterCount: number + distance: string | number + quickFilter: string | number + distanceData: any[] + quickFilterData: any[] + distanceQuickFilter: { + distance: string + quick: string + } + timeBubbleData: BubbleOption[] + locationOptions: BubbleOption[] + gamePlayOptions: BubbleOption[] + gamesNum: number + isScrollTop: boolean +} + +export interface ListState { + matches: TennisMatch[] + loading: boolean + error: string | null + lastRefreshTime: string | null + isShowFilterPopup: boolean + filterOptions: IFilterOptions + filterCount: number + distance: string | number + quickFilter: string | number + distanceData: any[] + quickFilterData: any[] + distanceQuickFilter: { + distance: string + quick: string + } +} + +export interface ListActions { + fetchMatches: (params?: { + page?: number + pageSize?: number + location?: string + skillLevel?: string + }) => Promise + refreshMatches: () => Promise + clearError: () => void + updateState: (payload: Record) => void + updateFilterOptions: (payload: Record) => void + clearFilterOptions: () => void +} + +// 快捷筛选 +export interface MenuFilterProps { + options: { text: string; value: string }[]; + value: string; + onChange: (name: string, value: string) => void; + wrapperClassName?: string; + itemClassName?: string; + name: string; +} + +// 距离筛选 +export interface DistanceFilterProps { + options: BubbleOption[]; + value: string; + onChange: (name: string, value: string) => void; + wrapperClassName?: string; + itemClassName?: string; + name: string; +} + +// bubble 组件 +export interface BubbleOption { + id: string | number; + label: string; + value: string | number; + disabled?: boolean; + icon?: React.ReactNode; + description?: string; +} + +export interface BubbleProps { + options: BubbleOption[]; + value?: string | number | (string | number)[]; + onChange?: ( + name: string, + value: string | number | (string | number)[], + option: BubbleOption | BubbleOption[] + ) => void; + multiple?: boolean; + layout?: "horizontal" | "vertical" | "grid"; + columns?: number; + size?: "small" | "medium" | "large"; + className?: string; + itemClassName?: string; + style?: React.CSSProperties; + disabled?: boolean; + name: string; +} + +export interface BubbleItemProps { + option: BubbleOption; + isSelected: boolean; + size: 'small' | 'medium' | 'large'; + disabled: boolean; + onClick: (option: BubbleOption) => void; + itemClassName?: string; +} + +// FilterPopup 组件 +export interface FilterPopupProps { + onCancel: () => void; + onConfirm: () => void; + onChange: (params: Record) => void; + loading: boolean; + filterOptions: Record; + onClear: () => void; + visible: boolean; + onClose: () => void; + statusNavbarHeigh: number +} + +// 列表卡片 +export interface ListCardProps { + id: number; + title: string; + dateTime: string; + location: string; + distance: string; + registeredCount: number; + maxCount: number; + skillLevel: string; + matchType: string; + images: string[]; + shinei: string; + showSkeleton?: boolean; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5b1fef6..b558aa5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9237,7 +9237,16 @@ strict-uri-encode@^1.0.0: resolved "https://registry.npmmirror.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" integrity sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9333,7 +9342,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10329,7 +10345,7 @@ word-wrap@^1.2.5: resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10347,6 +10363,15 @@ wrap-ansi@^6.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"