Files
ymb-site/src/shared/components/CommentsSheet.jsx
T
Zhou Weidong ecfbd3b8b5 refactor: 企业级目录重构与播放/关于页体验升级
- 按 app / shared / entities / features 分层,路由懒加载并抽出布局组件
- 请求层改为可插拔拦截器,统一错误类型、超时取消与端点拆分
- 抽取播放器、骨架屏、网格等复用组件,点赞收藏接入统一交互反馈
- 关于页重做为无边框极光风格,补充 iOS / Google Play 下载入口
- 修复播放页滚轮切集吞事件、页面误滚动、后台持续播放与声音记忆问题
- 语言按本地偏好与 IP 识别,国旗改为内联 SVG
- 清理未使用的旧站点文件,补充 ESLint 配置
2026-11-12 12:32:29 +08:00

82 lines
3.3 KiB
React

// Author: fruit.com.cn
import { useEffect, useState } from 'react';
import { Send, X } from 'lucide-react';
import { engagementApi } from '../api/engagement';
function formatTime(value, htmlLang) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return date.toLocaleString(htmlLang, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
export function CommentsSheet({ video, locale, onClose, onPosted }) {
const [comments, setComments] = useState(null);
const [error, setError] = useState('');
const [text, setText] = useState('');
const [sending, setSending] = useState(false);
const loadComments = () => engagementApi.comments({ dramaId: video.id, episodeId: video.preview?.id })
.then((payload) => setComments(payload?.list || []))
.catch((loadError) => { setError(loadError.message || 'Request failed'); setComments([]); });
useEffect(() => {
let active = true;
setComments(null);
setError('');
engagementApi.comments({ dramaId: video.id, episodeId: video.preview?.id })
.then((payload) => active && setComments(payload?.list || []))
.catch((loadError) => active && setError(loadError.message || 'Request failed'));
return () => { active = false; };
}, [video.id, video.preview?.id]);
const submit = async (event) => {
event.preventDefault();
const content = text.trim();
if (!content || sending) return;
setSending(true);
setError('');
try {
await engagementApi.publishComment({ dramaId: video.id, episodeId: video.preview?.id, content });
setText('');
await loadComments();
onPosted?.();
} catch (postError) {
setError(postError.message || 'Request failed');
} finally {
setSending(false);
}
};
return (
<>
<button className="comments-backdrop" onClick={onClose} aria-label="Close comments" type="button" />
<section className="comments-sheet" role="dialog" aria-label={locale.comments}>
<header className="comments-header">
<h2>{locale.comments}</h2>
<button onClick={onClose} aria-label="Close" type="button"><X size={20} /></button>
</header>
<div className="comments-list">
{comments === null && <p className="comments-note">{locale.loading}</p>}
{error && <p className="comments-note warning">{error}</p>}
{comments && !comments.length && !error && <p className="comments-note">{locale.noComments}</p>}
{comments?.map((comment) => (
<div className="comment-row" key={comment.id}>
<span className="comment-avatar">{comment.nickname?.[0] || 'Y'}</span>
<div className="comment-body">
<b>{comment.nickname || 'Guest'}</b>
<p>{comment.content}</p>
<small>{formatTime(comment.createdAt, locale.htmlLang)}</small>
</div>
</div>
))}
</div>
<form className="comment-form" onSubmit={submit}>
<input value={text} onChange={(event) => setText(event.target.value)} placeholder={locale.commentPlaceholder} aria-label={locale.commentPlaceholder} />
<button type="submit" disabled={!text.trim() || sending} aria-label={locale.send}><Send size={18} /></button>
</form>
</section>
</>
);
}