Initial commit: React + Django 城市手册项目

- Django 4.2 + DRF + JWT + GraphQL
- React 18 + MobX + styled-components
- PostgreSQL 数据库
- Docker + Docker Compose + Nginx
- 完整的功能模块(用户、版块、文章、服务、交互、版主管理)
- 完整的文档(需求、部署、测试)
This commit is contained in:
mashen
2026-04-09 13:56:02 +00:00
commit c866e74ece
98 changed files with 7644 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { observer } from 'mobx-react-lite';
import { useNavigate } from 'react-router-dom';
import { useServiceStore } from '../../stores/ServiceStore';
import { useInteractionStore } from '../../stores/InteractionStore';
import Card from '../common/Card';
import Loading from '../common/Loading';
import ErrorMessage from '../common/ErrorMessage';
const ServiceCard = styled(Card)`
cursor: pointer;
`;
const Rating = styled.div`
display: flex;
align-items: center;
gap: 5px;
margin: 10px 0;
span {
font-weight: bold;
}
`;
const Star = styled.span`
color: ${props => props.filled ? '#ffc107' : '#dee2e6'};
font-size: 20px;
`;
const CommentsSection = styled.div`
margin-top: 40px;
`;
const CommentForm = styled.form`
margin-bottom: 20px;
textarea {
width: 100%;
min-height: 100px;
padding: 10px;
border: 1px solid #dee2e6;
border-radius: 5px;
resize: vertical;
}
`;
const ServiceDetailPage = observer(({ serviceId }) => {
const serviceStore = useServiceStore();
const interactionStore = useInteractionStore();
const navigate = useNavigate();
const [comment, setComment] = useState('');
const [liked, setLiked] = useState(false);
useEffect(() => {
serviceStore.fetchService(serviceId);
serviceStore.fetchServiceComments(serviceId);
serviceStore.fetchServiceStats(serviceId);
}, [serviceId, serviceStore]);
const handleLike = async () => {
const result = await serviceStore.likeService(serviceId);
if (result) {
setLiked(result.liked);
serviceStore.fetchServiceStats(serviceId);
}
};
const handleRate = async (score) => {
const result = await serviceStore.rateService(serviceId, score);
if (result.success) {
serviceStore.fetchServiceStats(serviceId);
}
};
const handleComment = async (e) => {
e.preventDefault();
if (!comment.trim()) return;
const result = await interactionStore.createComment('service', serviceId, comment);
if (result.success) {
setComment('');
serviceStore.fetchServiceComments(serviceId);
}
};
if (serviceStore.loading) {
return <Loading message="加载服务详情..." />;
}
if (serviceStore.error) {
return (
<ErrorMessage
message={serviceStore.error}
onDismiss={() => serviceStore.error = null}
/>
);
}
if (!serviceStore.currentService) {
return <ErrorMessage message="服务不存在" />;
}
const service = serviceStore.currentService;
const stats = serviceStore.currentService.stats || {};
return (
<div>
{service.image && (
<img
src={service.image}
alt={service.name}
style={{
width: '100%',
maxHeight: '400px',
objectFit: 'cover',
borderRadius: '8px',
marginBottom: '20px',
}}
/>
)}
<h1>{service.name}</h1>
<p>{service.category_display}</p>
<p>{service.description}</p>
{service.address && (
<p><strong>地址:</strong> {service.address}</p>
)}
{service.contact && (
<p><strong>联系方式:</strong> {service.contact}</p>
)}
<Rating>
<span>评分:</span>
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
filled={star <= Math.round(stats.avg_rating || 0)}
onClick={() => handleRate(star)}
style={{ cursor: 'pointer' }}
>
</Star>
))}
<span>
{stats.avg_rating || 0} ({stats.ratings_count || 0} 评分)
</span>
</Rating>
<p>
<strong>点赞:</strong> {stats.likes_count || 0} |
<strong>评论:</strong> {stats.comments_count || 0}
</p>
<CommentsSection>
<h2>评论 ({stats.comments_count || 0})</h2>
<CommentForm onSubmit={handleComment}>
<textarea
placeholder="写下你的评论..."
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
<button
type="submit"
style={{
padding: '10px 20px',
background: '#667eea',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginTop: '10px',
}}
>
发表评论
</button>
</CommentForm>
{service.currentService.comments && service.currentService.comments.map((comment) => (
<div
key={comment.id}
style={{
background: '#f8f9fa',
padding: '15px',
borderRadius: '5px',
marginBottom: '15px',
}}
>
<p><strong>{comment.author_username}</strong></p>
<p>{comment.content}</p>
<p style={{ color: '#999', fontSize: '14px' }}>
{comment.created_at}
</p>
</div>
))}
</CommentsSection>
</div>
);
});
export default ServiceDetailPage;