refactor: 企业级目录重构与播放/关于页体验升级
- 按 app / shared / entities / features 分层,路由懒加载并抽出布局组件 - 请求层改为可插拔拦截器,统一错误类型、超时取消与端点拆分 - 抽取播放器、骨架屏、网格等复用组件,点赞收藏接入统一交互反馈 - 关于页重做为无边框极光风格,补充 iOS / Google Play 下载入口 - 修复播放页滚轮切集吞事件、页面误滚动、后台持续播放与声音记忆问题 - 语言按本地偏好与 IP 识别,国旗改为内联 SVG - 清理未使用的旧站点文件,补充 ESLint 配置
This commit is contained in:
1 parent
8dd5283892
commit
ecfbd3b8b5
90 files changed
+4535
-886
No files matched your search
@@ -0,0 +1,81 @@
|
||||
// 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user