diff --git a/client/js/scenes/AICreateScene.js b/client/js/scenes/AICreateScene.js index 6d0b774..f3e492d 100644 --- a/client/js/scenes/AICreateScene.js +++ b/client/js/scenes/AICreateScene.js @@ -216,16 +216,17 @@ export default class AICreateScene extends BaseScene { ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '12px sans-serif'; ctx.textAlign = 'left'; - ctx.fillText('热门改写方向:', padding, y + 60); + ctx.fillText('热门改写方向:', padding, y + 55); - this.renderTags(ctx, this.rewriteTags, padding, y + 75, 'rewrite'); + const tagEndY = this.renderTags(ctx, this.rewriteTags, padding, y + 70, 'rewrite'); - // 选择故事 + // 选择故事 - 位置根据标签高度动态调整 ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '13px sans-serif'; - ctx.fillText('选择要改写的故事:', padding, y + 145); + ctx.textAlign = 'left'; + ctx.fillText('选择要改写的故事:', padding, tagEndY + 25); - this.renderStoryList(ctx, y + 160, 'rewrite'); + this.renderStoryList(ctx, tagEndY + 40, 'rewrite'); } renderContinueTab(ctx, startY) { @@ -240,15 +241,16 @@ export default class AICreateScene extends BaseScene { ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '12px sans-serif'; ctx.textAlign = 'left'; - ctx.fillText('续写方向:', padding, y + 60); + ctx.fillText('续写方向:', padding, y + 55); - this.renderTags(ctx, this.continueTags, padding, y + 75, 'continue'); + const tagEndY = this.renderTags(ctx, this.continueTags, padding, y + 70, 'continue'); ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '13px sans-serif'; - ctx.fillText('选择要续写的故事:', padding, y + 145); + ctx.textAlign = 'left'; + ctx.fillText('选择要续写的故事:', padding, tagEndY + 25); - this.renderStoryList(ctx, y + 160, 'continue'); + this.renderStoryList(ctx, tagEndY + 40, 'continue'); } renderCreateTab(ctx, startY) { @@ -265,24 +267,27 @@ export default class AICreateScene extends BaseScene { ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = '13px sans-serif'; ctx.textAlign = 'left'; - ctx.fillText('选择题材:', padding, y + 60); + ctx.fillText('选择题材:', padding, y + 55); - this.renderTags(ctx, this.genreTags, padding, y + 75, 'genre'); + const tagEndY = this.renderTags(ctx, this.genreTags, padding, y + 70, 'genre'); // 关键词输入 - ctx.fillText('故事关键词:', padding, y + 145); - this.renderInputBox(ctx, padding, y + 160, inputWidth, 45, this.createForm.keywords || '例如:霸总、契约婚姻、追妻火葬场', 'keywords'); + let currentY = tagEndY + 25; + ctx.fillText('故事关键词:', padding, currentY); + this.renderInputBox(ctx, padding, currentY + 15, inputWidth, 45, this.createForm.keywords || '例如:霸总、契约婚姻、追妻火葬场', 'keywords'); // 主角设定 - ctx.fillText('主角设定:', padding, y + 225); - this.renderInputBox(ctx, padding, y + 240, inputWidth, 45, this.createForm.protagonist || '例如:独立女性设计师', 'protagonist'); + currentY += 80; + ctx.fillText('主角设定:', padding, currentY); + this.renderInputBox(ctx, padding, currentY + 15, inputWidth, 45, this.createForm.protagonist || '例如:独立女性设计师', 'protagonist'); // 核心冲突 - ctx.fillText('核心冲突:', padding, y + 305); - this.renderInputBox(ctx, padding, y + 320, inputWidth, 45, this.createForm.conflict || '例如:假结婚变真爱', 'conflict'); + currentY += 80; + ctx.fillText('核心冲突:', padding, currentY); + this.renderInputBox(ctx, padding, currentY + 15, inputWidth, 45, this.createForm.conflict || '例如:假结婚变真爱', 'conflict'); // 开始创作按钮 - const btnY = y + 400; + const btnY = currentY + 85; const btnGradient = ctx.createLinearGradient(padding, btnY, this.screenWidth - padding, btnY); btnGradient.addColorStop(0, '#a855f7'); btnGradient.addColorStop(1, '#ec4899'); @@ -303,6 +308,7 @@ export default class AICreateScene extends BaseScene { const tagGap = 8; let currentX = startX; let currentY = startY; + let maxY = startY + tagHeight; // 记录最大Y坐标 if (!this.tagRects) this.tagRects = {}; this.tagRects[type] = []; @@ -314,6 +320,7 @@ export default class AICreateScene extends BaseScene { if (currentX + tagWidth > this.screenWidth - 15) { currentX = startX; currentY += tagHeight + tagGap; + maxY = currentY + tagHeight; } const isSelected = (type === 'genre' && this.createForm.genre === tag) || @@ -353,6 +360,8 @@ export default class AICreateScene extends BaseScene { currentX += tagWidth + tagGap; }); + + return maxY; // 返回标签区域结束的Y坐标 } renderInputBox(ctx, x, y, width, height, placeholder, field) { diff --git a/client/js/scenes/HomeScene.js b/client/js/scenes/HomeScene.js index 19992c7..31ca7d6 100644 --- a/client/js/scenes/HomeScene.js +++ b/client/js/scenes/HomeScene.js @@ -1,5 +1,5 @@ /** - * 首页场景 + * 首页场景 - 支持UGC */ import BaseScene from './BaseScene'; @@ -12,63 +12,84 @@ export default class HomeScene extends BaseScene { this.isDragging = false; this.lastTouchY = 0; this.scrollVelocity = 0; - this.currentTab = 0; // 0: 首页, 1: 发现, 2: 我的 + + // 底部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() { - if (this.selectedCategory === 0) { - return this.storyList; // 全部 + 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); } - const categoryName = this.categories[this.selectedCategory]; - return this.storyList.filter(s => s.category === categoryName); + + // 按分类筛选 + 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 = 120; - const gap = 15; - const startY = 150; // 故事列表起始位置 - const tabHeight = 65; - - // 内容总高度 + const cardHeight = 130; + const gap = 12; + const startY = 175; + const tabHeight = 60; const contentBottom = startY + stories.length * (cardHeight + gap); - // 可视区域底部(减去底部Tab栏) 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'); @@ -76,26 +97,9 @@ export default class HomeScene extends BaseScene { ctx.fillStyle = gradient; ctx.fillRect(0, 0, this.screenWidth, this.screenHeight); - // 添加星空装饰点 - this.renderStars(ctx); - - // 绘制顶部标题栏 - this.renderHeader(ctx); - - // 绘制分类标签 - this.renderCategories(ctx); - - // 绘制故事列表 - this.renderStoryList(ctx); - - // 绘制底部Tab栏 - this.renderTabBar(ctx); - } - - renderStars(ctx) { + // 星空点缀 ctx.fillStyle = 'rgba(255,255,255,0.3)'; - const stars = [[30, 80], [80, 30], [150, 60], [200, 25], [280, 70], [320, 40]]; - stars.forEach(([x, y]) => { + [[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(); @@ -103,128 +107,129 @@ export default class HomeScene extends BaseScene { } renderHeader(ctx) { - // 标题带渐变 - const titleGradient = ctx.createLinearGradient(20, 30, 200, 30); + // 标题 + const titleGradient = ctx.createLinearGradient(15, 30, 180, 30); titleGradient.addColorStop(0, '#ffd700'); titleGradient.addColorStop(1, '#ff6b6b'); ctx.fillStyle = titleGradient; - ctx.font = 'bold 26px sans-serif'; + ctx.font = 'bold 22px sans-serif'; ctx.textAlign = 'left'; - ctx.fillText('星域故事汇', 20, 50); + ctx.fillText('星域故事汇', 15, 42); - // 副标题 - ctx.fillStyle = 'rgba(255,255,255,0.6)'; - ctx.font = '13px sans-serif'; - ctx.fillText('每个选择,都是一个新世界', 20, 75); - - // AI创作入口按钮 - const btnWidth = 80; - const btnHeight = 32; - const btnX = this.screenWidth - btnWidth - 15; - const btnY = 35; - const btnGradient = ctx.createLinearGradient(btnX, btnY, btnX + btnWidth, btnY); - btnGradient.addColorStop(0, '#a855f7'); - btnGradient.addColorStop(1, '#ec4899'); - ctx.fillStyle = btnGradient; - this.roundRect(ctx, btnX, btnY, btnWidth, btnHeight, 16); + // 搜索按钮 + 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 = '#ffffff'; - ctx.font = 'bold 12px sans-serif'; + ctx.fillStyle = 'rgba(255,255,255,0.7)'; + ctx.font = '16px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText('✨ AI创作', btnX + btnWidth / 2, btnY + 21); - - this.aiCreateBtnRect = { x: btnX, y: btnY, width: btnWidth, height: btnHeight }; + 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 = 30; + const tagHeight = 28; let x = 15 - this.categoryScrollX; - const y = startY; - // 计算总宽度用于滚动限制 - ctx.font = '13px sans-serif'; + ctx.font = '12px sans-serif'; let totalWidth = 15; this.categories.forEach((cat) => { - totalWidth += ctx.measureText(cat).width + 28 + 12; + 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 + 28; + const textWidth = ctx.measureText(cat).width + 24; - // 记录每个标签的位置 - this.categoryRects.push({ - left: x + this.categoryScrollX, - right: x + this.categoryScrollX + textWidth, - index: index - }); + 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, y, x + textWidth, y); + 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, y, textWidth, tagHeight, 15); + this.roundRect(ctx, x, startY, textWidth, tagHeight, 14); ctx.fill(); - // 未选中态加边框 if (!isSelected) { - ctx.strokeStyle = 'rgba(255,255,255,0.15)'; + ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 1; - this.roundRect(ctx, x, y, textWidth, tagHeight, 15); + this.roundRect(ctx, x, startY, textWidth, tagHeight, 14); ctx.stroke(); } - // 标签文字 - ctx.fillStyle = isSelected ? '#ffffff' : 'rgba(255,255,255,0.7)'; - ctx.font = isSelected ? 'bold 13px sans-serif' : '13px sans-serif'; + 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, y + 20); + ctx.fillText(cat, x + textWidth / 2, startY + 18); } - - x += textWidth + 12; + x += textWidth + 10; }); } renderStoryList(ctx) { - const startY = 150; - const cardHeight = 120; - const cardPadding = 15; - const cardMargin = 15; - + const startY = 140; + const cardHeight = 130; + const cardMargin = 12; const stories = this.getFilteredStories(); - + if (stories.length === 0) { - ctx.fillStyle = '#666666'; + ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText('暂无该分类的故事', this.screenWidth / 2, 250); + 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 - 65); + 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 - 65) { - this.renderStoryCard(ctx, story, cardMargin, y, this.screenWidth - cardMargin * 2, cardHeight); + if (y > startY - cardHeight && y < this.screenHeight - 60) { + this.renderStoryCard(ctx, story, 12, y, this.screenWidth - 24, cardHeight); } }); @@ -232,73 +237,141 @@ export default class HomeScene extends BaseScene { } renderStoryCard(ctx, story, x, y, width, height) { - // 卡片背景 - 毛玻璃效果 + // 卡片背景 ctx.fillStyle = 'rgba(255,255,255,0.06)'; - this.roundRect(ctx, x, y, width, height, 16); + this.roundRect(ctx, x, y, width, height, 14); ctx.fill(); - - // 卡片高光边框 - ctx.strokeStyle = 'rgba(255,255,255,0.1)'; + ctx.strokeStyle = 'rgba(255,255,255,0.08)'; ctx.lineWidth = 1; - this.roundRect(ctx, x, y, width, height, 16); + this.roundRect(ctx, x, y, width, height, 14); ctx.stroke(); - // 封面区域 - 渐变色 - const coverWidth = 85; - const coverHeight = height - 24; - const coverGradient = ctx.createLinearGradient(x + 12, y + 12, x + 12 + coverWidth, y + 12 + coverHeight); + // 封面 + 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 + 12, y + 12, coverWidth, coverHeight, 12); + this.roundRect(ctx, x + 10, y + 10, coverW, coverH, 10); ctx.fill(); - // 封面上的分类名 - ctx.fillStyle = 'rgba(255,255,255,0.9)'; - ctx.font = 'bold 11px sans-serif'; + ctx.fillStyle = 'rgba(255,255,255,0.85)'; + ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(story.category || '故事', x + 12 + coverWidth / 2, y + 12 + coverHeight / 2 + 4); + 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 17px sans-serif'; + ctx.font = 'bold 15px sans-serif'; ctx.textAlign = 'left'; - const titleX = x + 110; - const maxTextWidth = width - 120; // 可用文字宽度 - const title = this.truncateText(ctx, story.title, maxTextWidth); - ctx.fillText(title, titleX, y + 35); + ctx.fillText(this.truncateText(ctx, story.title, maxW), textX, y + 28); - // 故事简介 - ctx.fillStyle = 'rgba(255,255,255,0.5)'; - ctx.font = '12px sans-serif'; - const desc = story.description || ''; - const shortDesc = this.truncateText(ctx, desc, maxTextWidth); - ctx.fillText(shortDesc, titleX, y + 58); - - // 统计信息带图标 - ctx.fillStyle = 'rgba(255,255,255,0.4)'; + // 作者信息 + ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.font = '11px sans-serif'; - const playText = `▶ ${this.formatNumber(story.play_count)}`; - const likeText = `♥ ${this.formatNumber(story.like_count)}`; - ctx.fillText(playText, titleX, y + 90); - ctx.fillText(likeText, titleX + 70, y + 90); + 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 tagGradient = ctx.createLinearGradient(x + width - 55, y + 12, x + width - 10, y + 12); - tagGradient.addColorStop(0, '#ff6b6b'); - tagGradient.addColorStop(1, '#ffd700'); - ctx.fillStyle = tagGradient; - this.roundRect(ctx, x + width - 55, y + 12, 45, 22, 11); + 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 10px sans-serif'; + ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText('精选', x + width - 32, y + 27); + 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'], @@ -315,113 +388,32 @@ export default class HomeScene extends BaseScene { return gradients[category] || ['#667eea', '#764ba2']; } - renderTabBar(ctx) { - const tabHeight = 65; - const y = this.screenHeight - tabHeight; - - // Tab栏背景 - 毛玻璃 - ctx.fillStyle = 'rgba(15, 12, 41, 0.95)'; - ctx.fillRect(0, y, this.screenWidth, tabHeight); - - // 顶部高光线 - const lineGradient = ctx.createLinearGradient(0, y, this.screenWidth, y); - lineGradient.addColorStop(0, 'rgba(255,107,107,0.3)'); - lineGradient.addColorStop(0.5, 'rgba(255,215,0,0.3)'); - lineGradient.addColorStop(1, 'rgba(255,107,107,0.3)'); - 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: '我的' } - ]; - - const tabWidth = this.screenWidth / tabs.length; - - tabs.forEach((tab, index) => { - const centerX = index * tabWidth + tabWidth / 2; - const isActive = index === this.currentTab; - - // 选中态指示器 - if (isActive) { - const indicatorGradient = ctx.createLinearGradient(centerX - 20, y + 3, centerX + 20, y + 3); - indicatorGradient.addColorStop(0, '#ff6b6b'); - indicatorGradient.addColorStop(1, '#ffd700'); - ctx.fillStyle = indicatorGradient; - this.roundRect(ctx, centerX - 20, y + 2, 40, 3, 1.5); - ctx.fill(); - } - - // 图标 - ctx.font = '22px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(tab.icon, centerX, y + 32); - - // 标签文字 - ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.4)'; - ctx.font = isActive ? 'bold 11px sans-serif' : '11px sans-serif'; - ctx.fillText(tab.label, centerX, y + 52); - }); - } - - // 获取分类颜色 - getCategoryColor(category) { - const colors = { - '都市言情': '#e94560', - '悬疑推理': '#4a90d9', - '古风宫廷': '#d4a574', - '校园青春': '#7ed957', - '修仙玄幻': '#9b59b6', - '穿越重生': '#f39c12', - '职场商战': '#3498db', - '科幻未来': '#1abc9c', - '恐怖惊悚': '#2c3e50', - '搞笑轻喜': '#f1c40f' - }; - return colors[category] || '#666666'; - } - - // 格式化数字 formatNumber(num) { - if (num >= 10000) { - return (num / 10000).toFixed(1) + 'w'; - } - if (num >= 1000) { - return (num / 1000).toFixed(1) + 'k'; - } + 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 truncated = text; - while (truncated.length > 0 && ctx.measureText(truncated + '...').width > maxWidth) { - truncated = truncated.slice(0, -1); - } - return truncated + '...'; + 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, width, height, radius) { + roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); + 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(); } @@ -432,9 +424,8 @@ export default class HomeScene extends BaseScene { this.touchStartX = touch.clientX; this.touchStartY = touch.clientY; this.hasMoved = false; - - // 判断是否在分类区域(y: 90-140) - if (touch.clientY >= 90 && touch.clientY <= 140) { + + if (touch.clientY >= 90 && touch.clientY <= 130) { this.isCategoryDragging = true; this.isDragging = false; } else { @@ -446,27 +437,19 @@ export default class HomeScene extends BaseScene { onTouchMove(e) { const touch = e.touches[0]; - - // 分类区域横向滑动 + if (this.isCategoryDragging) { const deltaX = this.lastTouchX - touch.clientX; - if (Math.abs(deltaX) > 2) { - this.hasMoved = true; - } + 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; - } - + 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)); @@ -477,87 +460,85 @@ export default class HomeScene extends BaseScene { onTouchEnd(e) { this.isDragging = false; this.isCategoryDragging = false; + if (this.hasMoved) return; - // 如果有滑动,不处理点击 - if (this.hasMoved) { - return; - } - - // 检测点击 const touch = e.changedTouches[0]; const x = touch.clientX; const y = touch.clientY; - // 检测AI创作按钮点击 - if (this.aiCreateBtnRect) { - const btn = this.aiCreateBtnRect; - if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) { - this.main.sceneManager.switchScene('aiCreate'); - return; - } - } - - // 检测Tab栏点击 - if (y > this.screenHeight - 65) { - const tabWidth = this.screenWidth / 3; - const tabIndex = Math.floor(x / tabWidth); - this.handleTabClick(tabIndex); + // 底部Tab点击 + if (y > this.screenHeight - 60) { + const tabW = this.screenWidth / 4; + const tabIndex = Math.floor(x / tabW); + this.handleBottomTabClick(tabIndex); return; } - // 检测分类标签点击 - if (y >= 90 && y <= 140) { + // 内容源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(); - console.log('选中分类:', this.categories[rect.index]); } return; } } } - handleTabClick(tabIndex) { - if (tabIndex === this.currentTab) return; + handleBottomTabClick(tabIndex) { + if (tabIndex === this.bottomTab && tabIndex === 0) return; - this.currentTab = tabIndex; - - if (tabIndex === 2) { - // 切换到个人中心 + 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 = 150; - const cardHeight = 120; - const cardMargin = 15; - + 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 }); } }