Merge remote-tracking branch origin into feature/juguohong/20250816

This commit is contained in:
李瑞
2025-09-07 18:56:23 +08:00
46 changed files with 4798 additions and 836 deletions

View File

@@ -1,5 +1,6 @@
export default defineAppConfig({
pages: [
'pages/login/index/index',
'pages/login/verification/index',
'pages/login/terms/index',
@@ -11,8 +12,11 @@ export default defineAppConfig({
'pages/detail/index',
'pages/message/index',
'pages/orderCheck/index',
'pages/userInfo/myself/index', // 个人中心
'pages/userInfo/edit/index', // 个人中心
'pages/userInfo/favorites/index', // 个人中心
'pages/userInfo/orders/index', // 个人中心
// 'pages/mapDisplay/index',
],

View File

@@ -1,11 +1,12 @@
import { Component, ReactNode } from 'react'
import Taro from '@tarojs/taro';
import './nutui-theme.scss'
import './app.scss'
import { useDictionaryStore } from './store/dictionaryStore'
import { useGlobalStore } from './store/global'
import { check_login_status } from './services/loginService';
// import { getNavbarHeight } from "@/utils/getNavbarHeight";
interface AppProps {
children: ReactNode
}
@@ -15,13 +16,6 @@ class App extends Component<AppProps> {
onLaunch() {
console.log('小程序启动,初始化逻辑写这里')
// 已经登录过就跳转到 列表页
let is_login = check_login_status()
if (is_login) {
Taro.redirectTo({
url: '/pages/list/index'
})
}
}
componentDidMount() {

View File

@@ -0,0 +1,51 @@
import React, { useEffect, useState } from 'react'
import Taro from '@tarojs/taro'
import { View } from '@tarojs/components'
import { check_login_status } from '@/services/loginService'
export function getCurrentFullPath(): string {
const pages = Taro.getCurrentPages()
const currentPage = pages.at(-1)
if (currentPage) {
console.log(currentPage, 'currentPage get')
const route = currentPage.route
const options = currentPage.options || {}
const query = Object.keys(options)
.map(key => `${key}=${options[key]}`)
.join('&')
return query ? `/${route}?${query}` : `/${route}`
}
return ''
}
export default function withAuth<P extends object>(WrappedComponent: React.ComponentType<P>) {
const ComponentWithAuth: React.FC<P> = (props: P) => {
const [authed, setAuthed] = useState(false)
useEffect(() => {
const is_login = check_login_status()
setAuthed(is_login)
if (!is_login) {
const currentPage = getCurrentFullPath()
Taro.redirectTo({
url: `/pages/login/index/index${
currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ''
}`,
})
}
}, [])
if (!authed) {
return <View style={{ width: '100vh', height: '100vw', backgroundColor: 'white', position: 'fixed', top: 0, left: 0, zIndex: 999 }} /> // 空壳,避免 children 渲染出错
}
return <WrappedComponent {...props} />
}
return ComponentWithAuth
}

View File

@@ -0,0 +1,178 @@
// 编辑弹窗组件样式
.edit_modal_overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: flex-end;
justify-content: center;
}
.edit_modal_container {
width: 100%;
background: #FAFAFA;
border-radius: 20px 20px 0px 0px;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
// 标题栏
.modal_header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
.modal_title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 22px;
line-height: 1.27em;
color: #000000;
flex: 1;
text-align: center;
}
.close_button {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 50%;
cursor: pointer;
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06);
.close_icon {
position: relative;
width: 24px;
height: 24px;
.close_line {
position: absolute;
top: 50%;
left: 50%;
width: 10px;
height: 2px;
background: #000000;
transform: translate(-50%, -50%) rotate(45deg);
&:nth-child(2) {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
}
}
}
// 内容区域
.modal_content {
padding: 0px 16px 20px;
display: flex;
flex-direction: column;
gap: 20px;
.input_container {
display: flex;
flex-direction: column;
gap: 8px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 10px 16px;
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06);
min-height: 120px;
.text_input {
flex: 1;
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.71em;
color: #000000;
border: none;
background: transparent;
outline: none;
resize: none;
min-height: 80px;
&::placeholder {
color: rgba(60, 60, 67, 0.3);
}
}
.char_count {
display: flex;
justify-content: flex-end;
align-items: center;
.count_text {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.71em;
color: rgba(60, 60, 67, 0.3);
}
}
}
.validation_message {
padding: 0px 8px;
.validation_text {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 12px;
line-height: 1.5em;
color: rgba(60, 60, 67, 0.6);
}
}
}
// 底部按钮
.modal_footer {
padding: 8px 10px;
display: flex;
gap: 8px;
.save_button {
flex: 1;
height: 52px;
background: #000000;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0px 8px 64px 0px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(32px);
.save_text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 16px;
line-height: 1.4em;
color: rgba(255, 255, 255, 0.3);
}
&:active {
transform: scale(0.98);
}
}
}

View File

@@ -0,0 +1,119 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Textarea, Button } from '@tarojs/components';
import Taro from '@tarojs/taro';
import './EditModal.scss';
interface EditModalProps {
visible: boolean;
title: string;
placeholder: string;
initialValue: string;
maxLength: number;
onSave: (value: string) => void;
onCancel: () => void;
validationMessage?: string;
}
const EditModal: React.FC<EditModalProps> = ({
visible,
title,
placeholder,
initialValue,
maxLength,
onSave,
onCancel,
validationMessage
}) => {
const [value, setValue] = useState(initialValue);
const [isValid, setIsValid] = useState(true);
useEffect(() => {
if (visible) {
setValue(initialValue);
}
}, [visible, initialValue]);
const handle_input_change = (e: any) => {
const new_value = e.detail.value;
setValue(new_value);
// 验证输入
const valid = new_value.length >= 2 && new_value.length <= maxLength;
setIsValid(valid);
};
const handle_save = () => {
if (!isValid) {
Taro.showToast({
title: validationMessage || `请填写 2-${maxLength} 个字符`,
icon: 'none',
duration: 2000
});
return;
}
onSave(value);
};
const handle_cancel = () => {
setValue(initialValue);
onCancel();
};
if (!visible) {
return null;
}
return (
<View className="edit_modal_overlay">
<View className="edit_modal_container">
{/* 标题栏 */}
<View className="modal_header">
<Text className="modal_title">{title}</Text>
<View className="close_button" onClick={handle_cancel}>
<View className="close_icon">
<View className="close_line"></View>
<View className="close_line"></View>
</View>
</View>
</View>
{/* 内容区域 */}
<View className="modal_content">
{/* 文本输入区域 */}
<View className="input_container">
<Textarea
className="text_input"
value={value}
placeholder={placeholder}
maxlength={maxLength}
onInput={handle_input_change}
autoFocus={true}
/>
<View className="char_count">
<Text className="count_text">{value.length}/{maxLength}</Text>
</View>
</View>
{/* 验证提示 */}
{!isValid && (
<View className="validation_message">
<Text className="validation_text">
{validationMessage || `请填写 2-${maxLength} 个字符`}
</Text>
</View>
)}
</View>
{/* 底部按钮 */}
<View className="modal_footer">
<View className="save_button" onClick={handle_save}>
<Text className="save_text"></Text>
</View>
</View>
</View>
</View>
);
};
export default EditModal;

View File

@@ -121,6 +121,18 @@
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
.upload-source-popup-item-text {
width: 100%;
height: 56px;
padding: 16px 24px;
box-sizing: border-box;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 24px;
text-align: center;
}
&:last-child {
border-bottom: none;
}

View File

@@ -0,0 +1,489 @@
@use '../../scss/common.scss' as *;
// 用户信息卡片样式
.user_info_card {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 16px;
// 基本信息
.basic_info {
display: flex;
align-items: center;
gap: 16px;
.avatar_container {
@include avatar-base(64px);
.avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.info_container {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.nickname {
@include text-primary;
}
.join_date {
@include text-secondary;
}
}
.tag_icon {
/* Auto layout */
display: flex;
flex-direction: row;
align-items: center;
padding: 0px;
gap: 8px;
width: 40px;
height: 40px;
/* Inside auto layout */
flex: none;
order: 2;
flex-grow: 0;
}
}
// 统计数据
.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;
/* Frame 1912054928 */
}
.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;
}
}
}
}
}

View File

@@ -0,0 +1,289 @@
import React from 'react';
import Taro from '@tarojs/taro';
import { View, Text, Image, Button } from '@tarojs/components';
import './index.scss';
// 用户信息接口
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;
phone?: string;
gender?: string;
}
// 用户信息卡片组件属性
interface UserInfoCardProps {
user_info: UserInfo;
is_current_user: boolean;
is_following?: boolean;
on_follow?: () => void;
on_message?: () => void;
on_share?: () => void;
}
// 处理编辑用户信息
const on_edit = () => {
Taro.navigateTo({
url: '/pages/userInfo/edit/index'
});
};
// 用户信息卡片组件
export const UserInfoCard: React.FC<UserInfoCardProps> = ({
user_info,
is_current_user,
is_following = false,
on_follow,
on_message,
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 className='tag_item' onClick={on_edit}>
<Image
className="tag_icon"
src={require('../../static/userInfo/edit.svg')}
/> </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_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>
);
};

View File

@@ -14,15 +14,17 @@ import CalendarCard, { DialogCalendarCard } from './CalendarCard'
import CommonDialog from './CommonDialog'
import PublishMenu from './PublishMenu/PublishMenu'
import UploadCover from './UploadCover'
import EditModal from './EditModal/index'
import withAuth from './Auth'
export {
ActivityTypeSwitch,
TextareaTag,
export {
ActivityTypeSwitch,
TextareaTag,
FormSwitch,
ImageUpload,
Range,
NumberInterval,
TimeSelector,
ImageUpload,
Range,
NumberInterval,
TimeSelector,
TitleTextarea,
CommonPopup,
DateTimePicker,
@@ -31,6 +33,8 @@ import UploadCover from './UploadCover'
DialogCalendarCard,
CommonDialog,
PublishMenu,
UploadCover
UploadCover,
EditModal,
withAuth,
}

38
src/config/api.ts Normal file
View File

@@ -0,0 +1,38 @@
// 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',
HOSTED_GAMES: '/user/games',
PARTICIPATED_GAMES: '/user/participated'
},
// 文件上传接口
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'
}
};

View File

