Files
ai_game/client/js/scenes/ProfileScene.js

950 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 个人中心场景 - 支持创作者功能
*/
import BaseScene from './BaseScene';
export default class ProfileScene extends BaseScene {
constructor(main, params) {
super(main, params);
// Tab: 0我的作品 1AI草稿 2收藏 3游玩记录
this.currentTab = params.tab || 0; // 支持传入初始tab
this.tabs = ['作品', 'AI草稿', '收藏', '记录'];
// 数据
this.myWorks = [];
this.drafts = [];
this.collections = [];
this.progress = [];
// 记录版本列表相关状态
this.recordViewMode = 'list'; // 'list' 故事列表 | 'versions' 版本列表
this.selectedStoryRecords = []; // 选中故事的记录列表
this.selectedStoryInfo = {}; // 选中故事的信息
// 统计
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 {
const userId = this.main.userManager.userId;
this.myWorks = await this.main.userManager.getMyWorks?.() || [];
// 加载 AI 改写草稿
this.drafts = await this.main.storyManager.getDrafts(userId) || [];
this.collections = await this.main.userManager.getCollections() || [];
// 加载游玩记录(故事列表)
this.progress = await this.main.userManager.getPlayRecords() || [];
// 计算统计
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();
}
// 刷新草稿列表
async refreshDrafts() {
if (this.main.userManager.isLoggedIn) {
try {
const userId = this.main.userManager.userId;
this.drafts = await this.main.storyManager.getDrafts(userId) || [];
this.calculateMaxScroll();
} catch (e) {
console.error('刷新草稿失败:', e);
}
}
}
getCurrentList() {
switch (this.currentTab) {
case 0: return this.myWorks;
case 1: return this.drafts;
case 2: return this.collections;
case 3:
// 记录 Tab根据视图模式返回不同列表
return this.recordViewMode === 'versions' ? this.selectedStoryRecords : 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();
// 记录 Tab 版本列表模式:显示返回按钮和标题
if (this.currentTab === 3 && this.recordViewMode === 'versions') {
this.renderVersionListHeader(ctx, startY);
}
const listStartY = (this.currentTab === 3 && this.recordViewMode === 'versions') ? startY + 45 : startY;
if (list.length === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
const emptyTexts = ['还没有发布作品,去创作吧', '草稿箱空空如也', '还没有收藏的故事', '还没有游玩记录'];
const emptyText = (this.currentTab === 3 && this.recordViewMode === 'versions')
? '该故事还没有游玩记录'
: emptyTexts[this.currentTab];
ctx.fillText(emptyText, this.screenWidth / 2, listStartY + 50);
// 创作引导按钮
if (this.currentTab === 0) {
const btnY = listStartY + 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 = listStartY + index * (cardH + gap) - this.scrollY;
if (y > listStartY - 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 if (this.currentTab === 3 && this.recordViewMode === 'versions') {
this.renderRecordVersionCard(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();
}
// 渲染版本列表头部(返回按钮+故事标题)
renderVersionListHeader(ctx, startY) {
const headerY = startY - 5;
// 返回按钮
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(' 返回', 15, headerY + 20);
this.versionBackBtnRect = { x: 5, y: headerY, width: 70, height: 35 };
// 故事标题
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
const title = this.selectedStoryInfo.title || '游玩记录';
ctx.fillText(this.truncateText(ctx, title, this.screenWidth - 120), this.screenWidth / 2, headerY + 20);
// 记录数量
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(`${this.selectedStoryRecords.length} 条记录`, this.screenWidth - 15, headerY + 20);
}
// 渲染单条游玩记录版本卡片
renderRecordVersionCard(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 circleX = x + 30;
const circleY = y + h / 2;
const circleR = 18;
const colors = this.getGradientColors(index);
const circleGradient = ctx.createLinearGradient(circleX - circleR, circleY - circleR, circleX + circleR, circleY + circleR);
circleGradient.addColorStop(0, colors[0]);
circleGradient.addColorStop(1, colors[1]);
ctx.fillStyle = circleGradient;
ctx.beginPath();
ctx.arc(circleX, circleY, circleR, 0, Math.PI * 2);
ctx.fill();
// 序号
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${index + 1}`, circleX, circleY + 5);
const textX = x + 65;
// 结局名称
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
const endingLabel = `结局:${item.endingName || '未知结局'}`;
ctx.fillText(this.truncateText(ctx, endingLabel, w - 150), textX, y + 28);
// 游玩时间
ctx.fillStyle = 'rgba(255,255,255,0.45)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'left';
const timeText = item.createdAt ? this.formatDateTime(item.createdAt) : '';
ctx.fillText(timeText, textX, y + 52);
// 删除按钮
ctx.fillStyle = 'rgba(239, 68, 68, 0.2)';
this.roundRect(ctx, x + w - 125, y + 28, 48, 26, 13);
ctx.fill();
ctx.fillStyle = '#ef4444';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('删除', x + w - 101, y + 45);
// 回放按钮
const btnGradient = ctx.createLinearGradient(x + w - 68, y + 28, x + w - 10, y + 28);
btnGradient.addColorStop(0, '#ff6b6b');
btnGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, x + w - 68, y + 28, 58, 26, 13);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('回放', x + w - 39, y + 45);
}
// 格式化日期时间
formatDateTime(dateStr) {
if (!dateStr) return '';
try {
// iOS 兼容:将 "2026-03-10 11:51" 转换为 "2026-03-10T11:51:00"
const isoStr = dateStr.replace(' ', 'T');
const date = new Date(isoStr);
if (isNaN(date.getTime())) return dateStr;
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours().toString().padStart(2, '0');
const minute = date.getMinutes().toString().padStart(2, '0');
return `${month}${day}${hour}:${minute}`;
} catch (e) {
return dateStr;
}
}
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标签
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 || item.storyTitle || 'AI改写', w - 180), textX, y + 25);
// 状态标签
const statusMap = {
'pending': { text: '等待中', color: '#888888' },
'processing': { text: '生成中', color: '#f59e0b' },
'completed': { text: '已完成', color: '#22c55e' },
'failed': { text: '失败', color: '#ef4444' }
};
const status = statusMap[item.status] || statusMap['pending'];
const titleWidth = ctx.measureText(this.truncateText(ctx, item.title || item.storyTitle || 'AI改写', w - 180)).width;
const statusW = ctx.measureText(status.text).width + 12;
ctx.fillStyle = status.color + '33';
this.roundRect(ctx, textX + titleWidth + 8, y + 12, statusW, 18, 9);
ctx.fill();
ctx.fillStyle = status.color;
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(status.text, textX + titleWidth + 8 + statusW / 2, y + 24);
// 改写指令
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'left';
const promptText = item.userPrompt ? `"「${item.userPrompt}」"` : '';
ctx.fillText(this.truncateText(ctx, promptText, w - 100), textX, y + 48);
// 时间
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.font = '10px sans-serif';
ctx.fillText(item.createdAt || '', textX, y + 68);
// 未读标记
if (!item.isRead && item.status === 'completed') {
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.arc(x + w - 20, y + 20, 5, 0, Math.PI * 2);
ctx.fill();
}
// 按钮
const btnY = y + 62;
// 删除按钮(所有状态都显示)
ctx.fillStyle = 'rgba(239, 68, 68, 0.2)';
this.roundRect(ctx, x + w - 55, btnY, 45, 24, 12);
ctx.fill();
ctx.fillStyle = '#ef4444';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('删除', x + w - 32, btnY + 16);
// 播放按钮(仅已完成状态)
if (item.status === 'completed') {
const btnGradient = ctx.createLinearGradient(textX, btnY, textX + 65, btnY);
btnGradient.addColorStop(0, '#a855f7');
btnGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, textX + 120, btnY, 60, 24, 12);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('播放', textX + 150, btnY + 16);
}
}
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';
// 记录Tab使用 storyTitle收藏Tab使用 story_title
const title = item.storyTitle || item.story_title || item.title || '未知';
ctx.fillText(this.truncateText(ctx, title, w - 150), textX, y + 28);
ctx.fillStyle = 'rgba(255,255,255,0.45)';
ctx.font = '11px sans-serif';
if (this.currentTab === 3) {
// 记录Tab只显示记录数量
ctx.fillText(`${item.recordCount || 0} 条记录`, textX, y + 50);
} else {
ctx.fillText(item.category || '', textX, y + 50);
}
// 查看按钮记录Tab/ 继续按钮收藏Tab
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(this.currentTab === 3 ? '查看' : '继续', 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.recordViewMode = 'list'; // 切换 Tab 时重置记录视图模式
this.calculateMaxScroll();
// 切换到 AI 草稿 tab 时刷新数据
if (rect.index === 1) {
this.refreshDrafts();
}
}
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 padding = 12;
const cardW = this.screenWidth - padding * 2;
// 记录 Tab 版本列表模式下,检测返回按钮
if (this.currentTab === 3 && this.recordViewMode === 'versions') {
if (this.versionBackBtnRect) {
const btn = this.versionBackBtnRect;
if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) {
this.recordViewMode = 'list';
this.scrollY = 0;
this.calculateMaxScroll();
return;
}
}
}
const listStartY = (this.currentTab === 3 && this.recordViewMode === 'versions') ? startY + 45 : startY;
const adjustedY = y + this.scrollY;
const index = Math.floor((adjustedY - listStartY) / (cardH + gap));
if (index >= 0 && index < list.length) {
const item = list[index];
const storyId = item.story_id || item.storyId || item.id;
// 计算卡片内的相对位置
const cardY = listStartY + index * (cardH + gap) - this.scrollY;
const relativeY = y - cardY;
// AI草稿 Tab 的按钮检测
if (this.currentTab === 1) {
const btnY = 62;
const btnH = 24;
// 检测删除按钮点击(右侧)
const deleteBtnX = padding + cardW - 55;
if (x >= deleteBtnX && x <= deleteBtnX + 45 && relativeY >= btnY && relativeY <= btnY + btnH) {
this.confirmDeleteDraft(item, index);
return;
}
// 检测播放按钮点击(左侧,仅已完成状态)
if (item.status === 'completed') {
const playBtnX = padding + 88 + 120;
if (x >= playBtnX && x <= playBtnX + 60 && relativeY >= btnY && relativeY <= btnY + btnH) {
this.main.sceneManager.switchScene('story', { storyId: item.storyId, draftId: item.id });
return;
}
}
// 点击卡片其他区域
if (item.status === 'completed') {
this.main.sceneManager.switchScene('story', { storyId: item.storyId, draftId: item.id });
} else if (item.status === 'failed') {
wx.showToast({ title: 'AI改写失败', icon: 'none' });
} else {
wx.showToast({ title: '正在生成中,请稍后', icon: 'none' });
}
return;
}
// 记录 Tab 处理
if (this.currentTab === 3) {
if (this.recordViewMode === 'list') {
// 故事列表模式:点击进入版本列表
this.showStoryVersions(item);
} else {
// 版本列表模式
const btnY = 28;
const btnH = 26;
// 检测删除按钮点击
const deleteBtnX = padding + cardW - 125;
if (x >= deleteBtnX && x <= deleteBtnX + 48 && relativeY >= btnY && relativeY <= btnY + btnH) {
this.confirmDeleteRecord(item, index);
return;
}
// 检测回放按钮点击
const replayBtnX = padding + cardW - 68;
if (x >= replayBtnX && x <= replayBtnX + 58 && relativeY >= btnY && relativeY <= btnY + btnH) {
this.startRecordReplay(item);
return;
}
// 点击卡片其他区域也进入回放
this.startRecordReplay(item);
}
return;
}
if (this.currentTab === 2) {
// 收藏 - 跳转播放
this.main.sceneManager.switchScene('story', { storyId });
}
// 作品Tab的按钮操作需要更精确判断暂略
}
}
// 显示故事的版本列表
async showStoryVersions(storyItem) {
const storyId = storyItem.story_id || storyItem.storyId || storyItem.id;
const storyTitle = storyItem.story_title || storyItem.title || '未知故事';
try {
wx.showLoading({ title: '加载中...' });
const records = await this.main.userManager.getPlayRecords(storyId);
wx.hideLoading();
if (records && records.length > 0) {
this.selectedStoryInfo = { id: storyId, title: storyTitle };
this.selectedStoryRecords = records;
this.recordViewMode = 'versions';
this.scrollY = 0;
this.calculateMaxScroll();
} else {
wx.showToast({ title: '暂无游玩记录', icon: 'none' });
}
} catch (e) {
wx.hideLoading();
console.error('加载版本列表失败:', e);
wx.showToast({ title: '加载失败', icon: 'none' });
}
}
// 开始回放记录
async startRecordReplay(recordItem) {
const recordId = recordItem.id;
const storyId = this.selectedStoryInfo.id;
// 进入故事场景,传入 playRecordId 参数
this.main.sceneManager.switchScene('story', {
storyId,
playRecordId: recordId
});
}
// 确认删除草稿
confirmDeleteDraft(item, index) {
wx.showModal({
title: '删除草稿',
content: `确定要删除「${item.title || 'AI改写'}」吗?`,
confirmText: '删除',
confirmColor: '#ef4444',
cancelText: '取消',
success: async (res) => {
if (res.confirm) {
const userId = this.main.userManager.userId;
const success = await this.main.storyManager.deleteDraft(item.id, userId);
if (success) {
// 从列表中移除
this.drafts.splice(index, 1);
this.calculateMaxScroll();
wx.showToast({ title: '删除成功', icon: 'success' });
} else {
wx.showToast({ title: '删除失败', icon: 'none' });
}
}
}
});
}
// 确认删除游玩记录
confirmDeleteRecord(item, index) {
wx.showModal({
title: '删除记录',
content: `确定要删除这条「${item.endingName || '未知结局'}」的记录吗?`,
confirmText: '删除',
confirmColor: '#ef4444',
cancelText: '取消',
success: async (res) => {
if (res.confirm) {
const success = await this.main.userManager.deletePlayRecord(item.id);
if (success) {
// 从版本列表中移除
this.selectedStoryRecords.splice(index, 1);
this.calculateMaxScroll();
wx.showToast({ title: '删除成功', icon: 'success' });
// 如果删光了,返回故事列表
if (this.selectedStoryRecords.length === 0) {
this.recordViewMode = 'list';
// 从 progress 列表中也移除该故事
const storyId = this.selectedStoryInfo.id;
const idx = this.progress.findIndex(p => (p.story_id || p.storyId) === storyId);
if (idx >= 0) {
this.progress.splice(idx, 1);
}
}
} else {
wx.showToast({ title: '删除失败', icon: 'none' });
}
}
}
});
}
}