refactor: 企业级目录重构与播放/关于页体验升级

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

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
// Author: fruit.com.cn
import { getImage, resolveMedia } from '../../shared/lib/media';
import { getText } from '../../shared/lib/text';
export function normalizeDrama(item = {}, lang = 'id') {
const id = String(item.id ?? item.dramaId ?? item.code ?? '');
const episodes = item.episodes || item.episodeList || (item.preview ? [item.preview] : []);
const preview = item.preview || episodes[0] || null;
return {
id,
title: getText(item.titleLocales || item.locales || item.title, lang) || item.name || 'YMB Drama',
channel: getText(item.channelLocales || item.channel, lang) || item.creatorName || 'YMB Originals',
genre: getText(item.categoryLocales || item.genre, lang) || getText(item.types?.[0], lang),
image: getImage(item),
duration: item.duration || preview?.duration || '',
views: item.views || item.viewCount || '',
likeCount: Number(item.likeCount) || 0,
favoriteCount: Number(item.favoriteCount) || 0,
commentCount: Number(item.commentCount) || 0,
description: getText(item.synopsisLocales, lang) || '',
episodes: episodes.map((episode) => ({ ...episode, playUrl: resolveMedia(episode.playUrl) })),
preview: preview ? { ...preview, playUrl: resolveMedia(preview.playUrl) } : null,
};
}
export function normalizeProgress(item = {}, lang = 'id') {
const durationMs = Number(item.durationMs) || 0;
const positionMs = Number(item.positionMs) || 0;
return {
id: String(item.dramaId ?? ''),
episodeId: String(item.episodeId ?? ''),
title: getText(item.titleLocales, lang) || item.dramaTitle || 'YMB Drama',
image: resolveMedia(item.dramaCoverLandscape || item.dramaCover || ''),
episodeNumber: item.episodeNumber == null ? null : Number(item.episodeNumber),
totalEpisodes: item.totalEpisodes == null ? null : Number(item.totalEpisodes),
positionMs,
durationMs,
fraction: durationMs > 0 ? Math.min(1, positionMs / durationMs) : 0,
viewCount: item.viewCount,
finished: Boolean(item.finished),
updatedAt: item.updatedAt || '',
};
}
export function uniqueDramas(items) {
const seen = new Set();
return items.filter((item) => {
if (seen.has(item.id)) return false;
seen.add(item.id);
return true;
});
}