@@ -17,7 +17,8 @@ const envConfigs: Record<EnvType, EnvConfig> = {
// 开发环境
development: {
name: '开发环境',
apiBaseURL: 'https://sit.light120.com',
apiBaseURL: 'https://sit.light120.com',
// apiBaseURL: 'http://localhost:9098',
timeout: 15000,
enableLog: true,
enableMock: true
@@ -26,7 +27,8 @@ const envConfigs: Record<EnvType, EnvConfig> = {
// 测试环境
test: {
name: '测试环境',
apiBaseURL: 'https://sit.light120.com',
apiBaseURL: 'https://sit.light120.com',
// apiBaseURL: 'http://localhost:9098',
timeout: 12000,
enableLog: true,
enableMock: false
@@ -53,10 +55,10 @@ export const getCurrentEnv = (): EnvType => {
// 在开发调试时,可以通过修改这里的逻辑来切换环境
// 默认在小程序中使用生产环境配置
if (currentEnv === Taro.ENV_TYPE.WEAPP) {
// 微信小程序环境
return 'production'
}
// if (currentEnv === Taro.ENV_TYPE.WEAPP) {
// // 微信小程序环境
// return 'production'
// }
// 默认返回开发环境(便于调试)
return 'development'

View File

@@ -1,26 +1,24 @@
import React, { useState, useEffect, useRef, useImperativeHandle, forwardRef } from 'react'
import { View, Text, Button, Swiper, SwiperItem, Image, Map, ScrollView } from '@tarojs/components'
import { Cell, Avatar, Progress, Popover } from '@nutui/nutui-react-taro'
import React, { useState, useRef, useImperativeHandle, forwardRef } from 'react'
import { View, Text, Image, Map, ScrollView } from '@tarojs/components'
import { Avatar, Popover } from '@nutui/nutui-react-taro'
import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow } from '@tarojs/taro'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
// 导入API服务
import DetailService from '../../services/detailService'
import { updateUserProfile, get_user_info } from '../../services/loginService'
import { getCurrentLocation } from '../../utils/locationUtils'
import { CommonPopup, withAuth } from '@/components'
import DetailService, { MATCH_STATUS} from '@/services/detailService'
import { getCurrentLocation, calculateDistance } from '@/utils/locationUtils'
import {
useUserInfo,
useUserActions,
} from '../../store/userStore'
import img from '../../config/images'
import { getTextColorOnImage } from '../../utils'
} from '@/store/userStore'
import img from '@/config/images'
// import { getTextColorOnImage } from '../../utils'
import './index.scss'
import { CommonPopup } from '@/components'
const images = [
'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1a35ebbf-2361-44da-b338-7608561d0b31.png',
'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/cf5a82ba-90af-4138-a1b3-9119adcde9e0.png',
'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/49d7cdf0-b03c-4a0f-91c6-e7778080cfcd.png'
]
dayjs.locale('zh-cn')
// 将·作为连接符插入到标签文本之间
function insertDotInTags(tags: string[]) {
return tags.join('-·-').split('-')
}
@@ -35,35 +33,35 @@ const SharePopup = forwardRef(({ id, from }: { id: string, from: string }, ref)
}
}))
function handleShareToWechat() {
useShareAppMessage(() => {
return {
title: '分享',
path: `/pages/detail/index?id=${id}&from=${from}`,
}
})
}
// function handleShareToWechat() {
// useShareAppMessage(() => {
// return {
// title: '分享',
// path: `/pages/detail/index?id=${id}&from=share`,
// }
// })
// }
function handleShareToWechatMoments() {
useShareTimeline(() => {
return {
title: '分享',
path: `/pages/detail/index?id=${id}&from=${from}`,
}
})
}
// function handleShareToWechatMoments() {
// useShareTimeline(() => {
// return {
// title: '分享',
// path: `/pages/detail/index?id=${id}&from=share`,
// }
// })
// }
function handleSaveToLocal() {
Taro.saveImageToPhotosAlbum({
filePath: images[0],
success: () => {
Taro.showToast({ title: '保存成功', icon: 'success' })
},
fail: () => {
Taro.showToast({ title: '保存失败', icon: 'none' })
},
})
}
// function handleSaveToLocal() {
// Taro.saveImageToPhotosAlbum({
// filePath: images[0],
// success: () => {
// Taro.showToast({ title: '保存成功', icon: 'success' })
// },
// fail: () => {
// Taro.showToast({ title: '保存失败', icon: 'none' })
// },
// })
// }
return (
<CommonPopup
@@ -74,18 +72,7 @@ const SharePopup = forwardRef(({ id, from }: { id: string, from: string }, ref)
style={{ minHeight: '100px' }}
>
<View catchMove className='share-popup-content'>
<View onClick={handleShareToWechat}>
<Image src={img.ICON_DETAIL_SHARE} />
<Text></Text>
</View>
<View onClick={handleShareToWechatMoments}>
<Image src={img.ICON_DETAIL_SHARE} />
<Text></Text>
</View>
<View onClick={handleSaveToLocal}>
<Image src={img.ICON_DETAIL_SHARE} />
<Text></Text>
</View>
</View>
</CommonPopup>
)
@@ -96,10 +83,10 @@ function StickyButton(props) {
const { handleShare, handleJoinGame, detail } = props
const userInfo = useUserInfo()
const { id } = userInfo
const { publisher_id, status } = detail || {}
const { publisher_id, match_status, price } = detail || {}
const role = Number(publisher_id) === id ? 'ownner' : 'visitor'
console.log(status, role)
console.log(match_status, role)
return (
<View className="sticky-bottom-bar">
<View className="sticky-bottom-bar-share-and-comment">
@@ -117,7 +104,110 @@ function StickyButton(props) {
<Text>🎾</Text>
<Text></Text>
<View className='game-price'>
<Text>¥ 65</Text>
<Text>¥ {price}</Text>
</View>
</View>
</View>
)
}
// 球局信息
function GameInfo(props) {
const { detail, currentLocation } = props
const { latitude, longitude, location, location_name, start_time, end_time } = detail || {}
const openMap = () => {
Taro.openLocation({
latitude, // 纬度(必填)
longitude, // 经度(必填)
name: location_name, // 位置名(可选)
address: location, // 地址详情(可选)
scale: 15, // 地图缩放级别1-28
})
}
const [c_latitude, c_longitude] = currentLocation
const distance = calculateDistance(c_latitude, c_longitude, latitude, longitude) / 1000
const startTime = dayjs(start_time)
const endTime = dayjs(end_time)
const game_length = endTime.diff(startTime, 'minutes') / 60
const startMonth = startTime.format('M')
const startDay = startTime.format('D')
const theDayOfWeek = startTime.format('dddd')
const startDate = `${startMonth}${startDay}${theDayOfWeek}`
const gameRange = `${startTime.format('HH:mm')} - ${endTime.format('HH:mm')}`
return (
<View className='detail-page-content-game-info'>
{/* Date and Weather */}
<View className='detail-page-content-game-info-date-weather'>
{/* Calendar and Date time */}
<View className='detail-page-content-game-info-date-weather-calendar-date'>
{/* Calendar */}
<View className='detail-page-content-game-info-date-weather-calendar-date-calendar'>
<View className="month">{startMonth}</View>
<View className="day">{startDay}</View>
</View>
{/* Date time */}
<View className='detail-page-content-game-info-date-weather-calendar-date-date'>
<View className="date">{startDate}</View>
<View className="venue-time">{gameRange} {game_length}</View>
</View>
</View>
{/* Weather */}
<View className='detail-page-content-game-info-date-weather-weather'>
{/* Weather icon */}
<View className='detail-page-content-game-info-date-weather-weather-icon'>
<Image className="weather-icon" src={img.ICON_WEATHER_SUN} />
</View>
{/* Weather text and temperature */}
<View className='detail-page-content-game-info-date-weather-weather-text-temperature'>
<Text>28 - 32</Text>
</View>
</View>
</View>
{/* Place */}
<View className='detail-page-content-game-info-place'>
{/* venue location message */}
<View className='location-message'>
{/* location icon */}
<View className='location-message-icon'>
<Image className='location-message-icon-image' src={img.ICON_DETAIL_MAP} />
</View>
{/* location message */}
<View className='location-message-text'>
{/* venue name and distance */}
<View className='location-message-text-name-distance' onClick={openMap}>
<Text>{location_name || '-'}</Text>
<Text>·</Text>
<Text>{distance.toFixed(1)}km</Text>
<Image className='location-message-text-name-distance-arrow' src={img.ICON_DETAIL_ARROW_RIGHT} />
</View>
{/* venue address */}
<View className='location-message-text-address'>
<Text>{location || '-'}</Text>
</View>
</View>
</View>
{/* venue map */}
<View className='location-map'>
{longitude && latitude && (
<Map
className='location-map-map'
longitude={latitude}
latitude={longitude}
markers={[{ id: 1, latitude: longitude, longitude: latitude, iconPath: require('@/static/detail/icon-stark.svg'), width: 16, height: 16 }]}
includePoints={[{ latitude: longitude, longitude: latitude }, { latitude: currentLocation[0], longitude: currentLocation[1] }]}
includePadding={{ left: 50, right: 50, top: 50, bottom: 50 }}
onError={() => {}}
// hide business msg
showLocation
theme='dark'
/>
)}
</View>
</View>
</View>
@@ -133,11 +223,13 @@ function Index() {
// const [textColor, setTextColor] = useState<string []>([])
const [detail, setDetail] = useState<any>(null)
const { params } = useRouter()
const [currentLocation, setCurrentLocation] = useState([0, 0])
const [currentLocation, setCurrentLocation] = useState<[number, number]>([0, 0])
const { id, autoShare, from } = params
const { fetchUserInfo, updateUserInfo } = useUserActions()
console.log('from', from)
console.group('params')
console.log(params)
console.groupEnd()
// 本地状态管理
const [loading, setLoading] = useState(false)
@@ -168,7 +260,7 @@ function Index() {
}
const fetchDetail = async () => {
const res = await DetailService.getDetail(242/* Number(id) */)
const res = await DetailService.getDetail(243/* Number(id) */)
if (res.code === 0) {
console.log(res.data)
setDetail(res.data)
@@ -192,19 +284,9 @@ function Index() {
sharePopupRef.current.show()
}
const openMap = () => {
Taro.openLocation({
latitude: detail?.longitude, // 纬度(必填)
longitude: detail?.latitude, // 经度(必填)
name: '上海体育场', // 位置名(可选)
address: '上海市徐汇区肇嘉浜路128号', // 地址详情(可选)
scale: 15, // 地图缩放级别1-28
})
}
const handleJoinGame = () => {
Taro.navigateTo({
url: `/pages/orderCheck/index?id=${id}`,
url: `/pages/orderCheck/index?gameId=${243/* id */}`,
})
}
@@ -285,13 +367,25 @@ function Index() {
},
]
function handleBack() {
const pages = Taro.getCurrentPages()
if (pages.length <= 1) {
Taro.redirectTo({
url: '/pages/list/index',
})
} else {
Taro.navigateBack()
}
}
console.log('detail', detail)
return (
<View className='detail-page'>
{/* custom navbar */}
<view className="custom-navbar">
<View className='detail-navigator'>
<View className='detail-navigator-back' onClick={() => { Taro.navigateBack() }}>
<View className='detail-navigator-back' onClick={handleBack}>
<Image className='detail-navigator-back-icon' src={img.ICON_ARROW_LEFT} />
</View>
<View className='detail-navigator-icon'>
@@ -299,7 +393,7 @@ function Index() {
</View>
</View>
</view>
<View className='detail-page-bg' style={{ backgroundImage: `url(${images[0]})` }} />
<View className='detail-page-bg' style={detail?.image_list?.[0] ? { backgroundImage: `url(${detail?.image_list?.[0]})` } : {}} />
<View className='detail-page-bg-text' />
{/* swiper */}
<View className="detail-swiper-container">
@@ -364,76 +458,7 @@ function Index() {
<Text className='detail-page-content-title-text'>{title}</Text>
</View>
{/* Date and Place and weather */}
<View className='detail-page-content-game-info'>
{/* Date and Weather */}
<View className='detail-page-content-game-info-date-weather'>
{/* Calendar and Date time */}
<View className='detail-page-content-game-info-date-weather-calendar-date'>
{/* Calendar */}
<View className='detail-page-content-game-info-date-weather-calendar-date-calendar'>
<View className="month">3</View>
<View className="day">25</View>
</View>
{/* Date time */}
<View className='detail-page-content-game-info-date-weather-calendar-date-date'>
<View className="date">325 </View>
<View className="venue-time">19:00-21:00 2</View>
</View>
</View>
{/* Weather */}
<View className='detail-page-content-game-info-date-weather-weather'>
{/* Weather icon */}
<View className='detail-page-content-game-info-date-weather-weather-icon'>
<Image className="weather-icon" src={img.ICON_WEATHER_SUN} />
</View>
{/* Weather text and temperature */}
<View className='detail-page-content-game-info-date-weather-weather-text-temperature'>
<Text>28 - 32</Text>
</View>
</View>
</View>
{/* Place */}
<View className='detail-page-content-game-info-place'>
{/* venue location message */}
<View className='location-message'>
{/* location icon */}
<View className='location-message-icon'>
<Image className='location-message-icon-image' src={img.ICON_DETAIL_MAP} />
</View>
{/* location message */}
<View className='location-message-text'>
{/* venue name and distance */}
<View className='location-message-text-name-distance' onClick={openMap}>
<Text></Text>
<Text>·</Text>
<Text>1.2km</Text>
<Image className='location-message-text-name-distance-arrow' src={img.ICON_DETAIL_ARROW_RIGHT} />
</View>
{/* venue address */}
<View className='location-message-text-address'>
<Text>128</Text>
</View>
</View>
</View>
{/* venue map */}
<View className='location-map'>
{longitude && latitude && (
<Map
className='location-map-map'
longitude={latitude}
latitude={longitude}
markers={[{ id: 1, latitude: longitude, longitude: latitude, iconPath: require('@/static/detail/icon-stark.svg'), width: 16, height: 16 }]}
includePoints={[{ latitude: longitude, longitude: latitude }, { latitude: currentLocation[0], longitude: currentLocation[1] }]}
includePadding={{ left: 50, right: 50, top: 50, bottom: 50 }}
onError={() => {}}
// hide business msg
showLocation
theme='dark'
/>
)}
</View>
</View>
</View>
<GameInfo detail={detail} currentLocation={currentLocation} />
{/* detail */}
<View className='detail-page-content-detail'>
{/* venue detail title and venue ordered status */}
@@ -619,4 +644,4 @@ function Index() {
)
}
export default Index
export default withAuth(Index)

View File

@@ -10,6 +10,8 @@ import CustomerNavBar from "@/container/listCustomNavbar";
import GuideBar from "@/components/GuideBar";
import ListContainer from "@/container/listContainer";
import DistanceQuickFilter from "@/components/DistanceQuickFilter";
import { withAuth } from "@/components";
// import img from "@/config/images";
const ListPage = () => {
// 从 store 获取数据和方法
@@ -191,4 +193,4 @@ const ListPage = () => {
);
};
export default ListPage;
export default withAuth(ListPage);

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { View, Text, Button, Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
import Taro, { useRouter } from '@tarojs/taro';
import { wechat_auth_login, save_login_state, check_login_status } from '../../../services/loginService';
import './index.scss';
@@ -8,9 +8,11 @@ const LoginPage: React.FC = () => {
const [is_loading, set_is_loading] = useState(false);
const [agree_terms, set_agree_terms] = useState(false);
const [show_terms_layer, set_show_terms_layer] = useState(false);
const { params: { redirect } } = useRouter();
// 微信授权登录
const handle_wechat_login = async (e: any) => {
@@ -42,7 +44,12 @@ const LoginPage: React.FC = () => {
save_login_state(response.token!, response.user_info!);
setTimeout(() => {
Taro.redirectTo({ url: '/pages/list/index' });
if (redirect) {
console.log('redirect:', decodeURIComponent(redirect))
Taro.redirectTo({ url: decodeURIComponent(redirect) });
} else {
Taro.redirectTo({ url: '/pages/list/index' });
}
}, 200);
} else {
Taro.showToast({
@@ -76,7 +83,7 @@ const LoginPage: React.FC = () => {
// 跳转到验证码页面
Taro.navigateTo({
url: '/pages/login/verification/index'
url: `/pages/login/verification/index?redirect=${redirect}`
});
};

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Input, Button, Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
import Taro, { useRouter } from '@tarojs/taro';
import { phone_auth_login, send_sms_code } from '../../../services/loginService';
import './index.scss';
@@ -12,6 +12,8 @@ const VerificationPage: React.FC = () => {
const [is_loading, setIsLoading] = useState(false);
const [code_input_focus, setCodeInputFocus] = useState(false);
const { params: { redirect } } = useRouter();
// 计算登录按钮是否应该启用
const can_login = phone.length === 11 && verification_code.length === 6 && !is_loading;
@@ -123,9 +125,13 @@ const VerificationPage: React.FC = () => {
if (result.success) {
setTimeout(() => {
Taro.redirectTo({
url: '/pages/list/index'
});
if (redirect) {
Taro.redirectTo({ url: decodeURIComponent(redirect) });
} else {
Taro.redirectTo({
url: '/pages/list/index'
});
}
}, 200);
} else {
Taro.showToast({

View File

@@ -1,80 +1,277 @@
@use '~@/scss/images.scss' as img;
$--Backgrounds-Primary: '#fff';
.message-container {
width: 100%;
height: 100vh;
background: radial-gradient(227.15% 100% at 50% 0%, #EEFFDC 0%, #FFF 36.58%), var(--Backgrounds-Primary, #FFF);
// padding-top: 100px;
box-sizing: border-box;
display: flex;
flex-direction: column;
.custom-navbar {
height: 56px; /* 通常与原生导航栏高度一致 */
display: flex;
align-items: center;
justify-content: flex-start;
// background-color: #fff;
color: #000;
padding-top: 44px; /* 适配状态栏 */
// 导航栏
.navbar {
height: 100px;
background: #FFFFFF;
padding-top: 44px;
position: sticky;
top: 0;
z-index: 100;
.message-navigator {
position: relative;
left: 15px;
top: -2px;
width: 80px;
height: 32px;
.navbar-content {
height: 56px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
justify-content: space-between;
padding: 0 15px;
.message-navigator-avatar {
width: 32px;
height: 32px;
}
.navbar-left {
display: flex;
align-items: center;
gap: 8px;
.message-navigator-title {
font-size: 16px;
font-weight: 500;
color: #000;
.navbar-avatar {
width: 28px;
height: 28px;
}
.navbar-title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 20px;
line-height: 1.4;
color: #000000;
}
}
}
}
.message-content {
.message-content-list {
// 消息列表
.message-list {
flex: 1;
overflow: hidden;
box-sizing: border-box;
margin-bottom:100px;
background-color: none !important;
.message-list-content {
display: flex;
flex-direction: column;
padding: 10px 15px;
padding: 0 12px;
gap: 8px;
}
// 系统消息样式
.system-message {
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);
box-sizing: border-box;
gap: 12px;
.message-item {
padding: 10px;
// border: 1px solid rgba(0, 0, 0, 0.1);
.message-header {
display: flex;
flex-direction: column;
gap: 10px;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
background-color: rgba(255, 255, 255, 0.5);
border-radius: 10px;
align-items: center;
padding: 12px 15px 0;
.message-title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 16px;
line-height: 1.5;
color: #000000;
flex: 1;
}
.message-time {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 12px;
line-height: 1.5;
color: rgba(60, 60, 67, 0.6);
}
}
.message-item-title {
font-size: 16px;
font-weight: 500;
color: #000;
.message-content {
padding: 8px 15px 0;
.message-text {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.43;
color: rgba(0, 0, 0, 0.7);
}
}
.message-item-content {
font-size: 14px;
color: #666;
.message-action {
padding: 12px 15px 0;
.action-divider {
height: 0.5px;
background: rgba(0, 0, 0, 0.08);
margin-bottom: 12px;
}
.action-button {
display: flex;
justify-content: space-between;
align-items: center;
.action-text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 14px;
line-height: 1.43;
color: rgba(0, 0, 0, 0.85);
}
.action-arrow {
width: 16px;
height: 16px;
}
}
}
}
// 用户消息样式
.user-message {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 15px;
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.08);
border-radius: 20px;
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06);
box-sizing: border-box;
.message-avatar {
position: relative;
flex-shrink: 0;
.unread-dot {
position: absolute;
top: -2px;
right: -2px;
width: 10px;
height: 10px;
background: #FF4848;
border-radius: 50%;
}
}
.message-info {
flex: 1;
min-width: 0;
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
.message-title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 16px;
line-height: 1.5;
color: #000000;
}
.message-time {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.71;
color: rgba(0, 0, 0, 0.35);
}
}
.message-content {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
.message-text {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.29;
color: rgba(0, 0, 0, 0.35);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.unread-indicator {
width: 10px;
height: 10px;
background: #FF4848;
border-radius: 50%;
flex-shrink: 0;
}
}
}
}
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 124px 16px;
height: 746px;
.empty-icon {
width: 300px;
height: 225px;
margin-bottom: 12px;
position: relative;
.empty-message-icon {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%);
border-radius: 12px;
position: relative;
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
background: #d0d0d0;
border-radius: 50%;
}
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 30px;
background: #a0a0a0;
border-radius: 50%;
}
}
}
.empty-text {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 14px;
line-height: 1.71;
color: rgba(0, 0, 0, 0.85);
}
}
}

View File

@@ -1,43 +1,184 @@
import React from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import { useState } from 'react'
import { View, Text, ScrollView, Image } from '@tarojs/components'
import { Avatar } from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import GuideBar from '@/components/GuideBar'
// import img from '@/config/images'
import { withAuth } from '@/components'
import './index.scss'
const Personal = () => {
const messageList = Array(10).fill(0).map((_, index) => ({
id: index + 1,
title: `消息${index + 1}消息${index + 1}消息${index + 1}消息${index + 1}`,
content: Array(Math.round(Math.random() * 40)).fill(0).map((_, index) => `消息${index + 1}`).join(''),
}))
// 消息类型定义
interface MessageItem {
id: string
type: 'system' | 'user' | 'like' | 'comment' | 'follow'
title: string
content: string
time: string
avatar?: string
isRead: boolean
hasAction?: boolean
actionText?: string
}
const Message = () => {
const [activeTab] = useState<'all' | 'like' | 'comment' | 'follow'>('all')
// 模拟消息数据
const messageList: MessageItem[] = [
{
id: '1',
type: 'system',
title: '球局报名确认',
content: '恭喜,你成功报名"世纪公园混双 · 8月20日"球局请提前15分钟到达球场门口等你。',
time: '今天 09:12',
isRead: false,
hasAction: true,
actionText: '查看详情'
},
{
id: '2',
type: 'system',
title: '新球友加入提醒',
content: 'Fiona 已加入"徐汇双打 · 今晚7点"的群聊,快去和她打个招呼吧~',
time: '昨天 09:12',
isRead: false,
hasAction: true,
actionText: '打个招呼'
},
{
id: '3',
type: 'system',
title: '场地变更通知',
content: '请注意,"张江中午快打"已改至世纪园区 3 号场集合。',
time: '2025-08-17 18:30',
isRead: true
},
{
id: '4',
type: 'system',
title: '系统维护提醒',
content: '系统将于 2025-08-20 凌晨 00:0002:00 暂停服务,届时活动发布、消息中心等功能可能无法使用,敬请谅解。',
time: '2025-08-17 18:30',
isRead: true
},
{
id: '5',
type: 'system',
title: '活动将近提醒',
content: '你的"宝山初学者约球"将在 2 小时后开始。快准备好球拍、毛巾和运动鞋,我们赛场见!',
time: '2025-08-17 18:30',
isRead: true
},
{
id: '6',
type: 'user',
title: '王晨',
content: '你好,昨天约的球场还在吗',
time: '09:34',
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg',
isRead: false
},
{
id: '7',
type: 'user',
title: '阿斌',
content: '七点到世纪公园东门集合可以吗?',
time: '昨天 22:10',
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg',
isRead: false
},
{
id: '8',
type: 'user',
title: 'Lili',
content: '我刚问了,还有一个小时的空场!',
time: '昨天 18:47',
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg',
isRead: true
}
]
// 过滤消息
const filteredMessages = messageList.filter(message => {
if (activeTab === 'all') return true
return message.type === activeTab
})
// 渲染消息项
const renderMessageItem = (message: MessageItem) => {
if (message.type === 'system') {
return (
<View className='message-item system-message' key={message.id}>
<View className='message-header'>
<Text className='message-title'>{message.title}</Text>
<Text className='message-time'>{message.time}</Text>
</View>
<View className='message-content'>
<Text className='message-text'>{message.content}</Text>
</View>
{message.hasAction && (
<View className='message-action'>
<View className='action-divider'></View>
<View className='action-button'>
<Text className='action-text'>{message.actionText}</Text>
<Image className='action-arrow' src={require('../../static/message/icon-message-arrow.svg')} />
</View>
</View>
)}
</View>
)
}
return (
<View className='message-item user-message' key={message.id}>
<View className='message-avatar'>
<Avatar src={message.avatar} size='48px' />
</View>
<View className='message-info'>
<View className='message-header'>
<Text className='message-title'>{message.title}</Text>
<Text className='message-time'>{message.time}</Text>
</View>
<View className='message-content'>
<Text className='message-text'>{message.content}</Text>
{!message.isRead && <View className='unread-indicator'></View>}
</View>
</View>
</View>
)
}
return (
<View className='message-container'>
<View className='custom-navbar'>
<View className='message-navigator'>
<Avatar className='message-navigator-avatar' src="https://img.yzcdn.cn/vant/cat.jpeg" />
<Text className='message-navigator-title'></Text>
{/* 导航栏 */}
<View className='navbar'>
<View className='navbar-content'>
<View className='navbar-left'>
<Avatar className='navbar-avatar' src="https://img.yzcdn.cn/vant/cat.jpeg" />
<Text className='navbar-title'></Text>
</View>
</View>
</View>
<ScrollView scrollY className='message-content'>
<View className='message-content-list'>
{messageList.map((item) => (
<View className='message-item' key={item.id}>
<View className='message-item-title'>
<Text>{item.title}</Text>
</View>
<View className='message-item-content'>
<Text>{item.content}</Text>
</View>
{/* 消息列表 */}
<ScrollView scrollY className='message-list'>
{filteredMessages.length > 0 ? (
<View className='message-list-content'>
{filteredMessages.map(renderMessageItem)}
</View>
) : (
<View className='empty-state'>
<View className='empty-icon'>
<View className='empty-message-icon'></View>
</View>
))}
</View>
<Text className='empty-text'></Text>
</View>
)}
</ScrollView>
{/* 底部导航 */}
<GuideBar currentPage='message' />
</View>
)
}
export default Personal
export default withAuth(Message)

View File

@@ -1,31 +1,71 @@
import React from 'react'
import React, { useState } from 'react'
import { View, Text, Button } from '@tarojs/components'
import Taro from '@tarojs/taro'
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
import { delay } from '@/utils'
import orderService from '@/services/orderService'
import detailService, { GameDetail } from '@/services/detailService'
import { withAuth } from '@/components'
const OrderCheck = () => {
const { params } = useRouter()
const { id, gameId } = params
const [detail ,setDetail] = useState<GameDetail | {}>({})
useDidShow(async () => {
const res = await detailService.getDetail(Number(gameId))
console.log(res)
if (res.code === 0) {
setDetail(res.data)
}
})
//TODO: get order msg from id
const handlePay = async () => {
Taro.showLoading({
title: '支付中...',
mask: true
})
await delay(2000)
Taro.hideLoading()
Taro.showToast({
title: '支付成功',
icon: 'success'
})
await delay(1000)
Taro.navigateBack({
delta: 1
})
const res = await orderService.createOrder(Number(gameId))
if (res.code === 0) {
const { payment_required, payment_params } = res.data
if (payment_required) {
const { timeStamp, nonceStr, package: package_, signType, paySign } = payment_params
await Taro.requestPayment({
timeStamp,
nonceStr,
package: package_,
signType,
paySign,
success: async () => {
Taro.hideLoading()
Taro.showToast({
title: '支付成功',
icon: 'success'
})
await delay(1000)
Taro.navigateBack({
delta: 1
})
},
fail: () => {
Taro.hideLoading()
Taro.showToast({
title: '支付失败',
icon: 'none'
})
}
})
}
}
}
return (
<View>
<Text>OrderCheck</Text>
<Text>{detail?.title || '-'}</Text>
<Text>¥{detail?.price || '-'}</Text>
<Button onClick={handlePay}></Button>
</View>
)
}
export default OrderCheck
export default withAuth(OrderCheck)

View File

@@ -4,6 +4,7 @@ import { Checkbox } from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import { type ActivityType } from '../../components/ActivityTypeSwitch'
import CommonDialog from '../../components/CommonDialog'
import { withAuth } from '@/components'
import PublishForm from './publishForm'
import { FormFieldConfig, publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema';
import { PublishBallFormData } from '../../../types/publishBall';
@@ -87,13 +88,13 @@ const PublishBall: React.FC = () => {
// 检查相邻两组数据是否相同
const checkAdjacentDataSame = (formDataArray: PublishBallFormData[]) => {
if (formDataArray.length < 2) return false
const lastIndex = formDataArray.length - 1
const secondLastIndex = formDataArray.length - 2
const lastData = formDataArray[lastIndex]
const secondLastData = formDataArray[secondLastIndex]
// 比较关键字段是否相同
return (JSON.stringify(lastData) === JSON.stringify(secondLastData))
}
@@ -117,7 +118,7 @@ const PublishBall: React.FC = () => {
}
}])
}
// 复制上一场数据
const handleCopyPrevious = (index: number) => {
@@ -372,7 +373,7 @@ const PublishBall: React.FC = () => {
<View className={styles['publish-ball']}>
{/* 活动类型切换 */}
<View className={styles['activity-type-switch']}>
{/* <ActivityTypeSwitch
{/* <ActivityTypeSwitch
value={activityType}
onChange={handleActivityTypeChange}
/> */}
@@ -452,7 +453,7 @@ const PublishBall: React.FC = () => {
{
activityType === 'group' && (
<View className={styles['submit-tip']}>
<Checkbox
<Checkbox
className={styles['submit-checkbox']}
checked={checked}
onChange={onCheckedChange}
@@ -466,4 +467,4 @@ const PublishBall: React.FC = () => {
)
}
export default PublishBall
export default withAuth(PublishBall)

View File

@@ -4,6 +4,7 @@ import { Input } from "@nutui/nutui-react-taro";
import { useEffect, useMemo, useRef } from "react";
import { useListState } from "@/store/listStore";
import img from "@/config/images";
import { withAuth } from "@/components";
import "./index.scss";
import Taro from "@tarojs/taro";
@@ -204,4 +205,4 @@ const ListSearch = () => {
</>
);
};
export default ListSearch;
export default withAuth(ListSearch);

View File

@@ -6,6 +6,7 @@ import ListContainer from "@/container/listContainer";
import img from "@/config/images";
import CustomerNavBar from "@/container/listCustomNavbar";
import DistanceQuickFilter from "@/components/DistanceQuickFilter";
import { withAuth } from "@/components";
import { useEffect } from "react";
import FilterPopup from "@/components/FilterPopup";
import "./index.scss";
@@ -153,4 +154,4 @@ const SearchResult = () => {
);
};
export default SearchResult;
export default withAuth(SearchResult);

View 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;
}
}
```

View 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

View 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框架版本

View 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. 支持异步操作和错误处理
## 扩展性
- 组件可复用性强
- 服务层易于扩展
- 支持更多用户功能扩展
- 便于维护和测试

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '编辑资料',
navigationStyle: 'custom'
})

View File

@@ -0,0 +1,279 @@
// 编辑资料页面样式
.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;
}
// 导航栏
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: 98px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 44px 44px 0px 0px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 21px 16px 0px;
box-sizing: border-box;
.navbar_left {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
cursor: pointer;
.back_icon {
width: 18px;
height: 18px;
}
}
.navbar_title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 17px;
line-height: 1.4em;
color: #000000;
text-align: center;
flex: 1;
}
.navbar_right {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
cursor: pointer;
.save_text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 16px;
line-height: 1.4em;
color: #000000;
}
}
}
// 主要内容区域
.main_content {
position: relative;
z-index: 5;
flex: 1;
margin-top: 98px;
box-sizing: border-box;
overflow-y: auto;
padding: 0px 16px;
padding-bottom: 48px;
// 头像编辑区域
.avatar_section {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-bottom: 48px;
margin-top: 98px;
.avatar_container {
position: relative;
.avatar {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
cursor: pointer;
box-shadow: 0px 8px 20px 0px rgba(0, 0, 0, 0.12), 0px 0px 1px 0px rgba(0, 0, 0, 0.2);
border: 0.5px solid rgba(255, 255, 255, 0.65);
overflow: hidden;
}
.avatar_overlay {
position: absolute;
bottom: 0;
right: 0;
width: 32px;
height: 32px;
background: #000000;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid rgba(0, 0, 0, 0.06);
z-index: 10;
.upload_icon {
width: 16px;
height: 16px;
}
}
}
}
// 表单区域
.form_section {
margin-bottom: 48px;
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06);
.form_group {
background: #FFFFFF;
border-radius: 12px;
overflow: hidden;
&:not(:first-child) {
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
.form_item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
min-height: 44px;
box-sizing: border-box;
.item_left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
.item_icon {
width: 16px;
height: 16px;
}
.item_label {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 14px;
line-height: 1.71em;
color: #000000;
}
}
.item_right {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
justify-content: flex-end;
.item_input {
flex: 1;
text-align: right;
font-family: 'PingFang SC';
font-weight: 600;
font-size: 14px;
line-height: 1.71em;
color: #000000;
border: none;
background: transparent;
outline: none;
&::placeholder {
color: rgba(0, 0, 0, 0.4);
}
}
.item_value {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 14px;
line-height: 1.71em;
color: #000000;
}
.bio_textarea {
flex: 1;
text-align: right;
font-family: 'PingFang SC';
font-weight: 600;
font-size: 14px;
line-height: 1.71em;
color: #000000;
border: none;
background: transparent;
outline: none;
min-height: 20px;
resize: none;
&::placeholder {
color: rgba(0, 0, 0, 0.4);
}
}
.arrow_icon {
width: 14px;
height: 14px;
opacity: 0.2;
}
}
.divider {
position: absolute;
left: 36px;
right: 12px;
height: 0.5px;
background: rgba(0, 0, 0, 0.06);
border-radius: 99px;
}
}
}
}
// 退出登录区域
.logout_section {
margin-top: 16px;
.logout_button {
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 16px;
padding: 2px 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
min-height: 48px;
.logout_text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 16px;
line-height: 1.4em;
color: #000000;
}
}
}
// 加载状态
.loading_container {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
.loading_text {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 16px;
line-height: 1.4em;
color: rgba(0, 0, 0, 0.6);
}
}
}

View File

@@ -0,0 +1,337 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Image, ScrollView, Input } from '@tarojs/components';
import Taro from '@tarojs/taro';
import './index.scss';
import { UserInfo } from '@/components/UserInfo';
import { UserService } from '@/services/userService';
import { EditModal } from '@/components';
const EditProfilePage: React.FC = () => {
// 用户信息状态
const [user_info, setUserInfo] = useState<UserInfo>({
id: '1',
nickname: '加载中...',
avatar: require('../../../static/userInfo/default_avatar.svg'),
join_date: '加载中...',
stats: {
following: 0,
friends: 0,
hosted: 0,
participated: 0
},
tags: ['加载中...'],
bio: '加载中...',
location: '加载中...',
occupation: '加载中...',
ntrp_level: 'NTRP 3.0',
phone: '',
gender: ''
});
// 表单状态
const [form_data, setFormData] = useState({
nickname: '',
bio: '',
location: '',
occupation: '',
ntrp_level: '4.0',
phone: '',
gender: '',
birthday: '2000-01-01'
});
// 加载状态
const [loading, setLoading] = useState(true);
// 编辑弹窗状态
const [edit_modal_visible, setEditModalVisible] = useState(false);
const [editing_field, setEditingField] = useState<string>('');
// 页面加载时初始化数据
useEffect(() => {
load_user_info();
}, []);
// 加载用户信息
const load_user_info = async () => {
try {
setLoading(true);
const user_data = await UserService.get_user_info();
setUserInfo(user_data);
setFormData({
nickname: user_data.nickname,
bio: user_data.bio,
location: user_data.location,
occupation: user_data.occupation,
ntrp_level: user_data.ntrp_level.replace('NTRP ', ''),
phone: user_data.phone || '',
gender: user_data.gender || '',
birthday: '2000-01-01' // 默认生日,实际应该从用户数据获取
});
} catch (error) {
console.error('加载用户信息失败:', error);
Taro.showToast({
title: '加载用户信息失败',
icon: 'error',
duration: 2000
});
} finally {
setLoading(false);
}
};
// 处理输入变化
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_open_edit_modal = (field: string) => {
setEditingField(field);
setEditModalVisible(true);
};
const handle_edit_modal_save = (value: string) => {
setFormData(prev => ({ ...prev, [editing_field]: value }));
setEditModalVisible(false);
setEditingField('');
};
const handle_edit_modal_cancel = () => {
setEditModalVisible(false);
setEditingField('');
};
// 处理退出登录
const handle_logout = () => {
Taro.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
// 清除用户数据
Taro.removeStorageSync('user_token');
Taro.removeStorageSync('user_info');
Taro.reLaunch({
url: '/pages/login/index/index'
});
}
}
});
};
return (
<View className="edit_profile_page">
{/* 导航栏 */}
{/* 主要内容 */}
<ScrollView className="main_content" scrollY>
{loading ? (
<View className="loading_container">
<Text className="loading_text">...</Text>
</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/userInfo/edit2.svg')}
/>
</View>
</View>
</View>
{/* 基本信息编辑 */}
<View className="form_section">
{/* 名字 */}
<View className="form_group">
<View className="form_item">
<View className="item_left">
<Image className="item_icon" src={require('../../../static/userInfo/user1.svg')} />
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Input
className="item_input"
value={form_data.nickname}
placeholder="188的王晨"
onInput={(e) => handle_input_change('nickname', e.detail.value)}
/>
<Image className="arrow_icon" src={require('../../../static/list/icon-list-right-arrow.svg')} />
</View>
</View>
<View className="divider"></View>
</View>
{/* 性别 */}
<View className="form_group">
<View className="form_item">
<View className="item_left">
<Image className="item_icon" src={require('../../../static/userInfo/user2.svg')} />
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text className="item_value">{form_data.gender || '男'}</Text>
<Image className="arrow_icon" src={require('../../../static/list/icon-list-right-arrow.svg')} />
</View>
</View>
<View className="divider"></View>
</View>
{/* 生日 */}
<View className="form_group">
<View className="form_item">
<View className="item_left">
<Image className="item_icon" src={require('../../../static/userInfo/tennis.svg')} />
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text className="item_value">{form_data.birthday}</Text>
<Image className="arrow_icon" src={require('../../../static/list/icon-list-right-arrow.svg')} />
</View>
</View>
</View>
</View>
{/* 简介编辑 */}
<View className="form_section">
<View className="form_group">
<View className="form_item" onClick={() => handle_open_edit_modal('bio')}>
<View className="item_left">
<Image className="item_icon" src={require('../../../static/userInfo/message.svg')} />
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text className="item_value">
{form_data.bio || '介绍一下自己'}
</Text>
<Image className="arrow_icon" src={require('../../../static/list/icon-list-right-arrow.svg')} />
</View>
</View>
</View>
</View>
{/* 地区、NTRP水平、职业 */}
<View className="form_section">
<View className="form_group">
{/* 地区 */}
<View className="form_item">
<View className="item_left">
<Image className="item_icon" src={require('../../../static/userInfo/location.svg')} />
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text className="item_value">{form_data.location || '上海 黄浦'}</Text>
<Image className="arrow_icon" src={require('../../../static/list/icon-list-right-arrow.svg')} />
</View>
</View>
<View className="divider"></View>
{/* NTRP水平 */}
<View className="form_item">
<View className="item_left">
<Image className="item_icon" src={require('../../../static/userInfo/tennis.svg')} />
<Text className="item_label">NTRP </Text>
</View>
<View className="item_right">
<Text className="item_value">{form_data.ntrp_level}</Text>
<Image className="arrow_icon" src={require('../../../static/list/icon-list-right-arrow.svg')} />
</View>
</View>
<View className="divider"></View>
{/* 职业 */}
<View className="form_item">
<View className="item_left">
<Image className="item_icon" src={require('../../../static/userInfo/sc.svg')} />
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text className="item_value">{form_data.occupation || '互联网'}</Text>
<Image className="arrow_icon" src={require('../../../static/list/icon-list-right-arrow.svg')} />
</View>
</View>
</View>
</View>
{/* 手机号 */}
<View className="form_section">
<View className="form_group">
<View className="form_item">
<View className="item_left">
<Image className="item_icon" src={require('../../../static/userInfo/message.svg')} />
<Text className="item_label"></Text>
</View>
<View className="item_right">
<Text className="item_value">{form_data.phone || '+86 130 1234 1234'}</Text>
<Image className="arrow_icon" src={require('../../../static/list/icon-list-right-arrow.svg')} />
</View>
</View>
<View className="divider"></View>
</View>
</View>
{/* 退出登录 */}
<View className="logout_section">
<View className="logout_button" onClick={handle_logout}>
<Text className="logout_text">退</Text>
</View>
</View>
</>
)}
</ScrollView>
{/* 编辑弹窗 */}
<EditModal
visible={edit_modal_visible}
title="编辑简介"
placeholder="介绍一下你的喜好,或者训练习惯"
initialValue={form_data[editing_field as keyof typeof form_data] || ''}
maxLength={100}
onSave={handle_edit_modal_save}
onCancel={handle_edit_modal_cancel}
validationMessage="请填写 2-100 个字符"
/>
</View>
);
};
export default EditProfilePage;

View File

@@ -0,0 +1,13 @@
import { View, } from '@tarojs/components';
const OrderPage: React.FC = () => {
return (
<View className="myself_page">
</View>)
}
export default OrderPage;

View File

@@ -1,3 +1,5 @@
@use '../../../scss/common.scss' as *;
// 个人页面样式
.myself_page {
min-height: 100vh;
@@ -23,59 +25,27 @@
flex-direction: column;
gap: 16px;
margin-bottom: 16px;
margin-top: 98px;
// 基本信息
.basic_info {
// 加载状态
.loading_container {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
padding: 40px 0;
.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);
}
.loading_text {
@include text-style(16px, 400, 1.4em);
color: $color-primary-lightest;
}
}
// 统计数据
.stats_section {
display: flex;
justify-content: space-between;
justify-content: flex-start;
align-items: center;
gap: 24px;
.stats_container {
display: flex;
@@ -88,127 +58,11 @@
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);
@include text-medium;
}
.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;
@include text-small;
}
}
}
@@ -226,14 +80,7 @@
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;
@include tag-base;
.tag_icon {
width: 12px;
@@ -241,22 +88,13 @@
}
.tag_text {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 11px;
line-height: 1.8em;
letter-spacing: -2.1%;
color: #000000;
@include text-tag;
}
}
}
.bio_text {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.571em;
color: rgba(0, 0, 0, 0.65);
@include text-body;
white-space: pre-line;
}
}
@@ -266,18 +104,15 @@
margin-bottom: 16px;
.action_card {
@include card-base;
display: flex;
align-items: center;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06);
overflow: hidden;
.action_content {
flex: 1;
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
@@ -295,18 +130,15 @@
}
.action_text {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 15px;
line-height: 1.4em;
color: #000000;
@include text-style(15px, 600, 1.4em);
color: $color-primary;
}
}
.action_divider {
width: 1px;
height: 16px;
background: rgba(0, 0, 0, 0.06);
background: $color-primary-lightest-5;
}
}
}
@@ -327,24 +159,19 @@
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);
@include text-primary;
transition: color 0.3s ease;
}
&.active {
.tab_text {
color: #000000;
color: $color-primary;
}
}
&:not(.active) {
.tab_text {
color: rgba(0, 0, 0, 0.2);
color: $color-primary-lightest-2;
}
}
}
@@ -361,30 +188,18 @@
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);
@include text-style(14px, 600, 1.4em, 2.71%);
color: $color-primary-light;
}
.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);
@include text-style(18px, 400, 1.4em, 2.11%);
color: $color-primary-lightest;
}
.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);
@include text-style(14px, 600, 1.4em, 2.71%);
color: $color-primary-light;
}
}
@@ -396,11 +211,8 @@
padding: 0 5px 15px;
.game_card {
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.08);
border-radius: 20px;
@include card-base;
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;
@@ -417,11 +229,8 @@
padding: 12px 15px 0;
.game_title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 16px;
line-height: 1.5em;
color: #000000;
@include text-style(16px, 600, 1.5em);
color: $color-primary;
}
.game_type_icon {
@@ -440,11 +249,7 @@
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);
@include text-caption;
}
}
@@ -458,19 +263,12 @@
.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);
@include text-caption;
}
.separator {
font-family: 'PingFang SC';
font-weight: 400;
font-size: 14px;
line-height: 1.3em;
color: rgba(60, 60, 67, 0.3);
@include text-style(14px, 400, 1.3em);
color: $color-text-tertiary;
}
}
@@ -488,7 +286,7 @@
width: 56.44px;
height: 56.44px;
border-radius: 9px;
border: 1.5px solid #FFFFFF;
border: 1.5px solid $color-white;
&:nth-child(1) {
top: 4.18px;
@@ -528,30 +326,17 @@
gap: -8px;
.participant_avatar {
width: 20px;
height: 20px;
border-radius: 50%;
border: 1px solid #FFFFFF;
@include avatar-base(20px);
border: 1px solid $color-white;
}
}
.participants_count {
background: #FFFFFF;
border: 0.5px solid rgba(0, 0, 0, 0.16);
border-radius: 999px;
@include tag-base;
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;
@include text-tag;
}
}
}
@@ -561,22 +346,10 @@
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;
@include tag-base;
.tag_text {
font-family: 'PingFang SC';
font-weight: 500;
font-size: 11px;
line-height: 1.8em;
letter-spacing: -2.1%;
color: #000000;
@include text-tag;
}
}
}
@@ -594,7 +367,7 @@
transform: translateX(-50%);
width: 140px;
height: 5px;
background: #000000;
background: $color-primary;
border-radius: 2.5px;
z-index: 10;
}

View File

@@ -1,47 +1,13 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Image, ScrollView, Button } from '@tarojs/components';
import { View, Text, Image, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro';
import './index.scss';
import GuideBar from '@/components/GuideBar'
// 用户信息接口
interface UserInfo {
id: string;
nickname: string;
avatar: string;
join_date: string;
stats: {
following: number;
friends: number;
hosted: number;
participated: number;
};
tags: string[];
bio: string;
location: string;
occupation: string;
ntrp_level: string;
}
// 球局记录接口
interface GameRecord {
id: string;
title: string;
date: string;
time: string;
duration: string;
location: string;
type: string;
distance: string;
participants: {
avatar: string;
nickname: string;
}[];
max_participants: number;
current_participants: number;
level_range: string;
game_type: string;
images: string[];
}
import { UserInfoCard, UserInfo } from '@/components/UserInfo/index'
import { UserService } from '@/services/userService'
import ListContainer from '@/container/listContainer'
import { TennisMatch } from '../../../../types/list/types'
import { withAuth } from '@/components';
const MyselfPage: React.FC = () => {
// 获取页面参数
@@ -51,51 +17,28 @@ const MyselfPage: React.FC = () => {
// 判断是否为当前用户
const is_current_user = !user_id;
// 模拟用户数据
const [user_info] = useState<UserInfo>({
// 用户信息状态
const [user_info, set_user_info] = useState<UserInfo>({
id: '1',
nickname: '188的王晨',
nickname: '加载中...',
avatar: require('../../../static/userInfo/default_avatar.svg'),
join_date: '2025年9月加入',
join_date: '加载中...',
stats: {
following: 124,
friends: 24,
hosted: 7,
participated: 24
following: 0,
friends: 0,
hosted: 0,
participated: 0
},
tags: ['上海黄浦', '互联网从业者', 'NTRP 4.0'],
bio: '网球入坑两年,偏好双打,正手进攻型选手\n平时在张江、世纪公园附近活动欢迎约球\n不卷分数但认真对待每一拍每一场球都想打得开心。有时候也会带相机来拍点照片📸',
location: '上海黄浦',
occupation: '互联网从业者',
ntrp_level: 'NTRP 4.0'
tags: ['加载中...'],
bio: '加载中...',
location: '加载中...',
occupation: '加载中...',
ntrp_level: 'NTRP 3.0'
});
// 模拟球局数据
const [game_records] = useState<GameRecord[]>([
{
id: '1',
title: '女生轻松双打',
date: '明天(周五)',
time: '下午5点',
duration: '2小时',
location: '仁恒河滨花园网球场',
type: '室外',
distance: '3.5km',
participants: [
{ avatar: require('../../../static/userInfo/user1.svg'), nickname: '用户1' },
{ avatar: require('../../../static/userInfo/user2.svg'), nickname: '用户2' }
],
max_participants: 4,
current_participants: 2,
level_range: '2.0 至 2.5',
game_type: '双打',
images: [
require('../../../static/userInfo/game1.svg'),
require('../../../static/userInfo/game2.svg'),
require('../../../static/userInfo/game3.svg')
]
}
]);
// 球局记录状态
const [game_records, set_game_records] = useState<TennisMatch[]>([]);
const [loading, set_loading] = useState(true);
// 关注状态
const [is_following, setIsFollowing] = useState(false);
@@ -103,168 +46,136 @@ const MyselfPage: React.FC = () => {
// 当前激活的标签页
const [active_tab, setActiveTab] = useState<'hosted' | 'participated'>('hosted');
// 加载用户数据
const load_user_data = async () => {
try {
set_loading(true);
// 获取用户信息(包含统计数据)
const user_data = await UserService.get_user_info(user_id);
set_user_info(user_data);
// 获取球局记录
let games_data;
if (active_tab === 'hosted') {
games_data = await UserService.get_hosted_games(user_id || '1');
} else {
games_data = await UserService.get_participated_games(user_id || '1');
}
set_game_records(games_data);
} catch (error) {
console.error('加载用户数据失败:', error);
Taro.showToast({
title: '加载失败,请重试',
icon: 'error',
duration: 2000
});
} finally {
set_loading(false);
}
};
// 页面加载时获取数据
useEffect(() => {
load_user_data();
}, [user_id]);
// 切换标签页时重新加载球局数据
useEffect(() => {
if (!loading) {
load_game_data();
}
}, [active_tab]);
// 加载球局数据
const load_game_data = async () => {
try {
let games_data;
if (active_tab === 'hosted') {
games_data = await UserService.get_hosted_games(user_id || '1');
} else {
games_data = await UserService.get_participated_games(user_id || '1');
}
set_game_records(games_data);
} catch (error) {
console.error('加载球局数据失败:', error);
}
};
// 处理关注/取消关注
const handle_follow = () => {
setIsFollowing(!is_following);
Taro.showToast({
title: is_following ? '已取消关注' : '关注成功',
icon: 'success',
duration: 1500
});
const handle_follow = async () => {
try {
const new_following_state = await UserService.toggle_follow(user_id || '1', is_following);
setIsFollowing(new_following_state);
Taro.showToast({
title: new_following_state ? '关注成功' : '已取消关注',
icon: 'success',
duration: 1500
});
} catch (error) {
console.error('关注操作失败:', error);
Taro.showToast({
title: '操作失败,请重试',
icon: 'error',
duration: 2000
});
}
};
// 处理分享
const handle_share = () => {
Taro.showShareMenu({
withShareTicket: true
});
};
// 处理返回
const handle_back = () => {
Taro.navigateBack();
};
// 处理编辑资料
const handle_edit_profile = () => {
Taro.navigateTo({
url: '/pages/userInfo/edit/index'
});
};
// 处理球局详情
const handle_game_detail = (game_id: string) => {
Taro.navigateTo({
url: `/pages/game/detail/index?id=${game_id}`
});
};
// 处理球局订单
const handle_game_orders = () => {
Taro.navigateTo({
url: '/pages/game/orders/index'
url: '/pages/userInfo/orders/index'
});
};
// 处理收藏
const handle_favorites = () => {
Taro.navigateTo({
url: '/pages/game/favorites/index'
url: '/pages/userInfo/favorites/index'
});
};
return (
<View className="myself_page">
{/* 主要内容 */}
<ScrollView className="main_content" scrollY>
<View className='main_content'>
{/* 用户信息区域 */}
<View className="user_info_section">
{/* 头像和基本信息 */}
<View className="basic_info">
<View className="avatar_container">
<Image className="avatar" src={user_info.avatar} />
{loading ? (
<View className="loading_container">
<Text className="loading_text">...</Text>
</View>
<View className="info_container">
<Text className="nickname">{user_info.nickname}</Text>
<Text className="join_date">{user_info.join_date}</Text>
</View>
</View>
{/* 统计数据 */}
<View className="stats_section">
<View className="stats_container">
<View className="stat_item">
<Text className="stat_number">{user_info.stats.following}</Text>
<Text className="stat_label"></Text>
</View>
<View className="stat_item">
<Text className="stat_number">{user_info.stats.friends}</Text>
<Text className="stat_label"></Text>
</View>
<View className="stat_item">
<Text className="stat_number">{user_info.stats.hosted}</Text>
<Text className="stat_label"></Text>
</View>
<View className="stat_item">
<Text className="stat_number">{user_info.stats.participated}</Text>
<Text className="stat_label"></Text>
</View>
</View>
<View className="action_buttons">
{/* 只有非当前用户才显示关注按钮 */}
{!is_current_user && (
<Button
className={`follow_button ${is_following ? 'following' : ''}`}
onClick={handle_follow}
>
<Image
className="button_icon"
src={require('../../../static/userInfo/plus.svg')}
/>
<Text className="button_text">
{is_following ? '已关注' : '关注'}
</Text>
</Button>
)}
{/* 只有非当前用户才显示消息按钮 */}
{!is_current_user && (
<Button className="message_button">
<Image
className="button_icon"
src={require('../../../static/userInfo/message.svg')}
/>
</Button>
)}
{/* 只有当前用户才显示编辑按钮 */}
{is_current_user && (
<Button className="edit_button" onClick={handle_edit_profile}>
<Text className="button_text"></Text>
</Button>
)}
{/* 只有当前用户才显示分享按钮 */}
{is_current_user && (
<Button className="share_button" onClick={handle_share}>
<Text className="button_text"></Text>
</Button>
)}
</View>
</View>
{/* 标签和简介 */}
<View className="tags_bio_section">
<View className="tags_container">
<View className="tag_item">
<Image
className="tag_icon"
src={require('../../../static/userInfo/location.svg')}
/>
<Text className="tag_text">{user_info.location}</Text>
</View>
<View className="tag_item">
<Text className="tag_text">{user_info.occupation}</Text>
</View>
<View className="tag_item">
<Text className="tag_text">{user_info.ntrp_level}</Text>
</View>
</View>
<Text className="bio_text">{user_info.bio}</Text>
</View>
) : (
<UserInfoCard
user_info={user_info}
is_current_user={is_current_user}
is_following={is_following}
on_follow={handle_follow}
/>
)}
{/* 球局订单和收藏功能 */}
<View className="quick_actions_section">
<View className="action_card">
<View className="action_content" onClick={handle_game_orders}>
<Image
className="action_icon"
src={require('../../../static/userInfo/tennis.svg')}
src={require('../../../static/userInfo/order_btn.svg')}
/>
<Text className="action_text"></Text>
<Text className="action_text"></Text>
</View>
<View className="action_divider"></View>
<View className="action_content" onClick={handle_favorites}>
<Image
className="action_icon"
src={require('../../../static/userInfo/tennis.svg')}
src={require('../../../static/userInfo/sc.svg')}
/>
<Text className="action_text"></Text>
</View>
@@ -286,93 +197,20 @@ const MyselfPage: React.FC = () => {
{/* 球局列表 */}
<View className="game_list_section">
<View className="date_header">
<Text className="date_text">528</Text>
<Text className="separator">/</Text>
<Text className="weekday_text"></Text>
</View>
{/* 球局卡片 */}
<View className="game_cards">
{game_records.map((game) => (
<View
key={game.id}
className="game_card"
onClick={() => handle_game_detail(game.id)}
>
{/* 球局标题和类型 */}
<View className="game_header">
<Text className="game_title">{game.title}</Text>
<View className="game_type_icon">
<Image
className="type_icon"
src={require('../../../static/userInfo/tennis.svg')}
/>
</View>
</View>
{/* 球局时间 */}
<View className="game_time">
<Text className="time_text">
{game.date} {game.time} {game.duration}
</Text>
</View>
{/* 球局地点和类型 */}
<View className="game_location">
<Text className="location_text">{game.location}</Text>
<Text className="separator">·</Text>
<Text className="type_text">{game.type}</Text>
<Text className="separator">·</Text>
<Text className="distance_text">{game.distance}</Text>
</View>
{/* 球局图片 */}
<View className="game_images">
{game.images.map((image, index) => (
<Image
key={index}
className="game_image"
src={image}
/>
))}
</View>
{/* 球局信息标签 */}
<View className="game_tags">
<View className="participants_info">
<View className="avatars">
{game.participants.map((participant, index) => (
<Image
key={index}
className="participant_avatar"
src={participant.avatar}
/>
))}
</View>
<View className="participants_count">
<Text className="count_text">
{game.current_participants}/{game.max_participants}
</Text>
</View>
</View>
<View className="game_info_tags">
<View className="info_tag">
<Text className="tag_text">{game.level_range}</Text>
</View>
<View className="info_tag">
<Text className="tag_text">{game.game_type}</Text>
</View>
</View>
</View>
</View>
))}
</View>
<ScrollView scrollY>
<ListContainer
data={game_records}
recommendList={[]}
loading={loading}
error={null}
reload={load_game_data}
/>
</ScrollView>
</View>
</ScrollView>
</View>
<GuideBar currentPage='personal' />
</View>
);
};
export default MyselfPage;
export default withAuth(MyselfPage);

View File

@@ -0,0 +1,13 @@
import { View, } from '@tarojs/components';
const OrderPage: React.FC = () => {
return (
<View className="myself_page">
</View>)
}
export default OrderPage;

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '用户主页',
navigationStyle: 'custom'
})

View 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;
}
}
}
}
}
}
}
}

View 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">
{/* 主要内容 */}
<View className="main_content" >
{/* 用户信息区域 */}
<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">529</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>
</View>
<GuideBar currentPage='personal' />
</View>
);
};
export default OtherUserPage;

121
src/scss/common.scss Normal file
View File

@@ -0,0 +1,121 @@
// 全局通用样式变量和混入
// 字体相关
$font-family-primary: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !default;
// 颜色相关
$color-primary: #000000 !default;
$color-primary-light: rgba(0, 0, 0, 0.85) !default;
$color-primary-medium: rgba(0, 0, 0, 0.65) !default;
$color-primary-lightest: rgba(0, 0, 0, 0.35) !default;
$color-primary-lightest-2: rgba(0, 0, 0, 0.2) !default;
$color-primary-lightest-3: rgba(0, 0, 0, 0.16) !default;
$color-primary-lightest-4: rgba(0, 0, 0, 0.1) !default;
$color-primary-lightest-5: rgba(0, 0, 0, 0.06) !default;
$color-primary-lightest-6: rgba(0, 0, 0, 0.08) !default;
$color-text-secondary: rgba(60, 60, 67, 0.6) !default;
$color-text-tertiary: rgba(60, 60, 67, 0.3) !default;
$color-white: #FFFFFF !default;
$color-background: #FAFAFA !default;
// 字体大小和行高混入
@mixin text-style($size: 14px, $weight: 400, $line-height: 1.4em, $letter-spacing: 0) {
font-family: $font-family-primary;
font-size: $size;
font-weight: $weight;
line-height: $line-height;
letter-spacing: $letter-spacing;
}
// 常用文本样式
@mixin text-primary {
@include text-style(20px, 600, 1.4em, 1.9%);
color: $color-primary;
}
@mixin text-secondary {
@include text-style(14px, 400, 1.4em, 2.7%);
color: $color-primary-lightest;
}
@mixin text-small {
@include text-style(12px, 500, 1.4em, 3.2%);
color: $color-primary-lightest;
}
@mixin text-medium {
@include text-style(18px, 600, 1.4em, 2.1%);
color: $color-primary-light;
}
@mixin text-body {
@include text-style(14px, 400, 1.571em, 2.7%);
color: $color-primary-medium;
}
@mixin text-caption {
@include text-style(12px, 400, 1.5em);
color: $color-text-secondary;
}
@mixin text-tag {
@include text-style(11px, 500, 1.8em, -2.1%);
color: $color-primary;
}
// 按钮样式混入
@mixin button-base {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
border: none;
outline: none;
}
@mixin button-primary {
@include button-base;
background: $color-primary;
border: 0.5px solid $color-primary-lightest-5;
border-radius: 999px;
color: $color-white;
}
@mixin button-secondary {
@include button-base;
background: $color-white;
border: 0.5px solid $color-primary-lightest-3;
border-radius: 999px;
color: $color-primary;
}
// 卡片样式混入
@mixin card-base {
background: $color-white;
border: 0.5px solid $color-primary-lightest-6;
border-radius: 20px;
box-shadow: 0px 4px 36px 0px $color-primary-lightest-5;
}
// 标签样式混入
@mixin tag-base {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
height: 20px;
background: $color-white;
border: 0.5px solid $color-primary-lightest-3;
border-radius: 999px;
}
// 头像样式混入
@mixin avatar-base($size: 64px) {
width: $size;
height: $size;
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);
}

View File

@@ -20,6 +20,12 @@ export interface GameDetail {
updated_at: string,
}
export enum MATCH_STATUS {
NOT_STARTED = 0, // 未开始
IN_PROGRESS = 1, //进行中
FINISHED = 2 //已结束
}
// 响应接口
export interface Response {
code: string

View File

@@ -353,7 +353,7 @@ export const refresh_login_status = async (): Promise<boolean> => {
// 获取用户详细信息
export const fetchUserProfile = async (): Promise<ApiResponse<UserInfoType>> => {
try {
const response = await httpService.post('/user/detail');
const response = await httpService.post('user/detail');
return response;
} catch (error) {
console.error('获取用户信息失败:', error);
@@ -364,7 +364,7 @@ export const fetchUserProfile = async (): Promise<ApiResponse<UserInfoType>> =>
// 更新用户信息
export const updateUserProfile = async (payload: Partial<UserInfoType>) => {
try {
const response = await httpService.post('user/update', payload);
const response = await httpService.post('/user/update', payload);
return response;
} catch (error) {
console.error('更新用户信息失败:', error);

View File

@@ -0,0 +1,46 @@
import httpService from './httpService'
import type { ApiResponse } from './httpService'
import { requestPayment } from '@tarojs/taro'
export interface SignType {
/** 仅在微信支付 v2 版本接口适用 */
MD5
/** 仅在微信支付 v2 版本接口适用 */
'HMAC-SHA256'
/** 仅在微信支付 v3 版本接口适用 */
RSA
}
export interface PayMentParams {
order_id: number,
order_no: string,
status: number,
appId: string,
timeStamp: string,
nonceStr: string,
package: string,
signType: keyof SignType,
paySign: string
}
// 用户接口
export interface OrderResponse {
participant_id: number,
payment_required: boolean,
payment_params: PayMentParams
}
// 发布球局类
class OrderService {
// 用户登录
async createOrder(game_id: number): Promise<ApiResponse<OrderResponse>> {
return httpService.post('/payment/create_order', { game_id }, {
showLoading: true,
})
}
// async getOrderInfo()
}
// 导出认证服务实例
export default new OrderService()

549
src/services/userService.ts Normal file
View File

@@ -0,0 +1,549 @@
import { UserInfo } from '@/components/UserInfo';
import Taro from '@tarojs/taro';
import { API_CONFIG } from '@/config/api';
import httpService from './httpService';
// 用户详情接口
interface UserDetailData {
id: number;
openid: string;
user_code: string | null;
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: number;
longitude: number;
subscribe_time: string;
last_login_time: string;
create_time: string;
last_modify_time: string;
stats: {
followers_count: number;
following_count: number;
hosted_games_count: number;
participated_games_count: number;
};
}
// 更新用户信息参数接口
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;
};
}
// 后端球局数据接口
interface BackendGameData {
id: number;
title: string;
description: string;
game_type?: string;
play_type: string;
publisher_id?: string;
venue_id?: string;
max_players?: number;
current_players?: number;
price: string;
price_mode: string;
court_type: string;
court_surface: string;
gender_limit?: string;
skill_level_min: string;
skill_level_max: string;
start_time: string;
end_time: string;
location_name: string | null;
location: string;
latitude?: number;
longitude?: number;
image_list?: string[];
description_tag?: string[];
venue_description_tag?: string[];
venue_image_list?: Array<{ id: string; url: string }>;
participant_count: number;
max_participants: number;
participant_info?: {
id: number;
status: string;
payment_status: string;
joined_at: string;
deposit_amount: number;
join_message: string;
skill_level: string;
contact_info: string;
};
venue_dtl?: {
id: number;
name: string;
address: string;
latitude: string;
longitude: string;
venue_type: string;
surface_type: string;
};
}
// 用户服务类
export class UserService {
// 数据转换函数将后端数据转换为ListContainer期望的格式
private static transform_game_data(backend_data: BackendGameData[]): any[] {
return backend_data.map(game => {
// 处理时间格式
const start_time = new Date(game.start_time);
const date_time = this.format_date_time(start_time);
// 处理技能等级
const skill_level = this.format_skill_level(game.skill_level_min, game.skill_level_max);
// 处理图片数组 - 兼容两种数据格式
let images: string[] = [];
if (game.image_list && game.image_list.length > 0) {
images = game.image_list.filter(img => img && img.trim() !== '');
} else if (game.venue_image_list && game.venue_image_list.length > 0) {
images = game.venue_image_list
.filter(img => img && img.url && img.url.trim() !== '')
.map(img => img.url);
}
// 处理距离 - 优先使用venue_dtl中的坐标其次使用game中的坐标
let latitude = game.latitude || 0;
let longitude = game.longitude || 0;
if (game.venue_dtl) {
latitude = parseFloat(game.venue_dtl.latitude) || latitude;
longitude = parseFloat(game.venue_dtl.longitude) || longitude;
}
const distance = this.calculate_distance(latitude, longitude);
// 处理地点信息 - 优先使用venue_dtl中的信息
let location = game.location_name || game.location || '未知地点';
if (game.venue_dtl && game.venue_dtl.name) {
location = game.venue_dtl.name;
}
// 处理人数统计 - 兼容不同的字段名
const registered_count = game.current_players || game.participant_count || 0;
const max_count = game.max_players || game.max_participants || 0;
return {
id: game.id,
title: game.title || '未命名球局',
dateTime: date_time,
location: location,
distance: distance,
registeredCount: registered_count,
maxCount: max_count,
skillLevel: skill_level,
matchType: game.play_type || '不限',
images: images,
shinei: game.court_type || '未知'
};
});
}
// 格式化时间显示
private static format_date_time(start_time: Date): string {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
const day_after_tomorrow = new Date(today.getTime() + 2 * 24 * 60 * 60 * 1000);
const start_date = new Date(start_time.getFullYear(), start_time.getMonth(), start_time.getDate());
let date_str = '';
if (start_date.getTime() === today.getTime()) {
date_str = '今天';
} else if (start_date.getTime() === tomorrow.getTime()) {
date_str = '明天';
} else if (start_date.getTime() === day_after_tomorrow.getTime()) {
date_str = '后天';
} else {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
date_str = weekdays[start_time.getDay()];
}
const time_str = `${start_time.getHours().toString().padStart(2, '0')}:${start_time.getMinutes().toString().padStart(2, '0')}`;
return `${date_str} ${time_str}`;
}
// 格式化技能等级
private static format_skill_level(min: string, max: string): string {
const min_num = parseInt(min) || 0;
const max_num = parseInt(max) || 0;
if (min_num === 0 && max_num === 0) {
return '不限';
}
if (min_num === max_num) {
return `${min_num}.0`;
}
return `${min_num}.0-${max_num}.0`;
}
// 计算距离(模拟实现,实际需要根据用户位置计算)
private static calculate_distance(latitude: number, longitude: number): string {
if (latitude === 0 && longitude === 0) {
return '未知距离';
}
// 这里应该根据用户当前位置计算实际距离
// 暂时返回模拟距离
const distances = ['1.2km', '2.5km', '3.8km', '5.1km', '7.3km'];
return distances[Math.floor(Math.random() * distances.length)];
}
// 获取用户信息
static async get_user_info(user_id?: string): Promise<UserInfo> {
try {
const response = await httpService.post<UserDetailData>(API_CONFIG.USER.DETAIL, user_id ? { user_id } : {}, {
needAuth: false,
showLoading: false
});
if (response.code === 0) {
const userData = response.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: userData.stats?.following_count || 0,
friends: userData.stats?.followers_count || 0,
hosted: userData.stats?.hosted_games_count || 0,
participated: userData.stats?.participated_games_count || 0
},
tags: [
userData.city || '未知地区',
userData.province || '未知省份',
'NTRP 3.0' // 默认等级,需要其他接口获取
],
bio: '这个人很懒,什么都没有写...',
location: userData.city || '未知地区',
occupation: '未知职业', // 需要其他接口获取
ntrp_level: 'NTRP 3.0', // 需要其他接口获取
phone: userData.phone || '',
gender: userData.gender || ''
};
} else {
throw new Error(response.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',
phone: '',
gender: ''
};
}
}
// 获取用户主办的球局
static async get_hosted_games(user_id: string): Promise<any[]> {
try {
const response = await httpService.post<any>(API_CONFIG.USER.HOSTED_GAMES, {
user_id
}, {
needAuth: false,
showLoading: false
});
if (response.code === 0) {
// 使用数据转换函数将后端数据转换为ListContainer期望的格式
return this.transform_game_data(response.data.rows || []);
} else {
throw new Error(response.message || '获取主办球局失败');
}
} catch (error) {
console.error('获取主办球局失败:', error);
// 返回符合ListContainer data格式的模拟数据
return [
{
id: 1,
title: '女生轻松双打',
dateTime: '明天(周五) 下午5点',
location: '仁恒河滨花园网球场',
distance: '3.5km',
registeredCount: 2,
maxCount: 4,
skillLevel: '2.0-2.5',
matchType: '双打',
images: [
require('../static/userInfo/game1.svg'),
require('../static/userInfo/game2.svg'),
require('../static/userInfo/game3.svg')
],
shinei: '室外'
},
{
id: 5,
title: '新手友好局',
dateTime: '周日 下午2点',
location: '徐汇网球中心',
distance: '1.8km',
registeredCount: 4,
maxCount: 6,
skillLevel: '1.5-2.0',
matchType: '双打',
images: [
require('../static/userInfo/game1.svg'),
require('../static/userInfo/game2.svg')
],
shinei: '室外'
}
];
}
}
// 获取用户参与的球局
static async get_participated_games(user_id: string): Promise<any[]> {
try {
const response = await httpService.post<any>(API_CONFIG.USER.PARTICIPATED_GAMES, {
user_id
}, {
needAuth: false,
showLoading: false
});
if (response.code === 0) {
// 使用数据转换函数将后端数据转换为ListContainer期望的格式
return this.transform_game_data(response.data.rows || []);
} else {
throw new Error(response.message || '获取参与球局失败');
}
} catch (error) {
console.error('获取参与球局失败:', error);
// 返回符合ListContainer data格式的模拟数据
return [
{
id: 2,
title: '周末双打练习',
dateTime: '后天(周六) 上午10点',
location: '上海网球中心',
distance: '5.2km',
registeredCount: 6,
maxCount: 8,
skillLevel: '3.0-3.5',
matchType: '双打',
images: [
require('../static/userInfo/game2.svg'),
require('../static/userInfo/game3.svg')
],
shinei: '室内'
},
{
id: 3,
title: '晨练单打',
dateTime: '明天(周五) 早上7点',
location: '浦东网球俱乐部',
distance: '2.8km',
registeredCount: 1,
maxCount: 2,
skillLevel: '2.5-3.0',
matchType: '单打',
images: [
require('../static/userInfo/game1.svg')
],
shinei: '室外'
},
{
id: 4,
title: '夜场混双',
dateTime: '今晚 晚上8点',
location: '虹桥网球中心',
distance: '4.1km',
registeredCount: 3,
maxCount: 4,
skillLevel: '3.5-4.0',
matchType: '混双',
images: [
require('../static/userInfo/game1.svg'),
require('../static/userInfo/game2.svg'),
require('../static/userInfo/game3.svg')
],
shinei: '室内'
}
];
}
}
// 获取用户球局记录(兼容旧方法)
static async get_user_games(user_id: string, type: 'hosted' | 'participated'): Promise<any[]> {
if (type === 'hosted') {
return this.get_hosted_games(user_id);
} else {
return this.get_participated_games(user_id);
}
}
// 关注/取消关注用户
static async toggle_follow(user_id: string, is_following: boolean): Promise<boolean> {
try {
const endpoint = is_following ? API_CONFIG.USER.UNFOLLOW : API_CONFIG.USER.FOLLOW;
const response = await httpService.post<any>(endpoint, { user_id }, {
needAuth: false,
showLoading: true,
loadingText: is_following ? '取消关注中...' : '关注中...'
});
if (response.code === 0) {
return !is_following;
} else {
throw new Error(response.message || '操作失败');
}
} catch (error) {
console.error('关注操作失败:', error);
throw error;
}
}
// 保存用户信息
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 httpService.post<any>(API_CONFIG.USER.UPDATE, updateParams, {
needAuth: false,
showLoading: true,
loadingText: '保存中...'
});
if (response.code === 0) {
return true;
} else {
throw new Error(response.message || '更新用户信息失败');
}
} catch (error) {
console.error('保存用户信息失败:', error);
throw error;
}
}
// 获取用户动态
static async get_user_activities(user_id: string, page: number = 1, limit: number = 10): Promise<any[]> {
try {
const response = await httpService.post<any>('/user/activities', {
user_id,
page,
limit
}, {
needAuth: false,
showLoading: false
});
if (response.code === 0) {
return response.data.activities || [];
} else {
throw new Error(response.message || '获取用户动态失败');
}
} catch (error) {
console.error('获取用户动态失败:', error);
return [];
}
}
// 上传头像
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 { code: number; message: string; data: 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');
}
}
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.33334 4L10.3333 8L6.33334 12" stroke="#3C3C43" stroke-opacity="0.6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 255 B

View File

@@ -0,0 +1,6 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="20" fill="white"/>
<rect x="0.25" y="0.25" width="39.5" height="39.5" rx="19.75" stroke="black" stroke-opacity="0.1" stroke-width="0.5"/>
<path d="M12.9167 27.5H27.9167" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.5833 21.1333V24.1667H17.6321L26.2499 15.545L23.2062 12.5L14.5833 21.1333Z" stroke="black" stroke-width="1.66667" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.6666 8.00001C14.6666 7.63181 14.3681 7.33334 13.9999 7.33334C13.6317 7.33334 13.3333 7.63181 13.3333 8.00001H14.6666ZM7.99992 2.66668C8.36812 2.66668 8.66659 2.3682 8.66659 2.00001C8.66659 1.63182 8.36812 1.33334 7.99992 1.33334V2.66668ZM12.9999 13.3333H2.99992V14.6667H12.9999V13.3333ZM2.66659 13V3.00001H1.33325V13H2.66659ZM13.3333 8.00001V13H14.6666V8.00001H13.3333ZM2.99992 2.66668H7.99992V1.33334H2.99992V2.66668ZM2.99992 13.3333C2.81583 13.3333 2.66659 13.1841 2.66659 13H1.33325C1.33325 13.9205 2.07944 14.6667 2.99992 14.6667V13.3333ZM12.9999 14.6667C13.9204 14.6667 14.6666 13.9205 14.6666 13H13.3333C13.3333 13.1841 13.184 13.3333 12.9999 13.3333V14.6667ZM2.66659 3.00001C2.66659 2.81592 2.81582 2.66668 2.99992 2.66668V1.33334C2.07945 1.33334 1.33325 2.07953 1.33325 3.00001H2.66659Z" fill="white"/>
<path d="M2 11.6667L5.56437 8.39933C5.81297 8.17143 6.19263 8.16509 6.4487 8.38459L10.6667 12" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.33325 10.3333L10.9244 8.74218C11.159 8.50762 11.5304 8.48122 11.7958 8.68028L13.9999 10.3333" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 4H14" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 2V6" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,9 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="16" fill="black"/>
<rect x="0.25" y="0.25" width="31.5" height="31.5" rx="15.75" stroke="black" stroke-opacity="0.06" stroke-width="0.5"/>
<path d="M22.6666 16C22.6666 15.6318 22.3681 15.3333 21.9999 15.3333C21.6317 15.3333 21.3333 15.6318 21.3333 16H22.6666ZM15.9999 10.6667C16.3681 10.6667 16.6666 10.3682 16.6666 10C16.6666 9.63182 16.3681 9.33334 15.9999 9.33334V10.6667ZM20.9999 21.3333H10.9999V22.6667H20.9999V21.3333ZM10.6666 21V11H9.33325V21H10.6666ZM21.3333 16V21H22.6666V16H21.3333ZM10.9999 10.6667H15.9999V9.33334H10.9999V10.6667ZM10.9999 21.3333C10.8158 21.3333 10.6666 21.1841 10.6666 21H9.33325C9.33325 21.9205 10.0794 22.6667 10.9999 22.6667V21.3333ZM20.9999 22.6667C21.9204 22.6667 22.6666 21.9205 22.6666 21H21.3333C21.3333 21.1841 21.184 21.3333 20.9999 21.3333V22.6667ZM10.6666 11C10.6666 10.8159 10.8158 10.6667 10.9999 10.6667V9.33334C10.0794 9.33334 9.33325 10.0795 9.33325 11H10.6666Z" fill="white"/>
<path d="M10 19.6667L13.5644 16.3993C13.813 16.1714 14.1926 16.1651 14.4487 16.3846L18.6667 20" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.3333 18.3333L18.9244 16.7422C19.159 16.5076 19.5304 16.4812 19.7958 16.6803L21.9999 18.3333" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 12H22" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 10V14" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,5 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5208 2.91666H16.5834C17.0436 2.91666 17.4167 3.28975 17.4167 3.74999V17.5C17.4167 17.9602 17.0436 18.3333 16.5834 18.3333H4.91671C4.45647 18.3333 4.08337 17.9602 4.08337 17.5V3.74999C4.08337 3.28975 4.45647 2.91666 4.91671 2.91666H7.41671H7.83337V4.16666H13.6667V2.91666H14.5208Z" stroke="#8D8D8D" stroke-width="1.66667" stroke-linejoin="round"/>
<path d="M13.6667 1.66666H7.83337V4.16666H13.6667V1.66666Z" stroke="#8D8D8D" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.9998 7.91666L8.66663 11.2505H12.835L9.50008 14.5841" stroke="black" stroke-opacity="0.2" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 786 B

View File

@@ -0,0 +1,4 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.33325 3.33333C2.33325 2.8731 2.70635 2.5 3.16659 2.5H8.16659L10.2499 5H17.3333C17.7935 5 18.1666 5.37308 18.1666 5.83333V16.6667C18.1666 17.1269 17.7935 17.5 17.3333 17.5H3.16659C2.70635 17.5 2.33325 17.1269 2.33325 16.6667V3.33333Z" stroke="#8D8D8D" stroke-width="1.66667" stroke-linejoin="round"/>
<path d="M10.25 8.33334L11.1846 10.3803L13.4203 10.6366L11.7622 12.158L12.2093 14.3634L10.25 13.2567L8.29075 14.3634L8.73788 12.158L7.07983 10.6366L9.31546 10.3803L10.25 8.33334Z" stroke="black" stroke-opacity="0.2" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 703 B