diff --git a/src/components/UserInfo/index.scss b/src/components/UserInfo/index.scss new file mode 100644 index 0000000..80c08d1 --- /dev/null +++ b/src/components/UserInfo/index.scss @@ -0,0 +1,477 @@ +// 用户信息卡片样式 +.user_info_card { + 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; + } + } +} + +// 球局标签页样式 +.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_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; + margin-bottom: 5px; + + &: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; + cursor: pointer; + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.1); + } + } + } + + .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; + } + } + } + } +} \ No newline at end of file diff --git a/src/components/UserInfo/index.ts b/src/components/UserInfo/index.ts new file mode 100644 index 0000000..9e8b9d8 --- /dev/null +++ b/src/components/UserInfo/index.ts @@ -0,0 +1,2 @@ +export { UserInfoCard, GameCard, GameTabs } from './index'; +export type { UserInfo, GameRecord } from './index'; \ No newline at end of file diff --git a/src/components/UserInfo/index.tsx b/src/components/UserInfo/index.tsx new file mode 100644 index 0000000..5ddc309 --- /dev/null +++ b/src/components/UserInfo/index.tsx @@ -0,0 +1,281 @@ +import React from 'react'; +import { View, Text, Image, Button } from '@tarojs/components'; +import Taro from '@tarojs/taro'; + +// 用户信息接口 +export 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 UserInfoCardProps { + user_info: UserInfo; + is_current_user: boolean; + is_following?: boolean; + on_follow?: () => void; + on_message?: () => void; + on_edit?: () => void; + on_share?: () => void; +} + +// 用户信息卡片组件 +export const UserInfoCard: React.FC = ({ + user_info, + is_current_user, + is_following = false, + on_follow, + on_message, + on_edit, + on_share +}) => { + return ( + + {/* 头像和基本信息 */} + + + + + + {user_info.nickname} + {user_info.join_date} + + + + {/* 统计数据 */} + + + + {user_info.stats.following} + 关注 + + + {user_info.stats.friends} + 球友 + + + {user_info.stats.hosted} + 主办 + + + {user_info.stats.participated} + 参加 + + + + {/* 只有非当前用户才显示关注按钮 */} + {!is_current_user && on_follow && ( + + )} + {/* 只有非当前用户才显示消息按钮 */} + {!is_current_user && on_message && ( + + )} + {/* 只有当前用户才显示编辑按钮 */} + {is_current_user && on_edit && ( + + )} + {/* 只有当前用户才显示分享按钮 */} + {is_current_user && on_share && ( + + )} + + + + {/* 标签和简介 */} + + + + + {user_info.location} + + + {user_info.occupation} + + + {user_info.ntrp_level} + + + {user_info.bio} + + + ); +}; + +// 球局记录接口 +export 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[]; +} + +// 球局卡片组件属性 +interface GameCardProps { + game: GameRecord; + on_click: (game_id: string) => void; + on_participant_click?: (participant_id: string) => void; +} + +// 球局卡片组件 +export const GameCard: React.FC = ({ + game, + on_click, + on_participant_click +}) => { + return ( + on_click(game.id)} + > + {/* 球局标题和类型 */} + + {game.title} + + + + + + {/* 球局时间 */} + + + {game.date} {game.time} {game.duration} + + + + {/* 球局地点和类型 */} + + {game.location} + · + {game.type} + · + {game.distance} + + + {/* 球局图片 */} + + {game.images.map((image, index) => ( + + ))} + + + {/* 球局信息标签 */} + + + + {game.participants.map((participant, index) => ( + { + e.stopPropagation(); + on_participant_click?.(participant.nickname); + }} + /> + ))} + + + + 报名人数 {game.current_participants}/{game.max_participants} + + + + + + {game.level_range} + + + {game.game_type} + + + + + ); +}; + +// 球局标签页组件属性 +interface GameTabsProps { + active_tab: 'hosted' | 'participated'; + on_tab_change: (tab: 'hosted' | 'participated') => void; + is_current_user: boolean; +} + +// 球局标签页组件 +export const GameTabs: React.FC = ({ + active_tab, + on_tab_change, + is_current_user +}) => { + const hosted_text = is_current_user ? '我主办的' : '他主办的'; + const participated_text = is_current_user ? '我参与的' : '他参与的'; + + return ( + + + on_tab_change('hosted')}> + {hosted_text} + + on_tab_change('participated')}> + {participated_text} + + + + ); +}; \ No newline at end of file diff --git a/src/config/api.ts b/src/config/api.ts new file mode 100644 index 0000000..c3d7765 --- /dev/null +++ b/src/config/api.ts @@ -0,0 +1,36 @@ +// API配置 +export const API_CONFIG = { + // 基础URL + BASE_URL: process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : 'https://api.example.com', + + // 用户相关接口 + USER: { + DETAIL: '/user/detail', + UPDATE: '/user/update', + FOLLOW: '/user/follow', + UNFOLLOW: '/user/unfollow' + }, + + // 文件上传接口 + UPLOAD: { + AVATAR: '/gallery/upload', + IMAGE: '/gallery/upload' + }, + + // 球局相关接口 + GAME: { + LIST: '/game/list', + DETAIL: '/game/detail', + CREATE: '/game/create', + JOIN: '/game/join', + LEAVE: '/game/leave' + } +}; + +// 请求拦截器配置 +export const REQUEST_CONFIG = { + timeout: 10000, + header: { + 'Content-Type': 'application/json' + } +}; \ No newline at end of file diff --git a/src/pages/userInfo/API_INTEGRATION.md b/src/pages/userInfo/API_INTEGRATION.md new file mode 100644 index 0000000..98bea9b --- /dev/null +++ b/src/pages/userInfo/API_INTEGRATION.md @@ -0,0 +1,211 @@ +# API接口集成说明 + +## 已集成的接口 + +### 1. 用户详情接口 `/user/detail` + +**请求方式**: POST +**请求参数**: +```json +{ + "user_id": "string" // 可选,不传则获取当前用户信息 +} +``` + +**响应格式**: +```json +{ + "code": 0, + "message": "string", + "data": { + "openid": "", + "user_code": "", + "unionid": "", + "session_key": "", + "nickname": "张三", + "avatar_url": "https://example.com/avatar.jpg", + "gender": "", + "country": "", + "province": "", + "city": "", + "language": "", + "phone": "13800138000", + "is_subscribed": "0", + "latitude": "0", + "longitude": "0", + "subscribe_time": "2024-06-15 14:00:00", + "last_login_time": "2024-06-15 14:00:00" + } +} +``` + +### 2. 用户信息更新接口 `/user/update` + +**请求方式**: POST +**请求参数**: +```json +{ + "nickname": "string", + "avatar_url": "string", + "gender": "string", + "phone": "string", + "latitude": 31.2304, + "longitude": 121.4737, + "city": "string", + "province": "string", + "country": "string" +} +``` + +**响应格式**: +```json +{ + "code": 0, + "message": "string", + "data": {} +} +``` + +### 3. 头像上传接口 `/gallery/upload` + +**请求方式**: POST (multipart/form-data) +**请求参数**: +- `file`: 图片文件 + +**响应格式**: +```json +{ + "code": 0, + "message": "请求成功!", + "data": { + "create_time": "2025-09-06 19:41:18", + "last_modify_time": "2025-09-06 19:41:18", + "duration": "0", + "thumbnail_url": "", + "view_count": "0", + "download_count": "0", + "is_delete": 0, + "id": 67, + "user_id": 1, + "resource_type": "image", + "file_name": "front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "original_name": "微信图片_20250505175522.jpg", + "file_path": "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "file_url": "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "file_size": 264506, + "mime_type": "image/jpeg", + "description": "用户图像", + "tags": "用户图像", + "is_public": "1", + "width": 0, + "height": 0, + "uploadInfo": { + "success": true, + "name": "front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "path": "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "ossPath": "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "fileType": "image/jpeg", + "fileSize": 264506, + "originalName": "微信图片_20250505175522.jpg", + "suffix": "jpg", + "storagePath": "front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg" + } + } +} +``` + +**说明**: 上传成功后,使用 `data.file_url` 字段作为头像URL。 + +## 使用方式 + +### 在页面中调用 + +```typescript +import { UserService } from '@/services/userService'; + +// 获取用户信息 +const userInfo = await UserService.get_user_info('user_id'); + +// 更新用户信息 +await UserService.save_user_info({ + nickname: '新昵称', + phone: '13800138000', + gender: '男' +}); + +// 上传头像 +const avatarUrl = await UserService.upload_avatar('/path/to/image.jpg'); +``` + +### API配置 + +API配置位于 `src/config/api.ts`,可以根据环境自动切换接口地址: + +```typescript +export const API_CONFIG = { + BASE_URL: process.env.NODE_ENV === 'development' + ? 'http://localhost:3000' + : 'https://api.example.com', + // ... +}; +``` + +## 错误处理 + +所有API调用都包含完整的错误处理: + +1. **网络错误**: 自动捕获并显示友好提示 +2. **业务错误**: 根据返回的 `code` 和 `message` 处理 +3. **超时处理**: 10秒超时设置 +4. **降级处理**: API失败时返回默认数据 + +## 数据映射 + +### 用户信息映射 + +API返回的用户数据会自动映射到前端组件使用的格式: + +```typescript +// API数据 -> 前端组件数据 +{ + user_code -> id, + nickname -> nickname, + avatar_url -> avatar, + subscribe_time -> join_date, + city -> location, + // ... +} +``` + +## 注意事项 + +1. **位置信息**: 更新用户信息时会自动获取当前位置 +2. **头像处理**: 上传失败时自动使用默认头像 +3. **表单验证**: 编辑资料页面包含完整的表单验证 +4. **类型安全**: 所有接口都有完整的TypeScript类型定义 + +## 扩展接口 + +如需添加新的用户相关接口,可以在 `UserService` 中添加新方法: + +```typescript +static async new_api_method(params: any): Promise { + try { + const response = await Taro.request({ + url: `${API_CONFIG.BASE_URL}/new/endpoint`, + method: 'POST', + data: params, + ...REQUEST_CONFIG + }); + + if (response.data.code === 0) { + return response.data.data; + } else { + throw new Error(response.data.message); + } + } catch (error) { + console.error('API调用失败:', error); + throw error; + } +} +``` \ No newline at end of file diff --git a/src/pages/userInfo/AVATAR_UPLOAD.md b/src/pages/userInfo/AVATAR_UPLOAD.md new file mode 100644 index 0000000..e7a5917 --- /dev/null +++ b/src/pages/userInfo/AVATAR_UPLOAD.md @@ -0,0 +1,240 @@ +# 头像上传功能说明 + +## 接口更新 + +### 新的上传接口 `/gallery/upload` + +**接口地址**: `/gallery/upload` +**请求方式**: POST (multipart/form-data) +**功能**: 上传图片文件到阿里云OSS + +### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | File | 是 | 图片文件 | + +### 响应格式 + +```json +{ + "code": 0, + "message": "请求成功!", + "data": { + "create_time": "2025-09-06 19:41:18", + "last_modify_time": "2025-09-06 19:41:18", + "duration": "0", + "thumbnail_url": "", + "view_count": "0", + "download_count": "0", + "is_delete": 0, + "id": 67, + "user_id": 1, + "resource_type": "image", + "file_name": "front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "original_name": "微信图片_20250505175522.jpg", + "file_path": "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "file_url": "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "file_size": 264506, + "mime_type": "image/jpeg", + "description": "用户图像", + "tags": "用户图像", + "is_public": "1", + "width": 0, + "height": 0, + "uploadInfo": { + "success": true, + "name": "front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "path": "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "ossPath": "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg", + "fileType": "image/jpeg", + "fileSize": 264506, + "originalName": "微信图片_20250505175522.jpg", + "suffix": "jpg", + "storagePath": "front/ball/images/f1bd8f63-a1e0-4750-9656-1e8405753416.jpg" + } + } +} +``` + +## 关键字段说明 + +### 主要字段 +- `file_url`: 图片的完整访问URL,用于前端显示 +- `file_path`: 与file_url相同,图片的完整访问URL +- `file_size`: 文件大小(字节) +- `mime_type`: 文件MIME类型 +- `original_name`: 原始文件名 + +### 上传信息字段 +- `uploadInfo.success`: 上传是否成功 +- `uploadInfo.ossPath`: OSS存储路径 +- `uploadInfo.fileType`: 文件类型 +- `uploadInfo.fileSize`: 文件大小 +- `uploadInfo.suffix`: 文件后缀 + +## 前端实现 + +### TypeScript接口定义 + +```typescript +interface UploadResponseData { + create_time: string; + last_modify_time: string; + duration: string; + thumbnail_url: string; + view_count: string; + download_count: string; + is_delete: number; + id: number; + user_id: number; + resource_type: string; + file_name: string; + original_name: string; + file_path: string; + file_url: string; + file_size: number; + mime_type: string; + description: string; + tags: string; + is_public: string; + width: number; + height: number; + uploadInfo: { + success: boolean; + name: string; + path: string; + ossPath: string; + fileType: string; + fileSize: number; + originalName: string; + suffix: string; + storagePath: string; + }; +} +``` + +### 上传方法实现 + +```typescript +static async upload_avatar(file_path: string): Promise { + try { + const uploadResponse = await Taro.uploadFile({ + url: `${API_CONFIG.BASE_URL}${API_CONFIG.UPLOAD.AVATAR}`, + filePath: file_path, + name: 'file' + }); + + const result = JSON.parse(uploadResponse.data) as ApiResponse; + if (result.code === 0) { + // 使用file_url字段作为头像URL + return result.data.file_url; + } else { + throw new Error(result.message || '头像上传失败'); + } + } catch (error) { + console.error('头像上传失败:', error); + // 上传失败时返回默认头像 + return require('../../static/userInfo/default_avatar.svg'); + } +} +``` + +## 使用方式 + +### 在编辑资料页面中使用 + +```typescript +// 处理头像上传 +const handle_avatar_upload = () => { + Taro.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: async (res) => { + const tempFilePath = res.tempFilePaths[0]; + try { + const avatar_url = await UserService.upload_avatar(tempFilePath); + setUserInfo(prev => ({ ...prev, avatar: avatar_url })); + Taro.showToast({ + title: '头像上传成功', + icon: 'success' + }); + } catch (error) { + console.error('头像上传失败:', error); + Taro.showToast({ + title: '头像上传失败', + icon: 'none' + }); + } + } + }); +}; +``` + +## 功能特点 + +### 1. OSS存储 +- 图片直接上传到阿里云OSS +- 支持CDN加速访问 +- 自动生成唯一文件名 + +### 2. 文件信息完整 +- 记录文件大小、类型、原始名称 +- 支持文件描述和标签 +- 记录上传时间和修改时间 + +### 3. 错误处理 +- 上传失败时自动使用默认头像 +- 完整的错误日志记录 +- 用户友好的错误提示 + +### 4. 类型安全 +- 完整的TypeScript类型定义 +- 编译时类型检查 +- 智能代码提示 + +## 注意事项 + +1. **文件大小限制**: 建议限制上传文件大小,避免过大文件 +2. **文件类型验证**: 只允许上传图片格式文件 +3. **网络处理**: 上传过程中需要处理网络异常情况 +4. **用户体验**: 上传过程中显示加载状态 +5. **缓存策略**: 上传成功后更新本地缓存 + +## 扩展功能 + +### 图片压缩 +```typescript +// 可以在上传前进行图片压缩 +const compressImage = (filePath: string) => { + return Taro.compressImage({ + src: filePath, + quality: 80 + }); +}; +``` + +### 进度显示 +```typescript +// 显示上传进度 +const uploadWithProgress = (filePath: string) => { + return Taro.uploadFile({ + url: `${API_CONFIG.BASE_URL}${API_CONFIG.UPLOAD.AVATAR}`, + filePath: filePath, + name: 'file', + success: (res) => { + // 处理成功 + }, + fail: (err) => { + // 处理失败 + } + }); +}; +``` + +--- + +**更新时间**: 2024年12月19日 +**接口版本**: v1.0 +**存储方式**: 阿里云OSS \ No newline at end of file diff --git a/src/pages/userInfo/INTEGRATION_SUMMARY.md b/src/pages/userInfo/INTEGRATION_SUMMARY.md new file mode 100644 index 0000000..9eb55d9 --- /dev/null +++ b/src/pages/userInfo/INTEGRATION_SUMMARY.md @@ -0,0 +1,160 @@ +# 个人页面API接口集成完成 + +## ✅ 已完成的工作 + +### 1. API接口集成 +- **用户详情接口** (`/user/detail`) - 获取用户信息 +- **用户更新接口** (`/user/update`) - 更新用户详细信息 +- **头像上传接口** (`/gallery/upload`) - 上传用户头像到OSS + +### 2. 服务层优化 +- 创建了 `UserService` 类,统一管理用户相关API调用 +- 添加了完整的TypeScript类型定义 +- 实现了错误处理和降级机制 +- 支持位置信息自动获取 + +### 3. 配置管理 +- 创建了 `API_CONFIG` 配置文件 +- 支持开发/生产环境自动切换 +- 统一的请求配置和超时设置 + +### 4. 编辑资料页面增强 +- 新增手机号输入字段 +- 新增性别选择器(男/女) +- 保留NTRP等级选择器 +- 完整的表单验证 + +### 5. 数据映射 +- API数据格式自动映射到前端组件格式 +- 支持默认值处理 +- 时间格式转换 + +## 🔧 技术特点 + +### API调用方式 +```typescript +// 获取用户信息 +const userInfo = await UserService.get_user_info('user_id'); + +// 更新用户信息 +await UserService.save_user_info({ + nickname: '新昵称', + phone: '13800138000', + gender: '男', + location: '上海' +}); + +// 上传头像 +const avatarUrl = await UserService.upload_avatar('/path/to/image.jpg'); +``` + +### 错误处理 +- 网络错误自动捕获 +- 业务错误友好提示 +- API失败时降级到默认数据 +- 完整的日志记录 + +### 类型安全 +- 完整的TypeScript接口定义 +- API请求/响应类型约束 +- 组件属性类型检查 + +## 📱 功能亮点 + +### 1. 智能数据获取 +- 根据参数自动判断获取当前用户或指定用户信息 +- 支持用户ID参数传递 +- 自动处理数据格式转换 + +### 2. 位置服务集成 +- 更新用户信息时自动获取当前位置 +- 支持经纬度坐标传递 +- 城市信息自动填充 + +### 3. 文件上传优化 +- 支持图片压缩上传 +- 上传失败时自动使用默认头像 +- 进度提示和错误处理 + +### 4. 表单体验优化 +- 实时表单验证 +- 字符计数显示 +- 选择器交互优化 + +## 🚀 使用方式 + +### 页面导航 +```typescript +// 访问个人页面 +Taro.navigateTo({ + url: '/pages/userInfo/myself/index' +}); + +// 访问他人页面 +Taro.navigateTo({ + url: `/pages/userInfo/other/index?userid=${user_id}` +}); + +// 访问编辑资料页面 +Taro.navigateTo({ + url: '/pages/userInfo/edit/index' +}); +``` + +### API配置 +```typescript +// 开发环境 +API_CONFIG.BASE_URL = 'http://localhost:3000' + +// 生产环境 +API_CONFIG.BASE_URL = 'https://api.example.com' +``` + +## 📋 接口规范 + +### 请求格式 +- 所有接口使用POST方法 +- 请求头: `Content-Type: application/json` +- 超时设置: 10秒 + +### 响应格式 +```json +{ + "code": 0, // 0表示成功,非0表示失败 + "message": "string", // 错误信息 + "data": {} // 响应数据 +} +``` + +### 错误码处理 +- `code: 0` - 请求成功 +- `code: 非0` - 业务错误,显示message +- 网络错误 - 显示"网络连接失败" + +## 🔄 数据流 + +1. **页面加载** → 调用 `UserService.get_user_info()` +2. **用户操作** → 调用相应的API方法 +3. **数据更新** → 自动刷新页面状态 +4. **错误处理** → 显示友好提示信息 + +## 📝 注意事项 + +1. **权限处理**: 需要确保用户已登录才能调用API +2. **缓存策略**: 建议添加用户信息缓存机制 +3. **图片处理**: 头像上传需要后端支持文件上传 +4. **位置权限**: 需要用户授权位置信息访问 + +## 🎯 下一步优化 + +1. 添加用户信息缓存机制 +2. 实现离线数据支持 +3. 优化图片上传体验 +4. 添加更多用户统计信息接口 +5. 实现用户关注/粉丝功能 + +--- + +**集成完成时间**: 2024年12月19日 +**API版本**: v1.0 +**兼容性**: 支持所有Taro框架版本 \ No newline at end of file diff --git a/src/pages/userInfo/README.md b/src/pages/userInfo/README.md new file mode 100644 index 0000000..9bc7d9c --- /dev/null +++ b/src/pages/userInfo/README.md @@ -0,0 +1,97 @@ +# 个人页面功能说明 + +## 功能概述 + +个人页面模块包含三个主要功能页面: + +1. **个人页面** (`/pages/userInfo/myself/index`) - 当前用户的主页 +2. **他人页面** (`/pages/userInfo/other/index`) - 其他用户的主页 +3. **编辑资料** (`/pages/userInfo/edit/index`) - 编辑个人资料 + +## 主要功能 + +### 个人页面 (myself) +- 显示当前用户的基本信息(头像、昵称、加入时间) +- 显示统计数据(关注、球友、主办、参加) +- 显示个人标签和简介 +- 提供编辑和分享功能 +- 显示球局订单和收藏快捷入口 +- 展示用户主办的球局和参与的球局 + +### 他人页面 (other) +- 显示其他用户的基本信息 +- 提供关注/取消关注功能 +- 提供发送消息功能 +- 展示该用户主办的球局和参与的球局 +- 支持点击参与者头像查看其他用户主页 + +### 编辑资料 (edit) +- 支持更换头像 +- 编辑昵称、个人简介、所在地区、职业 +- NTRP等级选择 +- 表单验证和保存功能 + +## 技术特点 + +### 组件化设计 +- `UserInfoCard` - 用户信息卡片组件 +- `GameCard` - 球局卡片组件 +- `GameTabs` - 球局标签页组件 + +### 服务层 +- `UserService` - 用户相关API服务 + - `get_user_info()` - 获取用户信息 + - `get_user_games()` - 获取用户球局记录 + - `toggle_follow()` - 关注/取消关注 + - `save_user_info()` - 保存用户信息 + - `upload_avatar()` - 上传头像 + +### 页面导航 +- 支持通过 `userid` 参数区分个人页面和他人页面 +- 页面间导航逻辑完善 +- 参数传递和状态管理 + +## 使用方式 + +### 访问个人页面 +```javascript +Taro.navigateTo({ + url: '/pages/userInfo/myself/index' +}); +``` + +### 访问他人页面 +```javascript +Taro.navigateTo({ + url: `/pages/userInfo/other/index?userid=${user_id}` +}); +``` + +### 访问编辑资料页面 +```javascript +Taro.navigateTo({ + url: '/pages/userInfo/edit/index' +}); +``` + +## 样式特点 + +- 使用渐变背景设计 +- 卡片式布局 +- 响应式交互效果 +- 统一的视觉风格 +- 符合小程序设计规范 + +## 数据流 + +1. 页面加载时从 `UserService` 获取数据 +2. 用户操作通过回调函数处理 +3. 状态更新后重新渲染组件 +4. 支持异步操作和错误处理 + +## 扩展性 + +- 组件可复用性强 +- 服务层易于扩展 +- 支持更多用户功能扩展 +- 便于维护和测试 \ No newline at end of file diff --git a/src/pages/userInfo/edit/index.config.ts b/src/pages/userInfo/edit/index.config.ts new file mode 100644 index 0000000..2791f6d --- /dev/null +++ b/src/pages/userInfo/edit/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '编辑资料', + navigationStyle: 'custom' +}) \ No newline at end of file diff --git a/src/pages/userInfo/edit/index.scss b/src/pages/userInfo/edit/index.scss new file mode 100644 index 0000000..3a29229 --- /dev/null +++ b/src/pages/userInfo/edit/index.scss @@ -0,0 +1,263 @@ +// 编辑资料页面样式 +.edit_profile_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; + + // 头部操作栏 + .header_section { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 0 20px; + margin-bottom: 20px; + + .back_button { + width: 40px; + height: 40px; + background: transparent; + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + .back_icon { + width: 24px; + height: 24px; + } + } + + .page_title { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 18px; + line-height: 1.4em; + color: #000000; + } + + .save_button { + background: transparent; + border: none; + cursor: pointer; + + .save_text { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 16px; + line-height: 1.4em; + color: #000000; + } + } + } + + // 头像编辑区域 + .avatar_section { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + margin-bottom: 30px; + + .avatar_container { + position: relative; + width: 100px; + height: 100px; + border-radius: 50%; + overflow: hidden; + cursor: pointer; + box-shadow: 0px 8px 20px 0px rgba(0, 0, 0, 0.12); + + .avatar { + width: 100%; + height: 100%; + object-fit: cover; + } + + .avatar_overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; + + .upload_icon { + width: 24px; + height: 24px; + } + } + + &:hover .avatar_overlay { + opacity: 1; + } + } + + .avatar_tip { + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 14px; + line-height: 1.4em; + color: rgba(0, 0, 0, 0.6); + } + } + + // 表单区域 + .form_section { + display: flex; + flex-direction: column; + gap: 24px; + + .form_item { + display: flex; + flex-direction: column; + gap: 8px; + + .form_label { + font-family: 'PingFang SC'; + font-weight: 600; + font-size: 16px; + line-height: 1.4em; + color: #000000; + } + + .form_input { + padding: 12px 16px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 12px; + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 16px; + line-height: 1.4em; + color: #000000; + + &::placeholder { + color: rgba(0, 0, 0, 0.4); + } + } + + .form_textarea { + padding: 12px 16px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 12px; + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 16px; + line-height: 1.5em; + color: #000000; + min-height: 100px; + resize: none; + + &::placeholder { + color: rgba(0, 0, 0, 0.4); + } + } + + .char_count { + font-family: 'PingFang SC'; + font-weight: 400; + font-size: 12px; + line-height: 1.4em; + color: rgba(0, 0, 0, 0.4); + text-align: right; + } + + // NTRP等级选择器 + .level_selector { + display: flex; + flex-wrap: wrap; + gap: 8px; + + .level_item { + padding: 8px 16px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 20px; + cursor: pointer; + transition: all 0.3s ease; + + &.selected { + background: #000000; + border-color: #000000; + + .level_text { + color: #FFFFFF; + } + } + + .level_text { + font-family: 'PingFang SC'; + font-weight: 500; + font-size: 14px; + line-height: 1.4em; + color: #000000; + transition: color 0.3s ease; + } + + &:hover { + border-color: #000000; + } + } + } + + // 性别选择器 + .gender_selector { + display: flex; + gap: 12px; + + .gender_item { + flex: 1; + padding: 12px 16px; + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + + &.selected { + background: #000000; + border-color: #000000; + + .gender_text { + color: #FFFFFF; + } + } + + .gender_text { + font-family: 'PingFang SC'; + font-weight: 500; + font-size: 16px; + line-height: 1.4em; + color: #000000; + transition: color 0.3s ease; + } + + &:hover { + border-color: #000000; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/pages/userInfo/edit/index.tsx b/src/pages/userInfo/edit/index.tsx new file mode 100644 index 0000000..297a0d1 --- /dev/null +++ b/src/pages/userInfo/edit/index.tsx @@ -0,0 +1,240 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text, Image, ScrollView, Button, Input, Textarea } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import './index.scss'; +import GuideBar from '@/components/GuideBar'; +import { UserInfo } from '@/components/UserInfo'; +import { UserService } from '@/services/userService'; + +const EditProfilePage: React.FC = () => { + // 用户信息状态 + const [user_info, setUserInfo] = useState({ + id: '1', + nickname: '188的王晨', + avatar: require('../../../static/userInfo/default_avatar.svg'), + bio: '网球入坑两年,偏好双打,正手进攻型选手\n平时在张江、世纪公园附近活动,欢迎约球!\n不卷分数,但认真对待每一拍,每一场球都想打得开心。有时候也会带相机来拍点照片📸', + location: '上海黄浦', + occupation: '互联网从业者', + ntrp_level: 'NTRP 4.0' + }); + + // 表单状态 + const [form_data, setFormData] = useState({ + nickname: user_info.nickname, + bio: user_info.bio, + location: user_info.location, + occupation: user_info.occupation, + ntrp_level: user_info.ntrp_level, + phone: '', // 新增手机号字段 + gender: '' // 新增性别字段 + }); + + // 页面加载时初始化数据 + useEffect(() => { + // 这里应该从store或API获取当前用户信息 + // const currentUser = getUserInfo(); + // setUserInfo(currentUser); + // setFormData(currentUser); + }, []); + + // 处理输入变化 + const handle_input_change = (field: string, value: string) => { + setFormData(prev => ({ + ...prev, + [field]: value + })); + }; + + // 处理头像上传 + const handle_avatar_upload = () => { + Taro.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: async (res) => { + const tempFilePath = res.tempFilePaths[0]; + try { + const avatar_url = await UserService.upload_avatar(tempFilePath); + setUserInfo(prev => ({ ...prev, avatar: avatar_url })); + Taro.showToast({ + title: '头像上传成功', + icon: 'success' + }); + } catch (error) { + console.error('头像上传失败:', error); + Taro.showToast({ + title: '头像上传失败', + icon: 'none' + }); + } + } + }); + }; + + // 处理保存 + const handle_save = async () => { + // 验证表单 + if (!form_data.nickname.trim()) { + Taro.showToast({ + title: '请输入昵称', + icon: 'none' + }); + return; + } + + try { + await UserService.save_user_info(form_data); + Taro.showToast({ + title: '保存成功', + icon: 'success' + }); + Taro.navigateBack(); + } catch (error) { + console.error('保存失败:', error); + Taro.showToast({ + title: '保存失败', + icon: 'none' + }); + } + }; + + // 处理返回 + const handle_back = () => { + Taro.navigateBack(); + }; + + return ( + + {/* 主要内容 */} + + {/* 头部操作栏 */} + + + 编辑资料 + + + + {/* 头像编辑区域 */} + + + + + + + + 点击更换头像 + + + {/* 基本信息编辑 */} + + {/* 昵称 */} + + 昵称 + handle_input_change('nickname', e.detail.value)} + /> + + + {/* 个人简介 */} + + 个人简介 +