This commit is contained in:
101
.github/workflows/deploy.yml
vendored
Normal file
101
.github/workflows/deploy.yml
vendored
Normal 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!"
|
||||
@@ -92,7 +92,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
|
||||
课程报名序列化器
|
||||
"""
|
||||
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:
|
||||
model = CourseEnrollment
|
||||
@@ -124,7 +124,7 @@ class ServiceOrderSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
service_name = serializers.CharField(source='service.title', read_only=True)
|
||||
# 接收前端传来的 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:
|
||||
model = ServiceOrder
|
||||
@@ -212,7 +212,7 @@ class OrderSerializer(serializers.ModelSerializer):
|
||||
salesperson_name = serializers.CharField(source='salesperson.name', read_only=True)
|
||||
salesperson_code = serializers.CharField(source='salesperson.code', read_only=True)
|
||||
# 接收前端传来的 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:
|
||||
model = Order
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- DB_NAME=market
|
||||
- DB_USER=market
|
||||
- DB_PASSWORD=123market
|
||||
- DB_HOST=6.6.6.66
|
||||
- DB_HOST=localhost
|
||||
- DB_PORT=5432
|
||||
|
||||
frontend:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user