Files
market_page/miniprogram/src/pages/forum/index.tsx
2026-02-12 19:59:03 +08:00

301 lines
9.5 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react'
import Taro, { usePullDownRefresh, useReachBottom, useDidShow } from '@tarojs/taro'
import { View, Text, Image, Swiper, SwiperItem, ScrollView } from '@tarojs/components'
import { AtSearchBar, AtTabs, AtIcon, AtActivityIndicator } from 'taro-ui'
import { getTopics, getAnnouncements, getStarUsers } from '../../api'
import './index.scss'
const ForumList = () => {
const [topics, setTopics] = useState<any[]>([])
const [announcements, setAnnouncements] = useState<any[]>([])
const [starUsers, setStarUsers] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [page, setPage] = useState(1)
const [searchText, setSearchText] = useState('')
const [currentTab, setCurrentTab] = useState(0)
const isMounted = useRef(false)
const categories = [
{ title: '全部话题', key: 'all' },
{ title: '技术讨论', key: 'discussion' },
{ title: '求助问答', key: 'help' },
{ title: '经验分享', key: 'share' },
{ title: '官方公告', key: 'notice' },
]
const fetchExtraData = async () => {
try {
const [announceRes, starRes] = await Promise.all([
getAnnouncements(),
getStarUsers()
])
setAnnouncements(Array.isArray(announceRes) ? announceRes : (announceRes.results || announceRes.data || []))
setStarUsers(Array.isArray(starRes) ? starRes : (starRes.data || []))
} catch (err) {
console.error('Fetch extra data failed', err)
}
}
const fetchList = async (reset = false) => {
if (loading) return
if (!reset && !hasMore) return
setLoading(true)
try {
const currentPage = reset ? 1 : page
const params: any = {
page: currentPage,
search: searchText
}
if (categories[currentTab].key !== 'all') {
params.category = categories[currentTab].key
}
const res = await getTopics(params)
let newTopics: any[] = []
let hasNextPage = false
if (Array.isArray(res)) {
newTopics = res
hasNextPage = false
} else {
newTopics = res.results || res.data || []
hasNextPage = !!res.next
}
if (reset) {
setTopics(newTopics)
} else {
setTopics(prev => [...prev, ...newTopics])
}
setHasMore(hasNextPage)
if (hasNextPage) {
setPage(currentPage + 1)
}
} catch (error) {
console.error(error)
Taro.showToast({ title: '加载失败', icon: 'none' })
} finally {
setLoading(false)
Taro.stopPullDownRefresh()
}
}
useDidShow(() => {
fetchList(true)
fetchExtraData()
})
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true
return
}
fetchList(true)
// fetchExtraData is covered by useDidShow usually, but if tab change needs it? likely not.
}, [currentTab])
usePullDownRefresh(() => {
fetchList(true)
fetchExtraData()
})
useReachBottom(() => {
fetchList(false)
})
const handleSearch = (value) => {
setSearchText(value)
}
const onSearchConfirm = () => {
fetchList(true)
}
const handleTabClick = (value) => {
setCurrentTab(value)
// useEffect will trigger fetch
}
const navigateToDetail = (id) => {
Taro.navigateTo({
url: `/subpackages/forum/detail/index?id=${id}`
})
}
const navigateToCreate = () => {
const token = Taro.getStorageSync('token')
if (!token) {
Taro.showToast({ title: '请先登录', icon: 'none' })
// Optional: Trigger login flow
return
}
Taro.navigateTo({
url: '/subpackages/forum/create/index'
})
}
const navigateToActivity = () => {
Taro.navigateTo({
url: '/subpackages/forum/activity/index'
})
}
const getCategoryLabel = (cat) => {
const map = {
'help': '求助',
'share': '分享',
'notice': '公告',
'discussion': '讨论'
}
return map[cat] || '讨论'
}
// Helper to extract first image from markdown
const getCoverImage = (content) => {
const match = content.match(/!\[.*?\]\((.*?)\)/)
return match ? match[1] : null
}
const stripMarkdown = (content) => {
return content.replace(/!\[.*?\]\(.*?\)/g, '[图片]').replace(/[#*`]/g, '')
}
return (
<View className='forum-page'>
<View className='hero-section'>
<View className='title'>
<Text className='highlight'>Quant Speed</Text> Developer Community
</View>
<View className='subtitle'> · · </View>
<View className='search-box'>
<AtSearchBar
value={searchText}
onChange={handleSearch}
onActionClick={onSearchConfirm}
onConfirm={onSearchConfirm}
placeholder='搜索感兴趣的话题...'
/>
<View className='create-btn' onClick={navigateToCreate}>
<AtIcon value='add' size='16' color='#fff' />
<Text style={{marginLeft: '4px'}}></Text>
</View>
<View className='create-btn' onClick={navigateToActivity} style={{marginLeft: '10px', background: 'rgba(255,255,255,0.2)'}}>
<AtIcon value='calendar' size='16' color='#fff' />
<Text style={{marginLeft: '4px'}}></Text>
</View>
</View>
</View>
{/* Announcements Section */}
{announcements.length > 0 && (
<View className='section-container'>
<View className='section-header'>
<AtIcon value='volume-plus' size='16' color='#ff4d4f' />
<Text className='section-title'></Text>
</View>
<Swiper
className='announcement-swiper'
vertical
autoplay
circular
interval={3000}
>
{announcements.map(item => (
<SwiperItem key={item.id}>
<View className='announcement-item'>
<Text className='item-text'>{item.title}</Text>
</View>
</SwiperItem>
))}
</Swiper>
</View>
)}
{/* Star Users Section */}
{starUsers.length > 0 && (
<View className='section-container'>
<View className='section-header'>
<AtIcon value='star' size='16' color='#ffd700' />
<Text className='section-title'></Text>
</View>
<ScrollView scrollX className='star-users-scroll'>
{starUsers.map(user => (
<View key={user.id} className='star-user-card'>
<Image src={user.avatar_url} className='user-avatar' />
<Text className='user-name'>{user.nickname}</Text>
<Text className='user-title'>{user.title || '专家'}</Text>
</View>
))}
</ScrollView>
</View>
)}
<View className='tabs-wrapper'>
<AtTabs
current={currentTab}
tabList={categories}
onClick={handleTabClick}
/>
</View>
<View className='topic-list'>
{topics.map(item => (
<View
key={item.id}
className={`topic-card ${item.is_pinned ? 'pinned' : ''}`}
onClick={() => navigateToDetail(item.id)}
>
<View className='card-header'>
{item.is_pinned && <Text className='tag pinned-tag'></Text>}
<Text className='tag'>{getCategoryLabel(item.category)}</Text>
{item.is_verified_owner && <Text className='tag verified-tag'></Text>}
<Text className='card-title'>{item.title}</Text>
</View>
<View className='card-content'>
{stripMarkdown(item.content)}
</View>
{getCoverImage(item.content) && (
<View className='card-image'>
<Image src={getCoverImage(item.content)} mode='aspectFill' />
</View>
)}
<View className='card-footer'>
<View className='author-info'>
<Image className='avatar' src={item.author_info?.avatar_url || 'https://via.placeholder.com/30'} />
<Text className={`nickname ${item.author_info?.is_star ? 'star' : ''}`}>
{item.author_info?.nickname || '匿名'}
</Text>
</View>
<View className='stats'>
<View className='stat-item'>
<Text>👁 {item.view_count || 0}</Text>
</View>
<View className='stat-item'>
<Text>💬 {item.replies?.length || 0}</Text>
</View>
</View>
</View>
</View>
))}
{loading && <View style={{textAlign: 'center', padding: 10}}><AtActivityIndicator color='#00b96b' /></View>}
{!loading && topics.length === 0 && <View className='empty-state'></View>}
</View>
<View className='fab' onClick={navigateToCreate}>
<AtIcon value='add' size='24' color='#fff' />
</View>
</View>
)
}
export default ForumList