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

1310 lines
43 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.avatarImage = null;
this.avatarImageLoaded = false;
// 统计
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();
this.loadAvatarImage();
}
// 加载头像图片
loadAvatarImage() {
let avatarUrl = this.main.userManager.avatarUrl;
if (!avatarUrl) return;
// 如果是相对路径,拼接完整 URL
if (avatarUrl.startsWith('/uploads')) {
avatarUrl = 'http://172.20.10.8:8000' + avatarUrl;
}
if (avatarUrl.startsWith('http')) {
this.avatarImage = wx.createImage();
this.avatarImage.onload = () => {
this.avatarImageLoaded = true;
};
this.avatarImage.onerror = () => {
this.avatarImageLoaded = false;
};
this.avatarImage.src = avatarUrl;
}
}
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);
this.renderLogoutButton(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);
}
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;
// 保存头像区域用于点击检测
this.avatarRect = { x: avatarX, y: avatarY, width: avatarSize, height: avatarSize };
// 如果有头像图片则绘制图片,否则绘制默认渐变头像
if (this.avatarImage && this.avatarImageLoaded) {
ctx.save();
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(this.avatarImage, avatarX, avatarY, avatarSize, avatarSize);
ctx.restore();
} else {
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);
}
// 编辑图标(头像右下角)
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.beginPath();
ctx.arc(avatarX + avatarSize - 8, avatarY + avatarSize - 8, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('✎', avatarX + avatarSize - 8, avatarY + avatarSize - 5);
// 昵称和ID
ctx.textAlign = 'left';
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 16px sans-serif';
const nickname = user.nickname || '游客用户';
ctx.fillText(nickname, avatarX + avatarSize + 15, avatarY + 22);
// 昵称旁边的编辑按钮
const nicknameWidth = ctx.measureText(nickname).width;
const editNicknameBtnX = avatarX + avatarSize + 15 + nicknameWidth + 8;
ctx.fillStyle = 'rgba(255,255,255,0.15)';
this.roundRect(ctx, editNicknameBtnX, avatarY + 8, 24, 20, 10);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('✎', editNicknameBtnX + 12, avatarY + 22);
this.editNicknameRect = { x: editNicknameBtnX, y: avatarY + 8, width: 24, height: 20 };
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'left';
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();
}
// 渲染底部退出登录按钮
renderLogoutButton(ctx) {
const btnW = 120;
const btnH = 40;
const btnX = (this.screenWidth - btnW) / 2;
const btnY = this.screenHeight - 70;
// 按钮背景
ctx.fillStyle = 'rgba(239, 68, 68, 0.15)';
this.roundRect(ctx, btnX, btnY, btnW, btnH, 20);
ctx.fill();
// 按钮边框
ctx.strokeStyle = 'rgba(239, 68, 68, 0.4)';
ctx.lineWidth = 1;
this.roundRect(ctx, btnX, btnY, btnW, btnH, 20);
ctx.stroke();
// 按钮文字
ctx.fillStyle = '#ef4444';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('退出登录', this.screenWidth / 2, btnY + 26);
// 保存按钮区域
this.logoutBtnRect = { x: btnX, y: btnY, width: btnW, height: btnH };
}
// 渲染版本列表头部(返回按钮+故事标题)
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 + 72);
// 未读标记
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 + 60;
const btnStartX = x + w - 170; // 从右边开始排列按钮
// 播放按钮(仅已完成状态)
if (item.status === 'completed') {
const btnGradient = ctx.createLinearGradient(btnStartX, btnY, btnStartX + 50, btnY);
btnGradient.addColorStop(0, '#a855f7');
btnGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, btnStartX, btnY, 50, 26, 13);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('播放', btnStartX + 25, btnY + 17);
// 发布按钮(仅已完成且未发布)
if (!item.publishedToCenter) {
ctx.fillStyle = 'rgba(34, 197, 94, 0.2)';
this.roundRect(ctx, btnStartX + 58, btnY, 50, 26, 13);
ctx.fill();
ctx.fillStyle = '#22c55e';
ctx.font = '11px sans-serif';
ctx.fillText('发布', btnStartX + 83, btnY + 17);
} else {
// 已发布标识
ctx.fillStyle = 'rgba(34, 197, 94, 0.15)';
this.roundRect(ctx, btnStartX + 58, btnY, 55, 26, 13);
ctx.fill();
ctx.fillStyle = '#22c55e';
ctx.font = '10px sans-serif';
ctx.fillText('已发布', btnStartX + 85, btnY + 17);
}
}
// 删除按钮(所有状态都显示,最右边)
ctx.fillStyle = 'rgba(239, 68, 68, 0.2)';
this.roundRect(ctx, x + w - 55, btnY, 45, 26, 13);
ctx.fill();
ctx.fillStyle = '#ef4444';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('删除', x + w - 32, btnY + 17);
}
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;
}
// 退出登录按钮
if (this.logoutBtnRect) {
const btn = this.logoutBtnRect;
if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) {
this.confirmLogout();
return;
}
}
// 头像点击(修改头像)
if (this.avatarRect) {
const rect = this.avatarRect;
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
this.chooseAndUploadAvatar();
return;
}
}
// 昵称编辑按钮点击
if (this.editNicknameRect) {
const rect = this.editNicknameRect;
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
this.showEditNicknameDialog();
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 = 60;
const btnH = 26;
const btnStartX = padding + cardW - 170;
// 检测删除按钮点击(最右侧)
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') {
if (x >= btnStartX && x <= btnStartX + 50 && relativeY >= btnY && relativeY <= btnY + btnH) {
this.main.sceneManager.switchScene('story', { storyId: item.storyId, draftId: item.id });
return;
}
// 检测发布按钮点击(仅未发布状态)
if (!item.publishedToCenter) {
const publishBtnX = btnStartX + 58;
if (x >= publishBtnX && x <= publishBtnX + 50 && relativeY >= btnY && relativeY <= btnY + btnH) {
this.confirmPublishDraft(item, index);
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' });
}
}
}
});
}
// 确认发布草稿到创作中心
confirmPublishDraft(item, index) {
wx.showModal({
title: '发布到创作中心',
content: `确定要将「${item.title || 'AI改写'}」发布到创作中心吗?`,
confirmText: '发布',
confirmColor: '#22c55e',
cancelText: '取消',
success: async (res) => {
if (res.confirm) {
wx.showLoading({ title: '发布中...' });
const success = await this.main.userManager.publishDraft(item.id);
wx.hideLoading();
if (success) {
// 更新本地状态
this.drafts[index].publishedToCenter = true;
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' });
}
}
}
});
}
// 显示设置菜单
showSettingsMenu() {
wx.showActionSheet({
itemList: ['修改头像', '修改昵称', '退出登录'],
success: (res) => {
switch (res.tapIndex) {
case 0:
this.chooseAndUploadAvatar();
break;
case 1:
this.showEditNicknameDialog();
break;
case 2:
this.confirmLogout();
break;
}
}
});
}
// 显示头像选项
showAvatarOptions() {
wx.showActionSheet({
itemList: ['从相册选择', '取消'],
success: (res) => {
if (res.tapIndex === 0) {
this.chooseAndUploadAvatar();
}
}
});
}
// 选择并上传头像
chooseAndUploadAvatar() {
console.log('[ProfileScene] 开始选择头像');
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed'],
success: async (res) => {
console.log('[ProfileScene] 选择图片成功:', res);
const tempFilePath = res.tempFiles[0].tempFilePath;
wx.showLoading({ title: '上传中...' });
try {
// 上传图片到服务器
const uploadRes = await this.uploadAvatar(tempFilePath);
console.log('[ProfileScene] 上传结果:', uploadRes);
if (uploadRes && uploadRes.url) {
// 更新用户头像
const success = await this.main.userManager.updateProfile(
this.main.userManager.nickname,
uploadRes.url
);
wx.hideLoading();
if (success) {
// 重新加载头像
this.loadAvatarImage();
wx.showToast({ title: '头像更新成功', icon: 'success' });
} else {
wx.showToast({ title: '更新失败', icon: 'none' });
}
} else {
wx.hideLoading();
wx.showToast({ title: '上传失败', icon: 'none' });
}
} catch (error) {
wx.hideLoading();
console.error('[ProfileScene] 上传头像失败:', error);
wx.showToast({ title: '上传失败', icon: 'none' });
}
},
fail: (err) => {
console.error('[ProfileScene] 选择图片失败:', err);
if (err.errMsg && err.errMsg.indexOf('cancel') === -1) {
wx.showToast({ title: '选择图片失败', icon: 'none' });
}
}
});
}
// 上传头像到服务器
uploadAvatar(filePath) {
return new Promise((resolve, reject) => {
const token = this.main.userManager.token || '';
const baseUrl = 'http://172.20.10.8:8000'; // 与 http.js 保持一致
wx.uploadFile({
url: `${baseUrl}/api/upload/avatar`,
filePath: filePath,
name: 'file',
header: {
'Authorization': token ? `Bearer ${token}` : ''
},
success: (res) => {
try {
const data = JSON.parse(res.data);
if (data.code === 0) {
resolve(data.data);
} else {
reject(new Error(data.message || '上传失败'));
}
} catch (e) {
reject(e);
}
},
fail: reject
});
});
}
// 修改昵称弹窗
showEditNicknameDialog() {
console.log('[ProfileScene] 显示修改昵称弹窗');
const currentNickname = this.main.userManager.nickname || '';
// 微信小游戏使用 wx.showModal 的 editable 参数
wx.showModal({
title: '修改昵称',
editable: true,
placeholderText: '请输入新昵称',
success: async (res) => {
console.log('[ProfileScene] showModal 回调:', res);
if (res.confirm) {
const newNickname = (res.content || '').trim();
console.log('[ProfileScene] 新昵称:', newNickname);
if (!newNickname) {
wx.showToast({ title: '昵称不能为空', icon: 'none' });
return;
}
if (newNickname === currentNickname) {
wx.showToast({ title: '昵称未变更', icon: 'none' });
return;
}
wx.showLoading({ title: '保存中...' });
try {
const success = await this.main.userManager.updateProfile(
newNickname,
this.main.userManager.avatarUrl || ''
);
wx.hideLoading();
if (success) {
wx.showToast({ title: '修改成功', icon: 'success' });
} else {
wx.showToast({ title: '修改失败', icon: 'none' });
}
} catch (e) {
wx.hideLoading();
console.error('[ProfileScene] 修改昵称失败:', e);
wx.showToast({ title: '修改失败', icon: 'none' });
}
}
},
fail: (err) => {
console.error('[ProfileScene] showModal 失败:', err);
}
});
}
// 确认退出登录
confirmLogout() {
wx.showModal({
title: '确认退出',
content: '退出登录后需要重新授权登录,确定退出吗?',
confirmText: '退出',
confirmColor: '#e74c3c',
success: (res) => {
if (res.confirm) {
// 执行退出登录
this.main.userManager.logout();
// 停止草稿检查
this.main.stopDraftChecker();
// 跳转到登录页
this.main.sceneManager.switchScene('login');
wx.showToast({ title: '已退出登录', icon: 'success' });
}
}
});
}
}