集成互动功能到帖子详情,删除独立互动页面
- 点赞/收藏改为纯 toggle,移除 unlike/unfavorite 参数 - 帖子详情 API 返回 isLiked/isFavorited 状态(SVG xlink:href 检测) - 前端两个切换按钮替代原四个独立按钮 - 修复 __INITIAL_STATE__ Vue 响应式代理序列化(structuredClone + fallback) - 修复 overlay 场景下点赞按钮误点 feed 列表元素(.last() 定位) - 删除 InteractionsPage 及相关路由/导航
This commit is contained in:
@@ -1,204 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Textarea } from '@/components/ui/Textarea';
|
||||
import { JsonViewer } from '@/components/ui/JsonViewer';
|
||||
import { useToast } from '@/context/ToastContext';
|
||||
import { toggleLike, toggleFavorite, postComment, replyComment } from '@/api/endpoints';
|
||||
|
||||
interface LogEntry {
|
||||
id: number;
|
||||
action: string;
|
||||
time: string;
|
||||
result: unknown;
|
||||
}
|
||||
|
||||
let logId = 0;
|
||||
|
||||
export function InteractionsPage() {
|
||||
const { toast } = useToast();
|
||||
const [feedId, setFeedId] = useState('');
|
||||
const [xsecToken, setXsecToken] = useState('');
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [log, setLog] = useState<LogEntry[]>([]);
|
||||
|
||||
const addLog = (action: string, result: unknown) => {
|
||||
setLog((prev) => [{ id: logId++, action, time: new Date().toLocaleTimeString(), result }, ...prev].slice(0, 50));
|
||||
};
|
||||
|
||||
// Comment state
|
||||
const [commentText, setCommentText] = useState('');
|
||||
|
||||
// Reply state
|
||||
const [replyText, setReplyText] = useState('');
|
||||
const [replyCommentId, setReplyCommentId] = useState('');
|
||||
const [replyUserId, setReplyUserId] = useState('');
|
||||
|
||||
const checkIds = () => {
|
||||
if (!feedId.trim() || !xsecToken.trim()) {
|
||||
toast('warning', 'Feed ID 和 xsec_token 为必填项');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleLike = useCallback(async (unlike: boolean) => {
|
||||
if (!checkIds()) return;
|
||||
setLoading(unlike ? 'unlike' : 'like');
|
||||
try {
|
||||
const res = await toggleLike(feedId, xsecToken, unlike);
|
||||
addLog(unlike ? '取消点赞' : '点赞', res);
|
||||
toast('success', unlike ? '已取消点赞' : '已点赞');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : '操作失败';
|
||||
addLog(unlike ? '取消点赞' : '点赞', { error: msg });
|
||||
toast('error', msg);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
}, [feedId, xsecToken, toast]);
|
||||
|
||||
const handleFavorite = useCallback(async (unfavorite: boolean) => {
|
||||
if (!checkIds()) return;
|
||||
setLoading(unfavorite ? 'unfavorite' : 'favorite');
|
||||
try {
|
||||
const res = await toggleFavorite(feedId, xsecToken, unfavorite);
|
||||
addLog(unfavorite ? '取消收藏' : '收藏', res);
|
||||
toast('success', unfavorite ? '已取消收藏' : '已收藏');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : '操作失败';
|
||||
addLog(unfavorite ? '取消收藏' : '收藏', { error: msg });
|
||||
toast('error', msg);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
}, [feedId, xsecToken, toast]);
|
||||
|
||||
const handleComment = useCallback(async () => {
|
||||
if (!checkIds() || !commentText.trim()) {
|
||||
toast('warning', '评论内容为必填项');
|
||||
return;
|
||||
}
|
||||
setLoading('comment');
|
||||
try {
|
||||
const res = await postComment(feedId, xsecToken, commentText);
|
||||
addLog('评论', res);
|
||||
toast('success', '评论已发布');
|
||||
setCommentText('');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : '操作失败';
|
||||
addLog('评论', { error: msg });
|
||||
toast('error', msg);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
}, [feedId, xsecToken, commentText, toast]);
|
||||
|
||||
const handleReply = useCallback(async () => {
|
||||
if (!checkIds() || !replyText.trim()) {
|
||||
toast('warning', '回复内容为必填项');
|
||||
return;
|
||||
}
|
||||
setLoading('reply');
|
||||
try {
|
||||
const res = await replyComment({
|
||||
feed_id: feedId,
|
||||
xsec_token: xsecToken,
|
||||
content: replyText,
|
||||
comment_id: replyCommentId || undefined,
|
||||
user_id: replyUserId || undefined,
|
||||
});
|
||||
addLog('回复', res);
|
||||
toast('success', '回复已发布');
|
||||
setReplyText('');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : '操作失败';
|
||||
addLog('回复', { error: msg });
|
||||
toast('error', msg);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
}, [feedId, xsecToken, replyText, replyCommentId, replyUserId, toast]);
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
<h1 className="text-2xl font-bold">互动</h1>
|
||||
|
||||
{/* Target */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">目标笔记</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input label="Feed ID" value={feedId} onChange={(e) => setFeedId(e.target.value)} placeholder="Feed ID" />
|
||||
<Input label="xsec_token" value={xsecToken} onChange={(e) => setXsecToken(e.target.value)} placeholder="xsec_token" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Like / Favorite */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">快捷操作</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button onClick={() => void handleLike(false)} loading={loading === 'like'} size="sm">
|
||||
点赞
|
||||
</Button>
|
||||
<Button onClick={() => void handleLike(true)} loading={loading === 'unlike'} variant="secondary" size="sm">
|
||||
取消点赞
|
||||
</Button>
|
||||
<Button onClick={() => void handleFavorite(false)} loading={loading === 'favorite'} size="sm">
|
||||
收藏
|
||||
</Button>
|
||||
<Button onClick={() => void handleFavorite(true)} loading={loading === 'unfavorite'} variant="secondary" size="sm">
|
||||
取消收藏
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Comment */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">发表评论</h2>
|
||||
<div className="space-y-3">
|
||||
<Textarea value={commentText} onChange={(e) => setCommentText(e.target.value)} placeholder="写评论..." />
|
||||
<Button onClick={() => void handleComment()} loading={loading === 'comment'} size="sm">
|
||||
发表评论
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Reply */}
|
||||
<Card>
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider mb-3">回复评论</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input label="评论 ID" value={replyCommentId} onChange={(e) => setReplyCommentId(e.target.value)} placeholder="可选" />
|
||||
<Input label="用户 ID" value={replyUserId} onChange={(e) => setReplyUserId(e.target.value)} placeholder="可选" />
|
||||
</div>
|
||||
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="写回复..." />
|
||||
<Button onClick={() => void handleReply()} loading={loading === 'reply'} size="sm">
|
||||
发送回复
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Log */}
|
||||
{log.length > 0 && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-dark-muted uppercase tracking-wider">操作日志</h2>
|
||||
<Button variant="ghost" size="sm" onClick={() => setLog([])}>清空</Button>
|
||||
</div>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{log.map((entry) => (
|
||||
<div key={entry.id} className="border-b border-dark-border/50 pb-2 last:border-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-mono text-dark-muted">{entry.time}</span>
|
||||
<span className="text-sm font-medium text-dark-accent">{entry.action}</span>
|
||||
</div>
|
||||
<JsonViewer data={entry.result} collapsed maxHeight="120px" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user