移动端
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

101
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,101 @@
# name: Deploy to Server
# on:
# push:
# branches:
# - main
# jobs:
# build-and-deploy:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# - name: Build Docker Images
# run: |
# echo "Building Backend Image..."
# docker build -t market-backend:latest ./backend
# echo "Building Frontend Image..."
# # 注意:这里我们传入了服务器的 IP 地址作为 API URL
# # 如果你的后端端口不是 8000请修改这里
# docker build -t market-frontend:latest \
# --build-arg VITE_API_URL=http://47.101.218.42:8000/api \
# ./frontend
# - name: Save Docker Images
# run: |
# echo "Saving images to tarball..."
# docker save market-backend:latest market-frontend:latest | gzip > market-images.tar.gz
# - name: Generate Production Compose File
# run: |
# # 生成生产环境专用的 docker-compose.prod.yml
# # 1. 使用构建好的镜像 (image) 替代构建指令 (build)
# # 2. 移除代码挂载 (volumes),确保使用镜像内的代码
# cat > docker-compose.prod.yml <<EOF
# services:
# backend:
# image: market-backend:latest
# command: sh -c "python manage.py collectstatic --noinput && python manage.py migrate && gunicorn --bind 0.0.0.0:8000 --access-logfile - --error-logfile - config.wsgi:application"
# ports:
# - "8000:8000"
# environment:
# - DB_NAME=\${DB_NAME:-market}
# - DB_USER=\${DB_USER:-market}
# - DB_PASSWORD=\${DB_PASSWORD:-123market}
# - DB_HOST=\${DB_HOST:-6.6.6.66}
# - DB_PORT=\${DB_PORT:-5432}
# # 如果需要持久化媒体文件,请取消下面的注释并在服务器上创建相应目录
# # volumes:
# # - ./media:/app/media
# frontend:
# image: market-frontend:latest
# ports:
# - "15173:15173"
# environment:
# - VITE_API_URL=http://47.101.218.42:8000/api
# depends_on:
# - backend
# EOF
# - name: Ensure target directory exists
# uses: appleboy/ssh-action@v1.0.3
# with:
# host: 47.101.218.42
# username: ecs-user
# password: 123quant-speed
# script: mkdir -p ~/data/dev/market_page
# - name: Copy files to server
# uses: appleboy/scp-action@v0.1.7
# with:
# host: 47.101.218.42
# username: ecs-user
# password: 123quant-speed
# source: "market-images.tar.gz,docker-compose.prod.yml"
# target: "~/data/dev/market_page"
# - name: Deploy on Server
# uses: appleboy/ssh-action@v1.0.3
# with:
# host: 47.101.218.42
# username: ecs-user
# password: 123quant-speed
# script: |
# cd ~/data/dev/market_page
# echo "1. Loading Docker images (this may take a while)..."
# gunzip -c market-images.tar.gz | sudo docker load
# echo "2. Restarting services..."
# # 使用 -f 指定生产环境配置文件
# echo "123quant-speed" | sudo -S docker compose -f docker-compose.prod.yml down
# echo "123quant-speed" | sudo -S docker compose -f docker-compose.prod.yml up -d
# echo "3. Cleaning up..."
# rm market-images.tar.gz
# echo "Deployment successful!"

View File

@@ -92,7 +92,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
课程报名序列化器 课程报名序列化器
""" """
course_title = serializers.CharField(source='course.title', read_only=True) course_title = serializers.CharField(source='course.title', read_only=True)
ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True, allow_null=True) ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
class Meta: class Meta:
model = CourseEnrollment model = CourseEnrollment
@@ -124,7 +124,7 @@ class ServiceOrderSerializer(serializers.ModelSerializer):
""" """
service_name = serializers.CharField(source='service.title', read_only=True) service_name = serializers.CharField(source='service.title', read_only=True)
# 接收前端传来的 ref_code # 接收前端传来的 ref_code
ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True, allow_null=True) ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
class Meta: class Meta:
model = ServiceOrder model = ServiceOrder
@@ -212,7 +212,7 @@ class OrderSerializer(serializers.ModelSerializer):
salesperson_name = serializers.CharField(source='salesperson.name', read_only=True) salesperson_name = serializers.CharField(source='salesperson.name', read_only=True)
salesperson_code = serializers.CharField(source='salesperson.code', read_only=True) salesperson_code = serializers.CharField(source='salesperson.code', read_only=True)
# 接收前端传来的 ref_code用于查找 Salesperson # 接收前端传来的 ref_code用于查找 Salesperson
ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True, allow_null=True) ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
class Meta: class Meta:
model = Order model = Order

View File

@@ -11,7 +11,7 @@ services:
- DB_NAME=market - DB_NAME=market
- DB_USER=market - DB_USER=market
- DB_PASSWORD=123market - DB_PASSWORD=123market
- DB_HOST=6.6.6.66 - DB_HOST=localhost
- DB_PORT=5432 - DB_PORT=5432
frontend: frontend:

