This commit is contained in:
2026-02-12 20:50:01 +08:00
parent d049f682f5
commit 414d3334fd
82 changed files with 1835 additions and 422 deletions

View File

@@ -1,78 +1,361 @@
.activity-detail {
padding-bottom: 80px;
.activity-detail-page {
min-height: 100vh;
background-color: #050505;
color: #fff;
padding-bottom: 100px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
.cover {
width: 100%;
display: block;
.loading-container, .error-container {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #888;
font-size: 16px;
}
.content {
padding: 20px;
/* Hero Section */
.hero-section {
position: relative;
height: 320px;
width: 100%;
overflow: hidden;
.title {
font-size: 24px;
font-weight: bold;
display: block;
margin-bottom: 20px;
.hero-bg {
width: 100%;
height: 100%;
object-fit: cover;
}
.meta-box {
background: #f9f9f9;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to top, #050505 0%, rgba(5,5,5,0.6) 50%, rgba(0,0,0,0.2) 100%);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 24px;
box-sizing: border-box;
.meta-row {
display: flex;
margin-bottom: 8px;
.status-tag {
align-self: flex-start;
background: var(--primary-cyan, #00f0ff);
color: #000;
font-weight: 800;
padding: 4px 12px;
border-radius: 4px;
font-size: 14px;
color: #333;
margin-bottom: 12px;
box-shadow: 0 0 10px rgba(0, 240, 255, 0.4);
}
.hero-title {
font-size: 32px;
font-weight: 800;
color: #fff;
text-shadow: 0 2px 10px rgba(0,0,0,0.5);
line-height: 1.3;
}
}
}
.main-content {
padding: 0 24px;
transform: translateY(-20px);
position: relative;
z-index: 10;
}
/* Info Grid */
.info-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
margin-bottom: 32px;
.info-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 16px;
display: flex;
align-items: center;
.info-text {
margin-left: 16px;
display: flex;
flex-direction: column;
.label {
color: #666;
width: 50px;
flex-shrink: 0;
font-size: 12px;
color: rgba(255,255,255,0.5);
margin-bottom: 4px;
}
&:last-child {
margin-bottom: 0;
.value {
font-size: 16px;
color: #fff;
font-weight: 500;
}
}
}
}
/* Stats Section */
.stats-section {
background: #111;
border-radius: 20px;
padding: 24px;
margin-bottom: 32px;
border: 1px solid rgba(255,255,255,0.05);
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
.stats-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 16px;
.stats-title {
font-size: 18px;
font-weight: 700;
color: #fff;
}
.stats-count {
.current {
font-size: 24px;
font-weight: 800;
color: var(--primary-cyan, #00f0ff);
}
.divider {
font-size: 16px;
color: #666;
margin: 0 4px;
}
.max {
font-size: 18px;
color: #888;
}
}
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
border-left: 4px solid #00b96b;
padding-left: 10px;
}
.progress-bar-container {
height: 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
position: relative;
overflow: hidden;
.description {
font-size: 16px;
line-height: 1.6;
color: #333;
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #00f0ff, #bd00ff);
border-radius: 4px;
transition: width 0.5s ease-out;
}
.progress-glow {
position: absolute;
top: 0;
width: 10px;
height: 100%;
background: #fff;
filter: blur(4px);
opacity: 0.8;
transform: translateX(-50%);
transition: left 0.5s ease-out;
}
}
}
.footer-bar {
/* Detail Section */
.detail-section {
.section-header {
display: flex;
align-items: center;
margin-bottom: 24px;
.title-text {
font-size: 22px;
font-weight: 800;
color: #fff;
margin-right: 16px;
white-space: nowrap;
}
.line {
flex: 1;
height: 1px;
background: linear-gradient(90deg, rgba(255,255,255,0.2), transparent);
}
}
.rich-text-wrapper {
color: #ccc;
font-size: 16px;
line-height: 1.8;
image {
max-width: 100%;
border-radius: 12px;
margin: 16px 0;
}
/* Markdown Styling */
h1, h2, h3, h4, h5, h6 { margin-top: 24px; margin-bottom: 16px; color: #fff; font-weight: 700; line-height: 1.4; }
h1 { font-size: 24px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 8px; }
h2 { font-size: 20px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 6px; }
h3 { font-size: 18px; }
p { margin-bottom: 16px; }
strong { font-weight: 800; color: #fff; }
em { font-style: italic; color: #aaa; }
del { text-decoration: line-through; color: #666; }
ul, ol { margin-bottom: 16px; padding-left: 20px; }
li { margin-bottom: 6px; list-style-position: outside; }
ul li { list-style-type: disc; }
ol li { list-style-type: decimal; }
blockquote {
border-left: 4px solid var(--primary-cyan, #00f0ff);
background: rgba(255, 255, 255, 0.05);
padding: 12px 16px;
margin: 16px 0;
border-radius: 4px;
color: #bbb;
font-style: italic;
p { margin-bottom: 0; }
}
a { color: var(--primary-cyan, #00f0ff); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s; }
hr {
height: 1px;
background: rgba(255,255,255,0.1);
border: none;
margin: 24px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 14px;
overflow-x: auto;
display: block;
th, td {
border: 1px solid rgba(255,255,255,0.1);
padding: 10px;
text-align: left;
}
th {
background: rgba(255,255,255,0.05);
font-weight: 700;
color: #fff;
}
tr:nth-child(even) {
background: rgba(255,255,255,0.02);
}
}
code {
background: rgba(255,255,255,0.1);
padding: 3px 6px;
border-radius: 4px;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
color: #ff7875;
font-size: 14px;
margin: 0 4px;
}
pre {
background: #161616;
padding: 16px;
border-radius: 12px;
overflow-x: auto;
margin: 16px 0;
border: 1px solid #333;
box-shadow: inset 0 0 20px rgba(0,0,0,0.5);
code {
background: transparent;
color: #a6e22e;
padding: 0;
font-size: 14px;
margin: 0;
white-space: pre;
}
}
}
}
/* Footer Action Bar */
.footer-action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 10px 20px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
padding-bottom: calc(10px + env(safe-area-inset-bottom));
background: rgba(10, 10, 10, 0.9);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
padding: 16px 24px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid rgba(255,255,255,0.1);
z-index: 100;
.btn-signup {
width: 100%;
background: #00b96b;
.left-info {
display: flex;
flex-direction: column;
.price {
font-size: 24px;
font-weight: 800;
color: var(--primary-cyan, #00f0ff);
}
.desc {
font-size: 12px;
color: #888;
}
}
.action-btn {
margin: 0;
padding: 0 40px;
height: 56px;
line-height: 56px;
border-radius: 28px;
font-size: 18px;
font-weight: 700;
border: none;
&[disabled] {
background: #ccc;
color: #fff;
&.active {
background: linear-gradient(90deg, #00f0ff, #00b96b);
color: #000;
box-shadow: 0 4px 15px rgba(0, 240, 255, 0.3);
&:active {
transform: scale(0.98);
}
}
&.disabled {
background: rgba(255,255,255,0.1);
color: #666;
}
}
}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'
import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'
import Taro, { useRouter, useShareAppMessage, useDidShow } from '@tarojs/taro'
import { View, Text, Image, Button, RichText } from '@tarojs/components'
import { AtIcon, AtProgress, AtModal, AtModalHeader, AtModalContent, AtModalAction, AtInput } from 'taro-ui'
import { getActivityDetail, signupActivity } from '../../../api'
import { marked } from 'marked'
import './detail.scss'
@@ -13,19 +14,41 @@ const ActivityDetail = () => {
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [htmlContent, setHtmlContent] = useState('')
const [signupPercentage, setSignupPercentage] = useState(0)
useEffect(() => {
// Signup Form State
const [showSignupModal, setShowSignupModal] = useState(false)
const [formData, setFormData] = useState<any>({})
// Function to refresh data, can be used in useDidShow
const refreshData = () => {
if (id) {
fetchDetail()
}
}
useDidShow(() => {
refreshData()
})
useEffect(() => {
refreshData()
}, [id])
const fetchDetail = async () => {
try {
const res = await getActivityDetail(Number(id))
setActivity(res.data)
if (res.data.description) {
const html = marked.parse(res.data.description)
const data = res.data || res
setActivity(data)
// Calculate signup progress
if (data.max_participants > 0) {
const percent = Math.min(100, Math.round((data.current_signups || 0) / data.max_participants * 100))
setSignupPercentage(percent)
}
if (data.description) {
const html = marked.parse(data.description)
setHtmlContent((html as string).replace(/<img/g, '<img style="max-width:100%;border-radius:8px;"'))
}
} catch (error) {
@@ -40,23 +63,58 @@ const ActivityDetail = () => {
const token = Taro.getStorageSync('token')
if (!token) {
Taro.showToast({ title: '请先登录', icon: 'none' })
// Optional: Redirect to login
// Taro.navigateTo({ url: '/pages/user/login' })
return
}
// Check if form config exists
if (activity.signup_form_config && activity.signup_form_config.length > 0) {
setFormData({}) // Reset form
setShowSignupModal(true)
return
}
// Direct signup if no config
submitSignup({})
}
const submitSignup = async (data: any) => {
setSubmitting(true)
try {
await signupActivity(Number(id))
await signupActivity(Number(id), { signup_info: data })
Taro.showToast({ title: '报名成功', icon: 'success' })
setShowSignupModal(false)
fetchDetail() // Refresh status
} catch (error) {
} catch (error: any) {
console.error(error)
const msg = error.response?.data?.error || '报名失败'
const msg = error.response?.data?.error || error.message || '报名失败'
Taro.showToast({ title: msg, icon: 'none' })
} finally {
setSubmitting(false)
}
}
const handleFormChange = (fieldName, value) => {
setFormData(prev => ({
...prev,
[fieldName]: value
}))
}
const handleModalConfirm = () => {
// Validate
if (activity.signup_form_config) {
for (const field of activity.signup_form_config) {
if (field.required && !formData[field.name]) {
Taro.showToast({ title: `请填写${field.label}`, icon: 'none' })
return
}
}
}
submitSignup(formData)
}
useShareAppMessage(() => {
return {
title: activity?.title || '社区活动',
@@ -64,49 +122,124 @@ const ActivityDetail = () => {
}
})
if (loading) return <View>Loading...</View>
if (!activity) return <View></View>
if (loading) return <View className='loading-container'><Text>Loading...</Text></View>
if (!activity) return <View className='error-container'><Text></Text></View>
const isFull = activity.max_participants > 0 && (activity.current_signups || 0) >= activity.max_participants
const isEnded = new Date(activity.end_time) < new Date()
const canSignup = activity.is_active && !isFull && !isEnded && !activity.has_signed_up
return (
<View className='activity-detail'>
<Image src={activity.cover_image} mode='widthFix' className='cover' />
<View className='content'>
<Text className='title'>{activity.title}</Text>
<View className='meta-box'>
<View className='meta-row'>
<Text className='label'></Text>
<Text>{new Date(activity.start_time).toLocaleString()} ~ {new Date(activity.end_time).toLocaleString()}</Text>
<View className='activity-detail-page'>
{/* Hero Banner */}
<View className='hero-section'>
<Image
src={activity.display_banner_url || activity.banner_url || activity.cover_image || 'https://via.placeholder.com/800x600'}
mode='aspectFill'
className='hero-bg'
/>
<View className='hero-overlay'>
<View className='status-tag'>
{isEnded ? '已结束' : (isFull ? '名额已满' : '报名中')}
</View>
<View className='meta-row'>
<Text className='label'></Text>
<Text>{activity.location || '线上活动'}</Text>
</View>
<View className='meta-row'>
<Text className='label'></Text>
<Text>{activity.current_signups} / {activity.max_participants > 0 ? activity.max_participants : '不限'}</Text>
</View>
</View>
<View className='description'>
<View className='section-title'></View>
<RichText nodes={htmlContent} />
<Text className='hero-title'>{activity.title}</Text>
</View>
</View>
<View className='footer-bar'>
<View className='main-content'>
{/* Info Cards */}
<View className='info-grid'>
<View className='info-card time'>
<AtIcon value='clock' size='18' color='#00f0ff' />
<View className='info-text'>
<Text className='label'></Text>
<Text className='value'>{new Date(activity.start_time).toLocaleString()}</Text>
</View>
</View>
<View className='info-card location'>
<AtIcon value='map-pin' size='18' color='#bd00ff' />
<View className='info-text'>
<Text className='label'></Text>
<Text className='value'>{activity.location || '线上直播'}</Text>
</View>
</View>
</View>
{/* Signup Stats */}
<View className='stats-section'>
<View className='stats-header'>
<Text className='stats-title'></Text>
<Text className='stats-count'>
<Text className='current'>{activity.current_signups || 0}</Text>
<Text className='divider'>/</Text>
<Text className='max'>{activity.max_participants > 0 ? activity.max_participants : '∞'}</Text>
</Text>
</View>
<View className='progress-bar-container'>
<View className='progress-bar' style={{width: `${signupPercentage}%`}} />
<View className='progress-glow' style={{left: `${signupPercentage}%`}} />
</View>
</View>
{/* Detail Content */}
<View className='detail-section'>
<View className='section-header'>
<Text className='title-text'></Text>
<View className='line' />
</View>
<View className='rich-text-wrapper'>
<RichText nodes={htmlContent} />
</View>
</View>
</View>
{/* Footer Action Bar */}
<View className='footer-action-bar'>
<View className='left-info'>
<Text className='price'></Text>
<Text className='desc'></Text>
</View>
<Button
className='btn-signup'
type='primary'
disabled={activity.has_signed_up || activity.status !== 'open' || submitting}
className={`action-btn ${canSignup ? 'active' : 'disabled'}`}
disabled={!canSignup || submitting}
onClick={handleSignup}
>
{activity.has_signed_up ? '已报名' : (activity.status === 'open' ? '立即报名' : '无法报名')}
{submitting ? '提交中...' : (
activity.has_signed_up ? '您已报名' : (
isEnded ? '活动已结束' : (
isFull ? '名额已满' : '立即报名'
)
)
)}
</Button>
</View>
{/* Signup Form Modal */}
<AtModal isOpened={showSignupModal} onClose={() => setShowSignupModal(false)}>
<AtModalHeader></AtModalHeader>
<AtModalContent>
<View className='signup-form'>
{activity.signup_form_config && activity.signup_form_config.map((field, idx) => (
<AtInput
key={idx}
name={field.name}
title={field.label}
type={field.type || 'text'}
placeholder={`请输入${field.label}`}
value={formData[field.name]}
onChange={(val) => handleFormChange(field.name, val)}
required={field.required}
/>
))}
</View>
</AtModalContent>
<AtModalAction>
<Button onClick={() => setShowSignupModal(false)}></Button>
<Button onClick={handleModalConfirm} style={{color: '#00b96b'}}></Button>
</AtModalAction>
</AtModal>
</View>
)
}
export default ActivityDetail
export default ActivityDetail

View File

@@ -1,108 +1,163 @@
.activity-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 20px;
background-color: #050505;
padding-bottom: 40px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
.tabs {
display: flex;
background: #fff;
padding: 10px 0;
.tabs-header {
background: rgba(10, 10, 10, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
padding: 12px 24px;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
z-index: 100;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
.tab {
flex: 1;
text-align: center;
font-size: 16px;
color: #666;
padding: 10px 0;
position: relative;
&.active {
color: #00b96b;
font-weight: bold;
&:after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 3px;
background: #00b96b;
border-radius: 2px;
.tabs-bg {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
display: flex;
padding: 4px;
.tab {
flex: 1;
text-align: center;
font-size: 15px;
color: #888;
padding: 8px 0;
border-radius: 8px;
transition: all 0.3s;
font-weight: 500;
&.active {
color: #000;
background: var(--primary-cyan, #00f0ff);
font-weight: 700;
box-shadow: 0 2px 8px rgba(0, 240, 255, 0.3);
}
}
}
}
.list-container {
padding: 15px;
padding: 24px;
}
.empty {
text-align: center;
color: #999;
padding: 50px 0;
color: #666;
padding: 80px 0;
font-size: 16px;
}
.activity-card {
background: #fff;
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
border-radius: 20px;
overflow: hidden;
margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
margin-bottom: 24px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
transition: transform 0.3s;
.cover {
&:active {
transform: scale(0.98);
}
.card-cover-wrapper {
position: relative;
height: 200px;
width: 100%;
height: 150px;
overflow: hidden;
.cover {
width: 100%;
height: 100%;
object-fit: cover;
}
.status-tag {
position: absolute;
top: 16px;
left: 16px;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
color: #fff;
font-size: 12px;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
}
.info {
padding: 15px;
padding: 20px;
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
margin-bottom: 12px;
.title {
font-size: 18px;
font-weight: bold;
flex: 1;
margin-right: 10px;
}
.status {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
background: #eee;
color: #666;
&.open { background: #e6ffed; color: #00b96b; }
&.upcoming { background: #e6f7ff; color: #1890ff; }
&.ended { background: #f5f5f5; color: #999; }
font-size: 20px;
font-weight: 700;
color: #fff;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.meta {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 14px;
color: #666;
gap: 8px;
margin-bottom: 20px;
.meta-item {
display: flex;
align-items: center;
font-size: 14px;
color: #999;
.icon {
margin-right: 8px;
font-size: 14px;
}
}
}
.signup-status {
margin-top: 10px;
text-align: right;
color: #00b96b;
font-size: 14px;
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
.participants {
.highlight {
color: var(--primary-cyan, #00f0ff);
font-weight: 700;
font-size: 16px;
}
.total {
color: #666;
font-size: 13px;
margin-left: 2px;
}
}
.btn-view {
margin: 0;
background: rgba(255, 255, 255, 0.1);
color: #fff;
font-size: 14px;
padding: 0 16px;
height: 32px;
line-height: 32px;
border-radius: 16px;
border: none;
}
}
}
}

View File

@@ -1,15 +1,22 @@
import React, { useState, useEffect } from 'react'
import Taro, { usePullDownRefresh } from '@tarojs/taro'
import Taro, { usePullDownRefresh, useRouter, useDidShow } from '@tarojs/taro'
import { View, Text, Image, Button } from '@tarojs/components'
import { getActivities, getMySignups } from '../../../api'
import './index.scss'
const ActivityList = () => {
const router = useRouter()
const [activities, setActivities] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const [tab, setTab] = useState<'all' | 'mine'>('all')
const [mySignups, setMySignups] = useState<any[]>([])
useEffect(() => {
if (router.params.tab === 'mine') {
setTab('mine')
}
}, [router.params.tab])
const fetchData = async () => {
setLoading(true)
try {
@@ -33,6 +40,10 @@ const ActivityList = () => {
fetchData()
}, [tab])
useDidShow(() => {
fetchData()
})
usePullDownRefresh(() => {
fetchData()
})
@@ -54,33 +65,59 @@ const ActivityList = () => {
const renderList = (list) => {
if (list.length === 0 && !loading) return <View className='empty'></View>
return list.map(item => (
<View key={item.id} className='activity-card' onClick={() => goDetail(item.id)}>
<Image src={item.cover_image || 'https://via.placeholder.com/350x150'} mode='aspectFill' className='cover' />
<View className='info'>
<View className='header'>
<Text className='title'>{item.title}</Text>
<Text className={`status ${item.status}`}>{getStatusText(item.status)}</Text>
</View>
<View className='meta'>
<Text>📅 {new Date(item.start_time).toLocaleDateString()}</Text>
<Text>📍 {item.location || '线上活动'}</Text>
</View>
{tab === 'mine' && (
<View className='signup-status'>
<Text></Text>
return list.map(item => {
// Handle API structure differences
// For 'mine' tab, item is the signup record. activity_info is the full activity object.
const activity = tab === 'mine' ? (item.activity_info || item.activity || item) : item
return (
<View key={activity.id} className='activity-card' onClick={() => goDetail(activity.id)}>
<View className='card-cover-wrapper'>
<Image
src={activity.display_banner_url || activity.banner_url || activity.cover_image || 'https://via.placeholder.com/350x150'}
mode='aspectFill'
className='cover'
/>
<View className='status-tag'>
{getStatusText(activity.status)}
</View>
</View>
<View className='info'>
<View className='header'>
<Text className='title'>{activity.title}</Text>
</View>
)}
</View>
</View>
))
<View className='meta'>
<View className='meta-item'>
<Text className='icon'>📅</Text>
<Text>{new Date(activity.start_time).toLocaleDateString()}</Text>
</View>
<View className='meta-item'>
<Text className='icon'>📍</Text>
<Text>{activity.location || '线上活动'}</Text>
</View>
</View>
<View className='card-footer'>
<View className='participants'>
<Text className='highlight'>{activity.current_signups || 0}</Text>
<Text className='total'>/{activity.max_participants > 0 ? activity.max_participants : '∞'} </Text>
</View>
<Button className='btn-view'></Button>
</View>
</View>
</View>
)
})
}
return (
<View className='activity-page'>
<View className='tabs'>
<View className={`tab ${tab === 'all' ? 'active' : ''}`} onClick={() => setTab('all')}></View>
<View className={`tab ${tab === 'mine' ? 'active' : ''}`} onClick={() => setTab('mine')}></View>
<View className='tabs-header'>
<View className='tabs-bg'>
<View className={`tab ${tab === 'all' ? 'active' : ''}`} onClick={() => setTab('all')}></View>
<View className={`tab ${tab === 'mine' ? 'active' : ''}`} onClick={() => setTab('mine')}></View>
</View>
</View>
<View className='list-container'>