Files
ai-learning-platform/frontend/src/app/circles/[id]/client.tsx
T
yuzhiran-dev 23edb74bce feat: 支付闭环 + 运营助手 Tool Calling + 管理后台完善
- 支付系统:微信支付 mock 自动完成、NATIVE 扫码支付、JSAPI 集成
- 运营助手:Tool Calling 架构,19 个可执行工具,AI 驱动操作
- 角色管理:表格布局 + Dialog 表单 + 权限勾选
- 配置统一:config.ts 单一数据源
- API 审计:补齐 status toggle / comments 端点
- 暗黑模式硬件编码颜色全部替换为 CSS 变量
2026-05-20 18:29:00 +08:00

184 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { Skeleton } from '@/components/ui/skeleton';
import { API_BASE } from '@/lib/config';
interface Post {
id: number;
title: string;
content: string;
tags?: string;
viewCount: number;
likeCount: number;
createdAt: string;
user?: { id: number; nickname: string; avatar?: string };
}
export default function CircleDetail() {
const params = useParams();
const circleId = Number(params.id);
const [circle, setCircle] = useState<any>(null);
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [isMember, setIsMember] = useState(false);
const [showCreateForm, setShowCreateForm] = useState(false);
const [formTitle, setFormTitle] = useState('');
const [formContent, setFormContent] = useState('');
useEffect(() => { loadData(); }, [circleId]);
async function loadData() {
try {
const token = localStorage.getItem('token');
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const [circleRes, postsRes] = await Promise.all([
fetch(`${API_BASE}/circles/${circleId}`, { headers }),
fetch(`${API_BASE}/circles/${circleId}/posts`, { headers }),
]);
if (circleRes.ok) setCircle(await circleRes.json());
if (postsRes.ok) {
const data = await postsRes.json();
setPosts(data.items || []);
}
if (token) {
const memRes = await fetch(`${API_BASE}/circles/${circleId}/membership`, {
headers: { Authorization: `Bearer ${token}` },
});
if (memRes.ok) {
const memData = await memRes.json();
setIsMember(memData.isMember);
}
}
} catch (e) { console.error(e) }
setLoading(false);
}
async function toggleJoin() {
const token = localStorage.getItem('token');
if (!token) return;
const method = isMember ? 'POST' : 'POST';
const url = isMember
? `${API_BASE}/circles/${circleId}/leave`
: `${API_BASE}/circles/${circleId}/join`;
try {
const res = await fetch(url, {
method,
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
});
if (res.ok) {
setIsMember(!isMember);
loadData();
}
} catch (e) { console.error(e) }
}
async function handleCreatePost() {
const token = localStorage.getItem('token');
if (!token || !formTitle || !formContent) return;
try {
const res = await fetch(`${API_BASE}/community/posts`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ title: formTitle, content: formContent, circleId }),
});
if (res.ok) {
setFormTitle('');
setFormContent('');
setShowCreateForm(false);
loadData();
}
} catch (e) { console.error(e) }
}
if (loading) return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<Skeleton className="h-8 w-48 mb-6" />
<Skeleton className="h-4 w-32 mb-8" />
<Skeleton className="h-64 w-full mb-4" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4" />
</div>
);
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mb-8">
<Link href="/circles" className="text-sm text-muted-foreground hover:text-brand-600 mb-2 inline-block">&larr; </Link>
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">{circle?.name || '圈子详情'}</h1>
<p className="mt-2 text-muted-foreground">{circle?.description}</p>
<div className="flex gap-4 mt-3 text-sm text-muted-foreground">
<span>{circle?._count?.members || 0} </span>
<span>{circle?._count?.posts || 0} </span>
</div>
</div>
<button onClick={toggleJoin}
className={`px-6 py-2 rounded-lg text-sm font-medium transition-colors ${
isMember ? 'bg-muted text-muted-foreground hover:bg-accent' : 'bg-brand-600 text-white hover:bg-brand-700'
}`}>
{isMember ? '退出圈子' : '加入圈子'}
</button>
</div>
</div>
<div className="mb-6">
<button onClick={() => setShowCreateForm(!showCreateForm)}
className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">
{showCreateForm ? '取消' : '发帖'}
</button>
</div>
{showCreateForm && (
<div className="bg-card rounded-xl border border-border p-6 mb-6">
<input value={formTitle} onChange={e => setFormTitle(e.target.value)}
placeholder="标题" className="w-full px-4 py-2 border border-border rounded-lg mb-3 text-sm" />
<textarea value={formContent} onChange={e => setFormContent(e.target.value)}
placeholder="内容..." rows={4} className="w-full px-4 py-2 border border-border rounded-lg mb-3 text-sm" />
<button onClick={handleCreatePost}
className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700"></button>
</div>
)}
{posts.length === 0 ? (
<div className="text-center py-20 text-muted-foreground">
<p></p>
</div>
) : (
<div className="space-y-4">
{posts.map(post => (
<Link key={post.id} href={`/community/${post.id}`}
className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 rounded-full bg-brand-100 flex items-center justify-center text-brand-600 text-xs font-bold">
{post.user?.nickname?.[0] || 'U'}
</div>
<div>
<div className="text-sm font-medium text-foreground">{post.user?.nickname || '匿名用户'}</div>
<div className="text-xs text-muted-foreground">{new Date(post.createdAt).toLocaleDateString()}</div>
</div>
</div>
<h3 className="font-semibold text-foreground mb-2">{post.title}</h3>
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">{post.content}</p>
<div className="flex items-center gap-6 text-sm text-muted-foreground">
<span>👁 {post.viewCount}</span>
<span> {post.likeCount}</span>
</div>
</Link>
))}
</div>
)}
</div>
);
}