301 lines
9.5 KiB
TypeScript
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
|