From 1f1516ae20e908375483c656d5c8934e69578add Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Thu, 12 Feb 2026 19:40:59 +0800 Subject: [PATCH] forum --- miniprogram/src/api/index.ts | 9 + miniprogram/src/app.config.ts | 4 +- miniprogram/src/pages/forum/index.tsx | 33 ++- miniprogram/src/pages/user/index.scss | 154 ++++++++++++++ miniprogram/src/pages/user/index.tsx | 192 ++++++++++++++++-- .../forum/activity/detail.config.ts | 3 + .../subpackages/forum/activity/detail.scss | 79 +++++++ .../src/subpackages/forum/activity/detail.tsx | 112 ++++++++++ .../forum/activity/index.config.ts | 4 + .../src/subpackages/forum/activity/index.scss | 109 ++++++++++ .../src/subpackages/forum/activity/index.tsx | 93 +++++++++ 11 files changed, 772 insertions(+), 20 deletions(-) create mode 100644 miniprogram/src/subpackages/forum/activity/detail.config.ts create mode 100644 miniprogram/src/subpackages/forum/activity/detail.scss create mode 100644 miniprogram/src/subpackages/forum/activity/detail.tsx create mode 100644 miniprogram/src/subpackages/forum/activity/index.config.ts create mode 100644 miniprogram/src/subpackages/forum/activity/index.scss create mode 100644 miniprogram/src/subpackages/forum/activity/index.tsx diff --git a/miniprogram/src/api/index.ts b/miniprogram/src/api/index.ts index 02fb530..52f4602 100644 --- a/miniprogram/src/api/index.ts +++ b/miniprogram/src/api/index.ts @@ -39,9 +39,18 @@ export const createTopic = (data: any) => request({ url: '/community/topics/', m export const updateTopic = (id: number, data: any) => request({ url: `/community/topics/${id}/`, method: 'PATCH', data }) export const getReplies = (params: any) => request({ url: '/community/replies/', data: params }) export const createReply = (data: any) => request({ url: '/community/replies/', method: 'POST', data }) +export const updateReply = (id: number, data: any) => request({ url: `/community/replies/${id}/`, method: 'PATCH', data }) +export const deleteReply = (id: number) => request({ url: `/community/replies/${id}/`, method: 'DELETE' }) +export const deleteTopic = (id: number) => request({ url: `/community/topics/${id}/`, method: 'DELETE' }) export const getStarUsers = () => request({ url: '/users/stars/' }) export const getAnnouncements = () => request({ url: '/community/announcements/' }) +// Activities +export const getActivities = () => request({ url: '/community/activities/' }) +export const getActivityDetail = (id: number) => request({ url: `/community/activities/${id}/` }) +export const signupActivity = (id: number) => request({ url: `/community/activities/${id}/signup/`, method: 'POST' }) +export const getMySignups = () => request({ url: '/community/activities/my_signups/' }) + // Upload Media for Forum export const uploadMedia = (filePath: string, type: 'image' | 'video') => { const BASE_URL = process.env.TARO_APP_API_URL || 'https://market.quant-speed.com/api' diff --git a/miniprogram/src/app.config.ts b/miniprogram/src/app.config.ts index 7701efd..e8c8ea5 100644 --- a/miniprogram/src/app.config.ts +++ b/miniprogram/src/app.config.ts @@ -27,7 +27,9 @@ export default defineAppConfig({ root: 'subpackages/forum', pages: [ 'detail/index', - 'create/index' + 'create/index', + 'activity/index', + 'activity/detail' ] } ], diff --git a/miniprogram/src/pages/forum/index.tsx b/miniprogram/src/pages/forum/index.tsx index 057f27d..d5dcb52 100644 --- a/miniprogram/src/pages/forum/index.tsx +++ b/miniprogram/src/pages/forum/index.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react' -import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro' +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' @@ -14,6 +14,7 @@ const ForumList = () => { const [page, setPage] = useState(1) const [searchText, setSearchText] = useState('') const [currentTab, setCurrentTab] = useState(0) + const isMounted = useRef(false) const categories = [ { title: '全部话题', key: 'all' }, @@ -45,8 +46,11 @@ const ForumList = () => { const currentPage = reset ? 1 : page const params: any = { page: currentPage, - search: searchText, - category: categories[currentTab].key !== 'all' ? categories[currentTab].key : undefined + search: searchText + } + + if (categories[currentTab].key !== 'all') { + params.category = categories[currentTab].key } const res = await getTopics(params) @@ -74,9 +78,18 @@ const ForumList = () => { } } - useEffect(() => { + 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(() => { @@ -119,6 +132,12 @@ const ForumList = () => { }) } + const navigateToActivity = () => { + Taro.navigateTo({ + url: '/subpackages/forum/activity/index' + }) + } + const getCategoryLabel = (cat) => { const map = { 'help': '求助', @@ -159,6 +178,10 @@ const ForumList = () => { 发布新帖 + + + 社区活动 + diff --git a/miniprogram/src/pages/user/index.scss b/miniprogram/src/pages/user/index.scss index 56fc6fb..0ffe8de 100644 --- a/miniprogram/src/pages/user/index.scss +++ b/miniprogram/src/pages/user/index.scss @@ -205,3 +205,157 @@ color: #333; } } + +/* Login Modal Styles */ +.login-modal-mask { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); + z-index: 100; + display: flex; + align-items: center; + justify-content: center; +} + +.login-modal-content { + width: 600px; + background: #1a1a1a; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 30px; + padding: 50px 40px; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 10px 40px rgba(0,0,0,0.5); + + .modal-header { + margin-bottom: 60px; + text-align: center; + + .modal-title { + display: block; + font-size: 40px; + font-weight: bold; + color: #fff; + margin-bottom: 16px; + } + + .modal-subtitle { + font-size: 28px; + color: #888; + } + } + + .modal-body { + width: 100%; + + .btn-modal-login { + width: 100%; + height: 90px; + line-height: 90px; + border-radius: 45px; + font-size: 32px; + margin-bottom: 40px; + background: #333; + color: #888; + border: none; + + &.primary { + background: linear-gradient(90deg, #00b96b 0%, #009959 100%); + color: #fff; + box-shadow: 0 8px 20px rgba(0, 185, 107, 0.3); + + &:active { + transform: scale(0.98); + } + } + } + + .agreement-box { + display: flex; + align-items: flex-start; + justify-content: center; + + .agreement-checkbox { + transform: scale(0.7); + margin-top: -4px; + } + + .agreement-text { + font-size: 24px; + color: #888; + line-height: 1.4; + + .link { + color: #00b96b; + display: inline; + } + } + } + } +} + +/* Agreement Detail Modal */ +.agreement-modal-mask { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.8); + z-index: 200; + display: flex; + align-items: center; + justify-content: center; +} + +.agreement-content { + width: 600px; + height: 80vh; + background: #fff; + border-radius: 20px; + display: flex; + flex-direction: column; + overflow: hidden; + + .agreement-title { + font-size: 36px; + font-weight: bold; + color: #333; + padding: 30px; + text-align: center; + border-bottom: 1px solid #eee; + } + + .agreement-scroll { + flex: 1; + padding: 30px; + overflow-y: auto; + + .p { + margin-bottom: 20px; + font-size: 28px; + color: #666; + line-height: 1.6; + } + } + + .btn-close { + height: 100px; + line-height: 100px; + text-align: center; + background: #f5f5f5; + color: #00b96b; + font-size: 32px; + font-weight: bold; + border-radius: 0; + + &:active { + background: #eee; + } + } +} diff --git a/miniprogram/src/pages/user/index.tsx b/miniprogram/src/pages/user/index.tsx index 883f6b5..8323f1c 100644 --- a/miniprogram/src/pages/user/index.tsx +++ b/miniprogram/src/pages/user/index.tsx @@ -1,10 +1,13 @@ -import { View, Text, Image, Button } from '@tarojs/components' +import { View, Text, Image, Button, Checkbox, CheckboxGroup, RichText } from '@tarojs/components' import Taro, { useDidShow } from '@tarojs/taro' import { useState } from 'react' import './index.scss' export default function UserIndex() { const [userInfo, setUserInfo] = useState(null) + const [showLoginModal, setShowLoginModal] = useState(false) + const [isAgreed, setIsAgreed] = useState(false) + const [showAgreement, setShowAgreement] = useState(false) // For showing agreement content useDidShow(() => { const info = Taro.getStorageSync('userInfo') @@ -23,7 +26,7 @@ export default function UserIndex() { const token = Taro.getStorageSync('token') if (token) { await Taro.request({ - url: 'https://market.quant-speed.com/api/wechat/update_user_info/', + url: 'https://market.quant-speed.com/api/wechat/update/', method: 'POST', header: { 'Authorization': `Bearer ${token}`, @@ -46,6 +49,107 @@ export default function UserIndex() { } } + const handleAvatarClick = async () => { + if (!userInfo) return + + try { + const { tempFilePaths } = await Taro.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'] }) + if (!tempFilePaths.length) return + + Taro.showLoading({ title: '上传中...' }) + + const token = Taro.getStorageSync('token') + const uploadRes = await Taro.uploadFile({ + url: 'https://market.quant-speed.com/api/upload/image/', + filePath: tempFilePaths[0], + name: 'file', + header: { + 'Authorization': `Bearer ${token}` + } + }) + + if (uploadRes.statusCode !== 200) { + throw new Error('上传失败') + } + + const data = JSON.parse(uploadRes.data) + const newAvatarUrl = data.url + + // 更新后端用户信息 + await Taro.request({ + url: 'https://market.quant-speed.com/api/wechat/update/', + method: 'POST', + header: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + data: { + avatar_url: newAvatarUrl + } + }) + + // 更新本地 userInfo + const updatedInfo = { ...userInfo, avatar_url: newAvatarUrl } + setUserInfo(updatedInfo) + Taro.setStorageSync('userInfo', updatedInfo) + + Taro.hideLoading() + Taro.showToast({ title: '头像更新成功', icon: 'success' }) + + } catch (e) { + Taro.hideLoading() + Taro.showToast({ title: '头像更新失败', icon: 'none' }) + console.error(e) + } + } + + const handleNicknameClick = () => { + if (!userInfo) return + + Taro.showModal({ + title: '修改昵称', + content: userInfo.nickname || '', + // @ts-ignore + editable: true, + placeholderText: '请输入新昵称', + success: async function (res) { + if (res.confirm && (res as any).content) { + const newNickname = (res as any).content + if (newNickname === userInfo.nickname) return + + try { + Taro.showLoading({ title: '更新中...' }) + const token = Taro.getStorageSync('token') + + await Taro.request({ + url: 'https://market.quant-speed.com/api/wechat/update/', + method: 'POST', + header: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + data: { + nickname: newNickname + } + }) + + // 更新本地 userInfo + const updatedInfo = { ...userInfo, nickname: newNickname } + setUserInfo(updatedInfo) + Taro.setStorageSync('userInfo', updatedInfo) + + Taro.hideLoading() + Taro.showToast({ title: '昵称已更新', icon: 'success' }) + } catch (e) { + Taro.hideLoading() + Taro.showToast({ title: '更新失败', icon: 'none' }) + console.error(e) + } + } + } + }) + } + const handleLogout = () => { Taro.showModal({ title: '提示', @@ -120,6 +224,7 @@ export default function UserIndex() { Taro.setStorageSync('token', res.data.token) Taro.setStorageSync('userInfo', res.data) setUserInfo(res.data) + setShowLoginModal(false) // Close modal on success Taro.showToast({ title: '授权登录成功', icon: 'success' }) } else { throw new Error(res.data.error || '登录失败') @@ -162,32 +267,41 @@ export default function UserIndex() { { label: '优惠券', value: '0' } ] + const handleAgreementCheck = (e) => { + setIsAgreed(!!e.detail.value.length) + } + + const handleShowAgreement = (e) => { + e.stopPropagation() + setShowAgreement(true) + } + + const handleLoginBtnClick = () => { + if (!isAgreed) { + Taro.showToast({ title: '请先阅读并同意用户协议', icon: 'none' }) + return + } + // If agreed, the button openType='getPhoneNumber' handles it. + } + return ( {/* Profile Card */} - + {userInfo && } - {userInfo?.nickname || '未登录用户'} + {userInfo?.nickname || '未登录用户'} ID: {userInfo ? (userInfo.phone_number || userInfo.id || '----') : '----'} {!userInfo && ( - {/* */} )} @@ -229,6 +343,56 @@ export default function UserIndex() { Quant Speed Market v1.0.0 Powered by Taro & React + + {/* Login Modal */} + {showLoginModal && ( + setShowLoginModal(false)}> + e.stopPropagation()}> + + 欢迎登录 Quant Speed + 登录后享受更多权益 + + + + + + + + + + + 我已阅读并同意 《用户协议》《隐私政策》 + + + + + + )} + + {/* Agreement Detail Modal */} + {showAgreement && ( + + + 用户协议与隐私政策 + + 1. 特别提示 + 在此特别提醒您(用户)在注册成为用户之前,请认真阅读本《用户协议》(以下简称“协议”),确保您充分理解本协议中各条款。请您审慎阅读并选择接受或不接受本协议。除非您接受本协议所有条款,否则您无权注册、登录或使用本协议所涉服务。您的注册、登录、使用等行为将视为对本协议的接受,并同意接受本协议各项条款的约束。 + 2. 账号注册 + 2.1 鉴于“Quant Speed”账号的绑定注册方式,您同意在注册时将您的手机号码及微信账号信息提供给“Quant Speed”用于注册。 + 3. 隐私保护 + 3.1 本小程序将严格保护您的个人信息安全。我们使用各种安全技术和程序来保护您的个人信息不被未经授权的访问、使用或泄漏。 + + + + + )} ) } diff --git a/miniprogram/src/subpackages/forum/activity/detail.config.ts b/miniprogram/src/subpackages/forum/activity/detail.config.ts new file mode 100644 index 0000000..4bc9104 --- /dev/null +++ b/miniprogram/src/subpackages/forum/activity/detail.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '活动详情' +}) diff --git a/miniprogram/src/subpackages/forum/activity/detail.scss b/miniprogram/src/subpackages/forum/activity/detail.scss new file mode 100644 index 0000000..b627612 --- /dev/null +++ b/miniprogram/src/subpackages/forum/activity/detail.scss @@ -0,0 +1,79 @@ +.activity-detail { + padding-bottom: 80px; + + .cover { + width: 100%; + display: block; + } + + .content { + padding: 20px; + + .title { + font-size: 24px; + font-weight: bold; + display: block; + margin-bottom: 20px; + } + + .meta-box { + background: #f9f9f9; + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; + + .meta-row { + display: flex; + margin-bottom: 8px; + font-size: 14px; + color: #333; + + .label { + color: #666; + width: 50px; + flex-shrink: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + } + + .section-title { + font-size: 18px; + font-weight: bold; + margin-bottom: 10px; + border-left: 4px solid #00b96b; + padding-left: 10px; + } + + .description { + font-size: 16px; + line-height: 1.6; + color: #333; + } + } + + .footer-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)); + + .btn-signup { + width: 100%; + background: #00b96b; + border: none; + + &[disabled] { + background: #ccc; + color: #fff; + } + } + } +} diff --git a/miniprogram/src/subpackages/forum/activity/detail.tsx b/miniprogram/src/subpackages/forum/activity/detail.tsx new file mode 100644 index 0000000..cf9f012 --- /dev/null +++ b/miniprogram/src/subpackages/forum/activity/detail.tsx @@ -0,0 +1,112 @@ +import React, { useState, useEffect } from 'react' +import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro' +import { View, Text, Image, Button, RichText } from '@tarojs/components' +import { getActivityDetail, signupActivity } from '../../../api' +import { marked } from 'marked' +import './detail.scss' + +const ActivityDetail = () => { + const router = useRouter() + const { id } = router.params + + const [activity, setActivity] = useState(null) + const [loading, setLoading] = useState(true) + const [submitting, setSubmitting] = useState(false) + const [htmlContent, setHtmlContent] = useState('') + + useEffect(() => { + if (id) { + fetchDetail() + } + }, [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) + setHtmlContent((html as string).replace(/ { + const token = Taro.getStorageSync('token') + if (!token) { + Taro.showToast({ title: '请先登录', icon: 'none' }) + return + } + + setSubmitting(true) + try { + await signupActivity(Number(id)) + Taro.showToast({ title: '报名成功', icon: 'success' }) + fetchDetail() // Refresh status + } catch (error) { + console.error(error) + const msg = error.response?.data?.error || '报名失败' + Taro.showToast({ title: msg, icon: 'none' }) + } finally { + setSubmitting(false) + } + } + + useShareAppMessage(() => { + return { + title: activity?.title || '社区活动', + path: `/subpackages/forum/activity/detail?id=${id}` + } + }) + + if (loading) return Loading... + if (!activity) return 活动不存在 + + return ( + + + + + {activity.title} + + + + 时间: + {new Date(activity.start_time).toLocaleString()} ~ {new Date(activity.end_time).toLocaleString()} + + + 地点: + {activity.location || '线上活动'} + + + 名额: + {activity.current_signups} / {activity.max_participants > 0 ? activity.max_participants : '不限'} + + + + + 活动详情 + + + + + + + + + ) +} + +export default ActivityDetail diff --git a/miniprogram/src/subpackages/forum/activity/index.config.ts b/miniprogram/src/subpackages/forum/activity/index.config.ts new file mode 100644 index 0000000..d07df9d --- /dev/null +++ b/miniprogram/src/subpackages/forum/activity/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '社区活动', + enablePullDownRefresh: true +}) diff --git a/miniprogram/src/subpackages/forum/activity/index.scss b/miniprogram/src/subpackages/forum/activity/index.scss new file mode 100644 index 0000000..88bbfe7 --- /dev/null +++ b/miniprogram/src/subpackages/forum/activity/index.scss @@ -0,0 +1,109 @@ +.activity-page { + min-height: 100vh; + background: #f5f5f5; + padding-bottom: 20px; + + .tabs { + display: flex; + background: #fff; + padding: 10px 0; + position: sticky; + top: 0; + z-index: 10; + box-shadow: 0 2px 5px rgba(0,0,0,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; + } + } + } + } + + .list-container { + padding: 15px; + } + + .empty { + text-align: center; + color: #999; + padding: 50px 0; + } + + .activity-card { + background: #fff; + border-radius: 12px; + overflow: hidden; + margin-bottom: 15px; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); + + .cover { + width: 100%; + height: 150px; + } + + .info { + padding: 15px; + + .header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 10px; + + .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; } + } + } + + .meta { + display: flex; + flex-direction: column; + gap: 5px; + font-size: 14px; + color: #666; + } + + .signup-status { + margin-top: 10px; + text-align: right; + color: #00b96b; + font-size: 14px; + } + } + } +} diff --git a/miniprogram/src/subpackages/forum/activity/index.tsx b/miniprogram/src/subpackages/forum/activity/index.tsx new file mode 100644 index 0000000..ac42009 --- /dev/null +++ b/miniprogram/src/subpackages/forum/activity/index.tsx @@ -0,0 +1,93 @@ +import React, { useState, useEffect } from 'react' +import Taro, { usePullDownRefresh } from '@tarojs/taro' +import { View, Text, Image, Button } from '@tarojs/components' +import { getActivities, getMySignups } from '../../../api' +import './index.scss' + +const ActivityList = () => { + const [activities, setActivities] = useState([]) + const [loading, setLoading] = useState(false) + const [tab, setTab] = useState<'all' | 'mine'>('all') + const [mySignups, setMySignups] = useState([]) + + const fetchData = async () => { + setLoading(true) + try { + if (tab === 'all') { + const res = await getActivities() + setActivities(res.results || res.data || []) + } else { + const res = await getMySignups() + setMySignups(res.results || res.data || []) + } + } catch (error) { + console.error(error) + Taro.showToast({ title: '加载失败', icon: 'none' }) + } finally { + setLoading(false) + Taro.stopPullDownRefresh() + } + } + + useEffect(() => { + fetchData() + }, [tab]) + + usePullDownRefresh(() => { + fetchData() + }) + + const goDetail = (id) => { + Taro.navigateTo({ url: `/subpackages/forum/activity/detail?id=${id}` }) + } + + const getStatusText = (status) => { + const map = { + 'upcoming': '即将开始', + 'open': '报名中', + 'ongoing': '进行中', + 'ended': '已结束' + } + return map[status] || status + } + + const renderList = (list) => { + if (list.length === 0 && !loading) return 暂无活动 + + return list.map(item => ( + goDetail(item.id)}> + + + + {item.title} + {getStatusText(item.status)} + + + 📅 {new Date(item.start_time).toLocaleDateString()} + 📍 {item.location || '线上活动'} + + {tab === 'mine' && ( + + 已报名 + + )} + + + )) + } + + return ( + + + setTab('all')}>精彩活动 + setTab('mine')}>我的报名 + + + + {renderList(tab === 'all' ? activities : mySignups)} + + + ) +} + +export default ActivityList