移动端
All checks were successful
Deploy to Server / deploy (push) Successful in 37s

This commit is contained in:
jeremygan2021
2026-02-24 16:09:07 +08:00
parent 76ce1225ec
commit fd33201793
6 changed files with 214 additions and 71 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Typography, Card, Avatar, Tag, Space, Button, Divider, Input, message, Upload, Tooltip } from 'antd';
import { Typography, Card, Avatar, Tag, Space, Button, Divider, Input, message, Upload, Tooltip, Grid } from 'antd';
import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, UploadOutlined, EditOutlined } from '@ant-design/icons';
import { getTopicDetail, createReply, uploadMedia } from '../api';
import { useAuth } from '../context/AuthContext';
@@ -17,11 +17,14 @@ import 'katex/dist/katex.min.css';
const { Title, Text } = Typography;
const { TextArea } = Input;
const { useBreakpoint } = Grid;
const ForumDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const { user } = useAuth();
const screens = useBreakpoint();
const isMobile = !screens.md;
const [loading, setLoading] = useState(true);
const [topic, setTopic] = useState(null);
@@ -156,7 +159,7 @@ const ForumDetail = () => {
};
return (
<div style={{ padding: '80px 20px 40px', minHeight: '100vh', maxWidth: 1000, margin: '0 auto' }}>
<div style={{ padding: isMobile ? '60px 10px 20px' : '80px 20px 40px', minHeight: '100vh', maxWidth: 1000, margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
<Button
type="text"
@@ -167,21 +170,15 @@ const ForumDetail = () => {
返回列表
</Button>
{/* Debug Info: Remove in production */}
{/* <div style={{ color: 'red', fontSize: 10 }}>
User ID: {user?.id} ({typeof user?.id})<br/>
Topic Author: {topic.author} ({typeof topic.author})<br/>
Match: {String(topic.author) === String(user?.id) ? 'Yes' : 'No'}
</div> */}
{user && String(topic.author) === String(user.id) && (
<Button
type="primary"
ghost
icon={<EditOutlined />}
onClick={() => setEditModalVisible(true)}
size={isMobile ? 'small' : 'middle'}
>
编辑帖子
{isMobile ? '编辑' : '编辑帖子'}
</Button>
)}
</div>
@@ -194,17 +191,17 @@ const ForumDetail = () => {
backdropFilter: 'blur(10px)',
marginBottom: 30
}}
styles={{ body: { padding: '30px' } }}
styles={{ body: { padding: isMobile ? '15px' : '30px' } }}
>
<div style={{ marginBottom: 20 }}>
{topic.is_pinned && <Tag color="red" style={{ marginRight: 10 }}>置顶</Tag>}
{topic.product_info && <Tag color="blue">{topic.product_info.name}</Tag>}
<Title level={2} style={{ color: '#fff', margin: '10px 0' }}>{topic.title}</Title>
<Title level={isMobile ? 3 : 2} style={{ color: '#fff', margin: '10px 0', wordBreak: 'break-word' }}>{topic.title}</Title>
<Space size="large" style={{ color: '#888', marginTop: 10 }}>
<Space size={isMobile ? 'small' : 'large'} style={{ color: '#888', marginTop: 10, flexWrap: 'wrap' }}>
<Space>
<Avatar src={topic.author_info?.avatar_url} icon={<UserOutlined />} />
<span style={{ color: '#ccc' }}>{topic.author_info?.nickname}</span>
<Avatar src={topic.author_info?.avatar_url} icon={<UserOutlined />} size={isMobile ? 'small' : 'default'} />
<span style={{ color: '#ccc', fontSize: isMobile ? 12 : 14 }}>{topic.author_info?.nickname}</span>
{topic.is_verified_owner && (
<Tooltip title="已验证购买过相关产品">
<CheckCircleFilled style={{ color: '#00b96b' }} />
@@ -213,11 +210,11 @@ const ForumDetail = () => {
</Space>
<Space>
<ClockCircleOutlined />
<span>{new Date(topic.created_at).toLocaleString()}</span>
<span style={{ fontSize: isMobile ? 12 : 14 }}>{new Date(topic.created_at).toLocaleString()}</span>
</Space>
<Space>
<EyeOutlined />
<span>{topic.view_count} 阅读</span>
<span style={{ fontSize: isMobile ? 12 : 14 }}>{topic.view_count} 阅读</span>
</Space>
</Space>
</div>
@@ -253,7 +250,7 @@ const ForumDetail = () => {
{/* Replies List */}
<div style={{ marginBottom: 30 }}>
<Title level={4} style={{ color: '#fff', marginBottom: 20 }}>
<Title level={isMobile ? 5 : 4} style={{ color: '#fff', marginBottom: 20 }}>
{topic.replies?.length || 0} 条回复
</Title>
@@ -266,18 +263,19 @@ const ForumDetail = () => {
marginBottom: 16,
borderRadius: 8
}}
styles={{ body: { padding: isMobile ? '15px' : '24px' } }}
>
<div style={{ display: 'flex', gap: 16 }}>
<Avatar src={reply.author_info?.avatar_url} icon={<UserOutlined />} />
<div style={{ display: 'flex', gap: isMobile ? 10 : 16 }}>
<Avatar src={reply.author_info?.avatar_url} icon={<UserOutlined />} size={isMobile ? 'small' : 'default'} />
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<Space>
<Text style={{ color: '#aaa', fontWeight: 'bold' }}>{reply.author_info?.nickname}</Text>
<Space size={isMobile ? 'small' : 'middle'} align="center">
<Text style={{ color: '#aaa', fontWeight: 'bold', fontSize: isMobile ? 13 : 14 }}>{reply.author_info?.nickname}</Text>
<Text style={{ color: '#666', fontSize: 12 }}>{new Date(reply.created_at).toLocaleString()}</Text>
</Space>
<Text style={{ color: '#444' }}>#{index + 1}</Text>
<Text style={{ color: '#444', fontSize: 12 }}>#{index + 1}</Text>
</div>
<div style={{ color: '#eee' }}>
<div style={{ color: '#eee', fontSize: isMobile ? 14 : 16 }}>
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeKatex, rehypeRaw]}
@@ -298,6 +296,7 @@ const ForumDetail = () => {
background: 'rgba(20,20,20,0.8)',
border: '1px solid rgba(255,255,255,0.1)'
}}
styles={{ body: { padding: isMobile ? '15px' : '24px' } }}
>
<Title level={5} style={{ color: '#fff', marginBottom: 16 }}>发表回复</Title>
{user ? (
@@ -309,7 +308,7 @@ const ForumDetail = () => {
placeholder="友善回复,分享你的见解... (支持 Markdown)"
style={{ marginBottom: 16, background: '#111', border: '1px solid #333', color: '#fff' }}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', justifyContent: 'space-between', alignItems: isMobile ? 'stretch' : 'center', gap: isMobile ? 10 : 0 }}>
<Upload
beforeUpload={handleReplyUpload}
showUploadList={false}
@@ -321,14 +320,15 @@ const ForumDetail = () => {
style={{
color: '#fff',
background: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,255,255,0.2)'
border: '1px solid rgba(255,255,255,0.2)',
width: isMobile ? '100%' : 'auto'
}}
>
插入图片/视频
</Button>
</Upload>
<Button type="primary" onClick={handleSubmitReply} loading={submitting}>
<Button type="primary" onClick={handleSubmitReply} loading={submitting} style={{ width: isMobile ? '100%' : 'auto' }}>
提交回复
</Button>
</div>

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Typography, Input, Button, List, Tag, Avatar, Card, Space, Spin, message, Badge, Tooltip, Tabs, Row, Col } from 'antd';
import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined, StarFilled, QuestionCircleOutlined, ShareAltOutlined, SoundOutlined } from '@ant-design/icons';
import { Typography, Input, Button, List, Tag, Avatar, Card, Space, Spin, message, Badge, Tooltip, Tabs, Row, Col, Grid, Carousel } from 'antd';
import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined, StarFilled, QuestionCircleOutlined, ShareAltOutlined, SoundOutlined, RightOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { getTopics, getStarUsers, getAnnouncements } from '../api';
@@ -9,8 +9,11 @@ import CreateTopicModal from '../components/CreateTopicModal';
import LoginModal from '../components/LoginModal';
const { Title, Text, Paragraph } = Typography;
const { useBreakpoint } = Grid;
const ForumList = () => {
const screens = useBreakpoint();
const isMobile = !screens.md; // roughly checks if screen is smaller than medium
const navigate = useNavigate();
const { user } = useAuth();
@@ -108,7 +111,7 @@ const ForumList = () => {
{/* Hero Section */}
<div style={{
textAlign: 'center',
padding: '80px 20px 40px',
padding: isMobile ? '40px 15px 20px' : '80px 20px 40px',
background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,185,107,0.1) 100%)'
}}>
<motion.div
@@ -116,17 +119,23 @@ const ForumList = () => {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<Title level={1} style={{ color: '#fff', fontFamily: "'Orbitron', sans-serif", marginBottom: 10 }}>
<Title level={isMobile ? 2 : 1} style={{ color: '#fff', fontFamily: "'Orbitron', sans-serif", marginBottom: 10 }}>
<span style={{ color: '#00b96b' }}>Quant Speed</span> Developer Community
</Title>
<Text style={{ color: '#888', fontSize: 18, maxWidth: 600, display: 'block', margin: '0 auto 30px' }}>
<Text style={{ color: '#888', fontSize: isMobile ? 14 : 18, maxWidth: 600, display: 'block', margin: '0 auto 30px' }}>
技术交流 · 硬件开发 · 官方支持 · 量迹生态
</Text>
</motion.div>
<div style={{ maxWidth: 600, margin: '0 auto', display: 'flex', gap: 10 }}>
<div style={{
maxWidth: 600,
margin: '0 auto',
display: 'flex',
gap: 10,
flexDirection: isMobile ? 'column' : 'row'
}}>
<Input
size="large"
size={isMobile ? "middle" : "large"}
placeholder="搜索感兴趣的话题..."
prefix={<SearchOutlined style={{ color: '#666' }} />}
style={{ borderRadius: 8, background: 'rgba(255,255,255,0.1)', border: '1px solid #333', color: '#fff' }}
@@ -135,10 +144,10 @@ const ForumList = () => {
/>
<Button
type="primary"
size="large"
size={isMobile ? "middle" : "large"}
icon={<PlusOutlined />}
onClick={handleCreateClick}
style={{ height: 'auto', borderRadius: 8 }}
style={{ height: 'auto', borderRadius: 8, width: isMobile ? '100%' : 'auto' }}
>
发布新帖
</Button>
@@ -146,14 +155,54 @@ const ForumList = () => {
</div>
{/* Content Section */}
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 20px' }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: isMobile ? '0 15px' : '0 20px' }}>
<Row gutter={24}>
<Col xs={24} md={18}>
{isMobile && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
style={{ marginBottom: 20 }}
>
{/* Mobile Announcements */}
<div style={{ background: 'rgba(255,77,79,0.1)', border: '1px solid rgba(255,77,79,0.3)', borderRadius: 8, padding: '8px 12px', marginBottom: 15, display: 'flex', alignItems: 'center' }}>
<SoundOutlined style={{ color: '#ff4d4f', marginRight: 10 }} />
<div style={{ flex: 1, overflow: 'hidden' }}>
<Carousel autoplay dots={false} effect="scrollx" style={{ width: '100%' }}>
{announcements.length > 0 ? announcements.map(item => (
<div key={item.id}>
<Text ellipsis style={{ color: '#fff', width: '100%', display: 'block' }}>
{item.title}
</Text>
</div>
)) : (
<div><Text style={{ color: '#888' }}>暂无公告</Text></div>
)}
</Carousel>
</div>
</div>
{/* Mobile Experts */}
<div style={{ overflowX: 'auto', whiteSpace: 'nowrap', paddingBottom: 5, display: 'flex', gap: 15, scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
{starUsers.map(u => (
<div key={u.id} style={{ textAlign: 'center', minWidth: 60 }}>
<Badge count={<StarFilled style={{ color: '#ffd700' }} />} offset={[-5, 5]}>
<Avatar size={48} src={u.avatar_url} icon={<UserOutlined />} style={{ border: '2px solid rgba(255, 215, 0, 0.3)' }} />
</Badge>
<div style={{ color: '#fff', fontSize: 12, marginTop: 5, width: 60, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{u.nickname}
</div>
</div>
))}
</div>
</motion.div>
)}
<Tabs
defaultActiveKey="all"
items={items}
onChange={setCategory}
tabBarStyle={{ color: '#fff' }}
tabBarStyle={{ color: '#fff', marginBottom: isMobile ? 10 : 16 }}
/>
<List
loading={loading}
@@ -174,29 +223,29 @@ const ForumList = () => {
backdropFilter: 'blur(10px)',
boxShadow: item.is_pinned ? '0 0 10px rgba(0, 185, 107, 0.1)' : 'none'
}}
bodyStyle={{ padding: '20px 24px' }}
bodyStyle={{ padding: isMobile ? '15px' : '20px 24px' }}
onClick={() => navigate(`/forum/${item.id}`)}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
{item.is_pinned && <Tag color="red" icon={<FireOutlined />}>置顶</Tag>}
<Tag icon={getCategoryIcon(item.category)} style={{ background: 'transparent', color: '#fff', border: '1px solid #444' }}>
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: isMobile ? 4 : 8, flexWrap: 'wrap' }}>
{item.is_pinned && <Tag color="red" icon={<FireOutlined />}>{isMobile ? '顶' : '置顶'}</Tag>}
<Tag icon={getCategoryIcon(item.category)} style={{ background: 'transparent', color: '#fff', border: '1px solid #444', fontSize: isMobile ? 10 : 12 }}>
{getCategoryLabel(item.category)}
</Tag>
{item.is_verified_owner && (
<Tooltip title="已验证购买过相关产品">
<Tag icon={<CheckCircleFilled />} color="#00b96b" style={{ margin: 0 }}>认证用户</Tag>
<Tag icon={<CheckCircleFilled />} color="#00b96b" style={{ margin: 0 }}>{isMobile ? '认证' : '认证用户'}</Tag>
</Tooltip>
)}
<Text style={{ color: '#fff', fontSize: 18, fontWeight: 'bold', cursor: 'pointer' }}>
<Text style={{ color: '#fff', fontSize: isMobile ? 16 : 18, fontWeight: 'bold', cursor: 'pointer', lineHeight: 1.3 }}>
{item.title}
</Text>
</div>
<Paragraph
ellipsis={{ rows: 2 }}
style={{ color: '#aaa', marginBottom: 12, fontSize: 14 }}
style={{ color: '#aaa', marginBottom: 12, fontSize: isMobile ? 13 : 14 }}
>
{item.content.replace(/!\[.*?\]\(.*?\)/g, '[图片]').replace(/[#*`]/g, '')} {/* Simple markdown strip */}
</Paragraph>
@@ -211,10 +260,10 @@ const ForumList = () => {
</div>
)}
<Space size="middle" style={{ color: '#666', fontSize: 13 }}>
<Space>
<Space size={isMobile ? 4 : "middle"} wrap style={{ color: '#666', fontSize: isMobile ? 12 : 13 }}>
<Space size={4}>
<Avatar src={item.author_info?.avatar_url} icon={<UserOutlined />} size="small" />
<Text style={{ color: item.author_info?.is_star ? '#ffd700' : '#888', fontWeight: item.author_info?.is_star ? 'bold' : 'normal' }}>
<Text style={{ color: item.author_info?.is_star ? '#ffd700' : '#888', fontWeight: item.author_info?.is_star ? 'bold' : 'normal', fontSize: isMobile ? 12 : 14 }}>
{item.author_info?.nickname || '匿名用户'}
</Text>
{item.author_info?.is_star && (
@@ -223,22 +272,22 @@ const ForumList = () => {
</Tooltip>
)}
</Space>
<span></span>
{!isMobile && <span></span>}
<span>{new Date(item.created_at).toLocaleDateString()}</span>
{item.product_info && (
<Tag color="blue" style={{ marginLeft: 8 }}>{item.product_info.name}</Tag>
<Tag color="blue" style={{ marginLeft: isMobile ? 4 : 8, fontSize: 12 }}>{item.product_info.name}</Tag>
)}
</Space>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8, minWidth: 80 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4, minWidth: isMobile ? 50 : 80, marginLeft: 10 }}>
<div style={{ textAlign: 'center' }}>
<MessageOutlined style={{ fontSize: 16, color: '#00b96b' }} />
<div style={{ color: '#fff', fontWeight: 'bold' }}>{item.replies?.length || 0}</div>
<MessageOutlined style={{ fontSize: isMobile ? 14 : 16, color: '#00b96b' }} />
<div style={{ color: '#fff', fontWeight: 'bold', fontSize: isMobile ? 12 : 14 }}>{item.replies?.length || 0}</div>
</div>
<div style={{ textAlign: 'center', marginTop: 5 }}>
<EyeOutlined style={{ fontSize: 16, color: '#666' }} />
<div style={{ color: '#888', fontSize: 12 }}>{item.view_count || 0}</div>
<div style={{ textAlign: 'center', marginTop: isMobile ? 2 : 5 }}>
<EyeOutlined style={{ fontSize: isMobile ? 14 : 16, color: '#666' }} />
<div style={{ color: '#888', fontSize: isMobile ? 10 : 12 }}>{item.view_count || 0}</div>
</div>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Card, List, Tag, Typography, message, Space, Statistic, Divider, Modal, Descriptions, Tabs } from 'antd';
import { MobileOutlined, LockOutlined, SearchOutlined, CarOutlined, InboxOutlined, SafetyCertificateOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, UserOutlined, EnvironmentOutlined, PhoneOutlined, CalendarOutlined } from '@ant-design/icons';
import { getMySignups } from '../api';
import { motion } from 'framer-motion';
import LoginModal from '../components/LoginModal';
import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom';
@@ -74,13 +75,6 @@ const MyOrders = () => {
}
};
const getOrderTypeTag = (order) => {
if (order.config) return <Tag color="blue">硬件</Tag>;
if (order.course) return <Tag color="purple">VC课程</Tag>;
if (order.activity) return <Tag color="orange">活动</Tag>;
return <Tag>其他</Tag>;
};
const getOrderTitle = (order) => {
if (order.config_name) return order.config_name;
if (order.course_title) return order.course_title;
@@ -113,7 +107,7 @@ const MyOrders = () => {
<Button type="primary" size="large" onClick={() => setLoginVisible(true)}>立即登录</Button>
</div>
) : (
<div>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<div style={{ marginBottom: 20, textAlign: 'right', color: '#fff' }}>
当前登录用户: <span style={{ color: '#00b96b', fontWeight: 'bold', marginRight: 10 }}>{user.nickname}</span>
<Button
@@ -139,7 +133,7 @@ const MyOrders = () => {
<Card
hoverable
onClick={() => showDetail(order)}
title={<Space>{getOrderTypeTag(order)}<span style={{ color: '#fff' }}>订单号: {order.id}</span> {getStatusTag(order.status)}</Space>}
title={<Space><span style={{ color: '#fff' }}>订单号: {order.id}</span> {getStatusTag(order.status)}</Space>}
style={{
background: 'rgba(0,0,0,0.6)',
border: '1px solid rgba(255,255,255,0.1)',
@@ -294,7 +288,7 @@ const MyOrders = () => {
)
}
]} />
</div>
</motion.div>
)}
<Modal
@@ -314,7 +308,6 @@ const MyOrders = () => {
<Descriptions.Item label="订单号">
<Paragraph copyable={{ text: currentOrder.id }} style={{ marginBottom: 0 }}>{currentOrder.id}</Paragraph>
</Descriptions.Item>
<Descriptions.Item label="订单类型">{getOrderTypeTag(currentOrder)}</Descriptions.Item>
<Descriptions.Item label="商品名称">{getOrderTitle(currentOrder)}</Descriptions.Item>
<Descriptions.Item label="下单时间">{new Date(currentOrder.created_at).toLocaleString()}</Descriptions.Item>
<Descriptions.Item label="状态更新时间">{new Date(currentOrder.updated_at).toLocaleString()}</Descriptions.Item>