11
This commit is contained in:
477
src/components/UserInfo/index.scss
Normal file
477
src/components/UserInfo/index.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/components/UserInfo/index.ts
Normal file
2
src/components/UserInfo/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { UserInfoCard, GameCard, GameTabs } from './index';
|
||||||
|
export type { UserInfo, GameRecord } from './index';
|
||||||
281
src/components/UserInfo/index.tsx
Normal file
281
src/components/UserInfo/index.tsx
Normal file
@@ -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<UserInfoCardProps> = ({
|
||||||
|
user_info,
|
||||||
|
is_current_user,
|
||||||
|
is_following = false,
|
||||||
|
on_follow,
|
||||||
|
on_message,
|
||||||
|
on_edit,
|
||||||
|
on_share
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<View className="user_info_card">
|
||||||
|
{/* 头像和基本信息 */}
|
||||||
|
<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 && on_follow && (
|
||||||
|
<Button
|
||||||
|
className={`follow_button ${is_following ? 'following' : ''}`}
|
||||||
|
onClick={on_follow}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="button_icon"
|
||||||
|
src={require('../../../static/userInfo/plus.svg')}
|
||||||
|
/>
|
||||||
|
<Text className="button_text">
|
||||||
|
{is_following ? '已关注' : '关注'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* 只有非当前用户才显示消息按钮 */}
|
||||||
|
{!is_current_user && on_message && (
|
||||||
|
<Button className="message_button" onClick={on_message}>
|
||||||
|
<Image
|
||||||
|
className="button_icon"
|
||||||
|
src={require('../../../static/userInfo/message.svg')}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* 只有当前用户才显示编辑按钮 */}
|
||||||
|
{is_current_user && on_edit && (
|
||||||
|
<Button className="edit_button" onClick={on_edit}>
|
||||||
|
<Text className="button_text">编辑</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* 只有当前用户才显示分享按钮 */}
|
||||||
|
{is_current_user && on_share && (
|
||||||
|
<Button className="share_button" onClick={on_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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 球局记录接口
|
||||||
|
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<GameCardProps> = ({
|
||||||
|
game,
|
||||||
|
on_click,
|
||||||
|
on_participant_click
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
className="game_card"
|
||||||
|
onClick={() => on_click(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}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
on_participant_click?.(participant.nickname);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 球局标签页组件属性
|
||||||
|
interface GameTabsProps {
|
||||||
|
active_tab: 'hosted' | 'participated';
|
||||||
|
on_tab_change: (tab: 'hosted' | 'participated') => void;
|
||||||
|
is_current_user: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 球局标签页组件
|
||||||
|
export const GameTabs: React.FC<GameTabsProps> = ({
|
||||||
|
active_tab,
|
||||||
|
on_tab_change,
|
||||||
|
is_current_user
|
||||||
|
}) => {
|
||||||
|
const hosted_text = is_current_user ? '我主办的' : '他主办的';
|
||||||
|
const participated_text = is_current_user ? '我参与的' : '他参与的';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="game_tabs_section">
|
||||||
|
<View className="tab_container">
|
||||||
|
<View className={`tab_item ${active_tab === 'hosted' ? 'active' : ''}`} onClick={() => on_tab_change('hosted')}>
|
||||||
|
<Text className="tab_text">{hosted_text}</Text>
|
||||||
|
</View>
|
||||||
|
<View className={`tab_item ${active_tab === 'participated' ? 'active' : ''}`} onClick={() => on_tab_change('participated')}>
|
||||||
|
<Text className="tab_text">{participated_text}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
36
src/config/api.ts
Normal file
36
src/config/api.ts
Normal file
@@ -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'
|
||||||
|
}
|
||||||
|
};
|
||||||
211
src/pages/userInfo/API_INTEGRATION.md
Normal file
211
src/pages/userInfo/API_INTEGRATION.md
Normal file
@@ -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<any> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
240
src/pages/userInfo/AVATAR_UPLOAD.md
Normal file
240
src/pages/userInfo/AVATAR_UPLOAD.md
Normal file
@@ -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<string> {
|
||||||
|
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<UploadResponseData>;
|
||||||
|
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
|
||||||
160
src/pages/userInfo/INTEGRATION_SUMMARY.md
Normal file
160
src/pages/userInfo/INTEGRATION_SUMMARY.md
Normal file
@@ -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框架版本
|
||||||
97
src/pages/userInfo/README.md
Normal file
97
src/pages/userInfo/README.md
Normal file
@@ -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. 支持异步操作和错误处理
|
||||||
|
|
||||||
|
## 扩展性
|
||||||
|
|
||||||
|
- 组件可复用性强
|
||||||
|
- 服务层易于扩展
|
||||||
|
- 支持更多用户功能扩展
|
||||||
|
- 便于维护和测试
|
||||||
4
src/pages/userInfo/edit/index.config.ts
Normal file
4
src/pages/userInfo/edit/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '编辑资料',
|
||||||
|
navigationStyle: 'custom'
|
||||||
|
})
|
||||||
263
src/pages/userInfo/edit/index.scss
Normal file
263
src/pages/userInfo/edit/index.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
240
src/pages/userInfo/edit/index.tsx
Normal file
240
src/pages/userInfo/edit/index.tsx
Normal file
@@ -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<UserInfo>({
|
||||||
|
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 (
|
||||||
|
<View className="edit_profile_page">
|
||||||
|
{/* 主要内容 */}
|
||||||
|
<ScrollView className="main_content" scrollY>
|
||||||
|
{/* 头部操作栏 */}
|
||||||
|
<View className="header_section">
|
||||||
|
<Button className="back_button" onClick={handle_back}>
|
||||||
|
<Image
|
||||||
|
className="back_icon"
|
||||||
|
src={require('../../../static/detail/icon-arrow-left.svg')}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Text className="page_title">编辑资料</Text>
|
||||||
|
<Button className="save_button" onClick={handle_save}>
|
||||||
|
<Text className="save_text">保存</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 头像编辑区域 */}
|
||||||
|
<View className="avatar_section">
|
||||||
|
<View className="avatar_container" onClick={handle_avatar_upload}>
|
||||||
|
<Image className="avatar" src={user_info.avatar} />
|
||||||
|
<View className="avatar_overlay">
|
||||||
|
<Image
|
||||||
|
className="upload_icon"
|
||||||
|
src={require('../../../static/publishBall/icon-upload.svg')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text className="avatar_tip">点击更换头像</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 基本信息编辑 */}
|
||||||
|
<View className="form_section">
|
||||||
|
{/* 昵称 */}
|
||||||
|
<View className="form_item">
|
||||||
|
<Text className="form_label">昵称</Text>
|
||||||
|
<Input
|
||||||
|
className="form_input"
|
||||||
|
value={form_data.nickname}
|
||||||
|
placeholder="请输入昵称"
|
||||||
|
onInput={(e) => handle_input_change('nickname', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 个人简介 */}
|
||||||
|
<View className="form_item">
|
||||||
|
<Text className="form_label">个人简介</Text>
|
||||||
|
<Textarea
|
||||||
|
className="form_textarea"
|
||||||
|
value={form_data.bio}
|
||||||
|
placeholder="介绍一下自己吧..."
|
||||||
|
maxlength={200}
|
||||||
|
onInput={(e) => handle_input_change('bio', e.detail.value)}
|
||||||
|
/>
|
||||||
|
<Text className="char_count">{form_data.bio.length}/200</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 所在地区 */}
|
||||||
|
<View className="form_item">
|
||||||
|
<Text className="form_label">所在地区</Text>
|
||||||
|
<Input
|
||||||
|
className="form_input"
|
||||||
|
value={form_data.location}
|
||||||
|
placeholder="请输入所在地区"
|
||||||
|
onInput={(e) => handle_input_change('location', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 职业 */}
|
||||||
|
<View className="form_item">
|
||||||
|
<Text className="form_label">职业</Text>
|
||||||
|
<Input
|
||||||
|
className="form_input"
|
||||||
|
value={form_data.occupation}
|
||||||
|
placeholder="请输入职业"
|
||||||
|
onInput={(e) => handle_input_change('occupation', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 手机号 */}
|
||||||
|
<View className="form_item">
|
||||||
|
<Text className="form_label">手机号</Text>
|
||||||
|
<Input
|
||||||
|
className="form_input"
|
||||||
|
value={form_data.phone}
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
type="number"
|
||||||
|
maxlength={11}
|
||||||
|
onInput={(e) => handle_input_change('phone', e.detail.value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* NTRP等级 */}
|
||||||
|
<View className="form_item">
|
||||||
|
<Text className="form_label">NTRP等级</Text>
|
||||||
|
<View className="level_selector">
|
||||||
|
{['1.0', '1.5', '2.0', '2.5', '3.0', '3.5', '4.0', '4.5', '5.0'].map((level) => (
|
||||||
|
<View
|
||||||
|
key={level}
|
||||||
|
className={`level_item ${form_data.ntrp_level.includes(level) ? 'selected' : ''}`}
|
||||||
|
onClick={() => handle_input_change('ntrp_level', `NTRP ${level}`)}
|
||||||
|
>
|
||||||
|
<Text className="level_text">{level}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 性别 */}
|
||||||
|
<View className="form_item">
|
||||||
|
<Text className="form_label">性别</Text>
|
||||||
|
<View className="gender_selector">
|
||||||
|
<View
|
||||||
|
className={`gender_item ${form_data.gender === '男' ? 'selected' : ''}`}
|
||||||
|
onClick={() => handle_input_change('gender', '男')}
|
||||||
|
>
|
||||||
|
<Text className="gender_text">男</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className={`gender_item ${form_data.gender === '女' ? 'selected' : ''}`}
|
||||||
|
onClick={() => handle_input_change('gender', '女')}
|
||||||
|
>
|
||||||
|
<Text className="gender_text">女</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
<GuideBar currentPage='personal' />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditProfilePage;
|
||||||
4
src/pages/userInfo/other/index.config.ts
Normal file
4
src/pages/userInfo/other/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '用户主页',
|
||||||
|
navigationStyle: 'custom'
|
||||||
|
})
|
||||||
490
src/pages/userInfo/other/index.scss
Normal file
490
src/pages/userInfo/other/index.scss
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
// 他人用户页面样式
|
||||||
|
.other_user_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签和简介
|
||||||
|
.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_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/pages/userInfo/other/index.tsx
Normal file
146
src/pages/userInfo/other/index.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { View, Text, ScrollView } from '@tarojs/components';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import './index.scss';
|
||||||
|
import GuideBar from '@/components/GuideBar';
|
||||||
|
import { UserInfoCard, GameCard, GameTabs, UserInfo, GameRecord } from '@/components/UserInfo';
|
||||||
|
import { UserService } from '@/services/userService';
|
||||||
|
|
||||||
|
const OtherUserPage: React.FC = () => {
|
||||||
|
// 获取页面参数
|
||||||
|
const instance = Taro.getCurrentInstance();
|
||||||
|
const user_id = instance.router?.params?.userid;
|
||||||
|
|
||||||
|
// 模拟用户数据
|
||||||
|
const [user_info, setUserInfo] = useState<UserInfo>({
|
||||||
|
id: user_id || '1',
|
||||||
|
nickname: '网球爱好者',
|
||||||
|
avatar: require('../../../static/userInfo/default_avatar.svg'),
|
||||||
|
join_date: '2024年3月加入',
|
||||||
|
stats: {
|
||||||
|
following: 89,
|
||||||
|
friends: 15,
|
||||||
|
hosted: 12,
|
||||||
|
participated: 35
|
||||||
|
},
|
||||||
|
tags: ['北京朝阳', '金融从业者', 'NTRP 3.5'],
|
||||||
|
bio: '热爱网球的金融从业者,周末喜欢约球\n技术还在提升中,欢迎一起切磋\n平时在朝阳公园附近活动',
|
||||||
|
location: '北京朝阳',
|
||||||
|
occupation: '金融从业者',
|
||||||
|
ntrp_level: 'NTRP 3.5'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟球局数据
|
||||||
|
const [game_records, setGameRecords] = useState<GameRecord[]>([]);
|
||||||
|
|
||||||
|
// 关注状态
|
||||||
|
const [is_following, setIsFollowing] = useState(false);
|
||||||
|
|
||||||
|
// 当前激活的标签页
|
||||||
|
const [active_tab, setActiveTab] = useState<'hosted' | 'participated'>('hosted');
|
||||||
|
|
||||||
|
// 页面加载时获取用户信息
|
||||||
|
useEffect(() => {
|
||||||
|
const load_user_data = async () => {
|
||||||
|
if (user_id) {
|
||||||
|
try {
|
||||||
|
const user_data = await UserService.get_user_info(user_id);
|
||||||
|
setUserInfo(user_data);
|
||||||
|
|
||||||
|
const games_data = await UserService.get_user_games(user_id, active_tab);
|
||||||
|
setGameRecords(games_data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户数据失败:', error);
|
||||||
|
Taro.showToast({
|
||||||
|
title: '加载失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
load_user_data();
|
||||||
|
}, [user_id, active_tab]);
|
||||||
|
|
||||||
|
// 处理关注/取消关注
|
||||||
|
const handle_follow = async () => {
|
||||||
|
try {
|
||||||
|
const new_follow_status = await UserService.toggle_follow(user_info.id, is_following);
|
||||||
|
setIsFollowing(new_follow_status);
|
||||||
|
Taro.showToast({
|
||||||
|
title: new_follow_status ? '关注成功' : '已取消关注',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('关注操作失败:', error);
|
||||||
|
Taro.showToast({
|
||||||
|
title: '操作失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理发送消息
|
||||||
|
const handle_send_message = () => {
|
||||||
|
Taro.navigateTo({
|
||||||
|
url: `/pages/message/chat/index?user_id=${user_info.id}&nickname=${user_info.nickname}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 处理球局详情
|
||||||
|
const handle_game_detail = (game_id: string) => {
|
||||||
|
Taro.navigateTo({
|
||||||
|
url: `/pages/detail/index?id=${game_id}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="other_user_page">
|
||||||
|
{/* 主要内容 */}
|
||||||
|
<ScrollView className="main_content" scrollY>
|
||||||
|
{/* 用户信息区域 */}
|
||||||
|
<View className="user_info_section">
|
||||||
|
<UserInfoCard
|
||||||
|
user_info={user_info}
|
||||||
|
is_current_user={false}
|
||||||
|
is_following={is_following}
|
||||||
|
on_follow={handle_follow}
|
||||||
|
on_message={handle_send_message}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局类型标签页 */}
|
||||||
|
<GameTabs
|
||||||
|
active_tab={active_tab}
|
||||||
|
on_tab_change={setActiveTab}
|
||||||
|
is_current_user={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 球局列表 */}
|
||||||
|
<View className="game_list_section">
|
||||||
|
<View className="date_header">
|
||||||
|
<Text className="date_text">5月29日</Text>
|
||||||
|
<Text className="separator">/</Text>
|
||||||
|
<Text className="weekday_text">星期六</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 球局卡片 */}
|
||||||
|
<View className="game_cards">
|
||||||
|
{game_records.map((game) => (
|
||||||
|
<GameCard
|
||||||
|
key={game.id}
|
||||||
|
game={game}
|
||||||
|
on_click={handle_game_detail}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
<GuideBar currentPage='personal' />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OtherUserPage;
|
||||||
251
src/services/userService.ts
Normal file
251
src/services/userService.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import { UserInfo, GameRecord } from '@/components/UserInfo';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { API_CONFIG, REQUEST_CONFIG } from '@/config/api';
|
||||||
|
|
||||||
|
// API响应接口
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户详情接口
|
||||||
|
interface UserDetailData {
|
||||||
|
openid: string;
|
||||||
|
user_code: string;
|
||||||
|
unionid: string;
|
||||||
|
session_key: string;
|
||||||
|
nickname: string;
|
||||||
|
avatar_url: string;
|
||||||
|
gender: string;
|
||||||
|
country: string;
|
||||||
|
province: string;
|
||||||
|
city: string;
|
||||||
|
language: string;
|
||||||
|
phone: string;
|
||||||
|
is_subscribed: string;
|
||||||
|
latitude: string;
|
||||||
|
longitude: string;
|
||||||
|
subscribe_time: string;
|
||||||
|
last_login_time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息参数接口
|
||||||
|
interface UpdateUserParams {
|
||||||
|
nickname: string;
|
||||||
|
avatar_url: string;
|
||||||
|
gender: string;
|
||||||
|
phone: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
city: string;
|
||||||
|
province: string;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传响应接口
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户服务类
|
||||||
|
export class UserService {
|
||||||
|
// 获取用户信息
|
||||||
|
static async get_user_info(user_id?: string): Promise<UserInfo> {
|
||||||
|
try {
|
||||||
|
const response = await Taro.request<ApiResponse<UserDetailData>>({
|
||||||
|
url: `${API_CONFIG.BASE_URL}${API_CONFIG.USER.DETAIL}`,
|
||||||
|
method: 'POST',
|
||||||
|
data: user_id ? { user_id } : {},
|
||||||
|
...REQUEST_CONFIG
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
const userData = response.data.data;
|
||||||
|
return {
|
||||||
|
id: userData.user_code || user_id || '1',
|
||||||
|
nickname: userData.nickname || '用户',
|
||||||
|
avatar: userData.avatar_url || require('../../static/userInfo/default_avatar.svg'),
|
||||||
|
join_date: userData.subscribe_time ? `${new Date(userData.subscribe_time).getFullYear()}年${new Date(userData.subscribe_time).getMonth() + 1}月加入` : '未知时间加入',
|
||||||
|
stats: {
|
||||||
|
following: 0, // 这些数据需要其他接口获取
|
||||||
|
friends: 0,
|
||||||
|
hosted: 0,
|
||||||
|
participated: 0
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
userData.city || '未知地区',
|
||||||
|
userData.province || '未知省份',
|
||||||
|
'NTRP 3.0' // 默认等级,需要其他接口获取
|
||||||
|
],
|
||||||
|
bio: '这个人很懒,什么都没有写...',
|
||||||
|
location: userData.city || '未知地区',
|
||||||
|
occupation: '未知职业', // 需要其他接口获取
|
||||||
|
ntrp_level: 'NTRP 3.0' // 需要其他接口获取
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '获取用户信息失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error);
|
||||||
|
// 返回默认用户信息
|
||||||
|
return {
|
||||||
|
id: user_id || '1',
|
||||||
|
nickname: '用户',
|
||||||
|
avatar: require('../../static/userInfo/default_avatar.svg'),
|
||||||
|
join_date: '未知时间加入',
|
||||||
|
stats: {
|
||||||
|
following: 0,
|
||||||
|
friends: 0,
|
||||||
|
hosted: 0,
|
||||||
|
participated: 0
|
||||||
|
},
|
||||||
|
tags: ['未知地区', '未知职业', 'NTRP 3.0'],
|
||||||
|
bio: '这个人很懒,什么都没有写...',
|
||||||
|
location: '未知地区',
|
||||||
|
occupation: '未知职业',
|
||||||
|
ntrp_level: 'NTRP 3.0'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户球局记录
|
||||||
|
static async get_user_games(user_id: string, type: 'hosted' | 'participated'): Promise<GameRecord[]> {
|
||||||
|
// 模拟API调用 - 这里应该调用真实的球局接口
|
||||||
|
console.log(`获取用户 ${user_id} 的${type === 'hosted' ? '主办' : '参与'}球局`);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const mock_games: GameRecord[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: type === 'hosted' ? '女生轻松双打' : '周末双打练习',
|
||||||
|
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')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
resolve(mock_games);
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关注/取消关注用户
|
||||||
|
static async toggle_follow(user_id: string, is_following: boolean): Promise<boolean> {
|
||||||
|
// 模拟API调用 - 这里应该调用真实的关注接口
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(`用户 ${user_id} 关注状态变更:`, !is_following);
|
||||||
|
resolve(!is_following);
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存用户信息
|
||||||
|
static async save_user_info(user_info: Partial<UserInfo> & { phone?: string; gender?: string }): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// 获取当前位置信息
|
||||||
|
const location = await Taro.getLocation({
|
||||||
|
type: 'wgs84'
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateParams: UpdateUserParams = {
|
||||||
|
nickname: user_info.nickname || '',
|
||||||
|
avatar_url: user_info.avatar || '',
|
||||||
|
gender: user_info.gender || '',
|
||||||
|
phone: user_info.phone || '',
|
||||||
|
latitude: location.latitude,
|
||||||
|
longitude: location.longitude,
|
||||||
|
city: user_info.location || '',
|
||||||
|
province: '', // 需要从用户信息中获取
|
||||||
|
country: '' // 需要从用户信息中获取
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await Taro.request<ApiResponse<any>>({
|
||||||
|
url: `${API_CONFIG.BASE_URL}${API_CONFIG.USER.UPDATE}`,
|
||||||
|
method: 'POST',
|
||||||
|
data: updateParams,
|
||||||
|
...REQUEST_CONFIG
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '更新用户信息失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存用户信息失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传头像
|
||||||
|
static async upload_avatar(file_path: string): Promise<string> {
|
||||||
|
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<UploadResponseData>;
|
||||||
|
if (result.code === 0) {
|
||||||
|
// 使用新的响应格式中的file_url字段
|
||||||
|
return result.data.file_url;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '头像上传失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('头像上传失败:', error);
|
||||||
|
// 如果上传失败,返回默认头像
|
||||||
|
return require('../../static/userInfo/default_avatar.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user