View File

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

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; 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 { 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 } from '@ant-design/icons'; import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined, StarFilled, QuestionCircleOutlined, ShareAltOutlined, SoundOutlined, RightOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { getTopics, getStarUsers, getAnnouncements } from '../api'; import { getTopics, getStarUsers, getAnnouncements } from '../api';
@@ -9,8 +9,11 @@ import CreateTopicModal from '../components/CreateTopicModal';
import LoginModal from '../components/LoginModal'; import LoginModal from '../components/LoginModal';
const { Title, Text, Paragraph } = Typography; const { Title, Text, Paragraph } = Typography;
const { useBreakpoint } = Grid;
const ForumList = () => { const ForumList = () => {
const screens = useBreakpoint();
const isMobile = !screens.md; // roughly checks if screen is smaller than medium
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
@@ -108,7 +111,7 @@ const ForumList = () => {
{/* Hero Section */} {/* Hero Section */}
<div style={{ <div style={{
textAlign: 'center', 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%)' background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,185,107,0.1) 100%)'
}}> }}>
<motion.div <motion.div
@@ -116,17 +119,23 @@ const ForumList = () => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }} 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 <span style={{ color: '#00b96b' }}>Quant Speed</span> Developer Community
</Title> </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> </Text>
</motion.div> </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 <Input
size="large" size={isMobile ? "middle" : "large"}
placeholder="搜索感兴趣的话题..." placeholder="搜索感兴趣的话题..."
prefix={<SearchOutlined style={{ color: '#666' }} />} prefix={<SearchOutlined style={{ color: '#666' }} />}
style={{ borderRadius: 8, background: 'rgba(255,255,255,0.1)', border: '1px solid #333', color: '#fff' }} style={{ borderRadius: 8, background: 'rgba(255,255,255,0.1)', border: '1px solid #333', color: '#fff' }}
@@ -135,10 +144,10 @@ const ForumList = () => {
/> />
<Button <Button
type="primary" type="primary"
size="large" size={isMobile ? "middle" : "large"}
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={handleCreateClick} onClick={handleCreateClick}
style={{ height: 'auto', borderRadius: 8 }} style={{ height: 'auto', borderRadius: 8, width: isMobile ? '100%' : 'auto' }}
> >
发布新帖 发布新帖
</Button> </Button>
@@ -146,14 +155,54 @@ const ForumList = () => {
</div> </div>
{/* Content Section */} {/* 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}> <Row gutter={24}>
<Col xs={24} md={18}> <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 <Tabs
defaultActiveKey="all" defaultActiveKey="all"
items={items} items={items}
onChange={setCategory} onChange={setCategory}
tabBarStyle={{ color: '#fff' }} tabBarStyle={{ color: '#fff', marginBottom: isMobile ? 10 : 16 }}
/> />
<List <List
loading={loading} loading={loading}
@@ -174,29 +223,29 @@ const ForumList = () => {
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
boxShadow: item.is_pinned ? '0 0 10px rgba(0, 185, 107, 0.1)' : 'none' 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}`)} onClick={() => navigate(`/forum/${item.id}`)}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}> <div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: isMobile ? 4 : 8, flexWrap: 'wrap' }}>
{item.is_pinned && <Tag color="red" icon={<FireOutlined />}>置顶</Tag>} {item.is_pinned && <Tag color="red" icon={<FireOutlined />}>{isMobile ? '顶' : '置顶'}</Tag>}
<Tag icon={getCategoryIcon(item.category)} style={{ background: 'transparent', color: '#fff', border: '1px solid #444' }}> <Tag icon={getCategoryIcon(item.category)} style={{ background: 'transparent', color: '#fff', border: '1px solid #444', fontSize: isMobile ? 10 : 12 }}>
{getCategoryLabel(item.category)} {getCategoryLabel(item.category)}
</Tag> </Tag>
{item.is_verified_owner && ( {item.is_verified_owner && (
<Tooltip title="已验证购买过相关产品"> <Tooltip title="已验证购买过相关产品">
<Tag icon={<CheckCircleFilled />} color="#00b96b" style={{ margin: 0 }}>认证用户</Tag> <Tag icon={<CheckCircleFilled />} color="#00b96b" style={{ margin: 0 }}>{isMobile ? '认证' : '认证用户'}</Tag>
</Tooltip> </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} {item.title}
</Text> </Text>
</div> </div>
<Paragraph <Paragraph
ellipsis={{ rows: 2 }} 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 */} {item.content.replace(/!\[.*?\]\(.*?\)/g, '[图片]').replace(/[#*`]/g, '')} {/* Simple markdown strip */}
</Paragraph> </Paragraph>
@@ -211,10 +260,10 @@ const ForumList = () => {
</div> </div>
)} )}
<Space size="middle" style={{ color: '#666', fontSize: 13 }}> <Space size={isMobile ? 4 : "middle"} wrap style={{ color: '#666', fontSize: isMobile ? 12 : 13 }}>
<Space> <Space size={4}>
<Avatar src={item.author_info?.avatar_url} icon={<UserOutlined />} size="small" /> <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 || '匿名用户'} {item.author_info?.nickname || '匿名用户'}
</Text> </Text>
{item.author_info?.is_star && ( {item.author_info?.is_star && (
@@ -223,22 +272,22 @@ const ForumList = () => {
</Tooltip> </Tooltip>
)} )}
</Space> </Space>
<span></span> {!isMobile && <span></span>}
<span>{new Date(item.created_at).toLocaleDateString()}</span> <span>{new Date(item.created_at).toLocaleDateString()}</span>
{item.product_info && ( {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> </Space>
</div> </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' }}> <div style={{ textAlign: 'center' }}>
<MessageOutlined style={{ fontSize: 16, color: '#00b96b' }} /> <MessageOutlined style={{ fontSize: isMobile ? 14 : 16, color: '#00b96b' }} />
<div style={{ color: '#fff', fontWeight: 'bold' }}>{item.replies?.length || 0}</div> <div style={{ color: '#fff', fontWeight: 'bold', fontSize: isMobile ? 12 : 14 }}>{item.replies?.length || 0}</div>
</div> </div>
<div style={{ textAlign: 'center', marginTop: 5 }}> <div style={{ textAlign: 'center', marginTop: isMobile ? 2 : 5 }}>
<EyeOutlined style={{ fontSize: 16, color: '#666' }} /> <EyeOutlined style={{ fontSize: isMobile ? 14 : 16, color: '#666' }} />
<div style={{ color: '#888', fontSize: 12 }}>{item.view_count || 0}</div> <div style={{ color: '#888', fontSize: isMobile ? 10 : 12 }}>{item.view_count || 0}</div>
</div> </div>
</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 { 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 { MobileOutlined, LockOutlined, SearchOutlined, CarOutlined, InboxOutlined, SafetyCertificateOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, UserOutlined, EnvironmentOutlined, PhoneOutlined, CalendarOutlined } from '@ant-design/icons';
import { getMySignups } from '../api'; import { getMySignups } from '../api';
import { motion } from 'framer-motion';
import LoginModal from '../components/LoginModal'; import LoginModal from '../components/LoginModal';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom'; 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) => { const getOrderTitle = (order) => {
if (order.config_name) return order.config_name; if (order.config_name) return order.config_name;
if (order.course_title) return order.course_title; if (order.course_title) return order.course_title;
@@ -113,7 +107,7 @@ const MyOrders = () => {
<Button type="primary" size="large" onClick={() => setLoginVisible(true)}>立即登录</Button> <Button type="primary" size="large" onClick={() => setLoginVisible(true)}>立即登录</Button>
</div> </div>
) : ( ) : (
<div> <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<div style={{ marginBottom: 20, textAlign: 'right', color: '#fff' }}> <div style={{ marginBottom: 20, textAlign: 'right', color: '#fff' }}>
当前登录用户: <span style={{ color: '#00b96b', fontWeight: 'bold', marginRight: 10 }}>{user.nickname}</span> 当前登录用户: <span style={{ color: '#00b96b', fontWeight: 'bold', marginRight: 10 }}>{user.nickname}</span>
<Button <Button
@@ -139,7 +133,7 @@ const MyOrders = () => {
<Card <Card
hoverable hoverable
onClick={() => showDetail(order)} 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={{ style={{
background: 'rgba(0,0,0,0.6)', background: 'rgba(0,0,0,0.6)',
border: '1px solid rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.1)',
@@ -294,7 +288,7 @@ const MyOrders = () => {
) )
} }
]} /> ]} />
</div> </motion.div>
)} )}
<Modal <Modal
@@ -314,7 +308,6 @@ const MyOrders = () => {
<Descriptions.Item label="订单号"> <Descriptions.Item label="订单号">
<Paragraph copyable={{ text: currentOrder.id }} style={{ marginBottom: 0 }}>{currentOrder.id}</Paragraph> <Paragraph copyable={{ text: currentOrder.id }} style={{ marginBottom: 0 }}>{currentOrder.id}</Paragraph>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="订单类型">{getOrderTypeTag(currentOrder)}</Descriptions.Item>
<Descriptions.Item label="商品名称">{getOrderTitle(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.created_at).toLocaleString()}</Descriptions.Item>
<Descriptions.Item label="状态更新时间">{new Date(currentOrder.updated_at).toLocaleString()}</Descriptions.Item> <Descriptions.Item label="状态更新时间">{new Date(currentOrder.updated_at).toLocaleString()}</Descriptions.Item>