This commit is contained in:
24
backend/community/migrations/0016_reply_likes_topic_likes.py
Normal file
24
backend/community/migrations/0016_reply_likes_topic_likes.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 6.0.1 on 2026-03-02 12:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('community', '0015_topic_status'),
|
||||||
|
('shop', '0039_vccourse_video_embed_code'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='reply',
|
||||||
|
name='likes',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='liked_replies', to='shop.wechatuser', verbose_name='点赞用户'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='topic',
|
||||||
|
name='likes',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='liked_topics', to='shop.wechatuser', verbose_name='点赞用户'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -143,6 +143,7 @@ class Topic(models.Model):
|
|||||||
related_course = models.ForeignKey(VCCourse, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联课程")
|
related_course = models.ForeignKey(VCCourse, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联课程")
|
||||||
|
|
||||||
view_count = models.IntegerField(default=0, verbose_name="浏览量")
|
view_count = models.IntegerField(default=0, verbose_name="浏览量")
|
||||||
|
likes = models.ManyToManyField(WeChatUser, related_name='liked_topics', blank=True, verbose_name="点赞用户")
|
||||||
is_pinned = models.BooleanField(default=False, verbose_name="置顶")
|
is_pinned = models.BooleanField(default=False, verbose_name="置顶")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||||
@@ -209,6 +210,7 @@ class Reply(models.Model):
|
|||||||
content = models.TextField(verbose_name="回复内容", help_text="支持Markdown格式")
|
content = models.TextField(verbose_name="回复内容", help_text="支持Markdown格式")
|
||||||
author = models.ForeignKey(WeChatUser, on_delete=models.CASCADE, related_name='replies', verbose_name="回复者")
|
author = models.ForeignKey(WeChatUser, on_delete=models.CASCADE, related_name='replies', verbose_name="回复者")
|
||||||
reply_to = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', verbose_name="回复楼层")
|
reply_to = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', verbose_name="回复楼层")
|
||||||
|
likes = models.ManyToManyField(WeChatUser, related_name='liked_replies', blank=True, verbose_name="点赞用户")
|
||||||
is_pinned = models.BooleanField(default=False, verbose_name="置顶")
|
is_pinned = models.BooleanField(default=False, verbose_name="置顶")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="回复时间")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="回复时间")
|
||||||
|
|
||||||
|
|||||||
@@ -88,12 +88,23 @@ class ReplySerializer(serializers.ModelSerializer):
|
|||||||
write_only=True,
|
write_only=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
like_count = serializers.IntegerField(source='likes.count', read_only=True)
|
||||||
|
is_liked = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Reply
|
model = Reply
|
||||||
fields = ['id', 'topic', 'content', 'author', 'author_info', 'reply_to', 'media', 'created_at', 'media_ids', 'is_pinned']
|
fields = ['id', 'topic', 'content', 'author', 'author_info', 'reply_to', 'media', 'created_at', 'media_ids', 'is_pinned', 'like_count', 'is_liked']
|
||||||
read_only_fields = ['author', 'created_at']
|
read_only_fields = ['author', 'created_at']
|
||||||
|
|
||||||
|
def get_is_liked(self, obj):
|
||||||
|
request = self.context.get('request')
|
||||||
|
if not request:
|
||||||
|
return False
|
||||||
|
user = get_current_wechat_user(request)
|
||||||
|
if user:
|
||||||
|
return obj.likes.filter(id=user.id).exists()
|
||||||
|
return False
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
media_ids = validated_data.pop('media_ids', [])
|
media_ids = validated_data.pop('media_ids', [])
|
||||||
reply = super().create(validated_data)
|
reply = super().create(validated_data)
|
||||||
@@ -106,6 +117,8 @@ class TopicSerializer(serializers.ModelSerializer):
|
|||||||
replies = ReplySerializer(many=True, read_only=True)
|
replies = ReplySerializer(many=True, read_only=True)
|
||||||
media = TopicMediaSerializer(many=True, read_only=True)
|
media = TopicMediaSerializer(many=True, read_only=True)
|
||||||
is_verified_owner = serializers.BooleanField(read_only=True)
|
is_verified_owner = serializers.BooleanField(read_only=True)
|
||||||
|
like_count = serializers.IntegerField(source='likes.count', read_only=True)
|
||||||
|
is_liked = serializers.SerializerMethodField()
|
||||||
|
|
||||||
product_info = ESP32ConfigSerializer(source='related_product', read_only=True)
|
product_info = ESP32ConfigSerializer(source='related_product', read_only=True)
|
||||||
service_info = ServiceSerializer(source='related_service', read_only=True)
|
service_info = ServiceSerializer(source='related_service', read_only=True)
|
||||||
@@ -125,10 +138,20 @@ class TopicSerializer(serializers.ModelSerializer):
|
|||||||
'related_service', 'service_info',
|
'related_service', 'service_info',
|
||||||
'related_course', 'course_info',
|
'related_course', 'course_info',
|
||||||
'view_count', 'is_pinned', 'created_at', 'updated_at',
|
'view_count', 'is_pinned', 'created_at', 'updated_at',
|
||||||
'is_verified_owner', 'replies', 'media', 'media_ids'
|
'is_verified_owner', 'replies', 'media', 'media_ids',
|
||||||
|
'like_count', 'is_liked'
|
||||||
]
|
]
|
||||||
read_only_fields = ['author', 'view_count', 'created_at', 'updated_at', 'is_verified_owner', 'status']
|
read_only_fields = ['author', 'view_count', 'created_at', 'updated_at', 'is_verified_owner', 'status']
|
||||||
|
|
||||||
|
def get_is_liked(self, obj):
|
||||||
|
request = self.context.get('request')
|
||||||
|
if not request:
|
||||||
|
return False
|
||||||
|
user = get_current_wechat_user(request)
|
||||||
|
if user:
|
||||||
|
return obj.likes.filter(id=user.id).exists()
|
||||||
|
return False
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
media_ids = validated_data.pop('media_ids', [])
|
media_ids = validated_data.pop('media_ids', [])
|
||||||
topic = super().create(validated_data)
|
topic = super().create(validated_data)
|
||||||
|
|||||||
@@ -284,6 +284,22 @@ class TopicViewSet(viewsets.ModelViewSet):
|
|||||||
return Response({'error': '请先登录'}, status=401)
|
return Response({'error': '请先登录'}, status=401)
|
||||||
return super().create(request, *args, **kwargs)
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def like(self, request, pk=None):
|
||||||
|
obj = self.get_object()
|
||||||
|
user = get_current_wechat_user(request)
|
||||||
|
if not user:
|
||||||
|
return Response({'error': '请先登录'}, status=401)
|
||||||
|
|
||||||
|
if obj.likes.filter(id=user.id).exists():
|
||||||
|
obj.likes.remove(user)
|
||||||
|
liked = False
|
||||||
|
else:
|
||||||
|
obj.likes.add(user)
|
||||||
|
liked = True
|
||||||
|
|
||||||
|
return Response({'liked': liked, 'count': obj.likes.count()})
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
instance.view_count += 1
|
instance.view_count += 1
|
||||||
@@ -310,6 +326,22 @@ class ReplyViewSet(viewsets.ModelViewSet):
|
|||||||
return Response({'error': '请先登录'}, status=401)
|
return Response({'error': '请先登录'}, status=401)
|
||||||
return super().create(request, *args, **kwargs)
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def like(self, request, pk=None):
|
||||||
|
obj = self.get_object()
|
||||||
|
user = get_current_wechat_user(request)
|
||||||
|
if not user:
|
||||||
|
return Response({'error': '请先登录'}, status=401)
|
||||||
|
|
||||||
|
if obj.likes.filter(id=user.id).exists():
|
||||||
|
obj.likes.remove(user)
|
||||||
|
liked = False
|
||||||
|
else:
|
||||||
|
obj.likes.add(user)
|
||||||
|
liked = True
|
||||||
|
|
||||||
|
return Response({'liked': liked, 'count': obj.likes.count()})
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
class TopicMediaViewSet(viewsets.ViewSet):
|
class TopicMediaViewSet(viewsets.ViewSet):
|
||||||
|
|||||||
@@ -51,9 +51,11 @@ export const uploadUserAvatar = (data) => {
|
|||||||
// Community / Forum API
|
// Community / Forum API
|
||||||
export const getTopics = (params) => api.get('/community/topics/', { params });
|
export const getTopics = (params) => api.get('/community/topics/', { params });
|
||||||
export const getTopicDetail = (id) => api.get(`/community/topics/${id}/`);
|
export const getTopicDetail = (id) => api.get(`/community/topics/${id}/`);
|
||||||
|
export const likeTopic = (id) => api.post(`/community/topics/${id}/like/`);
|
||||||
export const createTopic = (data) => api.post('/community/topics/', data);
|
export const createTopic = (data) => api.post('/community/topics/', data);
|
||||||
export const updateTopic = (id, data) => api.patch(`/community/topics/${id}/`, data);
|
export const updateTopic = (id, data) => api.patch(`/community/topics/${id}/`, data);
|
||||||
export const getReplies = (params) => api.get('/community/replies/', { params });
|
export const getReplies = (params) => api.get('/community/replies/', { params });
|
||||||
|
export const likeReply = (id) => api.post(`/community/replies/${id}/like/`);
|
||||||
export const createReply = (data) => api.post('/community/replies/', data);
|
export const createReply = (data) => api.post('/community/replies/', data);
|
||||||
export const uploadMedia = (data) => {
|
export const uploadMedia = (data) => {
|
||||||
return api.post('/community/media/', data, {
|
return api.post('/community/media/', data, {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
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, Grid, Modal } from 'antd';
|
import { Typography, Card, Avatar, Tag, Space, Button, Divider, Input, message, Upload, Tooltip, Grid, Modal } from 'antd';
|
||||||
import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, UploadOutlined, EditOutlined, StarFilled, CloseOutlined } from '@ant-design/icons';
|
import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, UploadOutlined, EditOutlined, StarFilled, CloseOutlined, LikeOutlined, LikeFilled } from '@ant-design/icons';
|
||||||
import { getTopicDetail, createReply, uploadMedia, getStarUsers } from '../api';
|
import { getTopicDetail, createReply, uploadMedia, getStarUsers, likeTopic, likeReply } from '../api';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import LoginModal from '../components/LoginModal';
|
import LoginModal from '../components/LoginModal';
|
||||||
import CreateTopicModal from '../components/CreateTopicModal';
|
import CreateTopicModal from '../components/CreateTopicModal';
|
||||||
@@ -94,6 +94,44 @@ const ForumDetail = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLikeTopic = async () => {
|
||||||
|
if (!user) {
|
||||||
|
setLoginModalVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await likeTopic(topic.id);
|
||||||
|
setTopic(prev => ({
|
||||||
|
...prev,
|
||||||
|
is_liked: res.data.liked,
|
||||||
|
like_count: res.data.count
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLikeReply = async (replyId) => {
|
||||||
|
if (!user) {
|
||||||
|
setLoginModalVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await likeReply(replyId);
|
||||||
|
setTopic(prev => ({
|
||||||
|
...prev,
|
||||||
|
replies: prev.replies.map(r => {
|
||||||
|
if (r.id === replyId) {
|
||||||
|
return { ...r, is_liked: res.data.liked, like_count: res.data.count };
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmitReply = async () => {
|
const handleSubmitReply = async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
setLoginModalVisible(true);
|
setLoginModalVisible(true);
|
||||||
@@ -255,6 +293,10 @@ const ForumDetail = () => {
|
|||||||
<EyeOutlined />
|
<EyeOutlined />
|
||||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{topic.view_count} 阅读</span>
|
<span style={{ fontSize: isMobile ? 12 : 14 }}>{topic.view_count} 阅读</span>
|
||||||
</Space>
|
</Space>
|
||||||
|
<Space style={{ cursor: 'pointer' }} onClick={handleLikeTopic}>
|
||||||
|
{topic.is_liked ? <LikeFilled style={{ color: '#00b96b' }} /> : <LikeOutlined />}
|
||||||
|
<span style={{ fontSize: isMobile ? 12 : 14, color: topic.is_liked ? '#00b96b' : 'inherit' }}>{topic.like_count || 0} 点赞</span>
|
||||||
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -318,6 +360,12 @@ const ForumDetail = () => {
|
|||||||
<Text style={{ color: '#aaa', fontWeight: 'bold', fontSize: isMobile ? 13 : 14 }}>{reply.author_info?.nickname}</Text>
|
<Text style={{ color: '#aaa', fontWeight: 'bold', fontSize: isMobile ? 13 : 14 }}>{reply.author_info?.nickname}</Text>
|
||||||
{reply.is_pinned && <Tag color="red" style={{ margin: 0 }}>置顶</Tag>}
|
{reply.is_pinned && <Tag color="red" style={{ margin: 0 }}>置顶</Tag>}
|
||||||
<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 size={isMobile ? 'small' : 'middle'} align="center">
|
||||||
|
<Space onClick={() => handleLikeReply(reply.id)} style={{ cursor: 'pointer' }}>
|
||||||
|
{reply.is_liked ? <LikeFilled style={{ color: '#00b96b' }} /> : <LikeOutlined style={{ color: '#666' }} />}
|
||||||
|
<span style={{ fontSize: 12, color: reply.is_liked ? '#00b96b' : '#666' }}>{reply.like_count || 0}</span>
|
||||||
|
</Space>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -326,8 +374,8 @@ const ForumDetail = () => {
|
|||||||
>
|
>
|
||||||
回复
|
回复
|
||||||
</Button>
|
</Button>
|
||||||
|
<Text style={{ color: '#444', fontSize: 12 }}>#{index + 1}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
<Text style={{ color: '#444', fontSize: 12 }}>#{index + 1}</Text>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: '#eee', fontSize: isMobile ? 14 : 16 }} className={styles['markdown-body']}>
|
<div style={{ color: '#eee', fontSize: isMobile ? 14 : 16 }} className={styles['markdown-body']}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|||||||
@@ -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, Grid, Carousel, Modal } from 'antd';
|
import { Typography, Input, Button, List, Tag, Avatar, Card, Space, Spin, message, Badge, Tooltip, Tabs, Row, Col, Grid, Carousel, Modal } from 'antd';
|
||||||
import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined, StarFilled, QuestionCircleOutlined, ShareAltOutlined, SoundOutlined, RightOutlined, CloseOutlined } from '@ant-design/icons';
|
import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined, StarFilled, QuestionCircleOutlined, ShareAltOutlined, SoundOutlined, RightOutlined, CloseOutlined, LikeOutlined } 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';
|
||||||
@@ -296,6 +296,10 @@ const ForumList = () => {
|
|||||||
<EyeOutlined style={{ fontSize: isMobile ? 14 : 16, color: '#666' }} />
|
<EyeOutlined style={{ fontSize: isMobile ? 14 : 16, color: '#666' }} />
|
||||||
<div style={{ color: '#888', fontSize: isMobile ? 10 : 12 }}>{item.view_count || 0}</div>
|
<div style={{ color: '#888', fontSize: isMobile ? 10 : 12 }}>{item.view_count || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ textAlign: 'center', marginTop: isMobile ? 2 : 5 }}>
|
||||||
|
<LikeOutlined style={{ fontSize: isMobile ? 14 : 16, color: '#666' }} />
|
||||||
|
<div style={{ color: '#888', fontSize: isMobile ? 10 : 12 }}>{item.like_count || 0}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -50,9 +50,11 @@ export const wechatLogin = (code: string) => request({ url: '/wechat/login/', me
|
|||||||
// Forum / Community
|
// Forum / Community
|
||||||
export const getTopics = (params: any) => request({ url: '/community/topics/', data: params })
|
export const getTopics = (params: any) => request({ url: '/community/topics/', data: params })
|
||||||
export const getTopicDetail = (id: number) => request({ url: `/community/topics/${id}/` })
|
export const getTopicDetail = (id: number) => request({ url: `/community/topics/${id}/` })
|
||||||
|
export const likeTopic = (id: number) => request({ url: `/community/topics/${id}/like/`, method: 'POST' })
|
||||||
export const createTopic = (data: any) => request({ url: '/community/topics/', method: 'POST', data })
|
export const createTopic = (data: any) => request({ url: '/community/topics/', method: 'POST', data })
|
||||||
export const updateTopic = (id: number, data: any) => request({ url: `/community/topics/${id}/`, method: 'PATCH', data })
|
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 getReplies = (params: any) => request({ url: '/community/replies/', data: params })
|
||||||
|
export const likeReply = (id: number) => request({ url: `/community/replies/${id}/like/`, method: 'POST' })
|
||||||
export const createReply = (data: any) => request({ url: '/community/replies/', method: 'POST', data })
|
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 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 deleteReply = (id: number) => request({ url: `/community/replies/${id}/`, method: 'DELETE' })
|
||||||
|
|||||||
@@ -310,6 +310,10 @@ const ForumList = () => {
|
|||||||
<AtIcon value='eye' size='14' color='#777' />
|
<AtIcon value='eye' size='14' color='#777' />
|
||||||
<Text>{item.view_count || 0}</Text>
|
<Text>{item.view_count || 0}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View className='stat-item'>
|
||||||
|
<AtIcon value='heart' size='14' color='#777' />
|
||||||
|
<Text>{item.like_count || 0}</Text>
|
||||||
|
</View>
|
||||||
<View className='stat-item'>
|
<View className='stat-item'>
|
||||||
<AtIcon value='message' size='14' color='#777' />
|
<AtIcon value='message' size='14' color='#777' />
|
||||||
<Text>{item.replies?.length || 0}</Text>
|
<Text>{item.replies?.length || 0}</Text>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow } from '@tarojs/taro'
|
import Taro, { useRouter, useShareAppMessage, useShareTimeline, useDidShow } from '@tarojs/taro'
|
||||||
import { View, Text, Image, Video, Input, ScrollView } from '@tarojs/components'
|
import { View, Text, Image, Video, Input, ScrollView } from '@tarojs/components'
|
||||||
import { AtActivityIndicator, AtIcon, AtActionSheet, AtActionSheetItem, AtFloatLayout } from 'taro-ui'
|
import { AtActivityIndicator, AtIcon, AtActionSheet, AtActionSheetItem, AtFloatLayout } from 'taro-ui'
|
||||||
import { getTopicDetail, createReply, uploadMedia, getStarUsers } from '../../../api'
|
import { getTopicDetail, createReply, uploadMedia, getStarUsers, likeTopic, likeReply } from '../../../api'
|
||||||
import MarkdownReader from '../../../components/MarkdownReader'
|
import MarkdownReader from '../../../components/MarkdownReader'
|
||||||
import './detail.scss'
|
import './detail.scss'
|
||||||
|
|
||||||
@@ -70,6 +70,48 @@ const ForumDetail = () => {
|
|||||||
setShowStarUsers(false)
|
setShowStarUsers(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLikeTopic = async () => {
|
||||||
|
const token = Taro.getStorageSync('token')
|
||||||
|
if (!token) {
|
||||||
|
Taro.showToast({ title: '请先登录', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await likeTopic(topic.id)
|
||||||
|
const data = res.data || res
|
||||||
|
setTopic(prev => ({
|
||||||
|
...prev,
|
||||||
|
is_liked: data.liked,
|
||||||
|
like_count: data.count
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
Taro.showToast({ title: '操作失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLikeReply = async (replyId) => {
|
||||||
|
const token = Taro.getStorageSync('token')
|
||||||
|
if (!token) {
|
||||||
|
Taro.showToast({ title: '请先登录', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await likeReply(replyId)
|
||||||
|
const data = res.data || res
|
||||||
|
setTopic(prev => ({
|
||||||
|
...prev,
|
||||||
|
replies: prev.replies.map(r => {
|
||||||
|
if (r.id === replyId) {
|
||||||
|
return { ...r, is_liked: data.liked, like_count: data.count }
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
Taro.showToast({ title: '操作失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 分享给好友
|
// 分享给好友
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
@@ -197,6 +239,10 @@ const ForumDetail = () => {
|
|||||||
<AtIcon value='eye' size='14' color='#666' style={{marginRight: 4}} />
|
<AtIcon value='eye' size='14' color='#666' style={{marginRight: 4}} />
|
||||||
<Text>{topic.view_count}</Text>
|
<Text>{topic.view_count}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={{display: 'flex', alignItems: 'center', marginLeft: 10}} onClick={handleLikeTopic}>
|
||||||
|
<AtIcon value={topic.is_liked ? 'heart-2' : 'heart'} size='14' color={topic.is_liked ? '#ff4d4f' : '#666'} style={{marginRight: 4}} />
|
||||||
|
<Text style={{color: topic.is_liked ? '#ff4d4f' : '#666'}}>{topic.like_count || 0}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
{userInfo && topic.author === userInfo.id && (
|
{userInfo && topic.author === userInfo.id && (
|
||||||
<View onClick={handleEdit} style={{display: 'flex', alignItems: 'center', marginLeft: 'auto', padding: '6px 12px', background: 'rgba(0, 185, 107, 0.15)', borderRadius: 20}}>
|
<View onClick={handleEdit} style={{display: 'flex', alignItems: 'center', marginLeft: 'auto', padding: '6px 12px', background: 'rgba(0, 185, 107, 0.15)', borderRadius: 20}}>
|
||||||
@@ -238,6 +284,10 @@ const ForumDetail = () => {
|
|||||||
<Text style={{fontSize: 10, color: '#666', marginTop: 2}}>#{idx + 1} • {new Date(reply.created_at).toLocaleDateString()}</Text>
|
<Text style={{fontSize: 10, color: '#666', marginTop: 2}}>#{idx + 1} • {new Date(reply.created_at).toLocaleDateString()}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={{display: 'flex', alignItems: 'center'}}>
|
<View style={{display: 'flex', alignItems: 'center'}}>
|
||||||
|
<View onClick={() => handleLikeReply(reply.id)} style={{marginRight: 10, display: 'flex', alignItems: 'center'}}>
|
||||||
|
<AtIcon value={reply.is_liked ? 'heart-2' : 'heart'} size='14' color={reply.is_liked ? '#ff4d4f' : '#ccc'} />
|
||||||
|
<Text style={{fontSize: 10, color: reply.is_liked ? '#ff4d4f' : '#999', marginLeft: 2}}>{reply.like_count || 0}</Text>
|
||||||
|
</View>
|
||||||
<View onClick={() => handleReplyToUser(reply.author_info?.nickname)} style={{marginRight: 10, padding: '2px 6px', background: '#f0f0f0', borderRadius: 4}}>
|
<View onClick={() => handleReplyToUser(reply.author_info?.nickname)} style={{marginRight: 10, padding: '2px 6px', background: '#f0f0f0', borderRadius: 4}}>
|
||||||
<Text style={{fontSize: 10, color: '#666'}}>回复</Text>
|
<Text style={{fontSize: 10, color: '#666'}}>回复</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user