合并代码
50
analyze.js
Normal file
@@ -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();
|
||||||
@@ -2,7 +2,7 @@ import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
|||||||
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
|
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
|
||||||
import devConfig from './dev'
|
import devConfig from './dev'
|
||||||
import prodConfig from './prod'
|
import prodConfig from './prod'
|
||||||
import vitePluginImp from 'vite-plugin-imp'
|
// import vitePluginImp from 'vite-plugin-imp'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||||
@@ -16,7 +16,7 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
|
|||||||
750: 1,
|
750: 1,
|
||||||
375: 2,
|
375: 2,
|
||||||
828: 1.81 / 2,
|
828: 1.81 / 2,
|
||||||
390: 1.92
|
390: 1.92
|
||||||
},
|
},
|
||||||
sourceRoot: 'src',
|
sourceRoot: 'src',
|
||||||
outputRoot: 'dist',
|
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) {
|
webpackChain(chain) {
|
||||||
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
|
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
|
||||||
}
|
},
|
||||||
|
// @ts-expect-error: Taro 类型定义缺少 mini.hot
|
||||||
|
hot: true,
|
||||||
},
|
},
|
||||||
h5: {
|
h5: {
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
|
|||||||
23
project.private.config.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
export default defineAppConfig({
|
export default defineAppConfig({
|
||||||
pages: [
|
pages: [
|
||||||
|
'pages/list/index',
|
||||||
|
// 'pages/userInfo/myself/index',
|
||||||
'pages/login/index/index',
|
'pages/login/index/index',
|
||||||
'pages/login/verification/index',
|
'pages/login/verification/index',
|
||||||
'pages/login/terms/index',
|
'pages/login/terms/index',
|
||||||
// 'pages/publishBall/index',
|
'pages/publishBall/index',
|
||||||
// 'pages/mapDisplay/index',
|
// 'pages/mapDisplay/index',
|
||||||
// 'pages/list/index',
|
'pages/detail/index',
|
||||||
'pages/index/index'
|
'pages/message/index',
|
||||||
|
'pages/personal/index',
|
||||||
|
'pages/orderCheck/index',
|
||||||
],
|
],
|
||||||
window: {
|
window: {
|
||||||
backgroundTextStyle: 'light',
|
backgroundTextStyle: 'light',
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'PoetsenOne';
|
||||||
|
src: url('./static/asserts/fonts/PoetsenOne-Regular.ttf') format('truetype');
|
||||||
|
}
|
||||||
17
src/app.ts
@@ -1,7 +1,10 @@
|
|||||||
import { Component, ReactNode } from 'react'
|
import { Component, ReactNode } from 'react'
|
||||||
import './app.scss'
|
|
||||||
import './nutui-theme.scss'
|
import './nutui-theme.scss'
|
||||||
|
import './app.scss'
|
||||||
import { useDictionaryStore } from './store/dictionaryStore'
|
import { useDictionaryStore } from './store/dictionaryStore'
|
||||||
|
import { useGlobalStore } from './store/global'
|
||||||
|
|
||||||
|
// import { getNavbarHeight } from "@/utils/getNavbarHeight";
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -11,11 +14,12 @@ class App extends Component<AppProps> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// 初始化字典数据
|
// 初始化字典数据
|
||||||
this.initDictionaryData()
|
this.initDictionaryData()
|
||||||
|
this.getNavBarHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidShow() {}
|
componentDidShow() { }
|
||||||
|
|
||||||
componentDidHide() {}
|
componentDidHide() { }
|
||||||
|
|
||||||
// 初始化字典数据
|
// 初始化字典数据
|
||||||
private async initDictionaryData() {
|
private async initDictionaryData() {
|
||||||
@@ -27,6 +31,13 @@ class App extends Component<AppProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取导航高度
|
||||||
|
getNavBarHeight = () => {
|
||||||
|
const { getNavbarHeightInfo } = useGlobalStore.getState()
|
||||||
|
getNavbarHeightInfo()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// this.props.children 是将要会渲染的页面
|
// this.props.children 是将要会渲染的页面
|
||||||
return this.props.children
|
return this.props.children
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BubbleOption } from './index';
|
import { BubbleItemProps } from '../../../types/list/types';
|
||||||
import styles from './bubbleItem.module.scss';
|
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<BubbleItemProps> = ({
|
const BubbleItem: React.FC<BubbleItemProps> = ({
|
||||||
option,
|
option,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
|||||||
@@ -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<string>('');
|
|
||||||
|
|
||||||
const locationOptions: BubbleOption[] = [
|
|
||||||
{ id: 1, label: '室内', value: 'indoor' },
|
|
||||||
{ id: 2, label: '室外', value: 'outdoor' },
|
|
||||||
{ id: 3, label: '半室外', value: 'semi-outdoor' }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Bubble
|
|
||||||
options={locationOptions}
|
|
||||||
value={selectedLocation}
|
|
||||||
onChange={(value) => setSelectedLocation(value as string)}
|
|
||||||
layout="horizontal"
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 时间选择器示例
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const TimeSelector: React.FC = () => {
|
|
||||||
const [selectedTime, setSelectedTime] = useState<string>('');
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Bubble
|
|
||||||
options={timeOptions}
|
|
||||||
value={selectedTime}
|
|
||||||
onChange={(value) => setSelectedTime(value as string)}
|
|
||||||
layout="grid"
|
|
||||||
columns={3}
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 多选模式
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const MultiSelectExample: React.FC = () => {
|
|
||||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const options: BubbleOption[] = [
|
|
||||||
{ id: 1, label: '运动', value: 'sports' },
|
|
||||||
{ id: 2, label: '音乐', value: 'music' },
|
|
||||||
{ id: 3, label: '阅读', value: 'reading' },
|
|
||||||
{ id: 4, label: '旅行', value: 'travel' }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValues}
|
|
||||||
onChange={(value) => setSelectedValues(value as string[])}
|
|
||||||
multiple={true}
|
|
||||||
layout="grid"
|
|
||||||
columns={2}
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 带图标和描述
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const IconExample: React.FC = () => {
|
|
||||||
const [selectedValue, setSelectedValue] = useState<string>('');
|
|
||||||
|
|
||||||
const options: BubbleOption[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
label: '网球',
|
|
||||||
value: 'tennis',
|
|
||||||
icon: '🎾',
|
|
||||||
description: '室内外均可'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
label: '篮球',
|
|
||||||
value: 'basketball',
|
|
||||||
icon: '🏀',
|
|
||||||
description: '室内场地'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={(value) => 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` 可以是字符串或数字类型
|
|
||||||
- 组件会自动处理选中状态的样式变化
|
|
||||||
- 支持图标和描述,让选项更加丰富
|
|
||||||
- 响应式设计,自动适应不同屏幕尺寸
|
|
||||||
@@ -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<string>('');
|
|
||||||
|
|
||||||
const locationOptions: BubbleOption[] = [
|
|
||||||
{ id: 1, label: '室内', value: 'indoor' },
|
|
||||||
{ id: 2, label: '室外', value: 'outdoor' },
|
|
||||||
{ id: 3, label: '半室外', value: 'semi-outdoor' }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>选择场地类型</h2>
|
|
||||||
<Bubble
|
|
||||||
options={locationOptions}
|
|
||||||
value={selectedLocation}
|
|
||||||
onChange={(value) => setSelectedLocation(value as string)}
|
|
||||||
layout="horizontal"
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
<p>您选择的场地类型: {selectedLocation}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 时间选择器
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const [selectedTime, setSelectedTime] = useState<string>('');
|
|
||||||
|
|
||||||
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' }
|
|
||||||
];
|
|
||||||
|
|
||||||
<Bubble
|
|
||||||
options={timeOptions}
|
|
||||||
value={selectedTime}
|
|
||||||
onChange={(value) => setSelectedTime(value as string)}
|
|
||||||
layout="grid"
|
|
||||||
columns={3}
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 多选模式
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const options: BubbleOption[] = [
|
|
||||||
{ id: 1, label: '运动', value: 'sports' },
|
|
||||||
{ id: 2, label: '音乐', value: 'music' },
|
|
||||||
{ id: 3, label: '阅读', value: 'reading' },
|
|
||||||
{ id: 4, label: '旅行', value: 'travel' }
|
|
||||||
];
|
|
||||||
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValues}
|
|
||||||
onChange={(value) => setSelectedValues(value as string[])}
|
|
||||||
multiple={true}
|
|
||||||
layout="grid"
|
|
||||||
columns={2}
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 带图标和描述
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const [selectedSport, setSelectedSport] = useState<string>('');
|
|
||||||
|
|
||||||
const sportOptions: BubbleOption[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
label: '网球',
|
|
||||||
value: 'tennis',
|
|
||||||
icon: '🎾',
|
|
||||||
description: '室内外均可'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
label: '篮球',
|
|
||||||
value: 'basketball',
|
|
||||||
icon: '🏀',
|
|
||||||
description: '室内场地'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
<Bubble
|
|
||||||
options={sportOptions}
|
|
||||||
value={selectedSport}
|
|
||||||
onChange={(value) => setSelectedSport(value as string)}
|
|
||||||
layout="vertical"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 不同布局方式
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// 水平布局 - 适合选项较少
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
layout="horizontal"
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
|
|
||||||
// 垂直布局 - 适合选项较多
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
layout="vertical"
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
|
|
||||||
// 网格布局 - 适合需要整齐排列
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
layout="grid"
|
|
||||||
columns={3}
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 不同尺寸
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// 小尺寸 - 适合紧凑界面
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
layout="horizontal"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
// 中尺寸 - 默认尺寸
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
layout="horizontal"
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
|
|
||||||
// 大尺寸 - 适合触摸设备
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
layout="horizontal"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 组件特性
|
|
||||||
|
|
||||||
- **通用性**: 不局限于特定功能,可用于任何选择场景
|
|
||||||
- **灵活布局**: 支持水平、垂直、网格三种布局方式
|
|
||||||
- **多尺寸支持**: 小、中、大三种尺寸适应不同场景
|
|
||||||
- **丰富内容**: 支持图标和描述,让选项更加丰富
|
|
||||||
- **响应式设计**: 自动适应不同屏幕尺寸
|
|
||||||
- **状态管理**: 内置选中状态管理,支持单选和多选
|
|
||||||
- **类型安全**: 完整的 TypeScript 类型定义
|
|
||||||
- **可访问性**: 支持键盘导航和屏幕阅读器
|
|
||||||
|
|
||||||
## 常见使用场景
|
|
||||||
|
|
||||||
1. **场地选择**: 室内/室外/半室外
|
|
||||||
2. **时间选择**: 时间段、日期范围
|
|
||||||
3. **分类选择**: 兴趣爱好、技能标签
|
|
||||||
4. **设置选项**: 主题、语言、通知设置
|
|
||||||
5. **筛选条件**: 价格范围、评分、距离等
|
|
||||||
6. **导航菜单**: 顶部导航、侧边栏菜单
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 确保传入的 `options` 数组不为空
|
|
||||||
2. 多选模式下,`value` 应该是数组类型
|
|
||||||
3. 单选模式下,`value` 可以是字符串或数字类型
|
|
||||||
4. 组件会自动处理选中状态的样式变化
|
|
||||||
5. 支持图标和描述,让选项更加丰富
|
|
||||||
6. 响应式设计,自动适应不同屏幕尺寸
|
|
||||||
7. 可以通过 `disabled` 属性禁用整个组件或单个选项
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import Bubble, { BubbleOption } from './index';
|
|
||||||
|
|
||||||
// 室内外选择示例(如UI图所示)
|
|
||||||
export const LocationSelector: React.FC = () => {
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<string>('');
|
|
||||||
|
|
||||||
const locationOptions: BubbleOption[] = [
|
|
||||||
{ id: 1, label: '室内', value: 'indoor' },
|
|
||||||
{ id: 2, label: '室外', value: 'outdoor' },
|
|
||||||
{ id: 3, label: '半室外', value: 'semi-outdoor' }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3>选择场地类型</h3>
|
|
||||||
<Bubble
|
|
||||||
options={locationOptions}
|
|
||||||
value={selectedLocation}
|
|
||||||
onChange={(value) => setSelectedLocation(value as string)}
|
|
||||||
layout="horizontal"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<p>当前选择: {selectedLocation || '未选择'}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 时间选择器示例
|
|
||||||
export const TimeSelector: React.FC = () => {
|
|
||||||
const [selectedTime, setSelectedTime] = useState<string>('');
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div>
|
|
||||||
<h3>选择时间段</h3>
|
|
||||||
<Bubble
|
|
||||||
options={timeOptions}
|
|
||||||
value={selectedTime}
|
|
||||||
onChange={(value) => setSelectedTime(value as string)}
|
|
||||||
layout="grid"
|
|
||||||
size="small"
|
|
||||||
columns={3}
|
|
||||||
/>
|
|
||||||
<p>当前选择: {selectedTime || '未选择'}</p>
|
|
||||||
<hr />
|
|
||||||
<h3>选择时间段</h3>
|
|
||||||
<Bubble
|
|
||||||
options={timeOptions}
|
|
||||||
value={selectedTime}
|
|
||||||
onChange={(value) => setSelectedTime(value as string)}
|
|
||||||
layout="grid"
|
|
||||||
columns={2}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<p>当前选择: {selectedTime || '未选择'}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 多选示例
|
|
||||||
export const MultiSelectExample: React.FC = () => {
|
|
||||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div style={{ padding: '20px' }}>
|
|
||||||
<h3>多选示例 - 兴趣爱好</h3>
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValues}
|
|
||||||
onChange={(value) => setSelectedValues(value as string[])}
|
|
||||||
multiple={true}
|
|
||||||
layout="grid"
|
|
||||||
columns={3}
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
<p>当前选择: {selectedValues.join(', ') || '未选择'}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 带图标和描述的示例
|
|
||||||
export const IconExample: React.FC = () => {
|
|
||||||
const [selectedValue, setSelectedValue] = useState<string>('');
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div style={{ padding: '20px' }}>
|
|
||||||
<h3>带图标和描述的运动选择</h3>
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={(value) => setSelectedValue(value as string)}
|
|
||||||
layout="vertical"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
<p>当前选择: {selectedValue || '未选择'}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 带禁用状态的示例
|
|
||||||
export const DisabledExample: React.FC = () => {
|
|
||||||
const [selectedValue, setSelectedValue] = useState<string>('');
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div style={{ padding: '20px' }}>
|
|
||||||
<h3>带禁用状态的示例</h3>
|
|
||||||
<Bubble
|
|
||||||
options={options}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={(value) => setSelectedValue(value as string)}
|
|
||||||
layout="grid"
|
|
||||||
columns={2}
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
<p>当前选择: {selectedValue || '未选择'}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 不同尺寸的示例
|
|
||||||
export const SizeExample: React.FC = () => {
|
|
||||||
const [selectedSize, setSelectedSize] = useState<string>('');
|
|
||||||
|
|
||||||
const sizeOptions: BubbleOption[] = [
|
|
||||||
{ id: 1, label: '小尺寸', value: 'small' },
|
|
||||||
{ id: 2, label: '中尺寸', value: 'medium' },
|
|
||||||
{ id: 3, label: '大尺寸', value: 'large' }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '20px' }}>
|
|
||||||
<h3>不同尺寸的示例</h3>
|
|
||||||
|
|
||||||
<h4>小尺寸</h4>
|
|
||||||
<Bubble
|
|
||||||
options={sizeOptions}
|
|
||||||
value={selectedSize}
|
|
||||||
onChange={(value) => setSelectedSize(value as string)}
|
|
||||||
layout="horizontal"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h4>中尺寸</h4>
|
|
||||||
<Bubble
|
|
||||||
options={sizeOptions}
|
|
||||||
value={selectedSize}
|
|
||||||
onChange={(value) => setSelectedSize(value as string)}
|
|
||||||
layout="horizontal"
|
|
||||||
size="medium"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h4>大尺寸</h4>
|
|
||||||
<Bubble
|
|
||||||
options={sizeOptions}
|
|
||||||
value={selectedSize}
|
|
||||||
onChange={(value) => setSelectedSize(value as string)}
|
|
||||||
layout="horizontal"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p>当前选择: {selectedSize || '未选择'}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 主示例组件
|
|
||||||
export const BubbleExamples: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Bubble 通用气泡组件示例</h1>
|
|
||||||
<LocationSelector />
|
|
||||||
<hr />
|
|
||||||
<TimeSelector />
|
|
||||||
<hr />
|
|
||||||
<MultiSelectExample />
|
|
||||||
<hr />
|
|
||||||
<IconExample />
|
|
||||||
<hr />
|
|
||||||
<DisabledExample />
|
|
||||||
<hr />
|
|
||||||
<SizeExample />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BubbleExamples;
|
|
||||||
@@ -1,32 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import BubbleItem from "./BubbleItem";
|
import BubbleItem from "./BubbleItem";
|
||||||
|
import {BubbleOption, BubbleProps} from '../../../types/list/types'
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Bubble: React.FC<BubbleProps> = ({
|
const Bubble: React.FC<BubbleProps> = ({
|
||||||
options,
|
options,
|
||||||
@@ -40,6 +15,7 @@ const Bubble: React.FC<BubbleProps> = ({
|
|||||||
itemClassName = "",
|
itemClassName = "",
|
||||||
style = {},
|
style = {},
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
name,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]);
|
const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]);
|
||||||
|
|
||||||
@@ -74,9 +50,10 @@ const Bubble: React.FC<BubbleProps> = ({
|
|||||||
const selectedOptions = options.filter((opt) =>
|
const selectedOptions = options.filter((opt) =>
|
||||||
newSelectedValues.includes(opt.value)
|
newSelectedValues.includes(opt.value)
|
||||||
);
|
);
|
||||||
onChange(newSelectedValues, selectedOptions);
|
onChange(name, newSelectedValues, selectedOptions);
|
||||||
} else {
|
} else {
|
||||||
onChange(option.value, option);
|
console.log('===111', name, option.value)
|
||||||
|
onChange(name, option.value, option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<MenuComponent
|
|
||||||
options={options}
|
|
||||||
value={value}
|
|
||||||
onChange={(val) => setValue(val)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,53 @@
|
|||||||
.menuWrap {
|
.menuWrap {
|
||||||
padding: 5px 20px 10px;
|
padding: 5px 20px 10px;
|
||||||
|
$height: 26px;
|
||||||
|
|
||||||
|
.menuIcon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
&.rotate {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.menuItem {
|
.menuItem {
|
||||||
|
width: 100vw;
|
||||||
left: 0;
|
left: 0;
|
||||||
border-bottom-left-radius: 30px;
|
border-bottom-left-radius: 30px;
|
||||||
border-bottom-right-radius: 30px;
|
border-bottom-right-radius: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
.nut-menu-bar {
|
:global(.nut-menu-bar) {
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.nut-menu-bar) {
|
:global(.nut-menu-bar) {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
line-height: 1;
|
width: 66px;
|
||||||
|
height: $height;
|
||||||
|
border-radius: $height;
|
||||||
|
line-height: $height;
|
||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
min-height: 28px;
|
|
||||||
min-width: 80px;
|
|
||||||
border-radius: 28px;
|
|
||||||
border: 1px solid #e5e5e5;
|
border: 1px solid #e5e5e5;
|
||||||
line-height: 28px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: max-content;
|
font-weight: 600;
|
||||||
.nut-menu-title {
|
}
|
||||||
color: inherit !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nut-menu-title-text {
|
:global(.nut-menu-title) {
|
||||||
padding-left: 0;
|
color: inherit !important;
|
||||||
}
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.nut-menu-title-text) {
|
||||||
|
--nutui-menu-title-padding: 0 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.positionWrap {
|
.positionWrap {
|
||||||
@@ -48,13 +66,15 @@
|
|||||||
.cityName {
|
.cityName {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #3C3C43;
|
color: #3c3c43;
|
||||||
}
|
}
|
||||||
|
|
||||||
.distanceWrap {
|
.distanceWrap {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.distanceBubbleItem {
|
.distanceBubbleItem {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
import { Menu } from "@nutui/nutui-react-taro";
|
import { Menu } from "@nutui/nutui-react-taro";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import { useState, useRef } from "react";
|
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 [isChange, setIsChange] = useState(false);
|
||||||
|
const [iOpen, setIsOpen] = useState(false);
|
||||||
const itemRef = useRef(null);
|
const itemRef = useRef(null);
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (name: string, value: string) => {
|
||||||
console.log("===value", value);
|
|
||||||
setIsChange(true);
|
setIsChange(true);
|
||||||
onChange && onChange(value);
|
onChange && onChange(name, value);
|
||||||
|
(itemRef.current as any)?.toggle(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const options: BubbleOption[] = [
|
const handleOpen = () => {
|
||||||
{ id: 0, label: "全城", value: "0" },
|
setIsOpen(true);
|
||||||
{ id: 1, label: "3km", value: "3" },
|
};
|
||||||
{ id: 2, label: "5km", value: "5" },
|
|
||||||
{ id: 3, label: "10km", value: "10" },
|
const handleClose = () => {
|
||||||
];
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
@@ -35,11 +35,25 @@ const MenuComponent = (props: IProps) => {
|
|||||||
isChange ? styles.active : ""
|
isChange ? styles.active : ""
|
||||||
}`}
|
}`}
|
||||||
activeColor="#000"
|
activeColor="#000"
|
||||||
|
onOpen={handleOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
icon={
|
||||||
|
<Image
|
||||||
|
className={`${styles.menuIcon} ${iOpen ? styles.rotate : ""}`}
|
||||||
|
src={isChange ? img.ICON_ARROW_DOWN_WHITE : img.ICON_ARROW_DOWN}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
title="全城"
|
title={value}
|
||||||
className={`${styles.menuItem} ${itemClassName}`}
|
className={`${styles.menuItem} ${itemClassName ? itemClassName : ""}`}
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
|
icon={
|
||||||
|
<Image
|
||||||
|
className={styles.itemIcon}
|
||||||
|
src={img.ICON_MENU_ITEM_SELECTED}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className={styles.positionWrap}>
|
<div className={styles.positionWrap}>
|
||||||
<p className={styles.title}>当前位置</p>
|
<p className={styles.title}>当前位置</p>
|
||||||
@@ -54,6 +68,7 @@ const MenuComponent = (props: IProps) => {
|
|||||||
size="small"
|
size="small"
|
||||||
columns={4}
|
columns={4}
|
||||||
itemClassName={styles.distanceBubbleItem}
|
itemClassName={styles.distanceBubbleItem}
|
||||||
|
name={name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface CommonPopupProps {
|
|||||||
zIndex?: number
|
zIndex?: number
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommonPopup: React.FC<CommonPopupProps> = ({
|
const CommonPopup: React.FC<CommonPopupProps> = ({
|
||||||
@@ -34,6 +35,7 @@ const CommonPopup: React.FC<CommonPopupProps> = ({
|
|||||||
position = 'bottom',
|
position = 'bottom',
|
||||||
round = true,
|
round = true,
|
||||||
zIndex,
|
zIndex,
|
||||||
|
style,
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -52,7 +54,7 @@ const CommonPopup: React.FC<CommonPopupProps> = ({
|
|||||||
closeable={false}
|
closeable={false}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
className={`${styles['common-popup']} ${className ? className : ''}`}
|
className={`${styles['common-popup']} ${className ? className : ''}`}
|
||||||
style={zIndex ? { zIndex } : undefined}
|
style={{ zIndex: zIndex ? zIndex : undefined, ...style }}
|
||||||
>
|
>
|
||||||
{showHeader && (
|
{showHeader && (
|
||||||
<View className={styles['common-popup__header']}>
|
<View className={styles['common-popup__header']}>
|
||||||
@@ -78,4 +80,4 @@ const CommonPopup: React.FC<CommonPopupProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommonPopup
|
export default CommonPopup
|
||||||
3
src/components/CourtType/index.module.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.courtTypeWrapper {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
31
src/components/CourtType/index.tsx
Normal file
@@ -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 (
|
||||||
|
<View className={styles.courtTypeWrapper}>
|
||||||
|
<TitleComponent title="场地类型" icon={<Image src={img.ICON_SITE} />} />
|
||||||
|
<Bubble
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
layout="grid"
|
||||||
|
size="small"
|
||||||
|
columns={3}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default GamePlayType;
|
||||||
50
src/components/CustomNavbar/index.module.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/components/CustomNavbar/index.tsx
Normal file
@@ -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 (
|
||||||
|
<View
|
||||||
|
className={styles.customerNavbar}
|
||||||
|
style={{ height: `${totalHeight}px` }}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className={styles.container}
|
||||||
|
style={{
|
||||||
|
height: `${navbarHeight}px`,
|
||||||
|
paddingTop: `${statusBarHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* logo */}
|
||||||
|
<Image src={img.ICON_LOGO} className={styles.logo} />
|
||||||
|
<View className={styles.line} />
|
||||||
|
<View className={styles.content}>
|
||||||
|
<View className={styles.cityWrapper}>
|
||||||
|
{/* 位置 */}
|
||||||
|
<Text className={styles.city}>{currentAddress}</Text>
|
||||||
|
{!getLocationLoading && (
|
||||||
|
<Image src={img.ICON_CHANGE} className={styles.change} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View className={styles.infoWrapper}>
|
||||||
|
<Text className={styles.info}>附近${gamesNum}场球局</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ListHeader;
|
||||||
3
src/components/GamePlayType/index.module.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.gamePlayWrapper {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
47
src/components/GamePlayType/index.tsx
Normal file
@@ -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 (
|
||||||
|
<View className={styles.gamePlayWrapper}>
|
||||||
|
<TitleComponent title="玩法" icon={<Image src={img.ICON_SITE} />} />
|
||||||
|
<Bubble
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
layout="grid"
|
||||||
|
size="small"
|
||||||
|
columns={3}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
{/* <PopupGameplay
|
||||||
|
onClose={() => {
|
||||||
|
console.log("onClose");
|
||||||
|
}}
|
||||||
|
onConfirm={() => {
|
||||||
|
console.log("onConfirm");
|
||||||
|
}}
|
||||||
|
visible={false}
|
||||||
|
options={[
|
||||||
|
{ label: "不限", value: "不限" },
|
||||||
|
{ label: "单打", value: "单打" },
|
||||||
|
{ label: "双打", value: "双打" },
|
||||||
|
{ label: "娱乐", value: "娱乐" },
|
||||||
|
{ label: "拉球", value: "拉球" },
|
||||||
|
]}
|
||||||
|
/> */}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default GamePlayType;
|
||||||
90
src/components/GuideBar/index.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/components/GuideBar/index.tsx
Normal file
@@ -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 (
|
||||||
|
<View className='guide-bar-container'>
|
||||||
|
<View className='guide-bar'>
|
||||||
|
{/* guide area on the left */}
|
||||||
|
<View className='guide-bar-pages'>
|
||||||
|
{guideItems.map((item) => (
|
||||||
|
<View
|
||||||
|
className={`guide-bar-pages-item ${currentPage === item.code ? 'guide-bar-pages-item-active' : ''}`}
|
||||||
|
onClick={() => handlePageChange(item.code)}
|
||||||
|
>
|
||||||
|
<Text>{item.text}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
{/* publish button on the right */}
|
||||||
|
<View className='guide-bar-publish' onClick={handlePublish}>
|
||||||
|
<Image className='guide-bar-publish-icon' src={img.ICON_GUIDE_BAR_PUBLISH} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GuideBar
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
.list {
|
.list {
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
margin-top: 12px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|||||||
295
src/components/ListCard/index.scss
Normal file
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/components/ListCard/index.tsx
Normal file
@@ -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<ListCardProps> = ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
dateTime,
|
||||||
|
location,
|
||||||
|
distance,
|
||||||
|
registeredCount,
|
||||||
|
maxCount,
|
||||||
|
skillLevel,
|
||||||
|
matchType,
|
||||||
|
images = [],
|
||||||
|
shinei,
|
||||||
|
}) => {
|
||||||
|
const renderItemImage = (src: string) => {
|
||||||
|
return <Image src={src} className="image" mode="aspectFill" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View className="single-image">
|
||||||
|
<View className="image-container">{renderItemImage(images[0])}</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (images?.length === 2) {
|
||||||
|
return (
|
||||||
|
<View className="double-image">
|
||||||
|
<View className="image-container">{renderItemImage(images[0])}</View>
|
||||||
|
<View className="image-container">{renderItemImage(images[1])}</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3张或更多图片
|
||||||
|
return (
|
||||||
|
<View className="triple-image">
|
||||||
|
<View className="image-container">{renderItemImage(images?.[0])}</View>
|
||||||
|
<View className="image-container">{renderItemImage(images?.[1])}</View>
|
||||||
|
<View className="image-container">{renderItemImage(images?.[2])}</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<View className="listCard">
|
||||||
|
<View className="listItem" onClick={handleViewDetail}>
|
||||||
|
{/* 左侧内容区域 */}
|
||||||
|
<View className="content">
|
||||||
|
{/* 标题 */}
|
||||||
|
<View className="titleWrapper">
|
||||||
|
<Text className="title">{title}</Text>
|
||||||
|
<Image
|
||||||
|
src={img.ICON_LIST_RIGHT_ARROW}
|
||||||
|
className="title-right-arrow"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 时间信息 */}
|
||||||
|
|
||||||
|
<View className="date-time">
|
||||||
|
<Text>{dateTime}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 地点,室内外,距离 */}
|
||||||
|
|
||||||
|
<View className="location">
|
||||||
|
<Text className="location-text location-position">{location}</Text>
|
||||||
|
<Text className="location-text location-time-distance">
|
||||||
|
{shinei && `・${shinei}`}
|
||||||
|
{distance && `・${distance}`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */}
|
||||||
|
<View className="bottom-info">
|
||||||
|
<View className="left-section">
|
||||||
|
<View className="avatar-group">
|
||||||
|
{Array.from({ length: Math.min(registeredCount, 3) }).map(
|
||||||
|
(_, index) => (
|
||||||
|
<View key={index} className="avatar">
|
||||||
|
<Image
|
||||||
|
className="avatar-image"
|
||||||
|
src="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="tags">
|
||||||
|
<View className="tag">
|
||||||
|
<Text className="tag-text">
|
||||||
|
报名人数 {registeredCount}/
|
||||||
|
<Text className="tag-text-max">{maxCount}</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className="tag">
|
||||||
|
<Text className="tag-text">{skillLevel}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="tag">
|
||||||
|
<Text className="tag-text">{matchType}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 右侧图片区域 */}
|
||||||
|
<View className="image-section">{renderImages()}</View>
|
||||||
|
</View>
|
||||||
|
{/* 畅打球局 */}
|
||||||
|
<View className="smoothPlayingGame">
|
||||||
|
<View className="smoothWrapper">
|
||||||
|
<Image src={img.ICON_LIST_PLAYING_GAME} className="iconListPlayingGame" />
|
||||||
|
<Text className="smoothTitle">畅打球局</Text>
|
||||||
|
</View>
|
||||||
|
<View className="line" />
|
||||||
|
<View>场馆方:</View>
|
||||||
|
<View className="localAreaWrapper">
|
||||||
|
<Image className="localArea" src="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center" />
|
||||||
|
<Text className="localAreaText">仁恒河滨花园网球场</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListCard;
|
||||||
@@ -1,35 +1,69 @@
|
|||||||
.list-item {
|
.list-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 16px;
|
padding: 12px 15px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 0.5px solid #f0f0f0;
|
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 {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
width: calc(100% - 122px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333333;
|
color: #000000;
|
||||||
line-height: 1.4;
|
line-height: 24px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-time {
|
.title-right-arrow {
|
||||||
font-size: 14px;
|
width: 16px;
|
||||||
color: #666666;
|
height: 16px;
|
||||||
line-height: 1.3;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location {
|
.location {
|
||||||
font-size: 14px;
|
display: flex;
|
||||||
color: #666666;
|
align-items: center;
|
||||||
line-height: 1.3;
|
}
|
||||||
|
|
||||||
|
.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 {
|
.bottom-info {
|
||||||
@@ -58,6 +92,8 @@
|
|||||||
border: 2px solid #ffffff;
|
border: 2px solid #ffffff;
|
||||||
margin-left: -8px;
|
margin-left: -8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
.avatar-image {
|
.avatar-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -89,8 +125,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
padding: 3px;
|
box-sizing: border-box;
|
||||||
border: 1px solid #f5f5f5;
|
padding: 0 6px;
|
||||||
|
border: 0.5px solid #00000029;
|
||||||
|
height: 20px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
min-width: 38px;
|
min-width: 38px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -98,7 +136,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-text-max {
|
.tag-text-max {
|
||||||
@@ -112,16 +150,32 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-basis: 100px;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 2px;
|
border: 1.5px solid #ffffff;
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: absolute;
|
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 {
|
.image {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
@@ -132,7 +186,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 88px;
|
width: 88px;
|
||||||
height: 88px;
|
height: 88px;
|
||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
transform: rotate(-10deg);
|
transform: rotate(-10deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,13 +208,13 @@
|
|||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
transform: rotate(-10deg);
|
transform: translateX(4px) rotate(-10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transform: rotate(10deg);
|
transform: translateX(-4px) rotate(10deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +234,7 @@
|
|||||||
width: 55px;
|
width: 55px;
|
||||||
height: 55px;
|
height: 55px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
transform: rotate(-10deg);
|
transform: translateX(4px) rotate(-10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
@@ -204,4 +261,4 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
56
src/components/ListCardSkeleton/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { View } from "@tarojs/components";
|
||||||
|
import { Skeleton } from "@nutui/nutui-react-taro";
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
|
const ListCard = () => {
|
||||||
|
return (
|
||||||
|
<View className="list-item">
|
||||||
|
{/* 左侧内容区域 */}
|
||||||
|
<View className="content">
|
||||||
|
{/* 标题 */}
|
||||||
|
<View className="titleWrapper">
|
||||||
|
<Skeleton visible={false} style={{ width: "180px" }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 时间信息 */}
|
||||||
|
<View className="date-time">
|
||||||
|
<Skeleton visible={false} style={{ width: "88px", }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 地点,室内外,距离 */}
|
||||||
|
|
||||||
|
<View className="location">
|
||||||
|
<Skeleton visible={false} style={{ width: "60px", }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */}
|
||||||
|
<View className="bottom-info">
|
||||||
|
<View className="left-section">
|
||||||
|
<View className="avatar-group">
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<View key={index} className="avatar">
|
||||||
|
<Skeleton visible={false} style={{ width: "20px", height: '0' }} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="tags">
|
||||||
|
<Skeleton visible={false} style={{ width: "64px" }} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 右侧图片区域 */}
|
||||||
|
<View className="image-section">
|
||||||
|
<View className="single-image">
|
||||||
|
<View className="image-container">
|
||||||
|
<Skeleton visible={false} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListCard;
|
||||||
@@ -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<ListItemProps> = ({
|
|
||||||
title,
|
|
||||||
dateTime,
|
|
||||||
location,
|
|
||||||
distance,
|
|
||||||
registeredCount,
|
|
||||||
maxCount,
|
|
||||||
skillLevel,
|
|
||||||
matchType,
|
|
||||||
images
|
|
||||||
}) => {
|
|
||||||
// 根据图片数量决定展示样式
|
|
||||||
const renderImages = () => {
|
|
||||||
if (images.length === 0) return null
|
|
||||||
|
|
||||||
if (images.length === 1) {
|
|
||||||
return (
|
|
||||||
<View className="single-image">
|
|
||||||
<View className="image-container">
|
|
||||||
<Image src={images[0]} className="image" mode="aspectFill" />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (images.length === 2) {
|
|
||||||
return (
|
|
||||||
<View className="double-image">
|
|
||||||
<View className="image-container">
|
|
||||||
<Image src={images[0]} className="image" mode="aspectFill" />
|
|
||||||
</View>
|
|
||||||
<View className="image-container">
|
|
||||||
<Image src={images[1]} className="image" mode="aspectFill" />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3张或更多图片
|
|
||||||
return (
|
|
||||||
<View className="triple-image">
|
|
||||||
<View className="image-container">
|
|
||||||
<Image src={images[0]} className="image" mode="aspectFill" />
|
|
||||||
</View>
|
|
||||||
<View className="image-container">
|
|
||||||
<Image src={images[1]} className="image" mode="aspectFill" />
|
|
||||||
</View>
|
|
||||||
<View className="image-container">
|
|
||||||
<Image src={images[2]} className="image" mode="aspectFill" />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="list-item">
|
|
||||||
{/* 左侧内容区域 */}
|
|
||||||
<View className="content">
|
|
||||||
{/* 标题 */}
|
|
||||||
<Text className="title">{title}</Text>
|
|
||||||
|
|
||||||
{/* 时间信息 */}
|
|
||||||
<Text className="date-time">{dateTime}</Text>
|
|
||||||
|
|
||||||
{/* 地点和距离 */}
|
|
||||||
<Text className="location">{location}・{distance}</Text>
|
|
||||||
|
|
||||||
{/* 底部信息行:头像组、报名人数、技能等级、比赛类型 */}
|
|
||||||
<View className="bottom-info">
|
|
||||||
<View className="left-section">
|
|
||||||
<View className="avatar-group">
|
|
||||||
{Array.from({ length: Math.min(registeredCount, 3) }).map((_, index) => (
|
|
||||||
<View key={index} className="avatar" >
|
|
||||||
<Image
|
|
||||||
className="avatar-image"
|
|
||||||
src="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center" className="avatar-image" mode="aspectFill" />
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="tags">
|
|
||||||
<View className="tag">
|
|
||||||
<Text className="tag-text">
|
|
||||||
报名人数 {registeredCount}/<Text className="tag-text-max">{maxCount}</Text>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className="tag">
|
|
||||||
<Text className="tag-text">{skillLevel}</Text>
|
|
||||||
</View>
|
|
||||||
<View className="tag">
|
|
||||||
<Text className="tag-text">{matchType}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 右侧图片区域 */}
|
|
||||||
<View className="image-section">
|
|
||||||
{renderImages()}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ListItem
|
|
||||||
44
src/components/ListLoadError/index.module.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/components/ListLoadError/index.tsx
Normal file
@@ -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 (
|
||||||
|
<View className={styles.listLoadError}>
|
||||||
|
<Image
|
||||||
|
className={styles.listLoadErrorImg}
|
||||||
|
src={img.ICON_LIST_LOAD_ERROR}
|
||||||
|
/>
|
||||||
|
<Text className={styles.listLoadErrorText}>加载失败</Text>
|
||||||
|
<Button className={styles.listLoadErrorBtn} onClick={handleReload}>
|
||||||
|
<Image src={img?.ICON_LIST_RELOAD} className={styles.reloadIcon} />
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ListLoadError;
|
||||||
@@ -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 (
|
|
||||||
<MenuComponent
|
|
||||||
options={options}
|
|
||||||
value={value}
|
|
||||||
onChange={(val) => setValue(val)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,31 +1,46 @@
|
|||||||
.menuWrap {
|
.menuWrap {
|
||||||
|
position: static;
|
||||||
padding: 5px 20px 10px;
|
padding: 5px 20px 10px;
|
||||||
|
--nutui-menu-title-padding: 0 6px 0 0;
|
||||||
|
$height: 26px;
|
||||||
|
|
||||||
.menuItem {
|
.menuItem {
|
||||||
|
width: 100vw;
|
||||||
left: 0;
|
left: 0;
|
||||||
border-bottom-left-radius: 30px;
|
border-bottom-left-radius: 30px;
|
||||||
border-bottom-right-radius: 30px;
|
border-bottom-right-radius: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menuIcon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
&.rotate {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemIcon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
:global(.nut-menu-bar) {
|
:global(.nut-menu-bar) {
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.nut-menu-bar) {
|
:global(.nut-menu-bar) {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
line-height: 1;
|
|
||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
min-height: 28px;
|
border-radius: $height;
|
||||||
min-width: 94px;
|
line-height: $height;
|
||||||
border-radius: 28px;
|
height: $height;
|
||||||
border: 1px solid #e5e5e5;
|
border: 1px solid #e5e5e5;
|
||||||
line-height: 28px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: max-content;
|
width: 94px;
|
||||||
|
|
||||||
.nut-menu-title-text {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.nut-menu-title) {
|
:global(.nut-menu-title) {
|
||||||
@@ -33,13 +48,18 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.nut-menu-title-text) {
|
||||||
|
--nutui-menu-title-padding: 0 6px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.nut-menu-container-item) {
|
:global(.nut-menu-container-item) {
|
||||||
color: #3c3c43;
|
color: #3c3c43;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.nut-menu-container-item.active) {
|
:global(.nut-menu-container-item.active) {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,34 +1,55 @@
|
|||||||
import { Menu } from "@nutui/nutui-react-taro";
|
import { Menu } from "@nutui/nutui-react-taro";
|
||||||
import styles from "./index.module.scss";
|
|
||||||
import { useState } from "react";
|
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 {
|
const MenuComponent = (props: MenuFilterProps) => {
|
||||||
options: { text: string; value: string }[];
|
const { options, value, onChange, wrapperClassName, itemClassName, name } =
|
||||||
value: string;
|
props;
|
||||||
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 [isChange, setIsChange] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (val: Record<string, string>) => {
|
||||||
setIsChange(true);
|
setIsChange(true);
|
||||||
onChange && onChange(value);
|
onChange && onChange(name, val.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
className={`${styles.menuWrap} ${wrapperClassName} ${isChange ? styles.active : ""}`}
|
className={`${styles.menuWrap} ${wrapperClassName} ${
|
||||||
|
isChange ? styles.active : ""
|
||||||
|
}`}
|
||||||
activeColor="#000"
|
activeColor="#000"
|
||||||
|
onOpen={handleOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
icon={
|
||||||
|
<Image
|
||||||
|
className={`${styles.menuIcon} ${isOpen ? styles.rotate : ""}`}
|
||||||
|
src={isChange ? img.ICON_ARROW_DOWN_WHITE : img.ICON_ARROW_DOWN}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
className={`${styles.menuItem} ${itemClassName}`}
|
className={`${styles.menuItem} ${itemClassName}`}
|
||||||
options={options}
|
options={options}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
icon={
|
||||||
|
<Image
|
||||||
|
className={styles.itemIcon}
|
||||||
|
src={img.ICON_MENU_ITEM_SELECTED}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<NtrpRange
|
|
||||||
min={2.0}
|
|
||||||
max={4.0}
|
|
||||||
step={0.5}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 属性说明
|
|
||||||
|
|
||||||
| 属性 | 类型 | 默认值 | 说明 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `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` 文件获取更多使用示例。
|
|
||||||
@@ -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 (
|
|
||||||
<div >
|
|
||||||
<h1>Range 组件示例</h1>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '40px' }}>
|
|
||||||
<h2>NTRP 水平区间选择器</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={1.0}
|
|
||||||
max={5.0}
|
|
||||||
step={0.5}
|
|
||||||
value={ntrpRange}
|
|
||||||
onChange={handleNtrpChange}
|
|
||||||
/>
|
|
||||||
<div >
|
|
||||||
当前选择范围: {ntrpRange[0]} - {ntrpRange[1]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '40px' }}>
|
|
||||||
<h2>自定义范围选择器</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={10}
|
|
||||||
value={customRange}
|
|
||||||
onChange={handleCustomChange}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#666' }}>
|
|
||||||
当前选择范围: {customRange[0]} - {customRange[1]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '40px' }}>
|
|
||||||
<h2>禁用状态</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
step={1}
|
|
||||||
value={[3, 7]}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#999' }}>
|
|
||||||
此选择器已被禁用
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '40px' }}>
|
|
||||||
<h2>测试说明</h2>
|
|
||||||
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.6' }}>
|
|
||||||
<p>1. 点击并拖拽左右滑块手柄</p>
|
|
||||||
<p>2. 查看控制台日志确认拖拽事件</p>
|
|
||||||
<p>3. 观察滑块位置和值的实时变化</p>
|
|
||||||
<p>4. 检查调试信息显示</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RangeExample;
|
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
|
import { View, Text, Image } from "@tarojs/components";
|
||||||
import { Range } from "@nutui/nutui-react-taro";
|
import { Range } from "@nutui/nutui-react-taro";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import TitleComponent from "../Title";
|
import TitleComponent from "../Title";
|
||||||
|
import img from "../../config/images";
|
||||||
|
|
||||||
interface RangeProps {
|
interface RangeProps {
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
step?: number;
|
step?: number;
|
||||||
value?: [number, number];
|
value?: [number, number];
|
||||||
onChange?: (value: [number, number]) => void;
|
onChange?: (name: string, value: [number, number]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
name: string;
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +25,7 @@ const NtrpRange: React.FC<RangeProps> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
|
name,
|
||||||
showTitle = true,
|
showTitle = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentValue, setCurrentValue] = useState<[number, number]>(value);
|
const [currentValue, setCurrentValue] = useState<[number, number]>(value);
|
||||||
@@ -32,7 +36,7 @@ const NtrpRange: React.FC<RangeProps> = ({
|
|||||||
|
|
||||||
const handleChange = (val: [number, number]) => {
|
const handleChange = (val: [number, number]) => {
|
||||||
setCurrentValue(val);
|
setCurrentValue(val);
|
||||||
onChange?.(val);
|
onChange?.(name, val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const marks = useMemo(() => {
|
const marks = useMemo(() => {
|
||||||
@@ -52,18 +56,16 @@ const NtrpRange: React.FC<RangeProps> = ({
|
|||||||
}, [JSON.stringify(currentValue || []), min, max]);
|
}, [JSON.stringify(currentValue || []), min, max]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.nutRange} ${className ? className : ''} `}>
|
<div className={`${styles.nutRange} ${className ? className : ""} `}>
|
||||||
{ showTitle && (
|
{showTitle && (
|
||||||
<div className={styles.nutRangeHeader}>
|
<div className={styles.nutRangeHeader}>
|
||||||
{/* <div className={styles.nutRangeHeaderLeft}>
|
<TitleComponent
|
||||||
<div className="ntrp-range__icon">icon</div>
|
title="NTRP水平区间"
|
||||||
<h3 className={styles.nutRangeHeaderTitle}>NTRP水平区间</h3>
|
icon={<Image src={img.ICON_PLAY} />}
|
||||||
</div> */}
|
/>
|
||||||
<TitleComponent title='NTRP水平区间'/>
|
|
||||||
<p className={styles.nutRangeHeaderContent}>{rangContent}</p>
|
<p className={styles.nutRangeHeaderContent}>{rangContent}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.rangeWrapper} rangeContent`}>
|
<div className={`${styles.rangeWrapper} rangeContent`}>
|
||||||
@@ -73,8 +75,9 @@ const NtrpRange: React.FC<RangeProps> = ({
|
|||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
step={step}
|
step={step}
|
||||||
// value={currentValue}
|
value={currentValue}
|
||||||
onEnd={handleChange}
|
onEnd={handleChange}
|
||||||
|
onChange={handleChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
defaultValue={[min, max]}
|
defaultValue={[min, max]}
|
||||||
className={styles.rangeHandle}
|
className={styles.rangeHandle}
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<div style={{ padding: '20px' }}>
|
|
||||||
<h2>简单测试</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={2.0}
|
|
||||||
max={4.0}
|
|
||||||
step={0.5}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
<p>当前值: {value[0]} - {value[1]}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimpleTest;
|
|
||||||
@@ -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 (
|
|
||||||
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
|
|
||||||
<h1>NtrpRange 样式测试</h1>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '30px' }}>
|
|
||||||
<h2>NTRP 水平区间选择器</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={2.0}
|
|
||||||
max={4.0}
|
|
||||||
step={0.5}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#666' }}>
|
|
||||||
当前选择范围: {value[0]} - {value[1]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '30px' }}>
|
|
||||||
<h2>自定义范围选择器</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={[20, 80]}
|
|
||||||
onChange={(val) => console.log('Custom range:', val)}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#666' }}>
|
|
||||||
固定范围: 20 - 80
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '30px' }}>
|
|
||||||
<h2>禁用状态</h2>
|
|
||||||
<NtrpRange
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
step={1}
|
|
||||||
value={[3, 7]}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '16px', fontSize: '14px', color: '#999' }}>
|
|
||||||
此选择器已被禁用
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '30px' }}>
|
|
||||||
<h2>样式说明</h2>
|
|
||||||
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.6' }}>
|
|
||||||
<p>✅ 网球图标 + "NTRP水平区间"标题</p>
|
|
||||||
<p>✅ 左右范围标签("2.0及以下"、"4.0及以上")</p>
|
|
||||||
<p>✅ 圆角矩形轨道容器,带有边框和阴影</p>
|
|
||||||
<p>✅ 白色圆形滑块手柄,黑色边框和阴影</p>
|
|
||||||
<p>✅ 黑色轨道填充条</p>
|
|
||||||
<p>✅ 五个浅灰色标记点</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StyleTest;
|
|
||||||
@@ -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 (
|
|
||||||
<div style={{ padding: '20px' }}>
|
|
||||||
<h1>NtrpRange 组件测试</h1>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
|
||||||
<NtrpRange
|
|
||||||
min={2.0}
|
|
||||||
max={4.0}
|
|
||||||
step={0.5}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ fontSize: '14px', color: '#666' }}>
|
|
||||||
当前值: {value[0].toFixed(1)} - {value[1].toFixed(1)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '20px', fontSize: '12px', color: '#999' }}>
|
|
||||||
<p>测试说明:</p>
|
|
||||||
<p>1. 拖拽左右滑块手柄</p>
|
|
||||||
<p>2. 观察值的变化</p>
|
|
||||||
<p>3. 检查样式是否正确</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestPage;
|
|
||||||
@@ -1,23 +1,59 @@
|
|||||||
.searchBar {
|
.searchBar {
|
||||||
--nutui-searchbar-padding: 10px 15px;
|
|
||||||
--nutui-searchbar-font-size: 16px;
|
--nutui-searchbar-font-size: 16px;
|
||||||
--nutui-searchbar-input-height: 44px;
|
--nutui-searchbar-input-height: 44px;
|
||||||
--nutui-searchbar-content-border-radius: 44px;
|
--nutui-searchbar-content-border-radius: 44px;
|
||||||
--nutui-searchbar-input-text-color: #000000;
|
--nutui-searchbar-input-text-color: #000000;
|
||||||
--nutui-searchbar-input-padding: 0 0 0 10px;
|
--nutui-searchbar-input-padding: 0 0 0 10px;
|
||||||
--nutui-searchbar-padding:0 15px;
|
--nutui-searchbar-padding: 10px 0 0 0;
|
||||||
// --nutui-searchbar-background: #ffffff;
|
|
||||||
:global(.nut-searchbar-content) {
|
:global(.nut-searchbar-content) {
|
||||||
box-shadow: 0 4px 48px #00000014;
|
box-shadow: 0 4px 48px #00000014;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.searchBarLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.searchBarRight {
|
.searchBarRight {
|
||||||
|
position: relative;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid #0000000F;
|
border: 1px solid #0000000f;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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'
|
interface IProps {
|
||||||
import styles from './index.module.scss'
|
handleFilterIcon: () => void;
|
||||||
|
isSelect: boolean;
|
||||||
const SearchBarComponent = () => {
|
filterCount: number;
|
||||||
return (
|
onChange: (value: string) => void;
|
||||||
<>
|
|
||||||
<SearchBar
|
|
||||||
// leftIn={
|
|
||||||
// <div>123</div>
|
|
||||||
// }
|
|
||||||
right={
|
|
||||||
<div className={styles.searchBarRight}>筛</div>
|
|
||||||
}
|
|
||||||
className={styles.searchBar}
|
|
||||||
placeholder='搜索上海的球局和场地'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SearchBarComponent
|
const SearchBarComponent = (props: IProps) => {
|
||||||
|
const { handleFilterIcon, isSelect, filterCount, onChange } = props;
|
||||||
|
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
onChange && onChange(value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchBar
|
||||||
|
leftIn={
|
||||||
|
<View className={styles.searchBarLeft}>
|
||||||
|
<Image className={styles.searchIcon} src={img.ICON_SEARCH} />
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<View
|
||||||
|
className={`${styles.searchBarRight} ${
|
||||||
|
isSelect ? styles.active : ""
|
||||||
|
}`}
|
||||||
|
onClick={handleFilterIcon}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={isSelect ? img.ICON_FILTER_SELECTED : img.ICON_FILTER}
|
||||||
|
className={styles.filterIcon}
|
||||||
|
/>
|
||||||
|
{isSelect && (
|
||||||
|
<Text className={styles.filterCount}>{filterCount}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
className={styles.searchBar}
|
||||||
|
placeholder="搜索上海的球局和场地"
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchBarComponent;
|
||||||
|
|||||||
@@ -4,8 +4,19 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,16 @@ import styles from "./index.module.scss";
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
title: string;
|
title: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
icon?: JSX.Element;
|
||||||
}
|
}
|
||||||
const TitleComponent = (props: IProps) => {
|
const TitleComponent = (props: IProps) => {
|
||||||
const { title, className } = props;
|
const { title, className, icon } = props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`${styles.titleContainer} ${className ? className : ""} `}
|
className={`${styles.titleContainer} ${className ? className : ""} `}
|
||||||
>
|
>
|
||||||
<div>图</div>
|
<div className={styles.icon}>{icon}</div>
|
||||||
<h1 className={styles.title}>{title}</h1>
|
<h1 className={styles.title}>{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
127
src/components/UploadCover/index.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
140
src/components/UploadCover/index.tsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<CommonPopup
|
||||||
|
visible={visible}
|
||||||
|
onClose={() => setVisible(false)}
|
||||||
|
round
|
||||||
|
position="bottom"
|
||||||
|
hideFooter
|
||||||
|
zIndex={1000}
|
||||||
|
>
|
||||||
|
<View className="upload-source-popup-container" style={{ height: source.length * 56 + 52 + 'px' }}>
|
||||||
|
{
|
||||||
|
source.map((item) => {
|
||||||
|
return (
|
||||||
|
<View className="upload-source-popup-item" key={item}>
|
||||||
|
{
|
||||||
|
item === 'album' ? (
|
||||||
|
<UploadFromWx onAdd={onWxAdd} maxCount={maxCount - value.length} />
|
||||||
|
) : (
|
||||||
|
<View className="upload-source-popup-item-text" onClick={() => uploadSourcePopupRef.current?.show(item, maxCount - value.length)}>
|
||||||
|
<Text>{sourceMap.get(item)}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</CommonPopup>
|
||||||
|
<UploadSourcePopup ref={uploadSourcePopupRef} onAdd={onAdd} />
|
||||||
|
<div className={`upload-cover-root ${value.length === 0 && align === 'center' ? 'upload-cover-act-center' : ''}`}>
|
||||||
|
{value.length < maxCount && (
|
||||||
|
<div className="upload-cover-act" onClick={() => setVisible(true)}>
|
||||||
|
<Image className='upload-cover-act-icon' src={img.ICON_ADD} />
|
||||||
|
<div className="upload-cover-text">添加活动封面</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`cover-image-list-container ${value.length === maxCount ? 'full' : ''}`}>
|
||||||
|
<div className="cover-image-list">
|
||||||
|
{
|
||||||
|
value.map((item) => {
|
||||||
|
return (
|
||||||
|
<View className="cover-image-item" key={item.id}>
|
||||||
|
<Image className="cover-image-item-image" src={item.url} />
|
||||||
|
<Image className="cover-image-item-delete" src={img.ICON_REMOVE} onClick={() => onDelete(item)} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
11
src/components/UploadCover/upload-from-wx.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
47
src/components/UploadCover/upload-from-wx.tsx
Normal file
@@ -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 (
|
||||||
|
<View onClick={handleImportFromWx}>
|
||||||
|
<Text className="upload-from-wx-text">从相册添加</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
156
src/components/UploadCover/upload-source-popup.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/components/UploadCover/upload-source-popup.tsx
Normal file
@@ -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<SourceType, string>([
|
||||||
|
['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<SourceType>('history')
|
||||||
|
const [maxCount, setMaxCount] = useState(9)
|
||||||
|
const [images, setImages] = useState<ImageItem[]>([])
|
||||||
|
const [selectedImages, setSelectedImages] = useState<ImageItem[]>([])
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<CommonPopup
|
||||||
|
visible={visible}
|
||||||
|
onClose={onClose}
|
||||||
|
round
|
||||||
|
hideFooter
|
||||||
|
position='bottom'
|
||||||
|
zIndex={1001}
|
||||||
|
>
|
||||||
|
<View className="upload-popup">
|
||||||
|
<View className="upload-popup-title">{sourceMap.get(sourceType)}</View>
|
||||||
|
{/* TODO: 分页 加载更多 */}
|
||||||
|
{/* TODO: 图片加载失败 */}
|
||||||
|
{/* TODO: 图片加载中 */}
|
||||||
|
<ScrollView
|
||||||
|
scrollY
|
||||||
|
className="upload-popup-scroll-view"
|
||||||
|
>
|
||||||
|
{images.length > 0 ? (
|
||||||
|
<View className="upload-popup-image-list">
|
||||||
|
{images.map(item => {
|
||||||
|
const isSelected = checkImageSelected(selectedImages, item)
|
||||||
|
return (
|
||||||
|
<View className={`upload-popup-image-item ${outOfMax ? 'disabled' : ''} ${isSelected ? 'selected' : ''}`} onClick={() => handleImageClick(item)}>
|
||||||
|
<Image className="upload-popup-image-item-image" src={item.url} />
|
||||||
|
<View className={`upload-popup-image-item-select ${isSelected ? 'selected' : ''}`}>
|
||||||
|
{isSelected ? (
|
||||||
|
<Image className="select-image-icon" src={img.ICON_CIRCLE_SELECT_ARROW} />
|
||||||
|
) : (
|
||||||
|
<Image className="select-image-icon" src={img.ICON_CIRCLE_UNSELECT} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className="upload-popup-image-list-empty">
|
||||||
|
<Image className="upload-popup-image-list-empty-image" src={emptyStatus} />
|
||||||
|
<Text className="upload-popup-image-list-empty-text">暂无内容</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
{images.length > 0 ? (
|
||||||
|
<View className="upload-popup-footer">
|
||||||
|
<Button className="upload-popup-footer-cancel" onClick={() => setVisible(false)}>取消</Button>
|
||||||
|
<Button className={`upload-popup-footer-confirm ${selectedImages.length > 0 ? 'active' : ''}`} type='primary' onClick={handleConfirm}>完成</Button>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className="upload-popup-footer">
|
||||||
|
<Button className="upload-popup-footer-cancel" onClick={() => setVisible(false)}>取消</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</CommonPopup>
|
||||||
|
{/* <View className="upload-source-popup-text" onClick={() => setVisible(true)}>{sourceMap.get(sourceType)}选取</View> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ import TimeSelector from './TimeSelector'
|
|||||||
import TitleTextarea from './TitleTextarea'
|
import TitleTextarea from './TitleTextarea'
|
||||||
import CommonPopup from './CommonPopup'
|
import CommonPopup from './CommonPopup'
|
||||||
import DateTimePicker from './DateTimePicker/DateTimePicker'
|
import DateTimePicker from './DateTimePicker/DateTimePicker'
|
||||||
|
<<<<<<< HEAD
|
||||||
import TimePicker from './TimePicker/TimePicker'
|
import TimePicker from './TimePicker/TimePicker'
|
||||||
import CalendarCard, { DialogCalendarCard } from './CalendarCard'
|
import CalendarCard, { DialogCalendarCard } from './CalendarCard'
|
||||||
import CommonDialog from './CommonDialog'
|
import CommonDialog from './CommonDialog'
|
||||||
@@ -31,4 +32,21 @@ import PublishMenu from './PublishMenu/PublishMenu'
|
|||||||
CommonDialog,
|
CommonDialog,
|
||||||
PublishMenu
|
PublishMenu
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
import UploadCover from './UploadCover'
|
||||||
|
|
||||||
|
export {
|
||||||
|
ActivityTypeSwitch,
|
||||||
|
TextareaTag,
|
||||||
|
FormSwitch,
|
||||||
|
ImageUpload,
|
||||||
|
Range,
|
||||||
|
NumberInterval,
|
||||||
|
TimeSelector,
|
||||||
|
TitleTextarea,
|
||||||
|
CommonPopup,
|
||||||
|
DateTimePicker,
|
||||||
|
UploadCover,
|
||||||
|
}
|
||||||
|
>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ export default {
|
|||||||
ICON_COST: require('@/static/publishBall/icon-cost.svg'),
|
ICON_COST: require('@/static/publishBall/icon-cost.svg'),
|
||||||
ICON_TIPS: require('@/static/publishBall/icon-tips.svg'),
|
ICON_TIPS: require('@/static/publishBall/icon-tips.svg'),
|
||||||
ICON_ARROW_RIGHT: require('@/static/publishBall/icon-arrow-right.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_SEARCH: require('@/static/publishBall/icon-search.svg'),
|
||||||
ICON_MAP: require('@/static/publishBall/icon-map.svg'),
|
ICON_MAP: require('@/static/publishBall/icon-map.svg'),
|
||||||
ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'),
|
ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'),
|
||||||
@@ -17,9 +28,30 @@ export default {
|
|||||||
ICON_ADD: require('@/static/publishBall/icon-add.svg'),
|
ICON_ADD: require('@/static/publishBall/icon-add.svg'),
|
||||||
ICON_COPY: require('@/static/publishBall/icon-arrow-right.svg'),
|
ICON_COPY: require('@/static/publishBall/icon-arrow-right.svg'),
|
||||||
ICON_DELETE: require('@/static/publishBall/icon-delete.svg'),
|
ICON_DELETE: require('@/static/publishBall/icon-delete.svg'),
|
||||||
|
<<<<<<< HEAD
|
||||||
ICON_RIGHT_MAX: require('@/static/publishBall/icon-right-max.svg'),
|
ICON_RIGHT_MAX: require('@/static/publishBall/icon-right-max.svg'),
|
||||||
ICON_PLUS: require('@/static/publishBall/icon-plus.svg'),
|
ICON_PLUS: require('@/static/publishBall/icon-plus.svg'),
|
||||||
ICON_GROUP: require('@/static/publishBall/icon-group.svg'),
|
ICON_GROUP: require('@/static/publishBall/icon-group.svg'),
|
||||||
ICON_PERSON: require('@/static/publishBall/icon-person.svg'),
|
ICON_PERSON: require('@/static/publishBall/icon-person.svg'),
|
||||||
ICON_PUBLISH: require('@/static/publishBall/icon-publish.png'),
|
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
|
||||||
}
|
}
|
||||||
8
src/container/listContainer/index.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.listContentWrapper {
|
||||||
|
padding: 0 5px;
|
||||||
|
background: #fafafa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
52
src/container/listContainer/index.tsx
Normal file
@@ -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 <ListCardSkeleton />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ListLoadError reload={reload} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && data.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{data.map((match, index) => (
|
||||||
|
<ListCard key={match.id || index} {...match} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
className="listContentWrapper"
|
||||||
|
// style={{
|
||||||
|
// minHeight: `calc(100vh - ${totalHeight}px)`,
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
{renderList()}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListContainer;
|
||||||
4
src/pages/detail/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '球局详情',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
1034
src/pages/detail/index.scss
Normal file
556
src/pages/detail/index.tsx
Normal file
@@ -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 (
|
||||||
|
<CommonPopup
|
||||||
|
title="分享"
|
||||||
|
visible={visible}
|
||||||
|
onClose={() => { setVisible(false) }}
|
||||||
|
hideFooter
|
||||||
|
style={{ minHeight: '100px' }}
|
||||||
|
>
|
||||||
|
<View catchMove className='share-popup-content'>
|
||||||
|
<View onClick={handleShareToWechat}>
|
||||||
|
<Image src={img.ICON_DETAIL_SHARE} />
|
||||||
|
<Text>分享到微信</Text>
|
||||||
|
</View>
|
||||||
|
<View onClick={handleShareToWechatMoments}>
|
||||||
|
<Image src={img.ICON_DETAIL_SHARE} />
|
||||||
|
<Text>分享朋友圈</Text>
|
||||||
|
</View>
|
||||||
|
<View onClick={handleSaveToLocal}>
|
||||||
|
<Image src={img.ICON_DETAIL_SHARE} />
|
||||||
|
<Text>保存到本地</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</CommonPopup>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function Index() {
|
||||||
|
// 使用Zustand store
|
||||||
|
// const userStats = useUserStats()
|
||||||
|
// const { incrementRequestCount, resetUserStats } = useUserActions()
|
||||||
|
|
||||||
|
const [current, setCurrent] = useState(0)
|
||||||
|
const [colors, setColors] = useState<string []>([])
|
||||||
|
const [detail, setDetail] = useState<any>(null)
|
||||||
|
const { params } = useRouter()
|
||||||
|
const { id, autoShare, from } = params
|
||||||
|
|
||||||
|
console.log('from', from)
|
||||||
|
|
||||||
|
// 本地状态管理
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const sharePopupRef = useRef<any>(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 (
|
||||||
|
<View className='detail-page'>
|
||||||
|
{/* custom navbar */}
|
||||||
|
<view className="custom-navbar">
|
||||||
|
<View className='detail-navigator'>
|
||||||
|
<View className='detail-navigator-back' onClick={() => { Taro.navigateBack() }}>
|
||||||
|
<Image className='detail-navigator-back-icon' src={img.ICON_ARROW_LEFT} />
|
||||||
|
</View>
|
||||||
|
<View className='detail-navigator-icon'>
|
||||||
|
<Image className='detail-navigator-logo-icon' src={img.ICON_LOGO_GO} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</view>
|
||||||
|
<View className='detail-page-bg' style={{ backgroundImage: `url(${images[current]})` }} />
|
||||||
|
<View className='detail-page-bg-text' />
|
||||||
|
{/* swiper */}
|
||||||
|
<Swiper
|
||||||
|
className='detail-swiper'
|
||||||
|
indicatorDots={false}
|
||||||
|
circular
|
||||||
|
nextMargin="20px"
|
||||||
|
onChange={(e) => { setCurrent(e.detail.current) }}
|
||||||
|
>
|
||||||
|
{images.map((imageUrl, index) => (
|
||||||
|
<SwiperItem
|
||||||
|
key={index}
|
||||||
|
className='detail-swiper-item'
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={imageUrl}
|
||||||
|
mode="aspectFill"
|
||||||
|
className='detail-swiper-item-image'
|
||||||
|
style={{
|
||||||
|
transform: index !== current ? 'scale(0.8) translateX(-12%)' : 'scale(0.95)', // 前后图缩小
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SwiperItem>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
{/* content */}
|
||||||
|
<View className='detail-page-content'>
|
||||||
|
{/* avatar and tags */}
|
||||||
|
<View className='detail-page-content-avatar-tags'>
|
||||||
|
<View className='detail-page-content-avatar-tags-avatar'>
|
||||||
|
{/* network image mock */}
|
||||||
|
<Image className='detail-page-content-avatar-tags-avatar-image' src="https://img.yzcdn.cn/vant/cat.jpeg" />
|
||||||
|
</View>
|
||||||
|
<View className='detail-page-content-avatar-tags-tags'>
|
||||||
|
{tags.map((tag, index) => (
|
||||||
|
<View key={index} className='detail-page-content-avatar-tags-tags-tag'>
|
||||||
|
{tag.icon && <Image src={tag.icon} />}
|
||||||
|
<Text>{tag.name}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* title */}
|
||||||
|
<View className='detail-page-content-title'>
|
||||||
|
<Text className='detail-page-content-title-text'>{title}</Text>
|
||||||
|
</View>
|
||||||
|
{/* Date and Place and weather */}
|
||||||
|
<View className='detail-page-content-game-info'>
|
||||||
|
{/* Date and Weather */}
|
||||||
|
<View className='detail-page-content-game-info-date-weather'>
|
||||||
|
{/* Calendar and Date time */}
|
||||||
|
<View className='detail-page-content-game-info-date-weather-calendar-date'>
|
||||||
|
{/* Calendar */}
|
||||||
|
<View className='detail-page-content-game-info-date-weather-calendar-date-calendar'>
|
||||||
|
<View className="month">3月</View>
|
||||||
|
<View className="day">25</View>
|
||||||
|
</View>
|
||||||
|
{/* Date time */}
|
||||||
|
<View className='detail-page-content-game-info-date-weather-calendar-date-date'>
|
||||||
|
<View className="date">3月25日 周一</View>
|
||||||
|
<View className="venue-time">19:00-21:00 (2小时)</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* Weather */}
|
||||||
|
<View className='detail-page-content-game-info-date-weather-weather'>
|
||||||
|
{/* Weather icon */}
|
||||||
|
<View className='detail-page-content-game-info-date-weather-weather-icon'>
|
||||||
|
<Image className="weather-icon" src={img.ICON_WEATHER_SUN} />
|
||||||
|
</View>
|
||||||
|
{/* Weather text and temperature */}
|
||||||
|
<View className='detail-page-content-game-info-date-weather-weather-text-temperature'>
|
||||||
|
<Text>28℃ - 32℃</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* Place */}
|
||||||
|
<View className='detail-page-content-game-info-place'>
|
||||||
|
{/* venue location message */}
|
||||||
|
<View className='location-message'>
|
||||||
|
{/* location icon */}
|
||||||
|
<View className='location-message-icon'>
|
||||||
|
<Image className='location-message-icon-image' src={img.ICON_DETAIL_MAP} />
|
||||||
|
</View>
|
||||||
|
{/* location message */}
|
||||||
|
<View className='location-message-text'>
|
||||||
|
{/* venue name and distance */}
|
||||||
|
<View className='location-message-text-name-distance' onClick={openMap}>
|
||||||
|
<Text>上海体育场</Text>
|
||||||
|
<Text>·</Text>
|
||||||
|
<Text>1.2km</Text>
|
||||||
|
<Image className='location-message-text-name-distance-arrow' src={img.ICON_DETAIL_ARROW_RIGHT} />
|
||||||
|
</View>
|
||||||
|
{/* venue address */}
|
||||||
|
<View className='location-message-text-address'>
|
||||||
|
<Text>上海市徐汇区肇嘉浜路128号</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* venue map */}
|
||||||
|
<View className='location-map'>
|
||||||
|
<Map
|
||||||
|
className='location-map-map'
|
||||||
|
longitude={longitude}
|
||||||
|
latitude={latitude}
|
||||||
|
onError={() => {}}
|
||||||
|
// hide business msg
|
||||||
|
showLocation
|
||||||
|
theme='dark'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* detail */}
|
||||||
|
<View className='detail-page-content-detail'>
|
||||||
|
{/* venue detail title and venue ordered status */}
|
||||||
|
<View className='venue-detail-title'>
|
||||||
|
<Text>场馆详情</Text>
|
||||||
|
<Text>·</Text>
|
||||||
|
<Text>已订场</Text>
|
||||||
|
<Popover
|
||||||
|
title="场地预定截图"
|
||||||
|
description={<View>
|
||||||
|
<Image src="https://img.yzcdn.cn/vant/cat.jpeg" />
|
||||||
|
</View>}
|
||||||
|
location='top'
|
||||||
|
visible={false}
|
||||||
|
>
|
||||||
|
<Image className='venue-detail-title-notice-icon' src={img.ICON_DETAIL_NOTICE} />
|
||||||
|
</Popover>
|
||||||
|
</View>
|
||||||
|
{/* venue detail content */}
|
||||||
|
<View className='venue-detail-content'>
|
||||||
|
{/* venue detail tags */}
|
||||||
|
<View className='venue-detail-content-tags'>
|
||||||
|
{insertDotInTags(detailTags).map((tag, index) => (
|
||||||
|
<View key={index} className='venue-detail-content-tags-tag'>
|
||||||
|
<Text>{tag}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
{/* venue remarks */}
|
||||||
|
<View className='venue-detail-content-remarks'>
|
||||||
|
<Text>其他这是用户在场地补充描述里自己写的东西啦啦啦啦啦啦啦啦啦啦啦啦</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* gameplay requirements */}
|
||||||
|
<View className='detail-page-content-gameplay-requirements'>
|
||||||
|
{/* title */}
|
||||||
|
<View className="gameplay-requirements-title">
|
||||||
|
<Text>玩法要求</Text>
|
||||||
|
</View>
|
||||||
|
{/* requirements */}
|
||||||
|
<View className='gameplay-requirements'>
|
||||||
|
{requirements.map((item, index) => (
|
||||||
|
<View key={index} className='gameplay-requirements-item'>
|
||||||
|
<Text className='gameplay-requirements-item-title'>{item.title}</Text>
|
||||||
|
<Text className='gameplay-requirements-item-desc'>{item.desc}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* participants */}
|
||||||
|
<View className='detail-page-content-participants'>
|
||||||
|
<View className='participants-title'>
|
||||||
|
<Text>参与者</Text>
|
||||||
|
<Text>·</Text>
|
||||||
|
<Text>剩余空位 3</Text>
|
||||||
|
</View>
|
||||||
|
<View className='participants-list'>
|
||||||
|
{/* application */}
|
||||||
|
<View className='participants-list-application' onClick={() => { Taro.showToast({ title: 'To be continued', icon: 'none' }) }}>
|
||||||
|
<Image className='participants-list-application-icon' src={img.ICON_DETAIL_APPLICATION_ADD} />
|
||||||
|
<Text className='participants-list-application-text'>申请加入</Text>
|
||||||
|
</View>
|
||||||
|
{/* participants list */}
|
||||||
|
<ScrollView className='participants-list-scroll' scrollX>
|
||||||
|
<View className='participants-list-scroll-content' style={{ width: `${participants.length * 103 + (participants.length - 1) * 8}px` }}>
|
||||||
|
{participants.map((participant) => (
|
||||||
|
<View key={participant.id} className='participants-list-item'>
|
||||||
|
{/* <Avatar className='participants-list-item-avatar' src={participant.user.avatar_url} /> */}
|
||||||
|
{/* network image mock random */}
|
||||||
|
<Avatar className='participants-list-item-avatar' src={`https://picsum.photos/800/600?random=${participant.id}`} />
|
||||||
|
<Text className='participants-list-item-name'>{participant.user.nickname || '未知'}</Text>
|
||||||
|
<Text className='participants-list-item-level'>{participant.level || '未知'}</Text>
|
||||||
|
<Text className='participants-list-item-role'>{participant.role || '参与者'}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* supplemental notes */}
|
||||||
|
<View className='detail-page-content-supplemental-notes'>
|
||||||
|
<View className='supplemental-notes-title'>
|
||||||
|
<Text>补充说明</Text>
|
||||||
|
</View>
|
||||||
|
<View className='supplemental-notes-content'>
|
||||||
|
{/* supplemental notes tags */}
|
||||||
|
<View className='supplemental-notes-content-tags'>
|
||||||
|
{insertDotInTags(supplementalNotesTags).map((tag, index) => (
|
||||||
|
<View key={index} className='supplemental-notes-content-tags-tag'>
|
||||||
|
<Text>{tag}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
{/* supplemental notes content */}
|
||||||
|
<View className='supplemental-notes-content-text'>
|
||||||
|
<Text>其他这是用户在补充说明里自己写的东西啦啦啦啦啦啦啦啦啦啦啦啦其他这是用户在补充说明里自己写的东西啦啦啦啦啦啦啦啦啦啦啦啦</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* organizer and recommend games by organizer */}
|
||||||
|
<View className='detail-page-content-organizer-recommend-games'>
|
||||||
|
{/* orgnizer title */}
|
||||||
|
<View className='organizer-title'>
|
||||||
|
<Text>组织者</Text>
|
||||||
|
</View>
|
||||||
|
{/* organizer avatar and name */}
|
||||||
|
<View className='organizer-avatar-name'>
|
||||||
|
<Avatar className='organizer-avatar-name-avatar' src="https://img.yzcdn.cn/vant/cat.jpeg" />
|
||||||
|
<View className='organizer-avatar-name-message'>
|
||||||
|
<Text className='organizer-avatar-name-message-name'>Light</Text>
|
||||||
|
<View className='organizer-avatar-name-message-stats'>
|
||||||
|
<Text>已组织 8 次</Text>
|
||||||
|
<View className='organizer-avatar-name-message-stats-separator' />
|
||||||
|
<Text>NTRP 3.5</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="organizer-actions">
|
||||||
|
<View className="organizer-actions-follow">
|
||||||
|
<Image className='organizer-actions-follow-icon' src={img.ICON_DETAIL_APPLICATION_ADD} />
|
||||||
|
<Text className='organizer-actions-follow-text'>关注</Text>
|
||||||
|
</View>
|
||||||
|
<View className="organizer-actions-comment">
|
||||||
|
<Image className='organizer-actions-comment-icon' src={img.ICON_DETAIL_COMMENT} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* recommend games by organizer */}
|
||||||
|
<View className='organizer-recommend-games'>
|
||||||
|
<View className='organizer-recommend-games-title'>
|
||||||
|
<Text>TA的更多活动</Text>
|
||||||
|
<Image className='organizer-recommend-games-title-arrow' src={img.ICON_DETAIL_ARROW_RIGHT} />
|
||||||
|
</View>
|
||||||
|
<ScrollView className='recommend-games-list' scrollX>
|
||||||
|
<View className='recommend-games-list-content'>
|
||||||
|
{recommendGames.map((game, index) => (
|
||||||
|
<View key={index} className='recommend-games-list-item'>
|
||||||
|
{/* game title */}
|
||||||
|
<View className='recommend-games-list-item-title'>
|
||||||
|
<Text>{game.title}</Text>
|
||||||
|
<Image className='recommend-games-list-item-title-arrow' src={img.ICON_DETAIL_ARROW_RIGHT} />
|
||||||
|
</View>
|
||||||
|
{/* game time and range */}
|
||||||
|
<View className='recommend-games-list-item-time-range'>
|
||||||
|
<Text>{game.time}</Text>
|
||||||
|
<Text>{game.timeLength}</Text>
|
||||||
|
</View>
|
||||||
|
{/* game location、vunue、distance */}
|
||||||
|
<View className='recommend-games-list-item-location-venue-distance'>
|
||||||
|
<Text>{game.venue}</Text>
|
||||||
|
<Text>·</Text>
|
||||||
|
<Text>{game.veuneType}</Text>
|
||||||
|
<Text>·</Text>
|
||||||
|
<Text>{game.distance}</Text>
|
||||||
|
</View>
|
||||||
|
{/* organizer avatar、applications、level requirements、play type */}
|
||||||
|
<View className='recommend-games-list-item-addon'>
|
||||||
|
<Avatar className='recommend-games-list-item-addon-avatar' src={game.avatar} />
|
||||||
|
<View className='recommend-games-list-item-addon-message'>
|
||||||
|
<View className='recommend-games-list-item-addon-message-applications'>
|
||||||
|
<Text>报名人数 {game.checkedApplications}/{game.applications}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='recommend-games-list-item-addon-message-level-requirements'>
|
||||||
|
<Text>{game.levelRequirements}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='recommend-games-list-item-addon-message-play-type'>
|
||||||
|
<Text>{game.playType}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* sticky bottom action bar */}
|
||||||
|
<View className="sticky-bottom-bar">
|
||||||
|
<View className="sticky-bottom-bar-share-and-comment">
|
||||||
|
<View className='sticky-bottom-bar-share' onClick={handleShare}>
|
||||||
|
<Image className='sticky-bottom-bar-share-icon' src={img.ICON_DETAIL_SHARE} />
|
||||||
|
<Text className='sticky-bottom-bar-share-text'>分享</Text>
|
||||||
|
</View>
|
||||||
|
<View className='sticky-bottom-bar-share-and-comment-separator' />
|
||||||
|
<View className='sticky-bottom-bar-comment' onClick={() => { Taro.showToast({ title: 'To be continued', icon: 'none' }) }}>
|
||||||
|
<Image className='sticky-bottom-bar-comment-icon' src={img.ICON_DETAIL_COMMENT_DARK} />
|
||||||
|
<Text className='sticky-bottom-bar-comment-text'>32</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="sticky-bottom-bar-join-game" onClick={handleJoinGame}>
|
||||||
|
<Text>🎾</Text>
|
||||||
|
<Text>立即加入</Text>
|
||||||
|
<View className='game-price'>
|
||||||
|
<Text>¥ 65</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{/* share popup */}
|
||||||
|
<SharePopup ref={sharePopupRef} id={id} from={from} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '首页'
|
|
||||||
})
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +1,81 @@
|
|||||||
import { Popup } from "@nutui/nutui-react-taro";
|
import { Popup } from "@nutui/nutui-react-taro";
|
||||||
import Range from "../../components/Range";
|
import Range from "../../components/Range";
|
||||||
import Bubble, { BubbleOption } from "../../components/Bubble";
|
import Bubble from "../../components/Bubble";
|
||||||
import styles from "./filterPopup.module.scss";
|
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[] = [
|
const FilterPopup = (props: FilterPopupProps) => {
|
||||||
{ id: 1, label: "晨间 6:00-10:00", value: "morning" },
|
const {
|
||||||
{ id: 2, label: "上午 10:00-12:00", value: "forenoon" },
|
loading,
|
||||||
{ id: 3, label: "中午 12:00-14:00", value: "noon" },
|
onCancel,
|
||||||
{ id: 4, label: "下午 14:00-18:00", value: "afternoon" },
|
onConfirm,
|
||||||
{ id: 5, label: "晚上 18:00-22:00", value: "evening" },
|
onChange,
|
||||||
{ id: 6, label: "夜间 22:00-24:00", value: "night" },
|
filterOptions,
|
||||||
];
|
onClear,
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
statusNavbarHeigh,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const locationOptions: BubbleOption[] = [
|
const store = useListStore() || {};
|
||||||
{ id: 1, label: "室内", value: "1" },
|
const { getDictionaryValue } = useDictionaryActions() || {};
|
||||||
{ id: 2, label: "室外", value: "2" },
|
const { timeBubbleData } = store;
|
||||||
{ id: 3, label: "半室外", value: "3" },
|
|
||||||
];
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup
|
<Popup
|
||||||
visible={true}
|
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
position="top"
|
position="top"
|
||||||
round
|
round
|
||||||
closeOnOverlayClick={false}
|
visible={visible}
|
||||||
onClose={() => {
|
onClose={onClose}
|
||||||
// setShowTop(false)
|
style={{ marginTop: statusNavbarHeigh + "px" }}
|
||||||
}}
|
overlayStyle={{ marginTop: statusNavbarHeigh + "px" }}
|
||||||
>
|
>
|
||||||
<div className={styles.filterPopupWrapper}>
|
<div className={styles.filterPopupWrapper}>
|
||||||
{/* 时间气泡选项 */}
|
{/* 时间气泡选项 */}
|
||||||
<Bubble
|
<Bubble
|
||||||
options={timeOptions}
|
options={timeBubbleData}
|
||||||
value={(value) => {}}
|
value={filterOptions?.time}
|
||||||
onChange={(value) => {}}
|
onChange={handleFilterChange}
|
||||||
layout="grid"
|
layout="grid"
|
||||||
size="small"
|
size="small"
|
||||||
columns={3}
|
columns={3}
|
||||||
|
name="time"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 范围选择 */}
|
{/* 范围选择 */}
|
||||||
@@ -49,19 +84,58 @@ const FilterPopup = () => {
|
|||||||
max={5.0}
|
max={5.0}
|
||||||
step={0.5}
|
step={0.5}
|
||||||
className={styles.filterPopupRange}
|
className={styles.filterPopupRange}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
value={filterOptions?.ntrp}
|
||||||
|
name="ntrp"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 场次气泡选项 */}
|
{/* 场次气泡选项 */}
|
||||||
<div>
|
{/* <div>
|
||||||
<TitleComponent title="场地类型" />
|
<TitleComponent
|
||||||
|
title="场地类型"
|
||||||
|
icon={<Image src={img.ICON_SITE} />}
|
||||||
|
/>
|
||||||
<Bubble
|
<Bubble
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={(value) => {}}
|
value={filterOptions?.site}
|
||||||
onChange={(value) => {}}
|
onChange={handleFilterChange}
|
||||||
layout="grid"
|
layout="grid"
|
||||||
size="small"
|
size="small"
|
||||||
columns={3}
|
columns={3}
|
||||||
|
name="site"
|
||||||
/>
|
/>
|
||||||
|
</div> */}
|
||||||
|
{/* CourtType */}
|
||||||
|
<CourtType
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
name="court_type"
|
||||||
|
options={locationOptions}
|
||||||
|
value={filterOptions?.site}
|
||||||
|
/>
|
||||||
|
{/* 玩法 */}
|
||||||
|
<GamePlayType
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
name="game_play"
|
||||||
|
options={gamePlayOptions}
|
||||||
|
value={filterOptions?.game_play}
|
||||||
|
/>
|
||||||
|
{/* 按钮 */}
|
||||||
|
<div className={styles.filterPopupBtnWrapper}>
|
||||||
|
<Button
|
||||||
|
className={styles.btn}
|
||||||
|
type="default"
|
||||||
|
onClick={handleClearFilter}
|
||||||
|
>
|
||||||
|
清空全部
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={`${styles.btn} ${styles.confirm}`}
|
||||||
|
type="default"
|
||||||
|
loading={loading}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
显示 332 场球局
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|||||||
@@ -1,8 +1,35 @@
|
|||||||
.filterPopupWrapper {
|
.filterPopupWrapper {
|
||||||
|
position: relative;
|
||||||
$m18: 18px;
|
$m18: 18px;
|
||||||
padding: $m18;
|
padding: $m18;
|
||||||
|
|
||||||
.filterPopupRange {
|
.filterPopupRange {
|
||||||
margin-top: $m18;
|
margin-top: $m18;
|
||||||
margin-bottom: $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '',
|
navigationBarTitleText: '',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
backgroundTextStyle: 'dark'
|
backgroundTextStyle: 'dark',
|
||||||
|
navigationStyle: 'custom',
|
||||||
})
|
})
|
||||||
|
|||||||
26
src/pages/list/index.module.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,51 @@
|
|||||||
import ListItem from "../../components/ListItem";
|
import Menu from "../../components/Menu";
|
||||||
import List from "../../components/List";
|
import CityFilter from "../../components/CityFilter";
|
||||||
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 SearchBar from "../../components/SearchBar";
|
||||||
import FilterPopup from "./FilterPopup";
|
import FilterPopup from "./FilterPopup";
|
||||||
|
import styles from "./index.module.scss";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro, { usePageScroll, useReachBottom } from "@tarojs/taro";
|
||||||
import {
|
import { useListStore } from "@/store/listStore";
|
||||||
useTennisMatches,
|
import { useGlobalState } from "@/store/global";
|
||||||
useTennisLoading,
|
import { View } from "@tarojs/components";
|
||||||
useTennisError,
|
import CustomerNavBar from "@/components/CustomNavbar";
|
||||||
useTennisLastRefresh,
|
import GuideBar from "@/components/GuideBar";
|
||||||
useTennisActions,
|
import ListContainer from "@/container/listContainer";
|
||||||
} from "../../store/listStore";
|
|
||||||
import "./index.scss";
|
|
||||||
|
|
||||||
const ListPage = () => {
|
const ListPage = () => {
|
||||||
// 从 store 获取数据和方法
|
// 从 store 获取数据和方法
|
||||||
const matches = useTennisMatches();
|
const store = useListStore() || {};
|
||||||
const loading = useTennisLoading();
|
|
||||||
const error = useTennisError();
|
const { statusNavbarHeightInfo } = useGlobalState() || {};
|
||||||
const lastRefreshTime = useTennisLastRefresh();
|
const {
|
||||||
const { fetchMatches, refreshMatches, clearError } = useTennisActions();
|
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(() => {
|
useEffect(() => {
|
||||||
// 页面加载时获取数据
|
// 页面加载时获取数据
|
||||||
@@ -59,147 +81,97 @@ const ListPage = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 错误处理
|
const toggleShowPopup = () => {
|
||||||
useEffect(() => {
|
updateState({ isShowFilterPopup: !isShowFilterPopup });
|
||||||
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) {
|
* @description 更新筛选条件
|
||||||
return (
|
* @param {Record<string, any>} params 筛选项
|
||||||
<div
|
*/
|
||||||
style={{
|
const handleUpdateFilterOptions = (params: Record<string, any>) => {
|
||||||
display: "flex",
|
updateFilterOptions(params);
|
||||||
flexDirection: "column",
|
};
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
height: "200px",
|
|
||||||
fontSize: "14px",
|
|
||||||
color: "#999",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ marginBottom: "10px" }}>加载中...</div>
|
|
||||||
<div style={{ fontSize: "12px", color: "#ccc" }}>
|
|
||||||
正在获取网球比赛数据
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误状态显示
|
const handleSearchChange = () => {};
|
||||||
if (error && matches.length === 0) {
|
|
||||||
return (
|
// 距离筛选
|
||||||
<div
|
const handleDistanceOrQuickChange = (name, value) => {
|
||||||
style={{
|
updateState({
|
||||||
display: "flex",
|
distanceQuickFilter: {
|
||||||
flexDirection: "column",
|
...distanceQuickFilter,
|
||||||
justifyContent: "center",
|
[name]: value,
|
||||||
alignItems: "center",
|
},
|
||||||
height: "200px",
|
});
|
||||||
fontSize: "14px",
|
};
|
||||||
color: "#999",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ marginBottom: "10px" }}>加载失败</div>
|
|
||||||
<div style={{ marginBottom: "15px", fontSize: "12px", color: "#ccc" }}>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => fetchMatches()}
|
|
||||||
style={{
|
|
||||||
padding: "8px 16px",
|
|
||||||
fontSize: "12px",
|
|
||||||
color: "#fff",
|
|
||||||
backgroundColor: "#007aff",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
重试
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<SearchBar />
|
<CustomerNavBar />
|
||||||
{/* 综合筛选 */}
|
|
||||||
<div>
|
|
||||||
<FilterPopup />
|
|
||||||
</div>
|
|
||||||
{/* 筛选 */}
|
|
||||||
<div>
|
|
||||||
{/* 全城筛选 */}
|
|
||||||
<CityFilter />
|
|
||||||
{/* 智能排序 */}
|
|
||||||
<Menu />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<View className={styles.listPage}>
|
||||||
|
<View
|
||||||
{/* 列表内容 */}
|
className={`${styles.listTopSearchWrapper} ${
|
||||||
<List>
|
isScrollTop ? styles.isScroll : ""
|
||||||
{matches.map((match, index) => (
|
}`}
|
||||||
<ListItem key={match.id || index} {...match} />
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
|
|
||||||
{/* 空状态 */}
|
|
||||||
{!loading && matches.length === 0 && (
|
|
||||||
<div
|
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
top: statusNavbarHeightInfo?.totalHeight,
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
height: "200px",
|
|
||||||
fontSize: "14px",
|
|
||||||
color: "#999",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: "10px" }}>暂无比赛数据</div>
|
<SearchBar
|
||||||
<button
|
handleFilterIcon={toggleShowPopup}
|
||||||
onClick={() => fetchMatches()}
|
isSelect={filterCount > 0}
|
||||||
style={{
|
filterCount={filterCount}
|
||||||
padding: "8px 16px",
|
onChange={handleSearchChange}
|
||||||
fontSize: "12px",
|
/>
|
||||||
color: "#fff",
|
{/* 综合筛选 */}
|
||||||
backgroundColor: "#007aff",
|
{isShowFilterPopup && (
|
||||||
border: "none",
|
<div>
|
||||||
borderRadius: "4px",
|
<FilterPopup
|
||||||
}}
|
loading={loading}
|
||||||
>
|
onCancel={toggleShowPopup}
|
||||||
重新加载
|
onConfirm={toggleShowPopup}
|
||||||
</button>
|
onChange={handleUpdateFilterOptions}
|
||||||
</div>
|
filterOptions={filterOptions}
|
||||||
)}
|
onClear={clearFilterOptions}
|
||||||
</div>
|
visible={isShowFilterPopup}
|
||||||
|
onClose={toggleShowPopup}
|
||||||
|
statusNavbarHeigh={statusNavbarHeightInfo?.totalHeight}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* 筛选 */}
|
||||||
|
<div className={styles.listTopFilterWrapper}>
|
||||||
|
{/* 全城筛选 */}
|
||||||
|
<CityFilter
|
||||||
|
options={distanceData}
|
||||||
|
value={distanceQuickFilter?.distance}
|
||||||
|
wrapperClassName={styles.menuFilter}
|
||||||
|
onChange={handleDistanceOrQuickChange}
|
||||||
|
name="distance"
|
||||||
|
/>
|
||||||
|
{/* 智能排序 */}
|
||||||
|
<Menu
|
||||||
|
options={quickFilterData}
|
||||||
|
value={distanceQuickFilter?.quick}
|
||||||
|
onChange={handleDistanceOrQuickChange}
|
||||||
|
wrapperClassName={styles.menuFilter}
|
||||||
|
name="quick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 列表内容 */}
|
||||||
|
<ListContainer
|
||||||
|
data={matches}
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
reload={refreshMatches}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<GuideBar currentPage="list" />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -370,7 +370,7 @@
|
|||||||
font-family: 'PingFang SC';
|
font-family: 'PingFang SC';
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #07C160;
|
color: #000000;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const LoginPage: React.FC = () => {
|
|||||||
const [show_terms_layer, set_show_terms_layer] = useState(false);
|
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) {
|
if (!agree_terms) {
|
||||||
set_show_terms_layer(true);
|
set_show_terms_layer(true);
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
@@ -21,9 +21,20 @@ const LoginPage: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否获取到手机号
|
||||||
|
if (!e.detail || !e.detail.code) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '获取手机号失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
set_is_loading(true);
|
set_is_loading(true);
|
||||||
try {
|
try {
|
||||||
const response = await wechat_auth_login();
|
// 传递手机号code给登录服务
|
||||||
|
const response = await wechat_auth_login(e.detail.code);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
save_login_state(response.token!, response.user_info!);
|
save_login_state(response.token!, response.user_info!);
|
||||||
|
|
||||||
@@ -99,7 +110,7 @@ const LoginPage: React.FC = () => {
|
|||||||
<View className="background_image">
|
<View className="background_image">
|
||||||
<Image
|
<Image
|
||||||
className="bg_img"
|
className="bg_img"
|
||||||
src={require('../../../static/login/login_bg.png')}
|
src="http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/2e00dea1-8723-42fe-ae42-84fe38e9ac3f.png"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<View className="bg_overlay"></View>
|
<View className="bg_overlay"></View>
|
||||||
@@ -123,7 +134,8 @@ const LoginPage: React.FC = () => {
|
|||||||
{/* 微信快捷登录 */}
|
{/* 微信快捷登录 */}
|
||||||
<Button
|
<Button
|
||||||
className={`login_button wechat_button ${is_loading ? 'loading' : ''}`}
|
className={`login_button wechat_button ${is_loading ? 'loading' : ''}`}
|
||||||
onClick={handle_wechat_login}
|
openType="getPhoneNumber"
|
||||||
|
onGetPhoneNumber={handle_wechat_login}
|
||||||
disabled={is_loading}
|
disabled={is_loading}
|
||||||
>
|
>
|
||||||
<View className="wechat_icon">
|
<View className="wechat_icon">
|
||||||
@@ -217,7 +229,7 @@ const LoginPage: React.FC = () => {
|
|||||||
{agree_terms ? '已同意' : '同意并继续'}
|
{agree_terms ? '已同意' : '同意并继续'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0px 24px ;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -181,6 +181,7 @@
|
|||||||
line-height: 1.43em;
|
line-height: 1.43em;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 条款详细内容
|
// 条款详细内容
|
||||||
@@ -192,6 +193,14 @@
|
|||||||
color: #000000;
|
color: #000000;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
|
padding: 0px 24px ;
|
||||||
|
|
||||||
|
.terms_first_line,
|
||||||
|
span.terms_first_line {
|
||||||
|
font-weight: 500;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部按钮
|
// 底部按钮
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const TermsPage: React.FC = () => {
|
|||||||
case 'terms':
|
case 'terms':
|
||||||
setPageTitle('条款和条件');
|
setPageTitle('条款和条件');
|
||||||
setTermsTitle('《开场的条款和条件》');
|
setTermsTitle('《开场的条款和条件》');
|
||||||
setTermsContent(`欢迎使用本平台(以下简称"本平台")发布与参与网球活动。为保障您的权益,请您务必仔细阅读并理解以下服务条款。
|
setTermsContent(`<span class="terms_first_line">欢迎使用本平台(以下简称"本平台")发布与参与网球活动。为保障您的权益,请您务必仔细阅读并理解以下服务条款。</span>
|
||||||
|
|
||||||
一、服务内容
|
一、服务内容
|
||||||
1. 本平台为用户提供活动发布、报名、聊天室沟通、活动提醒等服务。
|
1. 本平台为用户提供活动发布、报名、聊天室沟通、活动提醒等服务。
|
||||||
@@ -70,7 +70,7 @@ const TermsPage: React.FC = () => {
|
|||||||
case 'binding':
|
case 'binding':
|
||||||
setPageTitle('微信号绑定协议');
|
setPageTitle('微信号绑定协议');
|
||||||
setTermsTitle('《开场与微信号绑定协议》');
|
setTermsTitle('《开场与微信号绑定协议》');
|
||||||
setTermsContent(`欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。
|
setTermsContent(`<span class="terms_first_line">欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。</span>
|
||||||
|
|
||||||
一、绑定服务说明
|
一、绑定服务说明
|
||||||
1. 本平台提供微信账号绑定服务,用户可通过微信快捷登录方式使用平台功能。
|
1. 本平台提供微信账号绑定服务,用户可通过微信快捷登录方式使用平台功能。
|
||||||
@@ -115,7 +115,7 @@ const TermsPage: React.FC = () => {
|
|||||||
case 'privacy':
|
case 'privacy':
|
||||||
setPageTitle('隐私权政策');
|
setPageTitle('隐私权政策');
|
||||||
setTermsTitle('《隐私权政策》');
|
setTermsTitle('《隐私权政策》');
|
||||||
setTermsContent(`本平台(以下简称"我们")非常重视用户的隐私保护,本隐私权政策说明了我们如何收集、使用、存储和保护您的个人信息。
|
setTermsContent(`<span class="terms_first_line">本平台(以下简称"我们")非常重视用户的隐私保护,本隐私权政策说明了我们如何收集、使用、存储和保护您的个人信息。</span>
|
||||||
|
|
||||||
一、信息收集
|
一、信息收集
|
||||||
1. 注册信息:包括手机号码、微信账号、昵称、头像等基本信息。
|
1. 注册信息:包括手机号码、微信账号、昵称、头像等基本信息。
|
||||||
@@ -190,13 +190,14 @@ const TermsPage: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 条款详细内容 */}
|
{/* 条款详细内容 */}
|
||||||
<View className="terms_content">
|
<View
|
||||||
{termsContent}
|
className="terms_content"
|
||||||
</View>
|
dangerouslySetInnerHTML={{ __html: termsContent }}
|
||||||
|
/>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
5
src/pages/message/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '消息',
|
||||||
|
// navigationBarBackgroundColor: '#FAFAFA',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
80
src/pages/message/index.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/pages/message/index.tsx
Normal file
@@ -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 (
|
||||||
|
<View className='message-container'>
|
||||||
|
<View className='custom-navbar'>
|
||||||
|
<View className='message-navigator'>
|
||||||
|
<Avatar className='message-navigator-avatar' src="https://img.yzcdn.cn/vant/cat.jpeg" />
|
||||||
|
<Text className='message-navigator-title'>消息</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<ScrollView scrollY className='message-content'>
|
||||||
|
<View className='message-content-list'>
|
||||||
|
{messageList.map((item) => (
|
||||||
|
<View className='message-item' key={item.id}>
|
||||||
|
<View className='message-item-title'>
|
||||||
|
<Text>{item.title}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='message-item-content'>
|
||||||
|
<Text>{item.content}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
<GuideBar currentPage='message' />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Personal
|
||||||
4
src/pages/orderCheck/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '订单确认',
|
||||||
|
navigationBarBackgroundColor: '#FAFAFA'
|
||||||
|
})
|
||||||
1
src/pages/orderCheck/index.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@use '~@/scss/images.scss' as img;
|
||||||
12
src/pages/orderCheck/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
|
||||||
|
const OrderCheck = () => {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>OrderCheck</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrderCheck
|
||||||
5
src/pages/personal/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '个人中心',
|
||||||
|
// navigationBarBackgroundColor: '#FAFAFA',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
35
src/pages/personal/index.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/pages/personal/index.tsx
Normal file
@@ -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 (
|
||||||
|
<View className='personal-container'>
|
||||||
|
{id && (
|
||||||
|
<View className='personal-navigator' onClick={handleBack}>
|
||||||
|
<Image className='personal-navigator-back' src={img.ICON_NAVIGATOR_BACK} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View className='personal-content'>
|
||||||
|
<Text>Personal</Text>
|
||||||
|
</View>
|
||||||
|
<GuideBar currentPage='personal' />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Personal
|
||||||
@@ -3,7 +3,8 @@ import Taro from '@tarojs/taro'
|
|||||||
import { View, Text, Image } from '@tarojs/components'
|
import { View, Text, Image } from '@tarojs/components'
|
||||||
import images from '@/config/images'
|
import images from '@/config/images'
|
||||||
import TextareaTag from '@/components/TextareaTag'
|
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 { useDictionaryActions } from '@/store/dictionaryStore'
|
||||||
import './StadiumDetail.scss'
|
import './StadiumDetail.scss'
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ export interface Stadium {
|
|||||||
court_surface?: string
|
court_surface?: string
|
||||||
description?: string
|
description?: string
|
||||||
description_tag?: string[]
|
description_tag?: string[]
|
||||||
venue_image_list?: CoverImage[]
|
venue_image_list?: CoverImageValue[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StadiumDetailProps {
|
interface StadiumDetailProps {
|
||||||
@@ -103,7 +104,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
|||||||
court_type: court_type[0] || '',
|
court_type: court_type[0] || '',
|
||||||
court_surface: court_surface[0] || '',
|
court_surface: court_surface[0] || '',
|
||||||
additionalInfo: '',
|
additionalInfo: '',
|
||||||
venue_image_list: [] as CoverImage[],
|
venue_image_list: [] as CoverImageValue[],
|
||||||
description:{
|
description:{
|
||||||
description: '',
|
description: '',
|
||||||
description_tag: []
|
description_tag: []
|
||||||
@@ -216,9 +217,21 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
|||||||
if (item.type === 'image') {
|
if (item.type === 'image') {
|
||||||
return (
|
return (
|
||||||
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
|
<SectionContainer key={item.label} title={item.label} prop={item.prop}>
|
||||||
<CoverImageUpload
|
<UploadCover
|
||||||
images={formData[item.prop]}
|
value={formData[item.prop]}
|
||||||
onChange={(images) => updateFormData(item.prop, images)}
|
onChange={(value) => {
|
||||||
|
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'
|
||||||
/>
|
/>
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
)
|
||||||
@@ -230,4 +243,4 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default StadiumDetail
|
export default StadiumDetail
|
||||||
@@ -8,40 +8,40 @@ import PublishForm from './publishForm'
|
|||||||
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
|
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
|
||||||
import { PublishBallFormData } from '../../../types/publishBall';
|
import { PublishBallFormData } from '../../../types/publishBall';
|
||||||
import PublishService from '@/services/publishService';
|
import PublishService from '@/services/publishService';
|
||||||
import { getNextHourTime, getEndTime } from '@/utils/timeUtils';
|
import { getNextHourTime, getEndTime, delay } from '@/utils';
|
||||||
import images from '@/config/images'
|
import images from '@/config/images'
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
const defaultFormData: PublishBallFormData = {
|
const defaultFormData: PublishBallFormData = {
|
||||||
title: '',
|
title: '',
|
||||||
image_list: ['https://static-o.oss-cn-shenzhen.aliyuncs.com/images/tpbj/tpss10.jpg'],
|
image_list: ['https://static-o.oss-cn-shenzhen.aliyuncs.com/images/tpbj/tpss10.jpg'],
|
||||||
timeRange: {
|
timeRange: {
|
||||||
start_time: getNextHourTime(),
|
start_time: getNextHourTime(),
|
||||||
end_time: getEndTime(getNextHourTime())
|
end_time: getEndTime(getNextHourTime())
|
||||||
},
|
},
|
||||||
activityInfo: {
|
activityInfo: {
|
||||||
play_type: '不限',
|
play_type: '不限',
|
||||||
price: '',
|
price: '',
|
||||||
venue_id: null,
|
venue_id: null,
|
||||||
location_name: '',
|
location_name: '',
|
||||||
location: '',
|
location: '',
|
||||||
latitude: '',
|
latitude: '',
|
||||||
longitude: '',
|
longitude: '',
|
||||||
court_type: '',
|
court_type: '',
|
||||||
court_surface: '',
|
court_surface: '',
|
||||||
venue_description_tag: [],
|
venue_description_tag: [],
|
||||||
venue_description: '',
|
venue_description: '',
|
||||||
venue_image_list: [],
|
venue_image_list: [],
|
||||||
},
|
},
|
||||||
players: [1, 4],
|
players: [1, 4],
|
||||||
skill_level: [1.0, 5.0],
|
skill_level: [1.0, 5.0],
|
||||||
descriptionInfo: {
|
descriptionInfo: {
|
||||||
description: '',
|
description: '',
|
||||||
description_tag: [],
|
description_tag: [],
|
||||||
},
|
},
|
||||||
is_substitute_supported: true,
|
is_substitute_supported: true,
|
||||||
is_wechat_contact: true,
|
is_wechat_contact: true,
|
||||||
wechat_contact: '14223332214'
|
wechat_contact: '14223332214'
|
||||||
}
|
}
|
||||||
|
|
||||||
const PublishBall: React.FC = () => {
|
const PublishBall: React.FC = () => {
|
||||||
@@ -72,7 +72,7 @@ const PublishBall: React.FC = () => {
|
|||||||
const [formData, setFormData] = useState<PublishBallFormData[]>([
|
const [formData, setFormData] = useState<PublishBallFormData[]>([
|
||||||
defaultFormData
|
defaultFormData
|
||||||
])
|
])
|
||||||
|
|
||||||
// 删除确认弹窗状态
|
// 删除确认弹窗状态
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -225,6 +225,13 @@ const PublishBall: React.FC = () => {
|
|||||||
title: '发布成功',
|
title: '发布成功',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
})
|
})
|
||||||
|
delay(1000)
|
||||||
|
// 如果是个人球局,则跳转到详情页,并自动分享
|
||||||
|
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
|
||||||
|
Taro.navigateTo({
|
||||||
|
// @ts-expect-error: id
|
||||||
|
url: `/pages/detail/index?id=${res.data.id || 1}&from=publish&autoShare=1`
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: res.message,
|
title: res.message,
|
||||||
@@ -268,12 +275,16 @@ const PublishBall: React.FC = () => {
|
|||||||
<View className={styles['publish-ball']}>
|
<View className={styles['publish-ball']}>
|
||||||
{/* 活动类型切换 */}
|
{/* 活动类型切换 */}
|
||||||
<View className={styles['activity-type-switch']}>
|
<View className={styles['activity-type-switch']}>
|
||||||
|
<<<<<<< HEAD
|
||||||
{/* <ActivityTypeSwitch
|
{/* <ActivityTypeSwitch
|
||||||
|
=======
|
||||||
|
<ActivityTypeSwitch
|
||||||
|
>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034
|
||||||
value={activityType}
|
value={activityType}
|
||||||
onChange={handleActivityTypeChange}
|
onChange={handleActivityTypeChange}
|
||||||
/> */}
|
/> */}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className={styles['publish-ball__scroll']}>
|
<View className={styles['publish-ball__scroll']}>
|
||||||
{
|
{
|
||||||
formData.map((item, index) => (
|
formData.map((item, index) => (
|
||||||
@@ -283,19 +294,19 @@ const PublishBall: React.FC = () => {
|
|||||||
<View className={styles['session-header']}>
|
<View className={styles['session-header']}>
|
||||||
<View className={styles['session-title']}>
|
<View className={styles['session-title']}>
|
||||||
第{index + 1}场
|
第{index + 1}场
|
||||||
<View
|
<View
|
||||||
className={styles['session-delete']}
|
className={styles['session-delete']}
|
||||||
onClick={() => showDeleteConfirm(index)}
|
onClick={() => showDeleteConfirm(index)}
|
||||||
>
|
>
|
||||||
<Image src={images.ICON_DELETE} className={styles['session-delete-icon']} />
|
<Image src={images.ICON_DELETE} className={styles['session-delete-icon']} />
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className={styles['session-actions']}>
|
<View className={styles['session-actions']}>
|
||||||
|
|
||||||
{index > 0 && (
|
{index > 0 && (
|
||||||
<View
|
<View
|
||||||
className={styles['session-action-btn']}
|
className={styles['session-action-btn']}
|
||||||
onClick={() => handleCopyPrevious(index)}
|
onClick={() => handleCopyPrevious(index)}
|
||||||
>
|
>
|
||||||
复制上一场
|
复制上一场
|
||||||
@@ -304,10 +315,10 @@ const PublishBall: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<PublishForm
|
<PublishForm
|
||||||
formData={item}
|
formData={item}
|
||||||
onChange={(key, value) => updateFormData(key, value, index)}
|
onChange={(key, value) => updateFormData(key, value, index)}
|
||||||
optionsConfig={publishBallFormSchema}
|
optionsConfig={publishBallFormSchema}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
@@ -323,6 +334,7 @@ const PublishBall: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 删除确认弹窗 */}
|
{/* 删除确认弹窗 */}
|
||||||
|
<<<<<<< HEAD
|
||||||
<CommonDialog
|
<CommonDialog
|
||||||
visible={deleteConfirm.visible}
|
visible={deleteConfirm.visible}
|
||||||
cancelText="再想想"
|
cancelText="再想想"
|
||||||
@@ -332,6 +344,30 @@ const PublishBall: React.FC = () => {
|
|||||||
contentTitle="确认移除该场次?"
|
contentTitle="确认移除该场次?"
|
||||||
contentDesc="该操作不可恢复"
|
contentDesc="该操作不可恢复"
|
||||||
/>
|
/>
|
||||||
|
=======
|
||||||
|
{deleteConfirm.visible && (
|
||||||
|
<View className={styles['delete-modal']}>
|
||||||
|
<View className={styles['delete-modal__content']}>
|
||||||
|
<Text className={styles['delete-modal__title']}>确认移除该场次?</Text>
|
||||||
|
<Text className={styles['delete-modal__desc']}>该操作不可恢复</Text>
|
||||||
|
<View className={styles['delete-modal__actions']}>
|
||||||
|
<Button
|
||||||
|
className={styles['delete-modal__btn']}
|
||||||
|
onClick={closeDeleteConfirm}
|
||||||
|
>
|
||||||
|
再想想
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={styles['delete-modal__btn']}
|
||||||
|
onClick={confirmDelete}
|
||||||
|
>
|
||||||
|
确认移除
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034
|
||||||
|
|
||||||
{/* 完成按钮 */}
|
{/* 完成按钮 */}
|
||||||
<View className={styles['submit-section']}>
|
<View className={styles['submit-section']}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
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 FormBasicInfo from './components/FormBasicInfo'
|
||||||
import { type CoverImage } from '../../components/index.types'
|
import { type CoverImage } from '../../components/index.types'
|
||||||
import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema'
|
import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema'
|
||||||
@@ -22,18 +22,22 @@ const componentMap = {
|
|||||||
[FieldType.WECHATCONTACT]: WechatSwitch,
|
[FieldType.WECHATCONTACT]: WechatSwitch,
|
||||||
}
|
}
|
||||||
|
|
||||||
const PublishForm: React.FC<{
|
const PublishForm: React.FC<{
|
||||||
formData: PublishBallFormData,
|
formData: PublishBallFormData,
|
||||||
onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void,
|
onChange: (key: keyof PublishBallFormData, value: any, index?: number) => void,
|
||||||
optionsConfig: FormFieldConfig[] }> = ({ formData, onChange, optionsConfig }) => {
|
optionsConfig: FormFieldConfig[] }> = ({ formData, onChange, optionsConfig }) => {
|
||||||
const [coverImages, setCoverImages] = useState<CoverImage[]>([])
|
const [coverImages, setCoverImages] = useState<CoverImage[]>([])
|
||||||
|
|
||||||
// 字典数据相关
|
// 字典数据相关
|
||||||
const { getDictionaryValue } = useDictionaryActions()
|
const { getDictionaryValue } = useDictionaryActions()
|
||||||
|
|
||||||
// 处理封面图片变化
|
// 处理封面图片变化
|
||||||
const handleCoverImagesChange = (images: CoverImage[]) => {
|
const handleCoverImagesChange = (fn: (images: CoverImage[]) => CoverImage[]) => {
|
||||||
setCoverImages(images)
|
if (fn instanceof Function) {
|
||||||
|
setCoverImages(fn(coverImages))
|
||||||
|
} else {
|
||||||
|
setCoverImages(fn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新表单数据
|
// 更新表单数据
|
||||||
@@ -70,7 +74,7 @@ const PublishForm: React.FC<{
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是补充要求,从字典获取选项
|
// 如果是补充要求,从字典获取选项
|
||||||
if (item.prop === 'descriptionInfo') {
|
if (item.prop === 'descriptionInfo') {
|
||||||
const descriptionOptions = getDictionaryOptions('publishing_requirements', [])
|
const descriptionOptions = getDictionaryOptions('publishing_requirements', [])
|
||||||
@@ -79,7 +83,7 @@ const PublishForm: React.FC<{
|
|||||||
options: descriptionOptions
|
options: descriptionOptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -121,7 +125,7 @@ const PublishForm: React.FC<{
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 获取动态表单配置
|
// 获取动态表单配置
|
||||||
const dynamicConfig = getDynamicFormConfig()
|
const dynamicConfig = getDynamicFormConfig()
|
||||||
@@ -140,8 +144,8 @@ const PublishForm: React.FC<{
|
|||||||
}
|
}
|
||||||
if (item.type === FieldType.UPLOADIMAGE) {
|
if (item.type === FieldType.UPLOADIMAGE) {
|
||||||
/* 活动封面 */
|
/* 活动封面 */
|
||||||
return <ImageUpload
|
return <UploadCover
|
||||||
images={coverImages}
|
value={coverImages}
|
||||||
onChange={handleCoverImagesChange}
|
onChange={handleCoverImagesChange}
|
||||||
{...item.props}
|
{...item.props}
|
||||||
/>
|
/>
|
||||||
@@ -182,7 +186,7 @@ const PublishForm: React.FC<{
|
|||||||
value={formData[item.prop]}
|
value={formData[item.prop]}
|
||||||
onChange={(value) => updateFormData(item.prop as keyof PublishBallFormData, value)}
|
onChange={(value) => updateFormData(item.prop as keyof PublishBallFormData, value)}
|
||||||
{...optionProps}
|
{...optionProps}
|
||||||
placeholder={item.placeholder}
|
placeholder={item.placeholder}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
9
src/pages/userInfo/myself/index.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '个人主页',
|
||||||
|
navigationBarBackgroundColor: '#FFFFFF',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
backgroundColor: '#FAFAFA',
|
||||||
|
enablePullDownRefresh: false,
|
||||||
|
disableScroll: false,
|
||||||
|
navigationStyle: 'custom'
|
||||||
|
}
|
||||||
600
src/pages/userInfo/myself/index.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
377
src/pages/userInfo/myself/index.tsx
Normal file
@@ -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<UserInfo>({
|
||||||
|
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<GameRecord[]>([
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<View className="myself_page">
|
||||||
|
{/* 主要内容 */}
|
||||||
|
<ScrollView className="main_content" scrollY>
|
||||||
|
{/* 用户信息区域 */}
|
||||||
|
<View className="user_info_section">
|
||||||
|
{/* 头像和基本信息 */}
|
||||||
|
<View className="basic_info">
|
||||||
|
<View className="avatar_container">
|
||||||
|
<Image className="avatar" src={user_info.avatar} />
|
||||||
|
</View>
|
||||||
|
<View className="info_container">
|
||||||
|
<Text className="nickname">{user_info.nickname}</Text>
|
||||||
|
<Text className="join_date">{user_info.join_date}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 统计数据 */}
|
||||||
|
<View className="stats_section">
|
||||||
|
<View className="stats_container">
|
||||||
|
<View className="stat_item">
|
||||||
|
<Text className="stat_number">{user_info.stats.following}</Text>
|
||||||
|
<Text className="stat_label">关注</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat_item">
|
||||||
|
<Text className="stat_number">{user_info.stats.friends}</Text>
|
||||||
|
<Text className="stat_label">球友</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat_item">
|
||||||
|
<Text className="stat_number">{user_info.stats.hosted}</Text>
|
||||||
|
<Text className="stat_label">主办</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat_item">
|
||||||
|
<Text className="stat_number">{user_info.stats.participated}</Text>
|
||||||
|
<Text className="stat_label">参加</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="action_buttons">
|
||||||
|
{/* 只有非当前用户才显示关注按钮 */}
|
||||||
|
{!is_current_user && (
|
||||||
|
<Button
|
||||||
|
className={`follow_button ${is_following ? 'following' : ''}`}
|
||||||
|
onClick={handle_follow}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="button_icon"
|
||||||
|
src={require('../../../static/userInfo/plus.svg')}
|
||||||
|
/>
|
||||||
|
<Text className="button_text">
|
||||||
|
{is_following ? '已关注' : '关注'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* 只有非当前用户才显示消息按钮 */}
|
||||||
|
{!is_current_user && (
|
||||||
|
<Button className="message_button">
|
||||||
|
<Image
|
||||||
|
className="button_icon"
|
||||||
|
src={require('../../../static/userInfo/message.svg')}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* 只有当前用户才显示编辑按钮 */}
|
||||||
|
{is_current_user && (
|
||||||
|
<Button className="edit_button" onClick={handle_edit_profile}>
|
||||||
|
<Text className="button_text">编辑</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* 只有当前用户才显示分享按钮 */}
|
||||||
|
{is_current_user && (
|
||||||
|
<Button className="share_button" onClick={handle_share}>
|
||||||
|
<Text className="button_text">分享</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 标签和简介 */}
|
||||||
|
<View className="tags_bio_section">
|
||||||
|
<View className="tags_container">
|
||||||
|
<View className="tag_item">
|
||||||
|
<Image
|
||||||
|
className="tag_icon"
|
||||||
|
src={require('../../../static/userInfo/location.svg')}
|
||||||
|
/>
|
||||||
|
<Text className="tag_text">{user_info.location}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="tag_item">
|
||||||
|
<Text className="tag_text">{user_info.occupation}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="tag_item">
|
||||||
|
<Text className="tag_text">{user_info.ntrp_level}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text className="bio_text">{user_info.bio}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局订单和收藏功能 */}
|
||||||
|
<View className="quick_actions_section">
|
||||||
|
<View className="action_card">
|
||||||
|
<View className="action_content" onClick={handle_game_orders}>
|
||||||
|
<Image
|
||||||
|
className="action_icon"
|
||||||
|
src={require('../../../static/userInfo/tennis.svg')}
|
||||||
|
/>
|
||||||
|
<Text className="action_text">球局订单</Text>
|
||||||
|
</View>
|
||||||
|
<View className="action_divider"></View>
|
||||||
|
<View className="action_content" onClick={handle_favorites}>
|
||||||
|
<Image
|
||||||
|
className="action_icon"
|
||||||
|
src={require('../../../static/userInfo/tennis.svg')}
|
||||||
|
/>
|
||||||
|
<Text className="action_text">收藏</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局类型标签页 */}
|
||||||
|
<View className="game_tabs_section">
|
||||||
|
<View className="tab_container">
|
||||||
|
<View className={`tab_item ${active_tab === 'hosted' ? 'active' : ''}`} onClick={() => setActiveTab('hosted')}>
|
||||||
|
<Text className="tab_text">我主办的</Text>
|
||||||
|
</View>
|
||||||
|
<View className={`tab_item ${active_tab === 'participated' ? 'active' : ''}`} onClick={() => setActiveTab('participated')}>
|
||||||
|
<Text className="tab_text">我参与的</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局列表 */}
|
||||||
|
<View className="game_list_section">
|
||||||
|
<View className="date_header">
|
||||||
|
<Text className="date_text">5月28日</Text>
|
||||||
|
<Text className="separator">/</Text>
|
||||||
|
<Text className="weekday_text">星期三</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局卡片 */}
|
||||||
|
<View className="game_cards">
|
||||||
|
{game_records.map((game) => (
|
||||||
|
<View
|
||||||
|
key={game.id}
|
||||||
|
className="game_card"
|
||||||
|
onClick={() => handle_game_detail(game.id)}
|
||||||
|
>
|
||||||
|
{/* 球局标题和类型 */}
|
||||||
|
<View className="game_header">
|
||||||
|
<Text className="game_title">{game.title}</Text>
|
||||||
|
<View className="game_type_icon">
|
||||||
|
<Image
|
||||||
|
className="type_icon"
|
||||||
|
src={require('../../../static/userInfo/tennis.svg')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局时间 */}
|
||||||
|
<View className="game_time">
|
||||||
|
<Text className="time_text">
|
||||||
|
{game.date} {game.time} {game.duration}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局地点和类型 */}
|
||||||
|
<View className="game_location">
|
||||||
|
<Text className="location_text">{game.location}</Text>
|
||||||
|
<Text className="separator">·</Text>
|
||||||
|
<Text className="type_text">{game.type}</Text>
|
||||||
|
<Text className="separator">·</Text>
|
||||||
|
<Text className="distance_text">{game.distance}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局图片 */}
|
||||||
|
<View className="game_images">
|
||||||
|
{game.images.map((image, index) => (
|
||||||
|
<Image
|
||||||
|
key={index}
|
||||||
|
className="game_image"
|
||||||
|
src={image}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局信息标签 */}
|
||||||
|
<View className="game_tags">
|
||||||
|
<View className="participants_info">
|
||||||
|
<View className="avatars">
|
||||||
|
{game.participants.map((participant, index) => (
|
||||||
|
<Image
|
||||||
|
key={index}
|
||||||
|
className="participant_avatar"
|
||||||
|
src={participant.avatar}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<View className="participants_count">
|
||||||
|
<Text className="count_text">
|
||||||
|
报名人数 {game.current_participants}/{game.max_participants}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="game_info_tags">
|
||||||
|
<View className="info_tag">
|
||||||
|
<Text className="tag_text">{game.level_range}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="info_tag">
|
||||||
|
<Text className="tag_text">{game.game_type}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyselfPage;
|
||||||
@@ -16,7 +16,30 @@ $-images: (
|
|||||||
'icon-personal': '/publishBall/icon-personal.svg',
|
'icon-personal': '/publishBall/icon-personal.svg',
|
||||||
'icon-changda': '/publishBall/icon-changda.svg',
|
'icon-changda': '/publishBall/icon-changda.svg',
|
||||||
'icon-cost': '/publishBall/icon-cost.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;
|
) !default;
|
||||||
|
|
||||||
// 图片获取函数
|
// 图片获取函数
|
||||||
|
|||||||
41
src/services/detailService.ts
Normal file
@@ -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<ApiResponse<Response>> {
|
||||||
|
return httpService.post('/games/detail', { id }, {
|
||||||
|
showLoading: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出认证服务实例
|
||||||
|
export default new GameDetailService()
|
||||||
@@ -15,6 +15,7 @@ export interface RequestConfig {
|
|||||||
needAuth?: boolean // 是否需要token认证
|
needAuth?: boolean // 是否需要token认证
|
||||||
showLoading?: boolean // 是否显示加载提示
|
showLoading?: boolean // 是否显示加载提示
|
||||||
loadingText?: string // 加载提示文本
|
loadingText?: string // 加载提示文本
|
||||||
|
showToast?: boolean // 是否显示toast
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应数据接口
|
// 响应数据接口
|
||||||
@@ -58,7 +59,7 @@ class HttpService {
|
|||||||
// 构建完整URL
|
// 构建完整URL
|
||||||
private buildUrl(url: string, params?: Record<string, any>): string {
|
private buildUrl(url: string, params?: Record<string, any>): string {
|
||||||
const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`
|
const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
const searchParams = new URLSearchParams()
|
const searchParams = new URLSearchParams()
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
@@ -69,7 +70,7 @@ class HttpService {
|
|||||||
const queryString = searchParams.toString()
|
const queryString = searchParams.toString()
|
||||||
return queryString ? `${fullUrl}?${queryString}` : fullUrl
|
return queryString ? `${fullUrl}?${queryString}` : fullUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullUrl
|
return fullUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ class HttpService {
|
|||||||
|
|
||||||
const logMethod = console[level] || console.log
|
const logMethod = console[level] || console.log
|
||||||
const timestamp = new Date().toLocaleTimeString()
|
const timestamp = new Date().toLocaleTimeString()
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
logMethod(`[${timestamp}] HTTP ${level.toUpperCase()}: ${message}`, data)
|
logMethod(`[${timestamp}] HTTP ${level.toUpperCase()}: ${message}`, data)
|
||||||
} else {
|
} else {
|
||||||
@@ -165,9 +166,9 @@ class HttpService {
|
|||||||
// 处理业务错误
|
// 处理业务错误
|
||||||
private handleBusinessError(data: any): void {
|
private handleBusinessError(data: any): void {
|
||||||
const message = data.message || '操作失败'
|
const message = data.message || '操作失败'
|
||||||
|
|
||||||
this.log('error', `业务错误: ${message}`, data)
|
this.log('error', `业务错误: ${message}`, data)
|
||||||
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: message,
|
title: message,
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
@@ -187,7 +188,7 @@ class HttpService {
|
|||||||
} = config
|
} = config
|
||||||
|
|
||||||
const fullUrl = this.buildUrl(url, method === 'GET' ? params : undefined)
|
const fullUrl = this.buildUrl(url, method === 'GET' ? params : undefined)
|
||||||
|
|
||||||
this.log('info', `发起请求: ${method} ${fullUrl}`, {
|
this.log('info', `发起请求: ${method} ${fullUrl}`, {
|
||||||
data: method !== 'GET' ? data : undefined,
|
data: method !== 'GET' ? data : undefined,
|
||||||
params: method === 'GET' ? params : undefined
|
params: method === 'GET' ? params : undefined
|
||||||
@@ -223,18 +224,18 @@ class HttpService {
|
|||||||
return this.handleResponse<T>(response)
|
return this.handleResponse<T>(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log('error', '请求失败', error)
|
this.log('error', '请求失败', error)
|
||||||
|
|
||||||
// 在模拟模式下返回模拟数据
|
// 在模拟模式下返回模拟数据
|
||||||
if (envConfig.enableMock && isDevelopment()) {
|
if (envConfig.enableMock && isDevelopment()) {
|
||||||
this.log('info', '使用模拟数据')
|
this.log('info', '使用模拟数据')
|
||||||
return this.getMockResponse<T>(url, method)
|
return this.getMockResponse<T>(url, method)
|
||||||
}
|
}
|
||||||
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '网络连接失败',
|
title: '网络连接失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
|
|
||||||
throw error
|
throw error
|
||||||
} finally {
|
} finally {
|
||||||
// 隐藏加载提示
|
// 隐藏加载提示
|
||||||
@@ -247,7 +248,7 @@ class HttpService {
|
|||||||
// 获取模拟数据
|
// 获取模拟数据
|
||||||
private getMockResponse<T>(url: string, method: string): ApiResponse<T> {
|
private getMockResponse<T>(url: string, method: string): ApiResponse<T> {
|
||||||
this.log('info', `返回模拟数据: ${method} ${url}`)
|
this.log('info', `返回模拟数据: ${method} ${url}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
success: true,
|
success: true,
|
||||||
@@ -323,4 +324,4 @@ class HttpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出HTTP服务实例
|
// 导出HTTP服务实例
|
||||||
export default new HttpService()
|
export default new HttpService()
|
||||||
@@ -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响应格式
|
// 模拟API响应格式
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
code: number
|
code: number;
|
||||||
message: string
|
message: string;
|
||||||
data: T
|
data: T;
|
||||||
timestamp: number
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 模拟网球比赛数据
|
// 模拟网球比赛数据
|
||||||
const mockTennisMatches: TennisMatch[] = [
|
const mockTennisMatches: TennisMatch[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: "1",
|
||||||
title: '周一晚场浦东新区单打约球',
|
title: "周一晚场浦东新区单打约球",
|
||||||
dateTime: '明天(周五)下午5点 2小时',
|
dateTime: "明天(周五)下午5点 2小时",
|
||||||
location: '仁恒河滨花园网球场・室外',
|
location: "仁恒河滨花园网球场",
|
||||||
distance: '3.5km',
|
distance: "3.5km",
|
||||||
|
shinei: "室内",
|
||||||
registeredCount: 3,
|
registeredCount: 3,
|
||||||
maxCount: 4,
|
maxCount: 4,
|
||||||
skillLevel: '2.0 至 2.5',
|
skillLevel: "2.0 至 2.5",
|
||||||
matchType: '双打',
|
matchType: "双打",
|
||||||
images: [
|
images: [
|
||||||
'https://images.unsplash.com/photo-1554068865-24cecd4e34b8?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-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-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: "2",
|
||||||
title: '浦东新区单打约球',
|
title: "浦东新区单打约球",
|
||||||
dateTime: '明天(周五)下午5点 2小时',
|
dateTime: "明天(周五)下午5点 2小时",
|
||||||
location: '仁恒河滨花园网球场・室外',
|
location: "仁恒河滨花园网球场",
|
||||||
distance: '3.5km',
|
distance: "3.5km",
|
||||||
|
shinei: "室外",
|
||||||
registeredCount: 2,
|
registeredCount: 2,
|
||||||
maxCount: 4,
|
maxCount: 4,
|
||||||
skillLevel: '2.0 至 2.5',
|
skillLevel: "2.0 至 2.5",
|
||||||
matchType: '双打',
|
matchType: "双打",
|
||||||
images: [
|
images: [
|
||||||
'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?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-1551698618-1dfe5d97d256?w=200&h=200&fit=crop&crop=center",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: "3",
|
||||||
title: '黄浦区双打约球',
|
title: "黄浦区双打约球",
|
||||||
dateTime: '7月20日(周日)下午6点 2小时',
|
dateTime: "7月20日(周日)下午6点 2小时",
|
||||||
location: '仁恒河滨花园网球场・室外',
|
location: "仁恒河滨花园网球场",
|
||||||
distance: '3.5km',
|
distance: "3.5km",
|
||||||
registeredCount: 3,
|
registeredCount: 3,
|
||||||
maxCount: 4,
|
maxCount: 4,
|
||||||
skillLevel: '2.0 至 2.5',
|
skillLevel: "2.0 至 2.5",
|
||||||
matchType: '双打',
|
matchType: "双打",
|
||||||
images: [
|
images: [
|
||||||
'https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center'
|
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取网球比赛列表
|
* 获取网球比赛列表
|
||||||
@@ -95,59 +69,18 @@ const simulateTimeout = (): boolean => {
|
|||||||
* @returns Promise<TennisMatch[]>
|
* @returns Promise<TennisMatch[]>
|
||||||
*/
|
*/
|
||||||
export const getTennisMatches = async (params?: {
|
export const getTennisMatches = async (params?: {
|
||||||
page?: number
|
page?: number;
|
||||||
pageSize?: number
|
pageSize?: number;
|
||||||
location?: string
|
location?: string;
|
||||||
skillLevel?: string
|
skillLevel?: string;
|
||||||
}): Promise<TennisMatch[]> => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
console.log('API调用: getTennisMatches', params)
|
return httpService.post('/venues/list', params, { showLoading: false })
|
||||||
|
|
||||||
// 模拟网络延迟 (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) {
|
} catch (error) {
|
||||||
console.error('API调用失败:', error)
|
console.error("列表数据获取失败:", error);
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新网球比赛数据
|
* 刷新网球比赛数据
|
||||||
@@ -155,69 +88,12 @@ export const getTennisMatches = async (params?: {
|
|||||||
*/
|
*/
|
||||||
export const refreshTennisMatches = async (): Promise<TennisMatch[]> => {
|
export const refreshTennisMatches = async (): Promise<TennisMatch[]> => {
|
||||||
try {
|
try {
|
||||||
console.log('API调用: refreshTennisMatches')
|
|
||||||
|
|
||||||
// 模拟刷新延迟 (500-1000ms)
|
|
||||||
const delayTime = 500 + Math.random() * 500
|
|
||||||
await delay(delayTime)
|
|
||||||
|
|
||||||
// 模拟网络错误
|
|
||||||
if (simulateNetworkError()) {
|
|
||||||
throw new Error('刷新失败,请稍后重试')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成新的动态数据
|
// 生成新的动态数据
|
||||||
const matches = generateDynamicData()
|
const matches = generateDynamicData();
|
||||||
|
return matches;
|
||||||
console.log('API刷新成功:', matches.length, '条数据')
|
|
||||||
return matches
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API刷新失败:', error)
|
console.error("API刷新失败:", error);
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取比赛详情
|
|
||||||
* @param id 比赛ID
|
|
||||||
* @returns Promise<TennisMatch | null>
|
|
||||||
*/
|
|
||||||
export const getTennisMatchDetail = async (id: string): Promise<TennisMatch | null> => {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export interface VerifyCodeResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 微信授权登录
|
// 微信授权登录
|
||||||
export const wechat_auth_login = async (): Promise<LoginResponse> => {
|
export const wechat_auth_login = async (phone_code?: string): Promise<LoginResponse> => {
|
||||||
try {
|
try {
|
||||||
// 先进行微信登录获取code
|
// 先进行微信登录获取code
|
||||||
const login_result = await Taro.login();
|
const login_result = await Taro.login();
|
||||||
@@ -48,15 +48,12 @@ export const wechat_auth_login = async (): Promise<LoginResponse> => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用 httpService 调用微信授权接口,传递手机号code
|
||||||
|
const auth_response = await httpService.post('user/wx_auth', {
|
||||||
// 使用 httpService 调用微信授权接口
|
code: login_result.code,
|
||||||
const auth_response = await httpService.post('/user/wx_auth', {
|
phone_code: phone_code // 传递手机号加密code
|
||||||
code: login_result.code
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (auth_response.code === 0) {
|
if (auth_response.code === 0) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -89,7 +86,7 @@ export interface PhoneLoginParams {
|
|||||||
export const phone_auth_login = async (params: PhoneLoginParams): Promise<LoginResponse> => {
|
export const phone_auth_login = async (params: PhoneLoginParams): Promise<LoginResponse> => {
|
||||||
try {
|
try {
|
||||||
// 使用 httpService 调用验证验证码接口
|
// 使用 httpService 调用验证验证码接口
|
||||||
const verify_response = await httpService.post('/user/sms/verify', {
|
const verify_response = await httpService.post('user/sms/verify', {
|
||||||
phone: params.phone,
|
phone: params.phone,
|
||||||
code: params.verification_code
|
code: params.verification_code
|
||||||
});
|
});
|
||||||
@@ -127,7 +124,7 @@ export const phone_auth_login = async (params: PhoneLoginParams): Promise<LoginR
|
|||||||
// 发送短信验证码
|
// 发送短信验证码
|
||||||
export const send_sms_code = async (phone: string): Promise<SmsResponse> => {
|
export const send_sms_code = async (phone: string): Promise<SmsResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await httpService.post('/user/sms/send', {
|
const response = await httpService.post('user/sms/send', {
|
||||||
phone: phone
|
phone: phone
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,7 +152,7 @@ export const send_sms_code = async (phone: string): Promise<SmsResponse> => {
|
|||||||
// 验证短信验证码
|
// 验证短信验证码
|
||||||
export const verify_sms_code = async (phone: string, code: string): Promise<VerifyCodeResponse> => {
|
export const verify_sms_code = async (phone: string, code: string): Promise<VerifyCodeResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await httpService.post('/user/sms/verify', {
|
const response = await httpService.post('user/sms/verify', {
|
||||||
phone: phone,
|
phone: phone,
|
||||||
code: code
|
code: code
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ export interface PublishBallData {
|
|||||||
wechat_contact?: string // 微信联系
|
wechat_contact?: string // 微信联系
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface createGameData extends PublishBallData {
|
||||||
|
status: string,
|
||||||
|
created_at: string,
|
||||||
|
updated_at: string,
|
||||||
|
}
|
||||||
|
|
||||||
// 响应接口
|
// 响应接口
|
||||||
export interface Response {
|
export interface Response {
|
||||||
code: string
|
code: string
|
||||||
@@ -37,7 +43,6 @@ export interface Response {
|
|||||||
data: any
|
data: any
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应接口
|
|
||||||
export interface StadiumListResponse {
|
export interface StadiumListResponse {
|
||||||
rows: Stadium[]
|
rows: Stadium[]
|
||||||
}
|
}
|
||||||
@@ -51,10 +56,61 @@ export interface Stadium {
|
|||||||
latitude?: number
|
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 {
|
class PublishService {
|
||||||
// 发布
|
// 用户登录
|
||||||
async createPersonal(data: PublishBallData): Promise<ApiResponse<Response>> {
|
async createPersonal(data: PublishBallData): Promise<ApiResponse<createGameData>> {
|
||||||
return httpService.post('/games/create', data, {
|
return httpService.post('/games/create', data, {
|
||||||
showLoading: true,
|
showLoading: true,
|
||||||
loadingText: '发布中...'
|
loadingText: '发布中...'
|
||||||
@@ -66,14 +122,21 @@ class PublishService {
|
|||||||
return httpService.post('/venues/list', data, {
|
return httpService.post('/venues/list', data, {
|
||||||
showLoading: false })
|
showLoading: false })
|
||||||
}
|
}
|
||||||
|
<<<<<<< HEAD
|
||||||
// 畅打发布
|
// 畅打发布
|
||||||
async create_play_pmoothly(data: PublishBallData): Promise<ApiResponse<Response>> {
|
async create_play_pmoothly(data: PublishBallData): Promise<ApiResponse<Response>> {
|
||||||
return httpService.post('/games/create_play_pmoothly', data, {
|
return httpService.post('/games/create_play_pmoothly', data, {
|
||||||
showLoading: true,
|
showLoading: true,
|
||||||
loadingText: '发布中...'
|
loadingText: '发布中...'
|
||||||
|
=======
|
||||||
|
async getPictures(req: getPicturesReq): Promise<ApiResponse<getPicturesRes>> {
|
||||||
|
return httpService.post('/gallery/sys_img_list', req, {
|
||||||
|
showLoading: false,
|
||||||
|
showToast: false,
|
||||||
|
>>>>>>> d92419f3c5648a67d1b6857d66d6c92a4bece034
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出认证服务实例
|
// 导出认证服务实例
|
||||||
export default new PublishService()
|
export default new PublishService()
|
||||||
75
src/services/uploadFiles.ts
Normal file
@@ -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()
|
||||||
BIN
src/static/asserts/fonts/PoetsenOne-Regular.ttf
Normal file
22
src/static/common/guide-bar-publish.svg
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_3006_12308)">
|
||||||
|
<g filter="url(#filter0_d_3006_12308)">
|
||||||
|
<path d="M18.0479 5.5C19.1523 5.50156 20.0464 6.39843 20.0449 7.50293L20.0342 16H28.5C29.6046 16 30.5 16.8954 30.5 18C30.5 19.1046 29.6046 20 28.5 20H20.0283L20.0176 28.5029C20.016 29.6074 19.1191 30.5014 18.0146 30.5C16.9104 30.4982 16.0161 29.6014 16.0176 28.4971L16.0283 20H7.5C6.39543 20 5.5 19.1046 5.5 18C5.5 16.8954 6.39543 16 7.5 16H16.0342L16.0449 7.49707C16.0465 6.39276 16.9435 5.49874 18.0479 5.5Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_3006_12308" x="-6.5" y="-2.5" width="49" height="49" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="4"/>
|
||||||
|
<feGaussianBlur stdDeviation="6"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3006_12308"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3006_12308" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="clip0_3006_12308">
|
||||||
|
<rect width="36" height="36" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
3
src/static/common/navigator-back.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||||
|
<path d="M20.6667 24L12.6667 16L20.6667 8" stroke="black" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 234 B |
4
src/static/detail/icon-application-add.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.035 5.83337L14.0137 22.1667" stroke="white" stroke-opacity="0.6" stroke-width="2.22222" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5.83333 14H22.1667" stroke="white" stroke-opacity="0.6" stroke-width="2.22222" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
3
src/static/detail/icon-arrow-left.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 1L1 7L7 13" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 205 B |
3
src/static/detail/icon-arrow-right.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6.33334 4L10.3333 8L6.33334 12" stroke="white" stroke-opacity="0.8" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 253 B |
7
src/static/detail/icon-comment-dark.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.75 15.8333H9.16666V12.5H15V9.16663H18.3333V15.8333H16.25L15 17.0833L13.75 15.8333Z" stroke="black" stroke-opacity="0.85" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M1.66666 2.5H15V12.5H7.08333L5.41666 14.1667L3.75 12.5H1.66666V2.5Z" stroke="black" stroke-opacity="0.85" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M7.91666 7.5H8.33333" stroke="black" stroke-opacity="0.85" stroke-width="1.66667" stroke-linecap="round"/>
|
||||||
|
<path d="M10.8333 7.5H11.25" stroke="black" stroke-opacity="0.85" stroke-width="1.66667" stroke-linecap="round"/>
|
||||||
|
<path d="M5 7.5H5.41667" stroke="black" stroke-opacity="0.85" stroke-width="1.66667" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 836 B |
7
src/static/detail/icon-comment.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11 12.6667H7.33334V10H12V7.33337H14.6667V12.6667H13L12 13.6667L11 12.6667Z" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M1.33334 2H12V10H5.66668L4.33334 11.3333L3.00001 10H1.33334V2Z" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6.33334 6H6.66668" stroke="white" stroke-width="1.33333" stroke-linecap="round"/>
|
||||||
|
<path d="M8.66666 6H8.99999" stroke="white" stroke-width="1.33333" stroke-linecap="round"/>
|
||||||
|
<path d="M4 6H4.33333" stroke="white" stroke-width="1.33333" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 706 B |