/** * 个人中心场景 - 支持创作者功能 */ import BaseScene from './BaseScene'; export default class ProfileScene extends BaseScene { constructor(main, params) { super(main, params); // Tab: 0我的作品 1草稿箱 2收藏 3游玩记录 this.currentTab = 0; this.tabs = ['作品', '草稿', '收藏', '记录']; // 数据 this.myWorks = []; this.drafts = []; this.collections = []; this.progress = []; // 统计 this.stats = { works: 0, totalPlays: 0, totalLikes: 0, earnings: 0 }; // 滚动 this.scrollY = 0; this.maxScrollY = 0; this.isDragging = false; this.lastTouchY = 0; this.scrollVelocity = 0; this.hasMoved = false; } async init() { await this.loadData(); } async loadData() { if (this.main.userManager.isLoggedIn) { try { this.myWorks = await this.main.userManager.getMyWorks?.() || []; this.drafts = await this.main.userManager.getDrafts?.() || []; this.collections = await this.main.userManager.getCollections() || []; this.progress = await this.main.userManager.getProgress() || []; // 计算统计 this.stats.works = this.myWorks.length; this.stats.totalPlays = this.myWorks.reduce((sum, w) => sum + (w.play_count || 0), 0); this.stats.totalLikes = this.myWorks.reduce((sum, w) => sum + (w.like_count || 0), 0); this.stats.earnings = this.myWorks.reduce((sum, w) => sum + (w.earnings || 0), 0); } catch (e) { console.error('加载数据失败:', e); } } this.calculateMaxScroll(); } getCurrentList() { switch (this.currentTab) { case 0: return this.myWorks; case 1: return this.drafts; case 2: return this.collections; case 3: return this.progress; default: return []; } } calculateMaxScroll() { const list = this.getCurrentList(); const cardHeight = this.currentTab <= 1 ? 100 : 85; const gap = 10; const headerHeight = 260; const contentHeight = list.length * (cardHeight + gap) + headerHeight; this.maxScrollY = Math.max(0, contentHeight - this.screenHeight + 20); } 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.renderUserCard(ctx); this.renderTabs(ctx); this.renderList(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); } renderHeader(ctx) { ctx.fillStyle = '#ffffff'; ctx.font = '16px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('‹ 返回', 15, 35); ctx.textAlign = 'center'; ctx.font = 'bold 17px sans-serif'; ctx.fillText('个人中心', this.screenWidth / 2, 35); // 设置按钮 ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.font = '18px sans-serif'; ctx.textAlign = 'right'; ctx.fillText('⚙', this.screenWidth - 20, 35); } renderUserCard(ctx) { const cardY = 55; const cardH = 145; const user = this.main.userManager; // 卡片背景 ctx.fillStyle = 'rgba(255,255,255,0.06)'; this.roundRect(ctx, 12, cardY, this.screenWidth - 24, cardH, 14); ctx.fill(); // 头像 const avatarSize = 50; const avatarX = 30; const avatarY = cardY + 18; const avatarGradient = ctx.createLinearGradient(avatarX, avatarY, avatarX + avatarSize, avatarY + avatarSize); avatarGradient.addColorStop(0, '#ff6b6b'); avatarGradient.addColorStop(1, '#ffd700'); ctx.fillStyle = avatarGradient; ctx.beginPath(); ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 20px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(user.nickname ? user.nickname[0] : '游', avatarX + avatarSize / 2, avatarY + avatarSize / 2 + 7); // 昵称和ID ctx.textAlign = 'left'; ctx.fillStyle = '#ffffff'; ctx.font = 'bold 16px sans-serif'; ctx.fillText(user.nickname || '游客用户', avatarX + avatarSize + 15, avatarY + 22); ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.font = '11px sans-serif'; ctx.fillText(`ID: ${user.userId || '未登录'}`, avatarX + avatarSize + 15, avatarY + 42); // 创作者标签 if (this.myWorks.length > 0) { const tagX = avatarX + avatarSize + 15 + ctx.measureText(`ID: ${user.userId || '未登录'}`).width + 10; ctx.fillStyle = '#a855f7'; this.roundRect(ctx, tagX, avatarY + 30, 45, 18, 9); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('创作者', tagX + 22, avatarY + 42); } // 分割线 const lineY = cardY + 80; ctx.strokeStyle = 'rgba(255,255,255,0.08)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(25, lineY); ctx.lineTo(this.screenWidth - 25, lineY); ctx.stroke(); // 统计数据 const statsY = lineY + 30; const statW = (this.screenWidth - 24) / 4; const statsData = [ { num: this.stats.works, label: '作品' }, { num: this.stats.totalPlays, label: '播放' }, { num: this.stats.totalLikes, label: '获赞' }, { num: this.stats.earnings.toFixed(1), label: '收益' } ]; statsData.forEach((stat, i) => { const x = 12 + statW * i + statW / 2; ctx.fillStyle = '#ffffff'; ctx.font = 'bold 16px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(this.formatNumber(stat.num), x, statsY); ctx.fillStyle = 'rgba(255,255,255,0.45)'; ctx.font = '10px sans-serif'; ctx.fillText(stat.label, x, statsY + 15); }); } renderTabs(ctx) { const tabY = 210; const tabW = (this.screenWidth - 24) / this.tabs.length; this.tabRects = []; this.tabs.forEach((tab, index) => { const x = 12 + index * tabW; const isActive = index === this.currentTab; const centerX = x + tabW / 2; ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.45)'; ctx.font = isActive ? 'bold 13px sans-serif' : '13px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(tab, centerX, tabY + 12); // 下划线 if (isActive) { const lineGradient = ctx.createLinearGradient(centerX - 14, tabY + 20, centerX + 14, tabY + 20); lineGradient.addColorStop(0, '#ff6b6b'); lineGradient.addColorStop(1, '#ffd700'); ctx.fillStyle = lineGradient; this.roundRect(ctx, centerX - 14, tabY + 18, 28, 3, 1.5); ctx.fill(); } this.tabRects.push({ x, y: tabY - 5, width: tabW, height: 30, index }); }); } renderList(ctx) { const list = this.getCurrentList(); const startY = 250; const cardH = this.currentTab <= 1 ? 100 : 85; const gap = 10; const padding = 12; ctx.save(); ctx.beginPath(); ctx.rect(0, startY - 5, this.screenWidth, this.screenHeight - startY + 5); ctx.clip(); if (list.length === 0) { ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.font = '13px sans-serif'; ctx.textAlign = 'center'; const emptyTexts = ['还没有发布作品,去创作吧', '草稿箱空空如也', '还没有收藏的故事', '还没有游玩记录']; ctx.fillText(emptyTexts[this.currentTab], this.screenWidth / 2, startY + 50); // 创作引导按钮 if (this.currentTab === 0) { const btnY = startY + 80; const btnGradient = ctx.createLinearGradient(this.screenWidth / 2 - 50, btnY, this.screenWidth / 2 + 50, btnY); btnGradient.addColorStop(0, '#a855f7'); btnGradient.addColorStop(1, '#ec4899'); ctx.fillStyle = btnGradient; this.roundRect(ctx, this.screenWidth / 2 - 50, btnY, 100, 36, 18); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 13px sans-serif'; ctx.fillText('✨ 开始创作', this.screenWidth / 2, btnY + 23); this.createBtnRect = { x: this.screenWidth / 2 - 50, y: btnY, width: 100, height: 36 }; } ctx.restore(); return; } list.forEach((item, index) => { const y = startY + index * (cardH + gap) - this.scrollY; if (y > startY - cardH && y < this.screenHeight) { if (this.currentTab === 0) { this.renderWorkCard(ctx, item, padding, y, this.screenWidth - padding * 2, cardH, index); } else if (this.currentTab === 1) { this.renderDraftCard(ctx, item, padding, y, this.screenWidth - padding * 2, cardH, index); } else { this.renderSimpleCard(ctx, item, padding, y, this.screenWidth - padding * 2, cardH, index); } } }); ctx.restore(); } renderWorkCard(ctx, item, x, y, w, h, index) { // 卡片背景 ctx.fillStyle = 'rgba(255,255,255,0.05)'; this.roundRect(ctx, x, y, w, h, 12); ctx.fill(); // 封面 const coverW = 70, coverH = h - 16; const colors = this.getGradientColors(index); const coverGradient = ctx.createLinearGradient(x + 8, y + 8, x + 8 + coverW, y + 8 + coverH); coverGradient.addColorStop(0, colors[0]); coverGradient.addColorStop(1, colors[1]); ctx.fillStyle = coverGradient; this.roundRect(ctx, x + 8, y + 8, coverW, coverH, 10); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(item.category || '故事', x + 8 + coverW / 2, y + 8 + coverH / 2 + 3); const textX = x + 88; const maxW = w - 100; // 标题 ctx.fillStyle = '#ffffff'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(this.truncateText(ctx, item.title || '未命名', maxW - 60), textX, y + 25); // 审核状态标签 const statusMap = { 0: { text: '草稿', color: '#888888' }, 1: { text: '审核中', color: '#f59e0b' }, 2: { text: '已发布', color: '#22c55e' }, 3: { text: '已下架', color: '#ef4444' }, 4: { text: '被拒绝', color: '#ef4444' } }; const status = statusMap[item.status] || statusMap[0]; const statusW = ctx.measureText(status.text).width + 12; ctx.fillStyle = status.color + '33'; this.roundRect(ctx, textX + ctx.measureText(this.truncateText(ctx, item.title || '未命名', maxW - 60)).width + 8, y + 12, statusW, 18, 9); ctx.fill(); ctx.fillStyle = status.color; ctx.font = 'bold 10px sans-serif'; ctx.fillText(status.text, textX + ctx.measureText(this.truncateText(ctx, item.title || '未命名', maxW - 60)).width + 8 + statusW / 2, y + 24); // 数据统计 ctx.fillStyle = 'rgba(255,255,255,0.45)'; ctx.font = '11px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(`▶ ${this.formatNumber(item.play_count || 0)}`, textX, y + 50); ctx.fillText(`♥ ${this.formatNumber(item.like_count || 0)}`, textX + 55, y + 50); ctx.fillText(`💰 ${(item.earnings || 0).toFixed(1)}`, textX + 105, y + 50); // 操作按钮 const btnY = y + 65; const btns = ['编辑', '数据']; btns.forEach((btn, i) => { const btnX = textX + i * 55; ctx.fillStyle = 'rgba(255,255,255,0.1)'; this.roundRect(ctx, btnX, btnY, 48, 24, 12); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.7)'; ctx.font = '11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(btn, btnX + 24, btnY + 16); }); } renderDraftCard(ctx, item, x, y, w, h, index) { ctx.fillStyle = 'rgba(255,255,255,0.05)'; this.roundRect(ctx, x, y, w, h, 12); ctx.fill(); const coverW = 70, coverH = h - 16; const colors = this.getGradientColors(index); const coverGradient = ctx.createLinearGradient(x + 8, y + 8, x + 8 + coverW, y + 8 + coverH); coverGradient.addColorStop(0, colors[0]); coverGradient.addColorStop(1, colors[1]); ctx.fillStyle = coverGradient; this.roundRect(ctx, x + 8, y + 8, coverW, coverH, 10); ctx.fill(); // AI标签 if (item.source === 'ai') { ctx.fillStyle = '#a855f7'; this.roundRect(ctx, x + 8, y + 8, 28, 16, 8); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('AI', x + 22, y + 19); } const textX = x + 88; ctx.fillStyle = '#ffffff'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(this.truncateText(ctx, item.title || '未命名草稿', w - 180), textX, y + 25); ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.font = '11px sans-serif'; ctx.fillText(`创建于 ${item.created_at || '刚刚'}`, textX, y + 48); ctx.fillText(`${item.node_count || 0} 个节点`, textX + 100, y + 48); // 按钮 const btnY = y + 62; const btns = [{ text: '继续编辑', primary: true }, { text: '删除', primary: false }]; let btnX = textX; btns.forEach((btn) => { const btnW = btn.primary ? 65 : 45; if (btn.primary) { const btnGradient = ctx.createLinearGradient(btnX, btnY, btnX + btnW, btnY); btnGradient.addColorStop(0, '#a855f7'); btnGradient.addColorStop(1, '#ec4899'); ctx.fillStyle = btnGradient; } else { ctx.fillStyle = 'rgba(255,255,255,0.1)'; } this.roundRect(ctx, btnX, btnY, btnW, 26, 13); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = btn.primary ? 'bold 11px sans-serif' : '11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(btn.text, btnX + btnW / 2, btnY + 17); btnX += btnW + 8; }); } renderSimpleCard(ctx, item, x, y, w, h, index) { ctx.fillStyle = 'rgba(255,255,255,0.05)'; this.roundRect(ctx, x, y, w, h, 12); ctx.fill(); const coverW = 60, coverH = h - 16; const colors = this.getGradientColors(index); const coverGradient = ctx.createLinearGradient(x + 8, y + 8, x + 8 + coverW, y + 8 + coverH); coverGradient.addColorStop(0, colors[0]); coverGradient.addColorStop(1, colors[1]); ctx.fillStyle = coverGradient; this.roundRect(ctx, x + 8, y + 8, coverW, coverH, 8); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.85)'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(item.category || '故事', x + 8 + coverW / 2, y + 8 + coverH / 2 + 3); const textX = x + 78; ctx.fillStyle = '#ffffff'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(this.truncateText(ctx, item.story_title || item.title || '未知', w - 150), textX, y + 28); ctx.fillStyle = 'rgba(255,255,255,0.45)'; ctx.font = '11px sans-serif'; if (this.currentTab === 3 && item.is_completed) { ctx.fillStyle = '#4ade80'; ctx.fillText('✓ 已完成', textX, y + 50); } else if (this.currentTab === 3) { ctx.fillText('进行中...', textX, y + 50); } else { ctx.fillText(item.category || '', textX, y + 50); } // 继续按钮 const btnGradient = ctx.createLinearGradient(x + w - 58, y + 28, x + w - 10, y + 28); btnGradient.addColorStop(0, '#ff6b6b'); btnGradient.addColorStop(1, '#ffd700'); ctx.fillStyle = btnGradient; this.roundRect(ctx, x + w - 58, y + 28, 48, 26, 13); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('继续', x + w - 34, y + 45); } getGradientColors(index) { const colors = [ ['#ff758c', '#ff7eb3'], ['#667eea', '#764ba2'], ['#4facfe', '#00f2fe'], ['#43e97b', '#38f9d7'], ['#fa709a', '#fee140'], ['#a855f7', '#ec4899'] ]; return colors[index % colors.length]; } 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, maxW) { if (!text) return ''; if (ctx.measureText(text).width <= maxW) return text; let t = text; while (t.length > 0 && ctx.measureText(t + '...').width > maxW) 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.touchStartY = touch.clientY; this.isDragging = true; this.scrollVelocity = 0; this.hasMoved = false; } onTouchMove(e) { if (!this.isDragging) return; const touch = e.touches[0]; 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; if (this.hasMoved) return; const touch = e.changedTouches[0]; const x = touch.clientX; const y = touch.clientY; // 返回 if (y < 50 && x < 80) { this.main.sceneManager.switchScene('home'); return; } // Tab切换 if (this.tabRects) { for (const rect of this.tabRects) { if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) { if (this.currentTab !== rect.index) { this.currentTab = rect.index; this.scrollY = 0; this.calculateMaxScroll(); } return; } } } // 创作按钮 if (this.createBtnRect && this.currentTab === 0) { const btn = this.createBtnRect; if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) { this.main.sceneManager.switchScene('aiCreate'); return; } } // 卡片点击 this.handleCardClick(x, y); } handleCardClick(x, y) { const list = this.getCurrentList(); const startY = 250; const cardH = this.currentTab <= 1 ? 100 : 85; const gap = 10; const adjustedY = y + this.scrollY; const index = Math.floor((adjustedY - startY) / (cardH + gap)); if (index >= 0 && index < list.length) { const item = list[index]; const storyId = item.story_id || item.id; if (this.currentTab >= 2) { // 收藏/记录 - 跳转播放 this.main.sceneManager.switchScene('story', { storyId }); } else if (this.currentTab === 1) { // 草稿 - 跳转编辑(暂用AI创作) this.main.sceneManager.switchScene('aiCreate', { draftId: item.id }); } // 作品Tab的按钮操作需要更精确判断,暂略 } } }