Files
ai_game/client/js/scenes/HomeScene.js

546 lines
17 KiB
JavaScript
Raw Normal View History

/**
* 首页场景 - 支持UGC
*/
import BaseScene from './BaseScene';
export default class HomeScene extends BaseScene {
constructor(main, params) {
super(main, params);
this.storyList = [];
this.scrollY = 0;
this.maxScrollY = 0;
this.isDragging = false;
this.lastTouchY = 0;
this.scrollVelocity = 0;
// 底部Tab: 首页/发现/创作/我的
this.bottomTab = 0;
// 内容源Tab: 推荐/关注/最新/排行
this.contentTab = 0;
this.contentTabs = ['推荐', '关注', '最新', '排行'];
// 分类标签
this.categories = ['全部', '都市言情', '悬疑推理', '古风宫廷', '校园青春', '修仙玄幻', '穿越重生', '职场商战', '科幻未来', '恐怖惊悚', '搞笑轻喜'];
this.selectedCategory = 0;
// 分类横向滚动
this.categoryScrollX = 0;
this.maxCategoryScrollX = 0;
this.isCategoryDragging = false;
this.lastTouchX = 0;
this.categoryRects = [];
}
async init() {
this.storyList = this.main.storyManager.storyList;
this.calculateMaxScroll();
}
getFilteredStories() {
let stories = this.storyList;
// 按内容源筛选
if (this.contentTab === 1) {
// 关注:暂时返回空(需要后端支持)
stories = [];
} else if (this.contentTab === 2) {
// 最新:按时间排序
stories = [...stories].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
} else if (this.contentTab === 3) {
// 排行:按播放量排序
stories = [...stories].sort((a, b) => b.play_count - a.play_count);
}
// 按分类筛选
if (this.selectedCategory > 0) {
const categoryName = this.categories[this.selectedCategory];
stories = stories.filter(s => s.category === categoryName);
}
return stories;
}
calculateMaxScroll() {
const stories = this.getFilteredStories();
const cardHeight = 130;
const gap = 12;
const startY = 175;
const tabHeight = 60;
const contentBottom = startY + stories.length * (cardHeight + gap);
const visibleBottom = this.screenHeight - tabHeight;
this.maxScrollY = Math.max(0, contentBottom - visibleBottom);
}
update() {
if (!this.isDragging && Math.abs(this.scrollVelocity) > 0.5) {
this.scrollY += this.scrollVelocity;
this.scrollVelocity *= 0.95;
this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScrollY));
}
}
render(ctx) {
this.renderBackground(ctx);
this.renderHeader(ctx);
this.renderContentTabs(ctx);
this.renderCategories(ctx);
this.renderStoryList(ctx);
this.renderBottomTabBar(ctx);
}
renderBackground(ctx) {
const gradient = ctx.createLinearGradient(0, 0, 0, this.screenHeight);
gradient.addColorStop(0, '#0f0c29');
gradient.addColorStop(0.5, '#302b63');
gradient.addColorStop(1, '#24243e');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
// 星空点缀
ctx.fillStyle = 'rgba(255,255,255,0.3)';
[[30, 80], [80, 30], [150, 60], [200, 25], [280, 70], [320, 40]].forEach(([x, y]) => {
ctx.beginPath();
ctx.arc(x, y, 1, 0, Math.PI * 2);
ctx.fill();
});
}
renderHeader(ctx) {
// 标题
const titleGradient = ctx.createLinearGradient(15, 30, 180, 30);
titleGradient.addColorStop(0, '#ffd700');
titleGradient.addColorStop(1, '#ff6b6b');
ctx.fillStyle = titleGradient;
ctx.font = 'bold 22px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('星域故事汇', 15, 42);
// 搜索按钮
const searchX = this.screenWidth - 45;
ctx.fillStyle = 'rgba(255,255,255,0.1)';
ctx.beginPath();
ctx.arc(searchX, 35, 18, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('🔍', searchX, 41);
this.searchBtnRect = { x: searchX - 18, y: 17, width: 36, height: 36 };
}
renderContentTabs(ctx) {
const tabY = 60;
const tabWidth = (this.screenWidth - 30) / 4;
const padding = 15;
this.contentTabRects = [];
this.contentTabs.forEach((tab, index) => {
const x = padding + index * tabWidth;
const isActive = index === this.contentTab;
// 文字
ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.5)';
ctx.font = isActive ? 'bold 14px sans-serif' : '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(tab, x + tabWidth / 2, tabY + 12);
// 下划线指示器
if (isActive) {
const lineGradient = ctx.createLinearGradient(x + tabWidth / 2 - 12, tabY + 20, x + tabWidth / 2 + 12, tabY + 20);
lineGradient.addColorStop(0, '#ff6b6b');
lineGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = lineGradient;
this.roundRect(ctx, x + tabWidth / 2 - 12, tabY + 18, 24, 3, 1.5);
ctx.fill();
}
this.contentTabRects.push({ x, y: tabY - 5, width: tabWidth, height: 30, index });
});
}
renderCategories(ctx) {
const startY = 95;
const tagHeight = 28;
let x = 15 - this.categoryScrollX;
ctx.font = '12px sans-serif';
let totalWidth = 15;
this.categories.forEach((cat) => {
totalWidth += ctx.measureText(cat).width + 24 + 10;
});
this.maxCategoryScrollX = Math.max(0, totalWidth - this.screenWidth + 15);
this.categoryRects = [];
this.categories.forEach((cat, index) => {
const isSelected = index === this.selectedCategory;
const textWidth = ctx.measureText(cat).width + 24;
this.categoryRects.push({ left: x + this.categoryScrollX, right: x + this.categoryScrollX + textWidth, index });
if (x + textWidth > 0 && x < this.screenWidth) {
if (isSelected) {
const tagGradient = ctx.createLinearGradient(x, startY, x + textWidth, startY);
tagGradient.addColorStop(0, '#ff6b6b');
tagGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = tagGradient;
} else {
ctx.fillStyle = 'rgba(255,255,255,0.08)';
}
this.roundRect(ctx, x, startY, textWidth, tagHeight, 14);
ctx.fill();
if (!isSelected) {
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
ctx.lineWidth = 1;
this.roundRect(ctx, x, startY, textWidth, tagHeight, 14);
ctx.stroke();
}
ctx.fillStyle = isSelected ? '#ffffff' : 'rgba(255,255,255,0.6)';
ctx.font = isSelected ? 'bold 12px sans-serif' : '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(cat, x + textWidth / 2, startY + 18);
}
x += textWidth + 10;
});
}
renderStoryList(ctx) {
const startY = 140;
const cardHeight = 130;
const cardMargin = 12;
const stories = this.getFilteredStories();
if (stories.length === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
const emptyText = this.contentTab === 1 ? '关注作者后,这里会显示他们的故事' : '暂无故事';
ctx.fillText(emptyText, this.screenWidth / 2, 280);
return;
}
ctx.save();
ctx.beginPath();
ctx.rect(0, startY, this.screenWidth, this.screenHeight - startY - 60);
ctx.clip();
stories.forEach((story, index) => {
const y = startY + index * (cardHeight + cardMargin) - this.scrollY;
if (y > startY - cardHeight && y < this.screenHeight - 60) {
this.renderStoryCard(ctx, story, 12, y, this.screenWidth - 24, cardHeight);
}
});
ctx.restore();
}
renderStoryCard(ctx, story, x, y, width, height) {
// 卡片背景
ctx.fillStyle = 'rgba(255,255,255,0.06)';
this.roundRect(ctx, x, y, width, height, 14);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
this.roundRect(ctx, x, y, width, height, 14);
ctx.stroke();
// 封面
const coverW = 80, coverH = height - 20;
const coverGradient = ctx.createLinearGradient(x + 10, y + 10, x + 10 + coverW, y + 10 + coverH);
const colors = this.getCategoryGradient(story.category);
coverGradient.addColorStop(0, colors[0]);
coverGradient.addColorStop(1, colors[1]);
ctx.fillStyle = coverGradient;
this.roundRect(ctx, x + 10, y + 10, coverW, coverH, 10);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(story.category || '故事', x + 10 + coverW / 2, y + 10 + coverH / 2 + 4);
const textX = x + 100;
const maxW = width - 115;
// 故事标题
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 15px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(this.truncateText(ctx, story.title, maxW), textX, y + 28);
// 作者信息
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '11px sans-serif';
const authorName = story.author_name || '官方';
ctx.fillText(`@${authorName}`, textX, y + 48);
// 来源标签
const isOfficial = !story.source_type || story.source_type === 'official';
const tagText = isOfficial ? '官方' : (story.source_type === 'ai' ? 'AI' : 'UGC');
const tagColor = isOfficial ? '#ffd700' : (story.source_type === 'ai' ? '#a855f7' : '#4ade80');
const tagW = ctx.measureText(tagText).width + 12;
const tagX = textX + ctx.measureText(`@${authorName}`).width + 8;
ctx.fillStyle = tagColor + '22';
this.roundRect(ctx, tagX, y + 38, tagW, 16, 8);
ctx.fill();
ctx.fillStyle = tagColor;
ctx.font = 'bold 9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(tagText, tagX + tagW / 2, y + 49);
// 简介
ctx.fillStyle = 'rgba(255,255,255,0.45)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(this.truncateText(ctx, story.description || '', maxW), textX, y + 72);
// 统计
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '10px sans-serif';
ctx.fillText(`${this.formatNumber(story.play_count || 0)}`, textX, y + 100);
ctx.fillText(`${this.formatNumber(story.like_count || 0)}`, textX + 55, y + 100);
ctx.fillText(`💬 ${this.formatNumber(story.comment_count || 0)}`, textX + 105, y + 100);
// 精选标签
if (story.is_featured) {
const fGradient = ctx.createLinearGradient(x + width - 50, y + 10, x + width - 10, y + 10);
fGradient.addColorStop(0, '#ff6b6b');
fGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = fGradient;
this.roundRect(ctx, x + width - 50, y + 10, 40, 20, 10);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('精选', x + width - 30, y + 24);
}
}
renderBottomTabBar(ctx) {
const tabH = 60;
const y = this.screenHeight - tabH;
ctx.fillStyle = 'rgba(15, 12, 41, 0.98)';
ctx.fillRect(0, y, this.screenWidth, tabH);
const lineGradient = ctx.createLinearGradient(0, y, this.screenWidth, y);
lineGradient.addColorStop(0, 'rgba(255,107,107,0.2)');
lineGradient.addColorStop(0.5, 'rgba(255,215,0,0.2)');
lineGradient.addColorStop(1, 'rgba(255,107,107,0.2)');
ctx.strokeStyle = lineGradient;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(this.screenWidth, y);
ctx.stroke();
const tabs = [
{ icon: '🏠', label: '首页' },
{ icon: '🔥', label: '发现' },
{ icon: '✨', label: '创作' },
{ icon: '👤', label: '我的' }
];
const tabW = this.screenWidth / tabs.length;
this.bottomTabRects = [];
tabs.forEach((tab, index) => {
const centerX = index * tabW + tabW / 2;
const isActive = index === this.bottomTab;
if (isActive) {
const indGradient = ctx.createLinearGradient(centerX - 16, y + 2, centerX + 16, y + 2);
indGradient.addColorStop(0, '#ff6b6b');
indGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = indGradient;
this.roundRect(ctx, centerX - 16, y + 2, 32, 3, 1.5);
ctx.fill();
}
ctx.font = index === 2 ? '24px sans-serif' : '20px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(tab.icon, centerX, y + 28);
ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.4)';
ctx.font = isActive ? 'bold 10px sans-serif' : '10px sans-serif';
ctx.fillText(tab.label, centerX, y + 48);
this.bottomTabRects.push({ x: index * tabW, y, width: tabW, height: tabH, index });
});
}
getCategoryGradient(category) {
const gradients = {
'都市言情': ['#ff758c', '#ff7eb3'],
'悬疑推理': ['#667eea', '#764ba2'],
'古风宫廷': ['#f093fb', '#f5576c'],
'校园青春': ['#4facfe', '#00f2fe'],
'修仙玄幻': ['#43e97b', '#38f9d7'],
'穿越重生': ['#fa709a', '#fee140'],
'职场商战': ['#30cfd0', '#330867'],
'科幻未来': ['#a8edea', '#fed6e3'],
'恐怖惊悚': ['#434343', '#000000'],
'搞笑轻喜': ['#f6d365', '#fda085']
};
return gradients[category] || ['#667eea', '#764ba2'];
}
formatNumber(num) {
if (!num) return '0';
if (num >= 10000) return (num / 10000).toFixed(1) + 'w';
if (num >= 1000) return (num / 1000).toFixed(1) + 'k';
return num.toString();
}
truncateText(ctx, text, maxWidth) {
if (!text) return '';
if (ctx.measureText(text).width <= maxWidth) return text;
let t = text;
while (t.length > 0 && ctx.measureText(t + '...').width > maxWidth) t = t.slice(0, -1);
return t + '...';
}
roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
onTouchStart(e) {
const touch = e.touches[0];
this.lastTouchY = touch.clientY;
this.lastTouchX = touch.clientX;
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
this.hasMoved = false;
if (touch.clientY >= 90 && touch.clientY <= 130) {
this.isCategoryDragging = true;
this.isDragging = false;
} else {
this.isCategoryDragging = false;
this.isDragging = true;
}
this.scrollVelocity = 0;
}
onTouchMove(e) {
const touch = e.touches[0];
if (this.isCategoryDragging) {
const deltaX = this.lastTouchX - touch.clientX;
if (Math.abs(deltaX) > 2) this.hasMoved = true;
this.categoryScrollX += deltaX;
this.categoryScrollX = Math.max(0, Math.min(this.categoryScrollX, this.maxCategoryScrollX));
this.lastTouchX = touch.clientX;
return;
}
if (this.isDragging) {
const deltaY = this.lastTouchY - touch.clientY;
if (Math.abs(touch.clientY - this.touchStartY) > 5) this.hasMoved = true;
this.scrollVelocity = deltaY;
this.scrollY += deltaY;
this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScrollY));
this.lastTouchY = touch.clientY;
}
}
onTouchEnd(e) {
this.isDragging = false;
this.isCategoryDragging = false;
if (this.hasMoved) return;
const touch = e.changedTouches[0];
const x = touch.clientX;
const y = touch.clientY;
// 底部Tab点击
if (y > this.screenHeight - 60) {
const tabW = this.screenWidth / 4;
const tabIndex = Math.floor(x / tabW);
this.handleBottomTabClick(tabIndex);
return;
}
// 内容源Tab点击
if (this.contentTabRects) {
for (const rect of this.contentTabRects) {
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
if (this.contentTab !== rect.index) {
this.contentTab = rect.index;
this.scrollY = 0;
this.calculateMaxScroll();
}
return;
}
}
}
// 分类点击
if (y >= 90 && y <= 130) {
this.handleCategoryClick(x);
return;
}
// 故事卡片点击
this.handleStoryClick(x, y);
}
handleCategoryClick(x) {
const adjustedX = x + this.categoryScrollX;
for (const rect of this.categoryRects) {
if (adjustedX >= rect.left && adjustedX <= rect.right) {
if (this.selectedCategory !== rect.index) {
this.selectedCategory = rect.index;
this.scrollY = 0;
this.calculateMaxScroll();
}
return;
}
}
}
handleBottomTabClick(tabIndex) {
if (tabIndex === this.bottomTab && tabIndex === 0) return;
if (tabIndex === 0) {
this.bottomTab = 0;
} else if (tabIndex === 1) {
// 发现页(可跳转或在本页切换)
this.bottomTab = 1;
} else if (tabIndex === 2) {
// 创作页
this.main.sceneManager.switchScene('aiCreate');
} else if (tabIndex === 3) {
// 我的
this.main.sceneManager.switchScene('profile');
}
}
handleStoryClick(x, y) {
const startY = 140;
const cardHeight = 130;
const cardMargin = 12;
const stories = this.getFilteredStories();
const adjustedY = y + this.scrollY;
const index = Math.floor((adjustedY - startY) / (cardHeight + cardMargin));
if (index >= 0 && index < stories.length) {
const story = stories[index];
this.main.sceneManager.switchScene('story', { storyId: story.id });
}
}
}