950 lines
32 KiB
JavaScript
950 lines
32 KiB
JavaScript
/**
|
||
* 个人中心场景 - 支持创作者功能
|
||
*/
|
||
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' });
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|