feat: 游玩记录多版本功能 - 支持多版本记录存储和回放 - 相同路径自动去重只保留最新 - 版本列表支持删除功能 - AI草稿箱游玩不记录历史 - iOS日期格式兼容修复

This commit is contained in:
wangwuww111
2026-03-10 12:44:55 +08:00
parent 9948ccba8f
commit baf7dd1e2b
8 changed files with 693 additions and 35 deletions

View File

@@ -16,6 +16,11 @@ export default class ProfileScene extends BaseScene {
this.collections = [];
this.progress = [];
// 记录版本列表相关状态
this.recordViewMode = 'list'; // 'list' 故事列表 | 'versions' 版本列表
this.selectedStoryRecords = []; // 选中故事的记录列表
this.selectedStoryInfo = {}; // 选中故事的信息
// 统计
this.stats = {
works: 0,
@@ -45,7 +50,8 @@ export default class ProfileScene extends BaseScene {
// 加载 AI 改写草稿
this.drafts = await this.main.storyManager.getDrafts(userId) || [];
this.collections = await this.main.userManager.getCollections() || [];
this.progress = await this.main.userManager.getProgress() || [];
// 加载游玩记录(故事列表)
this.progress = await this.main.userManager.getPlayRecords() || [];
// 计算统计
this.stats.works = this.myWorks.length;
@@ -77,7 +83,9 @@ export default class ProfileScene extends BaseScene {
case 0: return this.myWorks;
case 1: return this.drafts;
case 2: return this.collections;
case 3: return this.progress;
case 3:
// 记录 Tab根据视图模式返回不同列表
return this.recordViewMode === 'versions' ? this.selectedStoryRecords : this.progress;
default: return [];
}
}
@@ -254,16 +262,26 @@ export default class ProfileScene extends BaseScene {
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 = ['还没有发布作品,去创作吧', '草稿箱空空如也', '还没有收藏的故事', '还没有游玩记录'];
ctx.fillText(emptyTexts[this.currentTab], this.screenWidth / 2, startY + 50);
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 = startY + 80;
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');
@@ -281,12 +299,14 @@ export default class ProfileScene extends BaseScene {
}
list.forEach((item, index) => {
const y = startY + index * (cardH + gap) - this.scrollY;
if (y > startY - cardH && y < this.screenHeight) {
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);
}
@@ -296,6 +316,112 @@ export default class ProfileScene extends BaseScene {
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)';
@@ -486,20 +612,20 @@ export default class ProfileScene extends BaseScene {
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(this.truncateText(ctx, item.story_title || item.title || '未知', w - 150), textX, y + 28);
// 记录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 && item.is_completed) {
ctx.fillStyle = '#4ade80';
ctx.fillText('✓ 已完成', textX, y + 50);
} else if (this.currentTab === 3) {
ctx.fillText('进行中...', textX, y + 50);
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');
@@ -509,7 +635,7 @@ export default class ProfileScene extends BaseScene {
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('继续', x + w - 34, y + 45);
ctx.fillText(this.currentTab === 3 ? '查看' : '继续', x + w - 34, y + 45);
}
getGradientColors(index) {
@@ -594,6 +720,7 @@ export default class ProfileScene extends BaseScene {
if (this.currentTab !== rect.index) {
this.currentTab = rect.index;
this.scrollY = 0;
this.recordViewMode = 'list'; // 切换 Tab 时重置记录视图模式
this.calculateMaxScroll();
// 切换到 AI 草稿 tab 时刷新数据
@@ -627,15 +754,29 @@ export default class ProfileScene extends BaseScene {
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 - startY) / (cardH + gap));
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 = startY + index * (cardH + gap) - this.scrollY;
const cardY = listStartY + index * (cardH + gap) - this.scrollY;
const relativeY = y - cardY;
// AI草稿 Tab 的按钮检测
@@ -670,14 +811,82 @@ export default class ProfileScene extends BaseScene {
return;
}
if (this.currentTab >= 2) {
// 收藏/记录 - 跳转播放
// 记录 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({
@@ -702,4 +911,39 @@ export default class ProfileScene extends BaseScene {
}
});
}
// 确认删除游玩记录
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' });
}
}
}
});
}
}