From 4f6ca7314885ff8379a72b5fb75e9b741d51780c Mon Sep 17 00:00:00 2001 From: juguohong Date: Sun, 17 Aug 2025 00:00:56 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E9=80=9A=E7=94=A8=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project.config.json | 67 ++++--- src/app.config.ts | 3 +- src/components/Bubble/BubbleItem.tsx | 35 ++++ src/components/Bubble/README.md | 208 ++++++++++++++++++++++ src/components/Bubble/USAGE.md | 211 ++++++++++++++++++++++ src/components/Bubble/bubbleItem.scss | 96 ++++++++++ src/components/Bubble/example.tsx | 235 ++++++++++++++++++++++++ src/components/Bubble/index.scss | 35 ++++ src/components/Bubble/index.tsx | 152 ++++++++++++++++ src/components/List/index.scss | 7 + src/components/List/index.tsx | 16 ++ src/components/ListItem/index.scss | 207 ++++++++++++++++++++++ src/components/ListItem/index.tsx | 121 +++++++++++++ src/components/Range/README.md | 93 ++++++++++ src/components/Range/example.tsx | 85 +++++++++ src/components/Range/index.scss | 246 ++++++++++++++++++++++++++ src/components/Range/index.tsx | 91 ++++++++++ src/components/Range/simple-test.tsx | 22 +++ src/components/Range/style-test.tsx | 68 +++++++ src/components/Range/test.tsx | 35 ++++ src/pages/list/index.config.ts | 5 + src/pages/list/index.scss | 0 src/pages/list/index.tsx | 209 ++++++++++++++++++++++ src/services/listApi.ts | 223 +++++++++++++++++++++++ src/store/listStore.ts | 108 +++++++++++ 25 files changed, 2554 insertions(+), 24 deletions(-) create mode 100644 src/components/Bubble/BubbleItem.tsx create mode 100644 src/components/Bubble/README.md create mode 100644 src/components/Bubble/USAGE.md create mode 100644 src/components/Bubble/bubbleItem.scss create mode 100644 src/components/Bubble/example.tsx create mode 100644 src/components/Bubble/index.scss create mode 100644 src/components/Bubble/index.tsx create mode 100644 src/components/List/index.scss create mode 100644 src/components/List/index.tsx create mode 100644 src/components/ListItem/index.scss create mode 100644 src/components/ListItem/index.tsx create mode 100644 src/components/Range/README.md create mode 100644 src/components/Range/example.tsx create mode 100644 src/components/Range/index.scss create mode 100644 src/components/Range/index.tsx create mode 100644 src/components/Range/simple-test.tsx create mode 100644 src/components/Range/style-test.tsx create mode 100644 src/components/Range/test.tsx create mode 100644 src/pages/list/index.config.ts create mode 100644 src/pages/list/index.scss create mode 100644 src/pages/list/index.tsx create mode 100644 src/services/listApi.ts create mode 100644 src/store/listStore.ts diff --git a/project.config.json b/project.config.json index 46dc372..8e39a33 100644 --- a/project.config.json +++ b/project.config.json @@ -1,25 +1,46 @@ { - "miniprogramRoot": "dist/", - "projectname": "playBallTogether", - "description": "playBallTogether", - "appid": "touristappid", - "setting": { - "urlCheck": true, - "es6": false, - "enhance": false, + "miniprogramRoot": "dist/", + "projectname": "playBallTogether", + "description": "playBallTogether", + "appid": "wx815b533167eb7b53", + "setting": { + "urlCheck": true, + "es6": true, + "enhance": true, + "postcss": false, + "preloadBackgroundData": false, + "minified": false, + "newFeature": true, + "coverView": true, + "nodeModules": false, + "autoAudits": false, + "showShadowRootInWxmlPanel": false, + "scopeDataCheck": false, + "uglifyFileName": false, + "checkInvalidKey": true, + "checkSiteMap": true, + "uploadWithSourceMap": true, "compileHotReLoad": false, - "postcss": false, - "preloadBackgroundData": false, - "minified": false, - "newFeature": true, - "autoAudits": false, - "coverView": true, - "showShadowRootInWxmlPanel": false, - "scopeDataCheck": false, - "useCompilerModule": false - }, - "compileType": "miniprogram", - "simulatorType": "wechat", - "simulatorPluginLibVersion": {}, - "condition": {} -} + "useMultiFrameRuntime": true, + "useApiHook": true, + "useApiHostProcess": false, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "enableEngineNative": false, + "useIsolateContext": true, + "useCompilerModule": false, + "userConfirmedUseCompilerModuleSwitch": false, + "userConfirmedBundleSwitch": false, + "packNpmManually": false, + "packNpmRelationList": [], + "minifyWXSS": true, + "minifyWXML": true + }, + "compileType": "miniprogram", + "simulatorType": "wechat", + "simulatorPluginLibVersion": {}, + "condition": {} +} \ No newline at end of file diff --git a/src/app.config.ts b/src/app.config.ts index 15c683b..5d3f7cb 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,6 +1,7 @@ export default defineAppConfig({ pages: [ - 'pages/index/index' + 'pages/index/index', + 'pages/list/index' ], window: { backgroundTextStyle: 'light', diff --git a/src/components/Bubble/BubbleItem.tsx b/src/components/Bubble/BubbleItem.tsx new file mode 100644 index 0000000..300c8e9 --- /dev/null +++ b/src/components/Bubble/BubbleItem.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { BubbleOption } from './index'; +import './bubbleItem.scss'; + +export interface BubbleItemProps { + option: BubbleOption; + isSelected: boolean; + size: 'small' | 'medium' | 'large'; + disabled: boolean; + onClick: (option: BubbleOption) => void; +} + +const BubbleItem: React.FC = ({ + option, + isSelected, + size, + disabled, + onClick +}) => { + return ( + + ); +}; + +export default BubbleItem; \ No newline at end of file diff --git a/src/components/Bubble/README.md b/src/components/Bubble/README.md new file mode 100644 index 0000000..90512d3 --- /dev/null +++ b/src/components/Bubble/README.md @@ -0,0 +1,208 @@ +# 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 new file mode 100644 index 0000000..afd455f --- /dev/null +++ b/src/components/Bubble/USAGE.md @@ -0,0 +1,211 @@ +# 如何使用 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/bubbleItem.scss b/src/components/Bubble/bubbleItem.scss new file mode 100644 index 0000000..e1a1dbb --- /dev/null +++ b/src/components/Bubble/bubbleItem.scss @@ -0,0 +1,96 @@ +.bubble-option { + position: relative; + border: 1px solid #e5e5e5; + outline: none; // 移除浏览器默认的outline + background: #ffffff; + color: #333333; + font-size: 14px; + font-weight: 400; + text-align: center; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + white-space: nowrap; + padding: 0; + font-size: 14px; + border-radius: 28px; + margin: 0; + height: 36px; + + // 移除浏览器默认样式 + &:focus { + outline: none; + border: none; + } + + &::after { + outline: none; + border: none; + } + + &:active { + outline: none; + border: none; + } + + // 尺寸变体 + &.small { + padding: 8px 12px; + min-height: 32px; + font-size: 12px; + } + + &.medium { + padding: 12px 16px; + min-height: 44px; + font-size: 14px; + } + + &.large { + padding: 16px 20px; + min-height: 56px; + font-size: 16px; + } + + &.selected { + background: #000000; + color: #ffffff; + } + + &.disabled { + background: #f5f5f5; + color: #999999; + cursor: not-allowed; + + &:hover { + background: #f5f5f5; + } + } + + // 图标样式 + .bubble-icon { + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 16px; + height: 16px; + } + } + + // 标签样式 + .bubble-label { + font-weight: 500; + } + + // 描述样式 + .bubble-description { + font-size: 12px; + opacity: 0.7; + font-weight: 400; + } +} \ No newline at end of file diff --git a/src/components/Bubble/example.tsx b/src/components/Bubble/example.tsx new file mode 100644 index 0000000..0ebcdf8 --- /dev/null +++ b/src/components/Bubble/example.tsx @@ -0,0 +1,235 @@ +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.scss b/src/components/Bubble/index.scss new file mode 100644 index 0000000..aa88fbb --- /dev/null +++ b/src/components/Bubble/index.scss @@ -0,0 +1,35 @@ +.bubble-container { + width: 100%; + + // 水平布局 + .bubble-horizontal { + display: flex; + gap: 12px; + flex-wrap: wrap; + justify-content: space-between; + + .bubble-option { + $gap: 8px; + $count: 3; + flex-basis: calc(100% / $count - $gap * 2); + gap: $gap; + } + } + + // 垂直布局 + .bubble-vertical { + display: flex; + flex-direction: column; + gap: 12px; + + .bubble-option { + width: 100%; + } + } + + // 网格布局 + .bubble-grid { + display: grid; + width: 100%; + } +} diff --git a/src/components/Bubble/index.tsx b/src/components/Bubble/index.tsx new file mode 100644 index 0000000..2c118d3 --- /dev/null +++ b/src/components/Bubble/index.tsx @@ -0,0 +1,152 @@ +import React, { useState, useEffect } from 'react'; +import './index.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; + style?: React.CSSProperties; + disabled?: boolean; +} + +const Bubble: React.FC = ({ + options, + value, + onChange, + multiple = false, + layout = 'horizontal', + columns = 3, + size = 'small', + className = '', + style = {}, + disabled = false +}) => { + const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]); + + // 同步外部传入的value + useEffect(() => { + if (value !== undefined) { + const newValues = Array.isArray(value) ? value : [value]; + setSelectedValues(newValues); + } + }, [value]); + + const handleOptionClick = (option: BubbleOption) => { + if (disabled || option.disabled) return; + + let newSelectedValues: (string | number)[]; + + if (multiple) { + if (selectedValues.includes(option.value)) { + newSelectedValues = selectedValues.filter(v => v !== option.value); + } else { + newSelectedValues = [...selectedValues, option.value]; + } + } else { + newSelectedValues = [option.value]; + } + + setSelectedValues(newSelectedValues); + + // 调用onChange回调,传递选中的值和对应的选项 + if (onChange) { + if (multiple) { + const selectedOptions = options.filter(opt => newSelectedValues.includes(opt.value)); + onChange(newSelectedValues, selectedOptions); + } else { + onChange(option.value, option); + } + } + }; + + const isSelected = (option: BubbleOption) => { + return selectedValues.includes(option.value); + }; + + const renderHorizontalLayout = () => ( +
+ {options.map((option) => ( + + ))} +
+ ); + + const renderVerticalLayout = () => ( +
+ {options.map((option) => ( + + ))} +
+ ); + + const renderGridLayout = () => ( +
+ {options.map((option) => ( + + ))} +
+ ); + + const renderLayout = () => { + switch (layout) { + case 'horizontal': + return renderHorizontalLayout(); + case 'vertical': + return renderVerticalLayout(); + case 'grid': + return renderGridLayout(); + default: + return renderHorizontalLayout(); + } + }; + + return ( +
+ {renderLayout()} +
+ ); +}; + +export default Bubble; diff --git a/src/components/List/index.scss b/src/components/List/index.scss new file mode 100644 index 0000000..391a9aa --- /dev/null +++ b/src/components/List/index.scss @@ -0,0 +1,7 @@ +.list { + background: #fafafa; + margin-top: 12px; + display: flex; + flex-direction: column; + gap: 5px; +} diff --git a/src/components/List/index.tsx b/src/components/List/index.tsx new file mode 100644 index 0000000..c93c214 --- /dev/null +++ b/src/components/List/index.tsx @@ -0,0 +1,16 @@ +import { View } from '@tarojs/components' +import './index.scss' + +interface ListProps { + children: React.ReactNode +} + +const List: React.FC = ({ children }) => { + return ( + + {children} + + ) +} + +export default List diff --git a/src/components/ListItem/index.scss b/src/components/ListItem/index.scss new file mode 100644 index 0000000..ded1507 --- /dev/null +++ b/src/components/ListItem/index.scss @@ -0,0 +1,207 @@ +.list-item { + display: flex; + padding: 16px; + background: #ffffff; + border-radius: 20px; + border: 0.5px solid #f0f0f0; +} + +.content { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; +} + +.title { + font-size: 16px; + font-weight: 600; + color: #333333; + line-height: 1.4; +} + +.date-time { + font-size: 14px; + color: #666666; + line-height: 1.3; +} + +.location { + font-size: 14px; + color: #666666; + line-height: 1.3; +} + +.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; + .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 { + padding: 3px; + border: 1px solid #f5f5f5; + border-radius: 20px; + min-width: 38px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: #000000; + font-size: 12px; +} + +.tag-text-max { + color: #666666; +} + +.image-section { + width: 100px; + height: 100px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + .image-container { + width: 100%; + height: 100%; + padding: 2px; + background: #ffffff; + border-radius: 10px; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); + overflow: hidden; + position: absolute; + .image { + border-radius: 10px; + } + } +} + +.single-image { + position: relative; + width: 88px; + height: 88px; + .image-container { + 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: rotate(-10deg); + } + + &:last-child { + right: 0; + z-index: 1; + transform: 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: 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; +} diff --git a/src/components/ListItem/index.tsx b/src/components/ListItem/index.tsx new file mode 100644 index 0000000..cc0f498 --- /dev/null +++ b/src/components/ListItem/index.tsx @@ -0,0 +1,121 @@ +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/Range/README.md b/src/components/Range/README.md new file mode 100644 index 0000000..5da1a01 --- /dev/null +++ b/src/components/Range/README.md @@ -0,0 +1,93 @@ +# 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 new file mode 100644 index 0000000..a9b65df --- /dev/null +++ b/src/components/Range/example.tsx @@ -0,0 +1,85 @@ +/* + * @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.scss b/src/components/Range/index.scss new file mode 100644 index 0000000..00bd829 --- /dev/null +++ b/src/components/Range/index.scss @@ -0,0 +1,246 @@ +.ntrp-range { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + user-select: none; + + &__header { + display: flex; + align-items: center; + margin-bottom: 16px; + } + + &__icon { + margin-right: 8px; + display: flex; + align-items: center; + + svg { + width: 20px; + height: 20px; + } + } + + &__title { + margin: 0; + font-size: 16px; + font-weight: 500; + color: #000; + line-height: 1.2; + } + + &__slider-container { + position: relative; + } + + &__labels { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + } + + &__label { + font-size: 14px; + color: #000; + font-weight: 400; + } + + &__track-container { + position: relative; + padding: 0 10px; + } + + &__track { + position: relative; + height: 40px; + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + display: flex; + align-items: center; + padding: 0 20px; + } + + &__markers { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; + } + + &__marker { + position: absolute; + width: 6px; + height: 6px; + background: #e0e0e0; + border-radius: 50%; + top: 50%; + transform: translate(-50%, -50%); + } + + &__nutui-wrapper { + position: relative; + width: 100%; + height: 100%; + z-index: 2; + + // 隐藏NutUI的默认标签 + .nut-range__label { + display: none !important; + } + + // 隐藏默认的轨道背景 + .nut-range__bar-box { + background: transparent !important; + height: 6px !important; + border-radius: 3px !important; + margin: 0 !important; + padding: 0 !important; + } + + // 隐藏默认的轨道 + .nut-range__bar { + background: transparent !important; + height: 0 !important; + margin: 0 !important; + } + + // 自定义活动轨道(黑色填充条) + .nut-range__bar--active { + background: #000 !important; + height: 6px !important; + border-radius: 3px !important; + margin: 0 !important; + } + + // 自定义滑块手柄样式 + .nut-range__button { + width: 24px !important; + height: 24px !important; + background: #fff !important; + border: 2px solid #000 !important; + border-radius: 50% !important; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important; + top: 50% !important; + transform: translate(-50%, -50%) !important; + transition: all 0.1s ease !important; + + &:hover { + transform: translate(-50%, -50%) scale(1.1) !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important; + } + + &:active { + transform: translate(-50%, -50%) scale(1.15) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25) !important; + } + } + + // 确保滑块在正确位置 + .nut-range__button-wrapper { + top: 50% !important; + transform: translateY(-50%) !important; + } + } + + // 响应式设计 + @media (max-width: 480px) { + &__track { + padding: 0 15px; + } + + &__nutui-wrapper { + .nut-range__button { + width: 20px !important; + height: 20px !important; + } + } + + &__title { + font-size: 14px; + } + + &__label { + font-size: 12px; + } + } +} + +// 全局NutUI样式覆盖 +.nut-range { + width: 100% !important; + height: 100% !important; + + .nut-range__bar-box { + margin: 0 !important; + padding: 0 !important; + } + + .nut-range__bar { + margin: 0 !important; + } +} + +.ntrp-range__header { + line-height: 20px; + display: flex; + align-items: center; + justify-content: space-between; + .ntrp-range__header-left { + display: flex; + align-items: center; + gap: 8px; + } + .ntrp-range__title { + font-weight: 600; + font-size: 14px; + color: #000000; + } + + .ntrp-range__content { + font-weight: 400; + font-size: 14px; + color: #3c3c34; + } +} + +.rangeWrapper { + display: flex; + align-items: center; + justify-content: space-between; + gap: 5px; + height: 44px; + border-radius: 12px; + border: 1px solid #e0e0e0; + padding: 0 10px; + .rangeHandle { + .nut-range-mark { + padding-top: 28px; + } + .nut-range-bar { + background: #000000; + height: 6px; + } + .nut-range-button { + border: none; + box-shadow: 0 6px 13px 0 rgba(0, 0, 0, 0.12); + } + + .nut-range-tick { + background: #3c3c3c; + height: 4px !important; + width: 4px !important; + } + } + + span { + font-size: 12px; + color: #999; + } + .rangeWrapper__min, + .rangeWrapper__max { + flex-shrink: 0; + font-size: 12px; + color: #000000; + } +} diff --git a/src/components/Range/index.tsx b/src/components/Range/index.tsx new file mode 100644 index 0000000..0523235 --- /dev/null +++ b/src/components/Range/index.tsx @@ -0,0 +1,91 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { Range } from "@nutui/nutui-react-taro"; +import "./index.scss"; + +interface RangeProps { + min?: number; + max?: number; + step?: number; + value?: [number, number]; + onChange?: (value: [number, number]) => void; + disabled?: boolean; +} + +const NtrpRange: React.FC = ({ + min = 1.0, + max = 5.0, + step = 0.5, + value = [min, max], + onChange, + disabled = false, +}) => { + const [currentValue, setCurrentValue] = useState<[number, number]>(value); + + useEffect(() => { + setCurrentValue(value); + }, [value]); + + const handleChange = (val: [number, number]) => { + console.log("Range value changed:", val); + setCurrentValue(val); + onChange?.(val); + }; + + const marks = useMemo(() => { + let marksMap = {}; + for (let i = min + step; i < max; i += step) { + marksMap[i] = ""; + } + return marksMap; + }, [min, max, step]); + + const rangContent = useMemo(() => { + const [start, end] = currentValue || []; + if (start === min && end === max) { + return "不限"; + } + return `${start.toFixed(1)} - ${end.toFixed(1)}之间`; + }, [currentValue, min, max]); + + return ( +
+
+
+
icon
+

NTRP水平区间

+
+

{rangContent}

+
+ +
+
+ {min} + + {max} +
+
+ + {/* 调试信息 */} +
+ 当前值: {currentValue[0].toFixed(1)} - {currentValue[1].toFixed(1)} +
+
+ ); +}; + +export default NtrpRange; diff --git a/src/components/Range/simple-test.tsx b/src/components/Range/simple-test.tsx new file mode 100644 index 0000000..f874642 --- /dev/null +++ b/src/components/Range/simple-test.tsx @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..60134d0 --- /dev/null +++ b/src/components/Range/style-test.tsx @@ -0,0 +1,68 @@ +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 new file mode 100644 index 0000000..624772c --- /dev/null +++ b/src/components/Range/test.tsx @@ -0,0 +1,35 @@ +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/pages/list/index.config.ts b/src/pages/list/index.config.ts new file mode 100644 index 0000000..5c5ad16 --- /dev/null +++ b/src/pages/list/index.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '列表', + enablePullDownRefresh: true, + backgroundTextStyle: 'dark' +}) diff --git a/src/pages/list/index.scss b/src/pages/list/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/list/index.tsx b/src/pages/list/index.tsx new file mode 100644 index 0000000..14b6563 --- /dev/null +++ b/src/pages/list/index.tsx @@ -0,0 +1,209 @@ +import ListItem from "../../components/ListItem"; +import List from "../../components/List"; +import Bubble from "../../components/Bubble/example"; +import Range from "../../components/Range/example"; +import "./index.scss"; +import { useEffect } from "react"; +import Taro from "@tarojs/taro"; +import { + useTennisMatches, + useTennisLoading, + useTennisError, + useTennisLastRefresh, + useTennisActions, +} from "../../store/listStore"; + +const ListPage = () => { + // 从 store 获取数据和方法 + const matches = useTennisMatches(); + const loading = useTennisLoading(); + const error = useTennisError(); + const lastRefreshTime = useTennisLastRefresh(); + const { fetchMatches, refreshMatches, clearError } = useTennisActions(); + + useEffect(() => { + // 页面加载时获取数据 + fetchMatches(); + }, [fetchMatches]); + + // 下拉刷新处理函数 - 使用Taro生命周期钩子 + Taro.usePullDownRefresh(() => { + console.log("触发下拉刷新"); + + // 调用 store 的刷新方法 + refreshMatches() + .then(() => { + // 刷新完成后停止下拉刷新动画 + Taro.stopPullDownRefresh(); + + // 显示刷新成功提示 + Taro.showToast({ + title: "刷新成功", + icon: "success", + duration: 1500, + }); + }) + .catch(() => { + // 刷新失败时也停止动画 + Taro.stopPullDownRefresh(); + + Taro.showToast({ + title: "刷新失败", + icon: "error", + duration: 1500, + }); + }); + }); + + // 错误处理 + 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(); + }; + + // 加载状态显示 + if (loading && matches.length === 0) { + return ( +
+
加载中...
+
+ 正在获取网球比赛数据 +
+
+ ); + } + + // 错误状态显示 + if (error && matches.length === 0) { + return ( +
+
加载失败
+
+ {error} +
+ +
+ ); + } + + return ( +
+ {/* 状态信息栏 */} + {lastRefreshTime && ( +
+ 最后更新: {formatRefreshTime(lastRefreshTime)} | 共 {matches.length}{" "} + 场比赛 +
+ )} + + {/* 范围选择 */} + + + {/* 气泡 */} + + + {/* 列表内容 */} + + {matches.map((match, index) => ( + + ))} + + + {/* 空状态 */} + {!loading && matches.length === 0 && ( +
+
暂无比赛数据
+ +
+ )} +
+ ); +}; + +export default ListPage; diff --git a/src/services/listApi.ts b/src/services/listApi.ts new file mode 100644 index 0000000..a937558 --- /dev/null +++ b/src/services/listApi.ts @@ -0,0 +1,223 @@ +import { TennisMatch } from '../store/listStore' + +// 模拟网络延迟 +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +// 模拟API响应格式 +interface ApiResponse { + code: number + message: string + data: T + timestamp: number +} + + +// 模拟网球比赛数据 +const mockTennisMatches: TennisMatch[] = [ + { + id: '1', + title: '周一晚场浦东新区单打约球', + dateTime: '明天(周五)下午5点 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', + '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', + registeredCount: 2, + maxCount: 4, + skillLevel: '2.0 至 2.5', + matchType: '双打', + images: [ + 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=200&h=200&fit=crop&crop=center', + 'https://images.unsplash.com/photo-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center' + ] + }, + { + id: '3', + title: '黄浦区双打约球', + dateTime: '7月20日(周日)下午6点 2小时', + location: '仁恒河滨花园网球场・室外', + distance: '3.5km', + registeredCount: 3, + maxCount: 4, + skillLevel: '2.0 至 2.5', + matchType: '双打', + images: [ + 'https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center' + ] + } +] + +// 模拟数据变化 +const generateDynamicData = (): TennisMatch[] => { + 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 +} + +/** + * 获取网球比赛列表 + * @param params 查询参数 + * @returns Promise + */ +export const getTennisMatches = async (params?: { + page?: number + pageSize?: number + location?: string + skillLevel?: string +}): Promise => { + 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 + + } catch (error) { + console.error('API调用失败:', error) + throw error + } +} + +/** + * 刷新网球比赛数据 + * @returns Promise + */ +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 + + } catch (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/store/listStore.ts b/src/store/listStore.ts new file mode 100644 index 0000000..d64d49e --- /dev/null +++ b/src/store/listStore.ts @@ -0,0 +1,108 @@ +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 +} + +// 完整的 Store 类型 +type TennisStore = TennisState & TennisActions + +// 创建 store +export const useTennisStore = create()((set, get) => ({ + // 初始状态 + matches: [], + loading: false, + error: null, + lastRefreshTime: null, + + // 获取比赛数据 + fetchMatches: async (params) => { + set({ loading: true, error: null }) + + try { + const matches = await getTennisMatches(params) + set({ + matches, + 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) + } + }, + + // 刷新比赛数据 + refreshMatches: async () => { + set({ loading: true, error: null }) + + try { + const matches = await getTennisMatches() + set({ + matches, + 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 }) + } +})) + +// 导出便捷的 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 +})) From db48e55b053d0f849f3263a8e8d922872db4e0f5 Mon Sep 17 00:00:00 2001 From: juguohong Date: Sun, 17 Aug 2025 18:36:43 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E7=AD=9B=E9=80=89=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/index.ts | 4 +- src/app.config.ts | 4 +- src/components/Bubble/BubbleItem.tsx | 18 +- ...bubbleItem.scss => bubbleItem.module.scss} | 14 +- .../Bubble/{index.scss => index.module.scss} | 19 +- src/components/Bubble/index.tsx | 56 ++-- src/components/CityFilter/example.tsx | 18 ++ src/components/CityFilter/index.module.scss | 59 +++++ src/components/CityFilter/index.tsx | 64 +++++ src/components/Menu/example.tsx | 18 ++ src/components/Menu/index.module.scss | 45 ++++ src/components/Menu/index.tsx | 37 +++ src/components/Range/index.module.scss | 81 ++++++ src/components/Range/index.scss | 246 ------------------ src/components/Range/index.tsx | 20 +- src/pages/list/index.tsx | 8 + types/css-modules.d.ts | 9 + 17 files changed, 406 insertions(+), 314 deletions(-) rename src/components/Bubble/{bubbleItem.scss => bubbleItem.module.scss} (89%) rename src/components/Bubble/{index.scss => index.module.scss} (51%) create mode 100644 src/components/CityFilter/example.tsx create mode 100644 src/components/CityFilter/index.module.scss create mode 100644 src/components/CityFilter/index.tsx create mode 100644 src/components/Menu/example.tsx create mode 100644 src/components/Menu/index.module.scss create mode 100644 src/components/Menu/index.tsx create mode 100644 src/components/Range/index.module.scss delete mode 100644 src/components/Range/index.scss create mode 100644 types/css-modules.d.ts diff --git a/config/index.ts b/config/index.ts index 3eb418b..2bc2144 100644 --- a/config/index.ts +++ b/config/index.ts @@ -46,7 +46,7 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { } }, cssModules: { - enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true + enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true config: { namingPattern: 'module', // 转换模式,取值为 global/module generateScopedName: '[name]__[local]___[hash:base64:5]' @@ -75,7 +75,7 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { config: {} }, cssModules: { - enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true + enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true config: { namingPattern: 'module', // 转换模式,取值为 global/module generateScopedName: '[name]__[local]___[hash:base64:5]' diff --git a/src/app.config.ts b/src/app.config.ts index 5d3f7cb..ea3393a 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,7 +1,7 @@ export default defineAppConfig({ pages: [ - 'pages/index/index', - 'pages/list/index' + 'pages/list/index', + 'pages/index/index' ], window: { backgroundTextStyle: 'light', diff --git a/src/components/Bubble/BubbleItem.tsx b/src/components/Bubble/BubbleItem.tsx index 300c8e9..0ede586 100644 --- a/src/components/Bubble/BubbleItem.tsx +++ b/src/components/Bubble/BubbleItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { BubbleOption } from './index'; -import './bubbleItem.scss'; +import styles from './bubbleItem.module.scss'; export interface BubbleItemProps { option: BubbleOption; @@ -8,6 +8,7 @@ export interface BubbleItemProps { size: 'small' | 'medium' | 'large'; disabled: boolean; onClick: (option: BubbleOption) => void; + itemClassName?: string; } const BubbleItem: React.FC = ({ @@ -15,19 +16,20 @@ const BubbleItem: React.FC = ({ isSelected, size, disabled, - onClick + onClick, + itemClassName }) => { return ( ); }; diff --git a/src/components/Bubble/bubbleItem.scss b/src/components/Bubble/bubbleItem.module.scss similarity index 89% rename from src/components/Bubble/bubbleItem.scss rename to src/components/Bubble/bubbleItem.module.scss index e1a1dbb..8c032ea 100644 --- a/src/components/Bubble/bubbleItem.scss +++ b/src/components/Bubble/bubbleItem.module.scss @@ -1,4 +1,4 @@ -.bubble-option { +.bubbleOption { position: relative; border: 1px solid #e5e5e5; outline: none; // 移除浏览器默认的outline @@ -14,11 +14,11 @@ justify-content: center; gap: 8px; white-space: nowrap; - padding: 0; font-size: 14px; border-radius: 28px; margin: 0; - height: 36px; + width: 116px; + height: 28px; // 移除浏览器默认样式 &:focus { @@ -38,8 +38,6 @@ // 尺寸变体 &.small { - padding: 8px 12px; - min-height: 32px; font-size: 12px; } @@ -71,7 +69,7 @@ } // 图标样式 - .bubble-icon { + .bubbleIcon { display: flex; align-items: center; justify-content: center; @@ -83,12 +81,12 @@ } // 标签样式 - .bubble-label { + .bubbleLabel { font-weight: 500; } // 描述样式 - .bubble-description { + .bubbleDescription { font-size: 12px; opacity: 0.7; font-weight: 400; diff --git a/src/components/Bubble/index.scss b/src/components/Bubble/index.module.scss similarity index 51% rename from src/components/Bubble/index.scss rename to src/components/Bubble/index.module.scss index aa88fbb..52b676d 100644 --- a/src/components/Bubble/index.scss +++ b/src/components/Bubble/index.module.scss @@ -1,34 +1,23 @@ -.bubble-container { +.bubbleContainer { width: 100%; // 水平布局 - .bubble-horizontal { + .bubbleHorizontal { display: flex; gap: 12px; flex-wrap: wrap; justify-content: space-between; - - .bubble-option { - $gap: 8px; - $count: 3; - flex-basis: calc(100% / $count - $gap * 2); - gap: $gap; - } } // 垂直布局 - .bubble-vertical { + .bubbleVertical { display: flex; flex-direction: column; gap: 12px; - - .bubble-option { - width: 100%; - } } // 网格布局 - .bubble-grid { + .bubbleGrid { display: grid; width: 100%; } diff --git a/src/components/Bubble/index.tsx b/src/components/Bubble/index.tsx index 2c118d3..fe56b6d 100644 --- a/src/components/Bubble/index.tsx +++ b/src/components/Bubble/index.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; -import './index.scss'; -import BubbleItem from './BubbleItem'; +import React, { useState, useEffect } from "react"; +import styles from "./index.module.scss"; +import BubbleItem from "./BubbleItem"; export interface BubbleOption { id: string | number; @@ -14,12 +14,16 @@ export interface BubbleOption { export interface BubbleProps { options: BubbleOption[]; value?: string | number | (string | number)[]; - onChange?: (value: string | number | (string | number)[], option: BubbleOption | BubbleOption[]) => void; + onChange?: ( + value: string | number | (string | number)[], + option: BubbleOption | BubbleOption[] + ) => void; multiple?: boolean; - layout?: 'horizontal' | 'vertical' | 'grid'; + layout?: "horizontal" | "vertical" | "grid"; columns?: number; - size?: 'small' | 'medium' | 'large'; + size?: "small" | "medium" | "large"; className?: string; + itemClassName?: string; style?: React.CSSProperties; disabled?: boolean; } @@ -29,12 +33,13 @@ const Bubble: React.FC = ({ value, onChange, multiple = false, - layout = 'horizontal', + layout = "horizontal", columns = 3, - size = 'small', - className = '', + size = "small", + className = "", + itemClassName = "", style = {}, - disabled = false + disabled = false, }) => { const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]); @@ -53,7 +58,7 @@ const Bubble: React.FC = ({ if (multiple) { if (selectedValues.includes(option.value)) { - newSelectedValues = selectedValues.filter(v => v !== option.value); + newSelectedValues = selectedValues.filter((v) => v !== option.value); } else { newSelectedValues = [...selectedValues, option.value]; } @@ -62,11 +67,13 @@ const Bubble: React.FC = ({ } setSelectedValues(newSelectedValues); - + // 调用onChange回调,传递选中的值和对应的选项 if (onChange) { if (multiple) { - const selectedOptions = options.filter(opt => newSelectedValues.includes(opt.value)); + const selectedOptions = options.filter((opt) => + newSelectedValues.includes(opt.value) + ); onChange(newSelectedValues, selectedOptions); } else { onChange(option.value, option); @@ -79,7 +86,7 @@ const Bubble: React.FC = ({ }; const renderHorizontalLayout = () => ( -
+
{options.map((option) => ( = ({ size={size} disabled={disabled} onClick={handleOptionClick} + itemClassName={itemClassName} /> ))}
); const renderVerticalLayout = () => ( -
+
{options.map((option) => ( = ({ size={size} disabled={disabled} onClick={handleOptionClick} + itemClassName={itemClassName} /> ))}
); const renderGridLayout = () => ( -
{options.map((option) => ( @@ -124,6 +133,7 @@ const Bubble: React.FC = ({ size={size} disabled={disabled} onClick={handleOptionClick} + itemClassName={itemClassName} /> ))}
@@ -131,11 +141,11 @@ const Bubble: React.FC = ({ const renderLayout = () => { switch (layout) { - case 'horizontal': + case "horizontal": return renderHorizontalLayout(); - case 'vertical': + case "vertical": return renderVerticalLayout(); - case 'grid': + case "grid": return renderGridLayout(); default: return renderHorizontalLayout(); @@ -143,7 +153,7 @@ const Bubble: React.FC = ({ }; return ( -
+
{renderLayout()}
); diff --git a/src/components/CityFilter/example.tsx b/src/components/CityFilter/example.tsx new file mode 100644 index 0000000..022297a --- /dev/null +++ b/src/components/CityFilter/example.tsx @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..2be87e2 --- /dev/null +++ b/src/components/CityFilter/index.module.scss @@ -0,0 +1,59 @@ +.menuWrap { + padding: 5px 20px 10px; + .menuItem { + left: 0; + border-bottom-left-radius: 30px; + border-bottom-right-radius: 30px; + } + &.active { + .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: 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; + } + + .nut-menu-title-text { + padding-left: 0; + } + } + + .positionWrap { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin-bottom: 16px; + } + + .title { + font-size: 14px; + font-weight: 600; + } + + .cityName { + font-size: 13px; + font-weight: 400; + } + .distanceWrap { + margin-bottom: 16px; + width: 100%; + } + .distanceBubbleItem { + width: auto; + } +} diff --git a/src/components/CityFilter/index.tsx b/src/components/CityFilter/index.tsx new file mode 100644 index 0000000..db40f86 --- /dev/null +++ b/src/components/CityFilter/index.tsx @@ -0,0 +1,64 @@ +import { Menu } from "@nutui/nutui-react-taro"; +import styles from "./index.module.scss"; +import { useState, useRef } from "react"; +import Bubble, { BubbleOption } from "../Bubble"; + +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 [isChange, setIsChange] = useState(false); + const itemRef = useRef(null); + + const handleChange = (value: string) => { + console.log("===value", value); + setIsChange(true); + onChange && onChange(value); + }; + + 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" }, + ]; + + return ( + + +
+

当前位置

+

上海市

+
+
+ +
+
+
+ ); +}; + +export default MenuComponent; diff --git a/src/components/Menu/example.tsx b/src/components/Menu/example.tsx new file mode 100644 index 0000000..022297a --- /dev/null +++ b/src/components/Menu/example.tsx @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..0867087 --- /dev/null +++ b/src/components/Menu/index.module.scss @@ -0,0 +1,45 @@ +.menuWrap { + padding: 5px 20px 10px; + .menuItem { + left: 0; + border-bottom-left-radius: 30px; + border-bottom-right-radius: 30px; + } + &.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: 1px solid #e5e5e5; + line-height: 28px; + font-size: 14px; + width: max-content; + + .nut-menu-title-text { + padding-left: 0; + } + } + + :global(.nut-menu-title) { + color: inherit !important; + font-weight: 600; + } + + :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; + } +} diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx new file mode 100644 index 0000000..0f3d435 --- /dev/null +++ b/src/components/Menu/index.tsx @@ -0,0 +1,37 @@ +import { Menu } from "@nutui/nutui-react-taro"; +import styles from "./index.module.scss"; +import { useState } from "react"; + +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 [isChange, setIsChange] = useState(false); + + const handleChange = (value: string) => { + setIsChange(true); + onChange && onChange(value); + }; + + return ( + + + + ); +}; + +export default MenuComponent; diff --git a/src/components/Range/index.module.scss b/src/components/Range/index.module.scss new file mode 100644 index 0000000..571e63d --- /dev/null +++ b/src/components/Range/index.module.scss @@ -0,0 +1,81 @@ + + +// 全局NutUI样式覆盖 +.nutRange { + width: 100% !important; + height: 100% !important; + + // .nut-range__bar-box { + // margin: 0 !important; + // padding: 0 !important; + // } + + // .nut-range__bar { + // margin: 0 !important; + // } +} + +.nutRangeHeader { + line-height: 20px; + display: flex; + align-items: center; + justify-content: space-between; + .nutRangeHeaderLeft { + display: flex; + align-items: center; + gap: 8px; + } + .nutRangeHeaderTitle { + font-weight: 600; + font-size: 14px; + color: #000000; + } + + .nutRangeHeaderContent { + font-weight: 400; + font-size: 14px; + color: #3c3c34; + } +} + +.rangeWrapper { + display: flex; + align-items: center; + justify-content: space-between; + gap: 5px; + height: 44px; + border-radius: 12px; + border: 1px solid #e0e0e0; + padding: 0 10px; + .rangeHandle { + :global(.nut-range-mark) { + padding-top: 28px; + left: 8px; + } + :global(.nut-range-bar) { + background: #000000; + height: 6px; + } + :global(.nut-range-button) { + border: none; + box-shadow: 0 6px 13px 0 rgba(0, 0, 0, 0.12); + } + + :global(.nut-range-tick) { + background: #3c3c3c; + height: 4px !important; + width: 4px !important; + } + } + + span { + font-size: 12px; + color: #999; + } + .rangeWrapperMin, + .rangeWrapperMax { + flex-shrink: 0; + font-size: 12px; + color: #000000; + } +} diff --git a/src/components/Range/index.scss b/src/components/Range/index.scss deleted file mode 100644 index 00bd829..0000000 --- a/src/components/Range/index.scss +++ /dev/null @@ -1,246 +0,0 @@ -.ntrp-range { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - user-select: none; - - &__header { - display: flex; - align-items: center; - margin-bottom: 16px; - } - - &__icon { - margin-right: 8px; - display: flex; - align-items: center; - - svg { - width: 20px; - height: 20px; - } - } - - &__title { - margin: 0; - font-size: 16px; - font-weight: 500; - color: #000; - line-height: 1.2; - } - - &__slider-container { - position: relative; - } - - &__labels { - display: flex; - justify-content: space-between; - margin-bottom: 12px; - } - - &__label { - font-size: 14px; - color: #000; - font-weight: 400; - } - - &__track-container { - position: relative; - padding: 0 10px; - } - - &__track { - position: relative; - height: 40px; - background: #fff; - border: 1px solid #e0e0e0; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - display: flex; - align-items: center; - padding: 0 20px; - } - - &__markers { - position: absolute; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 1; - } - - &__marker { - position: absolute; - width: 6px; - height: 6px; - background: #e0e0e0; - border-radius: 50%; - top: 50%; - transform: translate(-50%, -50%); - } - - &__nutui-wrapper { - position: relative; - width: 100%; - height: 100%; - z-index: 2; - - // 隐藏NutUI的默认标签 - .nut-range__label { - display: none !important; - } - - // 隐藏默认的轨道背景 - .nut-range__bar-box { - background: transparent !important; - height: 6px !important; - border-radius: 3px !important; - margin: 0 !important; - padding: 0 !important; - } - - // 隐藏默认的轨道 - .nut-range__bar { - background: transparent !important; - height: 0 !important; - margin: 0 !important; - } - - // 自定义活动轨道(黑色填充条) - .nut-range__bar--active { - background: #000 !important; - height: 6px !important; - border-radius: 3px !important; - margin: 0 !important; - } - - // 自定义滑块手柄样式 - .nut-range__button { - width: 24px !important; - height: 24px !important; - background: #fff !important; - border: 2px solid #000 !important; - border-radius: 50% !important; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important; - top: 50% !important; - transform: translate(-50%, -50%) !important; - transition: all 0.1s ease !important; - - &:hover { - transform: translate(-50%, -50%) scale(1.1) !important; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important; - } - - &:active { - transform: translate(-50%, -50%) scale(1.15) !important; - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25) !important; - } - } - - // 确保滑块在正确位置 - .nut-range__button-wrapper { - top: 50% !important; - transform: translateY(-50%) !important; - } - } - - // 响应式设计 - @media (max-width: 480px) { - &__track { - padding: 0 15px; - } - - &__nutui-wrapper { - .nut-range__button { - width: 20px !important; - height: 20px !important; - } - } - - &__title { - font-size: 14px; - } - - &__label { - font-size: 12px; - } - } -} - -// 全局NutUI样式覆盖 -.nut-range { - width: 100% !important; - height: 100% !important; - - .nut-range__bar-box { - margin: 0 !important; - padding: 0 !important; - } - - .nut-range__bar { - margin: 0 !important; - } -} - -.ntrp-range__header { - line-height: 20px; - display: flex; - align-items: center; - justify-content: space-between; - .ntrp-range__header-left { - display: flex; - align-items: center; - gap: 8px; - } - .ntrp-range__title { - font-weight: 600; - font-size: 14px; - color: #000000; - } - - .ntrp-range__content { - font-weight: 400; - font-size: 14px; - color: #3c3c34; - } -} - -.rangeWrapper { - display: flex; - align-items: center; - justify-content: space-between; - gap: 5px; - height: 44px; - border-radius: 12px; - border: 1px solid #e0e0e0; - padding: 0 10px; - .rangeHandle { - .nut-range-mark { - padding-top: 28px; - } - .nut-range-bar { - background: #000000; - height: 6px; - } - .nut-range-button { - border: none; - box-shadow: 0 6px 13px 0 rgba(0, 0, 0, 0.12); - } - - .nut-range-tick { - background: #3c3c3c; - height: 4px !important; - width: 4px !important; - } - } - - span { - font-size: 12px; - color: #999; - } - .rangeWrapper__min, - .rangeWrapper__max { - flex-shrink: 0; - font-size: 12px; - color: #000000; - } -} diff --git a/src/components/Range/index.tsx b/src/components/Range/index.tsx index 0523235..b1ddbf6 100644 --- a/src/components/Range/index.tsx +++ b/src/components/Range/index.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useMemo } from "react"; import { Range } from "@nutui/nutui-react-taro"; -import "./index.scss"; +import styles from "./index.module.scss"; interface RangeProps { min?: number; @@ -48,18 +48,18 @@ const NtrpRange: React.FC = ({ }, [currentValue, min, max]); return ( -
-
-
+
+
+
icon
-

NTRP水平区间

+

NTRP水平区间

-

{rangContent}

+

{rangContent}

-
- {min} +
+ {min} = ({ onChange={handleChange} disabled={disabled} defaultValue={[min, max]} - className="rangeHandle" + className={styles.rangeHandle} maxDescription={null} minDescription={null} currentDescription={null} marks={marks} style={{ color: "gold" }} /> - {max} + {max}
diff --git a/src/pages/list/index.tsx b/src/pages/list/index.tsx index 14b6563..bc5f426 100644 --- a/src/pages/list/index.tsx +++ b/src/pages/list/index.tsx @@ -2,6 +2,8 @@ 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 "./index.scss"; import { useEffect } from "react"; import Taro from "@tarojs/taro"; @@ -160,6 +162,12 @@ const ListPage = () => {
)} + {/* 全城筛选 */} + + + {/* 菜单 */} + + {/* 范围选择 */} diff --git a/types/css-modules.d.ts b/types/css-modules.d.ts new file mode 100644 index 0000000..01d9fe6 --- /dev/null +++ b/types/css-modules.d.ts @@ -0,0 +1,9 @@ +declare module '*.module.scss' { + const classes: { [key: string]: string }; + export default classes; +} + +declare module '*.module.css' { + const classes: { [key: string]: string }; + export default classes; +} From 169eaffb96f0602d3fca5c47c3884dd8b447b123 Mon Sep 17 00:00:00 2001 From: juguohong Date: Mon, 18 Aug 2025 00:36:57 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E5=88=97=E6=99=92=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Bubble/bubbleItem.module.scss | 4 +- src/components/Bubble/index.module.scss | 3 + src/components/Bubble/index.tsx | 4 +- src/components/CityFilter/index.module.scss | 1 + src/components/Range/index.tsx | 33 ++++----- src/components/SearchBar/index.module.scss | 23 +++++++ src/components/SearchBar/index.tsx | 22 ++++++ src/components/Title/index.module.scss | 11 +++ src/components/Title/index.tsx | 19 ++++++ src/pages/list/FilterPopup.tsx | 72 ++++++++++++++++++++ src/pages/list/filterPopup.module.scss | 8 +++ src/pages/list/index.config.ts | 2 +- src/pages/list/index.tsx | 41 ++++------- 13 files changed, 193 insertions(+), 50 deletions(-) create mode 100644 src/components/SearchBar/index.module.scss create mode 100644 src/components/SearchBar/index.tsx create mode 100644 src/components/Title/index.module.scss create mode 100644 src/components/Title/index.tsx create mode 100644 src/pages/list/FilterPopup.tsx create mode 100644 src/pages/list/filterPopup.module.scss diff --git a/src/components/Bubble/bubbleItem.module.scss b/src/components/Bubble/bubbleItem.module.scss index 8c032ea..a56297d 100644 --- a/src/components/Bubble/bubbleItem.module.scss +++ b/src/components/Bubble/bubbleItem.module.scss @@ -4,7 +4,6 @@ outline: none; // 移除浏览器默认的outline background: #ffffff; color: #333333; - font-size: 14px; font-weight: 400; text-align: center; cursor: pointer; @@ -12,9 +11,8 @@ display: flex; align-items: center; justify-content: center; - gap: 8px; white-space: nowrap; - font-size: 14px; + font-size: 12px; border-radius: 28px; margin: 0; width: 116px; diff --git a/src/components/Bubble/index.module.scss b/src/components/Bubble/index.module.scss index 52b676d..8a46b7b 100644 --- a/src/components/Bubble/index.module.scss +++ b/src/components/Bubble/index.module.scss @@ -20,5 +20,8 @@ .bubbleGrid { display: grid; width: 100%; + .bubbleOption { + width: 100%; + } } } diff --git a/src/components/Bubble/index.tsx b/src/components/Bubble/index.tsx index fe56b6d..264de4e 100644 --- a/src/components/Bubble/index.tsx +++ b/src/components/Bubble/index.tsx @@ -122,7 +122,7 @@ const Bubble: React.FC = ({ className={styles.bubbleGrid} style={{ gridTemplateColumns: `repeat(${columns}, 1fr)`, - gap: size === "small" ? "8px" : size === "large" ? "16px" : "12px", + gap: size === "small" ? "6px" : size === "large" ? "16px" : "12px", }} > {options.map((option) => ( @@ -133,7 +133,7 @@ const Bubble: React.FC = ({ size={size} disabled={disabled} onClick={handleOptionClick} - itemClassName={itemClassName} + itemClassName={itemClassName || styles.bubbleOption} /> ))}
diff --git a/src/components/CityFilter/index.module.scss b/src/components/CityFilter/index.module.scss index 2be87e2..aa8442f 100644 --- a/src/components/CityFilter/index.module.scss +++ b/src/components/CityFilter/index.module.scss @@ -48,6 +48,7 @@ .cityName { font-size: 13px; font-weight: 400; + color: #3C3C43; } .distanceWrap { margin-bottom: 16px; diff --git a/src/components/Range/index.tsx b/src/components/Range/index.tsx index b1ddbf6..84fe4b1 100644 --- a/src/components/Range/index.tsx +++ b/src/components/Range/index.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useMemo } from "react"; import { Range } from "@nutui/nutui-react-taro"; import styles from "./index.module.scss"; +import TitleComponent from "../Title"; interface RangeProps { min?: number; @@ -9,6 +10,7 @@ interface RangeProps { value?: [number, number]; onChange?: (value: [number, number]) => void; disabled?: boolean; + className?: string; } const NtrpRange: React.FC = ({ @@ -18,15 +20,15 @@ const NtrpRange: React.FC = ({ value = [min, max], onChange, disabled = false, + className, }) => { const [currentValue, setCurrentValue] = useState<[number, number]>(value); useEffect(() => { - setCurrentValue(value); - }, [value]); + value && setCurrentValue(value); + }, [JSON.stringify(value || [])]); const handleChange = (val: [number, number]) => { - console.log("Range value changed:", val); setCurrentValue(val); onChange?.(val); }; @@ -41,32 +43,33 @@ const NtrpRange: React.FC = ({ const rangContent = useMemo(() => { const [start, end] = currentValue || []; - if (start === min && end === max) { + if (Number(start) === Number(min) && Number(end) === Number(max)) { return "不限"; } return `${start.toFixed(1)} - ${end.toFixed(1)}之间`; - }, [currentValue, min, max]); + }, [JSON.stringify(currentValue || []), min, max]); return ( -
+
-
+ {/*
icon

NTRP水平区间

-
+
*/} +

{rangContent}

- {min} + {min.toFixed(1)} = ({ minDescription={null} currentDescription={null} marks={marks} - style={{ color: "gold" }} /> - {max} + {max.toFixed(1)}
- - {/* 调试信息 */} -
- 当前值: {currentValue[0].toFixed(1)} - {currentValue[1].toFixed(1)} -
); }; diff --git a/src/components/SearchBar/index.module.scss b/src/components/SearchBar/index.module.scss new file mode 100644 index 0000000..b376efc --- /dev/null +++ b/src/components/SearchBar/index.module.scss @@ -0,0 +1,23 @@ +.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; + :global(.nut-searchbar-content) { + box-shadow: 0 4px 48px #00000014; + } + .searchBarRight { + width: 44px; + height: 44px; + border-radius: 50%; + border: 1px solid #0000000F; + background-color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + } +} \ No newline at end of file diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx new file mode 100644 index 0000000..60fd06f --- /dev/null +++ b/src/components/SearchBar/index.tsx @@ -0,0 +1,22 @@ + +import { SearchBar } from '@nutui/nutui-react-taro' +import styles from './index.module.scss' + +const SearchBarComponent = () => { + return ( + <> + 123
+ // } + right={ +
+ } + className={styles.searchBar} + placeholder='搜索上海的球局和场地' + /> + + ) +} + +export default SearchBarComponent \ No newline at end of file diff --git a/src/components/Title/index.module.scss b/src/components/Title/index.module.scss new file mode 100644 index 0000000..a731606 --- /dev/null +++ b/src/components/Title/index.module.scss @@ -0,0 +1,11 @@ +.titleContainer { + display: flex; + align-items: center; + margin-bottom: 10px; + gap: 6px; +} +.title { + font-weight: 600; + font-size: 16px; + line-height: 20px; +} diff --git a/src/components/Title/index.tsx b/src/components/Title/index.tsx new file mode 100644 index 0000000..2eca985 --- /dev/null +++ b/src/components/Title/index.tsx @@ -0,0 +1,19 @@ +import styles from "./index.module.scss"; +interface IProps { + title: string; + className?: string; +} +const TitleComponent = (props: IProps) => { + const { title, className } = props; + return ( + <> +
+
+

{title}

+
+ + ); +}; +export default TitleComponent; diff --git a/src/pages/list/FilterPopup.tsx b/src/pages/list/FilterPopup.tsx new file mode 100644 index 0000000..1e31276 --- /dev/null +++ b/src/pages/list/FilterPopup.tsx @@ -0,0 +1,72 @@ +import { Popup } from "@nutui/nutui-react-taro"; +import Range from "../../components/Range"; +import Bubble, { BubbleOption } from "../../components/Bubble"; +import styles from "./filterPopup.module.scss"; +import TitleComponent from "src/components/Title"; + +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 locationOptions: BubbleOption[] = [ + { id: 1, label: "室内", value: "1" }, + { id: 2, label: "室外", value: "2" }, + { id: 3, label: "半室外", value: "3" }, +]; + +const FilterPopup = () => { + return ( + <> + { + // setShowTop(false) + }} + > +
+ {/* 时间气泡选项 */} + {}} + onChange={(value) => {}} + layout="grid" + size="small" + columns={3} + /> + + {/* 范围选择 */} + + + {/* 场次气泡选项 */} +
+ + {}} + onChange={(value) => {}} + layout="grid" + size="small" + columns={3} + /> +
+
+
+ + ); +}; + +export default FilterPopup; diff --git a/src/pages/list/filterPopup.module.scss b/src/pages/list/filterPopup.module.scss new file mode 100644 index 0000000..c1e834a --- /dev/null +++ b/src/pages/list/filterPopup.module.scss @@ -0,0 +1,8 @@ +.filterPopupWrapper { + $m18: 18px; + padding: $m18; + .filterPopupRange { + margin-top: $m18; + margin-bottom: $m18; + } +} \ No newline at end of file diff --git a/src/pages/list/index.config.ts b/src/pages/list/index.config.ts index 5c5ad16..17f6db0 100644 --- a/src/pages/list/index.config.ts +++ b/src/pages/list/index.config.ts @@ -1,5 +1,5 @@ export default definePageConfig({ - navigationBarTitleText: '列表', + navigationBarTitleText: '', enablePullDownRefresh: true, backgroundTextStyle: 'dark' }) diff --git a/src/pages/list/index.tsx b/src/pages/list/index.tsx index bc5f426..7587ed3 100644 --- a/src/pages/list/index.tsx +++ b/src/pages/list/index.tsx @@ -4,6 +4,8 @@ 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 SearchBar from "../../components/SearchBar"; +import FilterPopup from "./FilterPopup"; import "./index.scss"; import { useEffect } from "react"; import Taro from "@tarojs/taro"; @@ -146,33 +148,20 @@ const ListPage = () => { return (
- {/* 状态信息栏 */} - {lastRefreshTime && ( -
- 最后更新: {formatRefreshTime(lastRefreshTime)} | 共 {matches.length}{" "} - 场比赛 -
- )} + + {/* 综合筛选 */} +
+ +
+ {/* 筛选 */} +
+ {/* 全城筛选 */} + + {/* 智能排序 */} + +
- {/* 全城筛选 */} - - - {/* 菜单 */} - - - {/* 范围选择 */} - - - {/* 气泡 */} - + {/* 列表内容 */} From 55b9201c221059daceaec8a27c428f0aff0ab506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=9D=B0?= Date: Mon, 18 Aug 2025 14:07:34 +0800 Subject: [PATCH 4/4] feat: svg --- config/index.ts | 14 +++++++++- src/config/images.js | 11 ++++++++ src/scss/images.scss | 30 +++++++++++++++++++++ src/static/publishBall/icon-add.svg | 3 +++ src/static/publishBall/icon-arrow-right.svg | 3 +++ src/static/publishBall/icon-changda.svg | 21 +++++++++++++++ src/static/publishBall/icon-cost.svg | 7 +++++ src/static/publishBall/icon-gameplay.svg | 13 +++++++++ src/static/publishBall/icon-location.svg | 7 +++++ src/static/publishBall/icon-personal.svg | 19 +++++++++++++ src/static/publishBall/icon-remove.svg | 3 +++ src/static/publishBall/icon-tips.svg | 5 ++++ src/static/publishBall/icon-upload.svg | 20 ++++++++++++++ 13 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/config/images.js create mode 100644 src/scss/images.scss create mode 100644 src/static/publishBall/icon-add.svg create mode 100644 src/static/publishBall/icon-arrow-right.svg create mode 100644 src/static/publishBall/icon-changda.svg create mode 100644 src/static/publishBall/icon-cost.svg create mode 100644 src/static/publishBall/icon-gameplay.svg create mode 100644 src/static/publishBall/icon-location.svg create mode 100644 src/static/publishBall/icon-personal.svg create mode 100644 src/static/publishBall/icon-remove.svg create mode 100644 src/static/publishBall/icon-tips.svg create mode 100644 src/static/publishBall/icon-upload.svg diff --git a/config/index.ts b/config/index.ts index 3eb418b..267a4ac 100644 --- a/config/index.ts +++ b/config/index.ts @@ -3,6 +3,8 @@ import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin' import devConfig from './dev' import prodConfig from './prod' import vitePluginImp from 'vite-plugin-imp' +import path from 'path' + // https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数 export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { const baseConfig: UserConfigExport<'webpack5'> = { @@ -20,6 +22,16 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { plugins: ['@tarojs/plugin-html'], defineConstants: { }, + alias: { + '@': path.resolve(__dirname, '..', 'src'), + '@/assets': path.resolve(__dirname, '..', 'src/assets'), + '@/components': path.resolve(__dirname, '..', 'src/components'), + '@/utils': path.resolve(__dirname, '..', 'src/utils'), + '@/services': path.resolve(__dirname, '..', 'src/services'), + '@/store': path.resolve(__dirname, '..', 'src/store'), + '@/config': path.resolve(__dirname, '..', 'src/config'), + '@/static': path.resolve(__dirname, '..', 'src/static'), + }, copy: { patterns: [ ], @@ -29,7 +41,7 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { framework: 'react', compiler: { - type: 'webpack5', + type: 'webpack5', prebundle: { enable: false } diff --git a/src/config/images.js b/src/config/images.js new file mode 100644 index 0000000..ab20192 --- /dev/null +++ b/src/config/images.js @@ -0,0 +1,11 @@ +export default { + ICON_REMOVE: require('@/static/publishBall/icon-remove.svg'), + ICON_UPLOAD: require('@/static/publishBall/icon-upload.svg'), + ICON_LOCATION: require('@/static/publishBall/icon-location.svg'), + ICON_GAMEPLAY: require('@/static/publishBall/icon-gameplay.svg'), + ICON_PERSONAL: require('@/static/publishBall/icon-personal.svg'), + ICON_CHANGDA: require('@/static/publishBall/icon-changda.svg'), + 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'), + } \ No newline at end of file diff --git a/src/scss/images.scss b/src/scss/images.scss new file mode 100644 index 0000000..32d40b4 --- /dev/null +++ b/src/scss/images.scss @@ -0,0 +1,30 @@ +// src/scss/images.scss +// 暴露公共API (可选) +@forward 'sass:map'; +@forward 'sass:meta'; +@use 'sass:map'; + +// 使用私有变量命名 (前缀加 -) +$-static-path: '~@/static/' !default; + +// 图片映射表 +$-images: ( + 'icon-upload': '/publishBall/icon-upload.svg', + 'icon-add': '/publishBall/icon-add.svg', + 'icon-location': '/publishBall/icon-location.svg', + 'icon-gameplay': '/publishBall/icon-gameplay.svg', + 'icon-personal': '/publishBall/icon-personal.svg', + 'icon-changda': '/publishBall/icon-changda.svg', + 'icon-cost': '/publishBall/icon-cost.svg', + 'icon-remove': '/publishBall/icon-remove.svg' +) !default; + +// 图片获取函数 +@function taro-image($name) { + @if not map.has-key($-images, $name) { + @warn "Image `#{$name}` not found in $images map"; + @return url($-static-path + 'default.png'); + } + @return url($-static-path + map.get($-images, $name)); +} + diff --git a/src/static/publishBall/icon-add.svg b/src/static/publishBall/icon-add.svg new file mode 100644 index 0000000..5ef824e --- /dev/null +++ b/src/static/publishBall/icon-add.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/publishBall/icon-arrow-right.svg b/src/static/publishBall/icon-arrow-right.svg new file mode 100644 index 0000000..a490b6d --- /dev/null +++ b/src/static/publishBall/icon-arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/publishBall/icon-changda.svg b/src/static/publishBall/icon-changda.svg new file mode 100644 index 0000000..6cc0b19 --- /dev/null +++ b/src/static/publishBall/icon-changda.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/publishBall/icon-cost.svg b/src/static/publishBall/icon-cost.svg new file mode 100644 index 0000000..c7cd7e5 --- /dev/null +++ b/src/static/publishBall/icon-cost.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/static/publishBall/icon-gameplay.svg b/src/static/publishBall/icon-gameplay.svg new file mode 100644 index 0000000..63a7e62 --- /dev/null +++ b/src/static/publishBall/icon-gameplay.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/static/publishBall/icon-location.svg b/src/static/publishBall/icon-location.svg new file mode 100644 index 0000000..b5236dc --- /dev/null +++ b/src/static/publishBall/icon-location.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/static/publishBall/icon-personal.svg b/src/static/publishBall/icon-personal.svg new file mode 100644 index 0000000..30a0f54 --- /dev/null +++ b/src/static/publishBall/icon-personal.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/static/publishBall/icon-remove.svg b/src/static/publishBall/icon-remove.svg new file mode 100644 index 0000000..d63cdb8 --- /dev/null +++ b/src/static/publishBall/icon-remove.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/publishBall/icon-tips.svg b/src/static/publishBall/icon-tips.svg new file mode 100644 index 0000000..6a4eb85 --- /dev/null +++ b/src/static/publishBall/icon-tips.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/static/publishBall/icon-upload.svg b/src/static/publishBall/icon-upload.svg new file mode 100644 index 0000000..c72d2ce --- /dev/null +++ b/src/static/publishBall/icon-upload.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + +