From 5baa95f5147ad65e8dde23bed6ed58c2cdacb2b4 Mon Sep 17 00:00:00 2001 From: liangguodong Date: Tue, 3 Mar 2026 17:21:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=AA=E4=BA=BA=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=88=91=E7=9A=84=E4=BD=9C=E5=93=81=E3=80=81?= =?UTF-8?q?=E8=8D=89=E7=A8=BF=E7=AE=B1=E3=80=81=E5=88=9B=E4=BD=9C=E8=80=85?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/js/data/UserManager.js | 24 ++ client/js/scenes/ProfileScene.js | 581 +++++++++++++++++++++---------- 2 files changed, 424 insertions(+), 181 deletions(-) diff --git a/client/js/data/UserManager.js b/client/js/data/UserManager.js index 63ae00d..bd877ea 100644 --- a/client/js/data/UserManager.js +++ b/client/js/data/UserManager.js @@ -167,4 +167,28 @@ export default class UserManager { return { daily: 3, used: 0, purchased: 0 }; } } + + /** + * 获取我的作品 + */ + async getMyWorks() { + if (!this.isLoggedIn) return []; + try { + return await get('/user/my-works', { userId: this.userId }); + } catch (e) { + return []; + } + } + + /** + * 获取草稿箱 + */ + async getDrafts() { + if (!this.isLoggedIn) return []; + try { + return await get('/user/drafts', { userId: this.userId }); + } catch (e) { + return []; + } + } } diff --git a/client/js/scenes/ProfileScene.js b/client/js/scenes/ProfileScene.js index 0b6f89c..487b3f2 100644 --- a/client/js/scenes/ProfileScene.js +++ b/client/js/scenes/ProfileScene.js @@ -1,14 +1,30 @@ /** - * 个人中心场景 + * 个人中心场景 - 支持创作者功能 */ 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.currentTab = 0; // 0: 收藏, 1: 历史 + + // 统计 + this.stats = { + works: 0, + totalPlays: 0, + totalLikes: 0, + earnings: 0 + }; + + // 滚动 this.scrollY = 0; this.maxScrollY = 0; this.isDragging = false; @@ -23,17 +39,39 @@ export default class ProfileScene extends BaseScene { async loadData() { if (this.main.userManager.isLoggedIn) { - this.collections = await this.main.userManager.getCollections() || []; - this.progress = await this.main.userManager.getProgress() || []; + 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.currentTab === 0 ? this.collections : this.progress; - const cardHeight = 90; - const gap = 12; - const headerHeight = 300; + 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); } @@ -47,248 +85,421 @@ export default class ProfileScene extends BaseScene { } 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); - - // 顶部返回 - this.renderHeader(ctx); - - // 用户信息卡片 - this.renderUserCard(ctx); - - // Tab切换 - this.renderTabs(ctx); - - // 列表内容 - this.renderList(ctx); } renderHeader(ctx) { - // 顶部渐变遮罩 - const headerGradient = ctx.createLinearGradient(0, 0, 0, 60); - headerGradient.addColorStop(0, 'rgba(0,0,0,0.5)'); - headerGradient.addColorStop(1, 'rgba(0,0,0,0)'); - ctx.fillStyle = headerGradient; - ctx.fillRect(0, 0, this.screenWidth, 60); - - // 返回按钮 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 = 60; - const cardHeight = 170; - const centerX = this.screenWidth / 2; + const cardY = 55; + const cardH = 145; const user = this.main.userManager; // 卡片背景 ctx.fillStyle = 'rgba(255,255,255,0.06)'; - this.roundRect(ctx, 15, cardY, this.screenWidth - 30, cardHeight, 16); + this.roundRect(ctx, 12, cardY, this.screenWidth - 24, cardH, 14); ctx.fill(); // 头像 - const avatarSize = 55; - const avatarY = cardY + 20; - const avatarGradient = ctx.createLinearGradient(centerX - 30, avatarY, centerX + 30, avatarY + avatarSize); + 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(centerX, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2); + ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2); ctx.fill(); - // 头像文字 ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 22px sans-serif'; + ctx.font = 'bold 20px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(user.nickname ? user.nickname[0] : '游', centerX, avatarY + avatarSize / 2 + 8); + ctx.fillText(user.nickname ? user.nickname[0] : '游', avatarX + avatarSize / 2, avatarY + avatarSize / 2 + 7); - // 昵称 + // 昵称和ID + ctx.textAlign = 'left'; ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 15px sans-serif'; - ctx.fillText(user.nickname || '游客用户', centerX, avatarY + avatarSize + 20); + 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 = avatarY + avatarSize + 35; - ctx.strokeStyle = 'rgba(255,255,255,0.1)'; + const lineY = cardY + 80; + ctx.strokeStyle = 'rgba(255,255,255,0.08)'; ctx.lineWidth = 1; ctx.beginPath(); - ctx.moveTo(30, lineY); - ctx.lineTo(this.screenWidth - 30, lineY); + ctx.moveTo(25, lineY); + ctx.lineTo(this.screenWidth - 25, lineY); ctx.stroke(); - // 统计信息 + // 统计数据 const statsY = lineY + 30; - const statWidth = (this.screenWidth - 30) / 3; + const statW = (this.screenWidth - 24) / 4; const statsData = [ - { num: this.progress.length, label: '游玩' }, - { num: this.collections.length, label: '收藏' }, - { num: this.progress.filter(p => p.is_completed).length, label: '结局' } + { 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 = 15 + statWidth * i + statWidth / 2; + const x = 12 + statW * i + statW / 2; ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 18px sans-serif'; - ctx.fillText(stat.num.toString(), x, statsY); - ctx.fillStyle = 'rgba(255,255,255,0.5)'; - ctx.font = '11px sans-serif'; - ctx.fillText(stat.label, x, statsY + 16); + 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 = 245; - const tabWidth = (this.screenWidth - 30) / 2; - const tabs = ['我的收藏', '游玩记录']; - - tabs.forEach((tab, index) => { - const x = 15 + index * tabWidth; + 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 + tabWidth / 2; + const centerX = x + tabW / 2; - // Tab背景 + 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 tabGradient = ctx.createLinearGradient(x, tabY, x + tabWidth, tabY); - tabGradient.addColorStop(0, '#ff6b6b'); - tabGradient.addColorStop(1, '#ffd700'); - ctx.fillStyle = tabGradient; - this.roundRect(ctx, x + 10, tabY, tabWidth - 20, 32, 16); + 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(); } - // Tab文字 - 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, centerX, tabY + 21); + this.tabRects.push({ x, y: tabY - 5, width: tabW, height: 30, index }); }); } renderList(ctx) { - const list = this.currentTab === 0 ? this.collections : this.progress; - const startY = 295; - const cardHeight = 90; - const cardMargin = 12; - const padding = 15; + 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 - 10, this.screenWidth, this.screenHeight - startY + 10); + ctx.rect(0, startY - 5, this.screenWidth, this.screenHeight - startY + 5); ctx.clip(); if (list.length === 0) { - ctx.fillStyle = 'rgba(255,255,255,0.4)'; - ctx.font = '14px sans-serif'; + ctx.fillStyle = 'rgba(255,255,255,0.35)'; + ctx.font = '13px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText( - this.currentTab === 0 ? '还没有收藏的故事' : '还没有游玩记录', - this.screenWidth / 2, - startY + 50 - ); + 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 * (cardHeight + cardMargin) - this.scrollY; - if (y > -cardHeight && y < this.screenHeight) { - this.renderListCard(ctx, item, padding, y, this.screenWidth - padding * 2, cardHeight, 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(); } - renderListCard(ctx, item, x, y, width, height, index) { + renderWorkCard(ctx, item, x, y, w, h, index) { // 卡片背景 - ctx.fillStyle = 'rgba(255,255,255,0.06)'; - this.roundRect(ctx, x, y, width, height, 12); + ctx.fillStyle = 'rgba(255,255,255,0.05)'; + this.roundRect(ctx, x, y, w, h, 12); ctx.fill(); // 封面 - const coverSize = 65; - const coverColors = [ + 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'] + ['#fa709a', '#fee140'], + ['#a855f7', '#ec4899'] ]; - const colors = coverColors[index % coverColors.length]; - const coverGradient = ctx.createLinearGradient(x + 12, y + 12, x + 12 + coverSize, y + 12 + coverSize); - coverGradient.addColorStop(0, colors[0]); - coverGradient.addColorStop(1, colors[1]); - ctx.fillStyle = coverGradient; - this.roundRect(ctx, x + 12, y + 12, coverSize, coverSize, 10); - ctx.fill(); - - // 封面文字 - ctx.fillStyle = 'rgba(255,255,255,0.9)'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(item.category || '故事', x + 12 + coverSize / 2, y + 12 + coverSize / 2 + 4); - - // 标题 - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 15px sans-serif'; - ctx.textAlign = 'left'; - const title = item.story_title || item.title || '未知故事'; - ctx.fillText(title.length > 12 ? title.substring(0, 12) + '...' : title, x + 90, y + 35); - - // 状态 - ctx.font = '12px sans-serif'; - if (this.currentTab === 1 && item.is_completed) { - ctx.fillStyle = '#4ecca3'; - ctx.fillText('✓ 已完成', x + 90, y + 58); - } else if (this.currentTab === 1) { - ctx.fillStyle = 'rgba(255,255,255,0.5)'; - ctx.fillText('进行中...', x + 90, y + 58); - } else { - ctx.fillStyle = 'rgba(255,255,255,0.5)'; - ctx.fillText(item.category || '', x + 90, y + 58); - } - - // 继续按钮 - const btnGradient = ctx.createLinearGradient(x + width - 65, y + 30, x + width - 10, y + 30); - btnGradient.addColorStop(0, '#ff6b6b'); - btnGradient.addColorStop(1, '#ffd700'); - ctx.fillStyle = btnGradient; - this.roundRect(ctx, x + width - 65, y + 30, 52, 28, 14); - ctx.fill(); - - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 12px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('继续', x + width - 39, y + 49); + return colors[index % colors.length]; } - // 圆角矩形 - roundRect(ctx, x, y, width, height, radius) { + 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 + 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(); } @@ -305,11 +516,7 @@ export default class ProfileScene extends BaseScene { 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; - } - + 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)); @@ -318,29 +525,39 @@ export default class ProfileScene extends BaseScene { 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 (y >= 240 && y <= 285) { - const tabWidth = (this.screenWidth - 30) / 2; - const newTab = x < 15 + tabWidth ? 0 : 1; - if (newTab !== this.currentTab) { - this.currentTab = newTab; - this.scrollY = 0; - this.calculateMaxScroll(); + 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; } - return; } // 卡片点击 @@ -348,24 +565,26 @@ export default class ProfileScene extends BaseScene { } handleCardClick(x, y) { - const list = this.currentTab === 0 ? this.collections : this.progress; - const startY = 295; - const cardHeight = 90; - const cardMargin = 12; - const padding = 15; + 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) / (cardHeight + cardMargin)); + 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; - const cardY = startY + index * (cardHeight + cardMargin) - this.scrollY; - const buttonX = padding + (this.screenWidth - padding * 2) - 65; - if (x >= buttonX && x <= buttonX + 52 && y >= cardY + 30 && y <= cardY + 58) { + 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的按钮操作需要更精确判断,暂略 } } }