feat: 游玩记录支持区分原故事和AI草稿,已下架草稿显示标签
This commit is contained in:
@@ -203,6 +203,29 @@ export default class UserManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏草稿
|
||||
*/
|
||||
async collectDraft(draftId, isCollected) {
|
||||
if (!this.isLoggedIn) return;
|
||||
await put(`/drafts/${draftId}/collect`, null, {
|
||||
params: { userId: this.userId, isCollected }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取草稿收藏状态
|
||||
*/
|
||||
async getDraftCollectStatus(draftId) {
|
||||
if (!this.isLoggedIn) return false;
|
||||
try {
|
||||
const res = await get(`/drafts/${draftId}/collect-status`, { userId: this.userId });
|
||||
return res?.isCollected || false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收藏列表
|
||||
*/
|
||||
@@ -323,12 +346,13 @@ export default class UserManager {
|
||||
/**
|
||||
* 保存游玩记录
|
||||
*/
|
||||
async savePlayRecord(storyId, endingName, endingType, pathHistory) {
|
||||
async savePlayRecord(storyId, endingName, endingType, pathHistory, draftId = null) {
|
||||
if (!this.isLoggedIn) return null;
|
||||
try {
|
||||
return await post('/user/play-record', {
|
||||
userId: this.userId,
|
||||
storyId,
|
||||
draftId, // AI草稿ID,原故事为null
|
||||
endingName,
|
||||
endingType: endingType || '',
|
||||
pathHistory: pathHistory || []
|
||||
|
||||
@@ -43,9 +43,48 @@ export default class EndingScene extends BaseScene {
|
||||
this.showButtons = true;
|
||||
}, 1500);
|
||||
|
||||
// 保存游玩记录(回放模式和AI草稿不保存)
|
||||
if (!this.isReplay && !this.draftId) {
|
||||
// 保存游玩记录
|
||||
this.checkAndSavePlayRecord();
|
||||
|
||||
// 加载收藏状态
|
||||
this.loadCollectStatus();
|
||||
}
|
||||
|
||||
async checkAndSavePlayRecord() {
|
||||
// 回放模式不保存
|
||||
if (this.isReplay) return;
|
||||
|
||||
// 原故事:直接保存
|
||||
if (!this.draftId) {
|
||||
this.savePlayRecord();
|
||||
return;
|
||||
}
|
||||
|
||||
// AI草稿:检查是否已发布,已发布才保存
|
||||
try {
|
||||
const userId = this.main.userManager.userId;
|
||||
const drafts = await this.main.storyManager.getDrafts(userId) || [];
|
||||
const draft = drafts.find(d => d.id === this.draftId);
|
||||
if (draft?.publishedToCenter) {
|
||||
this.savePlayRecord();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('检查草稿发布状态失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async loadCollectStatus() {
|
||||
try {
|
||||
if (this.draftId) {
|
||||
// AI草稿:获取草稿收藏状态
|
||||
this.isCollected = await this.main.userManager.getDraftCollectStatus(this.draftId);
|
||||
} else {
|
||||
// 原故事:获取故事收藏状态
|
||||
const progress = await this.main.userManager.getProgress(this.storyId);
|
||||
this.isCollected = progress?.isCollected || false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载收藏状态失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +95,13 @@ export default class EndingScene extends BaseScene {
|
||||
const endingName = this.ending?.name || '未知结局';
|
||||
const endingType = this.ending?.type || '';
|
||||
|
||||
// 调用保存接口
|
||||
// 调用保存接口(传入 draftId 区分原故事和AI草稿)
|
||||
await this.main.userManager.savePlayRecord(
|
||||
this.storyId,
|
||||
endingName,
|
||||
endingType,
|
||||
pathHistory
|
||||
pathHistory,
|
||||
this.draftId // AI草稿ID,原故事为null
|
||||
);
|
||||
console.log('游玩记录保存成功');
|
||||
} catch (e) {
|
||||
@@ -1193,7 +1233,13 @@ export default class EndingScene extends BaseScene {
|
||||
|
||||
handleCollect() {
|
||||
this.isCollected = !this.isCollected;
|
||||
this.main.userManager.collectStory(this.storyId, this.isCollected);
|
||||
if (this.draftId) {
|
||||
// AI草稿:收藏草稿
|
||||
this.main.userManager.collectDraft(this.draftId, this.isCollected);
|
||||
} else {
|
||||
// 原故事:收藏故事
|
||||
this.main.userManager.collectStory(this.storyId, this.isCollected);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动草稿完成轮询(每5秒检查一次,持续2分钟)
|
||||
|
||||
@@ -73,18 +73,21 @@ export default class ProfileScene extends BaseScene {
|
||||
if (this.main.userManager.isLoggedIn) {
|
||||
try {
|
||||
const userId = this.main.userManager.userId;
|
||||
this.myWorks = await this.main.userManager.getMyWorks?.() || [];
|
||||
// 加载已发布到创作中心的作品(改写+续写)
|
||||
const publishedRewrites = await this.main.userManager.getPublishedDrafts('rewrite') || [];
|
||||
const publishedContinues = await this.main.userManager.getPublishedDrafts('continue') || [];
|
||||
this.myWorks = [...publishedRewrites, ...publishedContinues];
|
||||
// 加载 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);
|
||||
this.stats.totalPlays = this.myWorks.reduce((sum, w) => sum + (w.playCount || 0), 0);
|
||||
this.stats.totalLikes = this.myWorks.reduce((sum, w) => sum + (w.likeCount || 0), 0);
|
||||
this.stats.earnings = 0; // 暂无收益功能
|
||||
} catch (e) {
|
||||
console.error('加载数据失败:', e);
|
||||
}
|
||||
@@ -333,25 +336,25 @@ export default class ProfileScene extends BaseScene {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.35)';
|
||||
ctx.font = '13px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
const emptyTexts = ['还没有发布作品,去创作吧', '草稿箱空空如也', '还没有收藏的故事', '还没有游玩记录'];
|
||||
const emptyTexts = ['还没有发布作品,去草稿箱发布吧', '草稿箱空空如也', '还没有收藏的故事', '还没有游玩记录'];
|
||||
const emptyText = (this.currentTab === 3 && this.recordViewMode === 'versions')
|
||||
? '该故事还没有游玩记录'
|
||||
: emptyTexts[this.currentTab];
|
||||
ctx.fillText(emptyText, this.screenWidth / 2, listStartY + 50);
|
||||
|
||||
// 创作引导按钮
|
||||
// 作品Tab引导按钮 - 跳转到草稿箱
|
||||
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);
|
||||
this.roundRect(ctx, this.screenWidth / 2 - 55, btnY, 110, 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.fillText('前往草稿箱', this.screenWidth / 2, btnY + 23);
|
||||
this.createBtnRect = { x: this.screenWidth / 2 - 55, y: btnY, width: 110, height: 36 };
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
@@ -431,7 +434,10 @@ export default class ProfileScene extends BaseScene {
|
||||
|
||||
// 渲染单条游玩记录版本卡片
|
||||
renderRecordVersionCard(ctx, item, x, y, w, h, index) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.05)';
|
||||
const isUnpublished = item.draftId && item.isPublished === false;
|
||||
|
||||
// 已下架的卡片背景更暗
|
||||
ctx.fillStyle = isUnpublished ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.05)';
|
||||
this.roundRect(ctx, x, y, w, h, 12);
|
||||
ctx.fill();
|
||||
|
||||
@@ -441,34 +447,49 @@ export default class ProfileScene extends BaseScene {
|
||||
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]);
|
||||
circleGradient.addColorStop(0, isUnpublished ? 'rgba(128,128,128,0.5)' : colors[0]);
|
||||
circleGradient.addColorStop(1, isUnpublished ? 'rgba(96,96,96,0.5)' : colors[1]);
|
||||
ctx.fillStyle = circleGradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(circleX, circleY, circleR, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// 序号
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillStyle = isUnpublished ? 'rgba(255,255,255,0.5)' : '#ffffff';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`${index + 1}`, circleX, circleY + 5);
|
||||
|
||||
const textX = x + 65;
|
||||
const maxTextWidth = w - 200; // 留出按钮空间
|
||||
|
||||
// 结局名称
|
||||
ctx.fillStyle = '#ffffff';
|
||||
// 结局名称(只显示结局,不显示草稿标题)
|
||||
ctx.fillStyle = isUnpublished ? 'rgba(255,255,255,0.5)' : '#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.fillText(this.truncateText(ctx, endingLabel, maxTextWidth - 60), textX, y + 28);
|
||||
|
||||
// 已下架标签(固定在结局名称右边)
|
||||
if (isUnpublished) {
|
||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.3)';
|
||||
this.roundRect(ctx, x + w - 185, y + 15, 44, 18, 9);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ef4444';
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('已下架', x + w - 163, y + 27);
|
||||
}
|
||||
|
||||
// 游玩时间
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.45)';
|
||||
// 游玩时间(如果是草稿,显示草稿标题)
|
||||
ctx.fillStyle = isUnpublished ? 'rgba(255,255,255,0.25)' : '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);
|
||||
let subText = item.createdAt ? this.formatDateTime(item.createdAt) : '';
|
||||
if (item.draftTitle) {
|
||||
subText = this.truncateText(ctx, item.draftTitle, 100) + ' · ' + subText;
|
||||
}
|
||||
ctx.fillText(subText, textX, y + 52);
|
||||
|
||||
// 删除按钮
|
||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.2)';
|
||||
@@ -481,8 +502,8 @@ export default class ProfileScene extends BaseScene {
|
||||
|
||||
// 回放按钮
|
||||
const btnGradient = ctx.createLinearGradient(x + w - 68, y + 28, x + w - 10, y + 28);
|
||||
btnGradient.addColorStop(0, '#ff6b6b');
|
||||
btnGradient.addColorStop(1, '#ffd700');
|
||||
btnGradient.addColorStop(0, isUnpublished ? '#888888' : '#ff6b6b');
|
||||
btnGradient.addColorStop(1, isUnpublished ? '#666666' : '#ffd700');
|
||||
ctx.fillStyle = btnGradient;
|
||||
this.roundRect(ctx, x + w - 68, y + 28, 58, 26, 13);
|
||||
ctx.fill();
|
||||
@@ -526,10 +547,12 @@ export default class ProfileScene extends BaseScene {
|
||||
this.roundRect(ctx, x + 8, y + 8, coverW, coverH, 10);
|
||||
ctx.fill();
|
||||
|
||||
// 类型标签
|
||||
const typeText = item.draftType === 'continue' ? '续写' : '改写';
|
||||
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);
|
||||
ctx.fillText(typeText, x + 8 + coverW / 2, y + 8 + coverH / 2 + 3);
|
||||
|
||||
const textX = x + 88;
|
||||
const maxW = w - 100;
|
||||
@@ -538,46 +561,55 @@ export default class ProfileScene extends BaseScene {
|
||||
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 title = item.title || item.storyTitle || '未命名';
|
||||
ctx.fillText(this.truncateText(ctx, 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);
|
||||
// 已发布标签
|
||||
const statusText = '已发布';
|
||||
const statusW = ctx.measureText(statusText).width + 12;
|
||||
ctx.fillStyle = 'rgba(34, 197, 94, 0.2)';
|
||||
const titleWidth = ctx.measureText(this.truncateText(ctx, title, maxW - 60)).width;
|
||||
this.roundRect(ctx, textX + titleWidth + 8, y + 12, statusW, 18, 9);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = status.color;
|
||||
ctx.fillStyle = '#22c55e';
|
||||
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.textAlign = 'center';
|
||||
ctx.fillText(statusText, textX + titleWidth + 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);
|
||||
ctx.fillText(`原故事: ${item.storyTitle || ''}`, textX, y + 48);
|
||||
|
||||
// 操作按钮
|
||||
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);
|
||||
});
|
||||
// 创建时间
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.35)';
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.fillText(item.createdAt || '', textX, y + 68);
|
||||
|
||||
// 按钮区域
|
||||
const btnY = y + 55;
|
||||
|
||||
// 取消发布按钮
|
||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.2)';
|
||||
this.roundRect(ctx, x + w - 125, btnY, 60, 26, 13);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ef4444';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('取消发布', x + w - 95, btnY + 17);
|
||||
|
||||
// 播放按钮
|
||||
const btnGradient = ctx.createLinearGradient(x + w - 58, btnY, x + w - 10, btnY);
|
||||
btnGradient.addColorStop(0, '#a855f7');
|
||||
btnGradient.addColorStop(1, '#ec4899');
|
||||
ctx.fillStyle = btnGradient;
|
||||
this.roundRect(ctx, x + w - 58, btnY, 48, 26, 13);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('播放', x + w - 34, btnY + 17);
|
||||
}
|
||||
|
||||
renderDraftCard(ctx, item, x, y, w, h, index) {
|
||||
@@ -719,7 +751,7 @@ export default class ProfileScene extends BaseScene {
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
// 记录Tab使用 storyTitle,收藏Tab使用 story_title
|
||||
// 记录Tab使用 storyTitle,收藏Tab使用 storyTitle
|
||||
const title = item.storyTitle || item.story_title || item.title || '未知';
|
||||
ctx.fillText(this.truncateText(ctx, title, w - 150), textX, y + 28);
|
||||
|
||||
@@ -728,11 +760,14 @@ export default class ProfileScene extends BaseScene {
|
||||
if (this.currentTab === 3) {
|
||||
// 记录Tab:只显示记录数量
|
||||
ctx.fillText(`${item.recordCount || 0} 条记录`, textX, y + 50);
|
||||
} else if (this.currentTab === 2 && item.versionCount > 1) {
|
||||
// 收藏Tab:显示版本数量
|
||||
ctx.fillText(`${item.versionCount} 个版本`, textX, y + 50);
|
||||
} else {
|
||||
ctx.fillText(item.category || '', textX, y + 50);
|
||||
}
|
||||
|
||||
// 查看按钮(记录Tab)/ 继续按钮(收藏Tab)
|
||||
// 查看按钮(记录Tab/收藏Tab/作品Tab)
|
||||
const btnGradient = ctx.createLinearGradient(x + w - 58, y + 28, x + w - 10, y + 28);
|
||||
btnGradient.addColorStop(0, '#ff6b6b');
|
||||
btnGradient.addColorStop(1, '#ffd700');
|
||||
@@ -742,7 +777,7 @@ export default class ProfileScene extends BaseScene {
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(this.currentTab === 3 ? '查看' : '继续', x + w - 34, y + 45);
|
||||
ctx.fillText('查看', x + w - 34, y + 45);
|
||||
}
|
||||
|
||||
getGradientColors(index) {
|
||||
@@ -867,11 +902,14 @@ export default class ProfileScene extends BaseScene {
|
||||
}
|
||||
}
|
||||
|
||||
// 创作按钮
|
||||
// 前往草稿箱按钮
|
||||
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');
|
||||
// 切换到草稿箱 Tab
|
||||
this.currentTab = 1;
|
||||
this.scrollY = 0;
|
||||
this.calculateMaxScroll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -985,17 +1023,54 @@ export default class ProfileScene extends BaseScene {
|
||||
}
|
||||
|
||||
if (this.currentTab === 2) {
|
||||
// 收藏 - 跳转播放
|
||||
this.main.sceneManager.switchScene('story', { storyId });
|
||||
// 收藏 - 检查版本数量
|
||||
if (item.versionCount > 1) {
|
||||
// 多版本:弹出选择框
|
||||
this.showVersionSelector(item);
|
||||
} else if (item.versions && item.versions.length > 0) {
|
||||
// 单版本:直接播放
|
||||
const version = item.versions[0];
|
||||
if (version.draftId) {
|
||||
this.main.sceneManager.switchScene('story', { storyId: item.storyId, draftId: version.draftId });
|
||||
} else {
|
||||
this.main.sceneManager.switchScene('story', { storyId: item.storyId });
|
||||
}
|
||||
} else {
|
||||
// 兼容旧数据
|
||||
this.main.sceneManager.switchScene('story', { storyId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 作品Tab - 点击按钮进入草稿播放或取消发布
|
||||
if (this.currentTab === 0) {
|
||||
const btnY = 55;
|
||||
const btnH = 26;
|
||||
|
||||
// 检测取消发布按钮点击
|
||||
const unpublishBtnX = padding + cardW - 125;
|
||||
if (x >= unpublishBtnX && x <= unpublishBtnX + 60 && relativeY >= btnY && relativeY <= btnY + btnH) {
|
||||
this.confirmUnpublishWork(item, index);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测播放按钮点击
|
||||
const playBtnX = padding + cardW - 58;
|
||||
if (x >= playBtnX && x <= playBtnX + 48 && relativeY >= btnY && relativeY <= btnY + btnH) {
|
||||
this.main.sceneManager.switchScene('story', { storyId: item.storyId, draftId: item.id });
|
||||
return;
|
||||
}
|
||||
|
||||
// 点击卡片其他区域也进入播放
|
||||
this.main.sceneManager.switchScene('story', { storyId: item.storyId, draftId: item.id });
|
||||
}
|
||||
// 作品Tab的按钮操作需要更精确判断,暂略
|
||||
}
|
||||
}
|
||||
|
||||
// 显示故事的版本列表
|
||||
async showStoryVersions(storyItem) {
|
||||
const storyId = storyItem.story_id || storyItem.storyId || storyItem.id;
|
||||
const storyTitle = storyItem.story_title || storyItem.title || '未知故事';
|
||||
const storyTitle = storyItem.storyTitle || storyItem.story_title || storyItem.title || '未知故事';
|
||||
|
||||
try {
|
||||
wx.showLoading({ title: '加载中...' });
|
||||
@@ -1022,11 +1097,13 @@ export default class ProfileScene extends BaseScene {
|
||||
async startRecordReplay(recordItem) {
|
||||
const recordId = recordItem.id;
|
||||
const storyId = this.selectedStoryInfo.id;
|
||||
const draftId = recordItem.draftId; // AI草稿ID,原故事为null
|
||||
|
||||
// 进入故事场景,传入 playRecordId 参数
|
||||
// 进入故事场景,传入 playRecordId 参数(draftId用于回放草稿内容)
|
||||
this.main.sceneManager.switchScene('story', {
|
||||
storyId,
|
||||
playRecordId: recordId
|
||||
playRecordId: recordId,
|
||||
draftId // 回放草稿内容
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1069,8 +1146,18 @@ export default class ProfileScene extends BaseScene {
|
||||
const success = await this.main.userManager.publishDraft(item.id);
|
||||
wx.hideLoading();
|
||||
if (success) {
|
||||
// 更新本地状态
|
||||
// 更新草稿箱状态
|
||||
this.drafts[index].publishedToCenter = true;
|
||||
// 同步添加到作品列表
|
||||
this.myWorks.push({
|
||||
id: item.id,
|
||||
storyId: item.storyId,
|
||||
storyTitle: item.storyTitle,
|
||||
title: item.title,
|
||||
draftType: item.draftType || 'rewrite',
|
||||
createdAt: item.createdAt
|
||||
});
|
||||
this.stats.works = this.myWorks.length;
|
||||
wx.showToast({ title: '发布成功', icon: 'success' });
|
||||
} else {
|
||||
wx.showToast({ title: '发布失败', icon: 'none' });
|
||||
@@ -1080,6 +1167,70 @@ export default class ProfileScene extends BaseScene {
|
||||
});
|
||||
}
|
||||
|
||||
// 确认取消发布作品
|
||||
confirmUnpublishWork(item, index) {
|
||||
wx.showModal({
|
||||
title: '取消发布',
|
||||
content: `确定要将「${item.title || 'AI改写'}」从创作中心移除吗?草稿仍会保留在草稿箱中。`,
|
||||
confirmText: '取消发布',
|
||||
confirmColor: '#ef4444',
|
||||
cancelText: '返回',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
const success = await this.main.userManager.unpublishDraft(item.id);
|
||||
wx.hideLoading();
|
||||
if (success) {
|
||||
// 从作品列表中移除
|
||||
this.myWorks.splice(index, 1);
|
||||
this.stats.works = this.myWorks.length;
|
||||
this.calculateMaxScroll();
|
||||
// 同步更新草稿箱状态
|
||||
const draftIndex = this.drafts.findIndex(d => d.id === item.id);
|
||||
if (draftIndex !== -1) {
|
||||
this.drafts[draftIndex].publishedToCenter = false;
|
||||
}
|
||||
wx.showToast({ title: '已取消发布', icon: 'success' });
|
||||
} else {
|
||||
wx.showToast({ title: '操作失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示版本选择弹窗(收藏Tab用)
|
||||
showVersionSelector(item) {
|
||||
const versions = item.versions || [];
|
||||
const itemList = versions.map(v => {
|
||||
if (v.type === 'original') {
|
||||
return '原版故事';
|
||||
} else if (v.type === 'rewrite') {
|
||||
return `AI改写: ${v.title}`;
|
||||
} else if (v.type === 'continue') {
|
||||
return `AI续写: ${v.title}`;
|
||||
}
|
||||
return v.title;
|
||||
});
|
||||
|
||||
wx.showActionSheet({
|
||||
itemList,
|
||||
success: (res) => {
|
||||
const selectedVersion = versions[res.tapIndex];
|
||||
if (selectedVersion) {
|
||||
if (selectedVersion.draftId) {
|
||||
this.main.sceneManager.switchScene('story', {
|
||||
storyId: item.storyId,
|
||||
draftId: selectedVersion.draftId
|
||||
});
|
||||
} else {
|
||||
this.main.sceneManager.switchScene('story', { storyId: item.storyId });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确认删除游玩记录
|
||||
confirmDeleteRecord(item, index) {
|
||||
wx.showModal({
|
||||
|
||||
@@ -123,6 +123,7 @@ class StoryDraft(Base):
|
||||
is_read = Column(Boolean, default=False) # 用户是否已查看
|
||||
published_to_center = Column(Boolean, default=False) # 是否发布到创作中心
|
||||
draft_type = Column(String(20), default="rewrite") # 草稿类型: rewrite/continue/create
|
||||
is_collected = Column(Boolean, default=False) # 用户是否收藏
|
||||
|
||||
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||
completed_at = Column(TIMESTAMP, default=None)
|
||||
|
||||
@@ -65,6 +65,7 @@ class PlayRecord(Base):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
story_id = Column(Integer, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False)
|
||||
draft_id = Column(Integer, default=None) # AI草稿ID,原故事为空
|
||||
ending_name = Column(String(100), nullable=False) # 结局名称
|
||||
ending_type = Column(String(20), default="") # 结局类型 (good/bad/hidden/rewrite)
|
||||
path_history = Column(JSON, nullable=False) # 完整的选择路径
|
||||
|
||||
@@ -747,3 +747,42 @@ async def unpublish_draft_from_center(
|
||||
|
||||
return {"code": 0, "message": "已从创作中心移除"}
|
||||
|
||||
|
||||
@router.put("/{draft_id}/collect")
|
||||
async def collect_draft(
|
||||
draft_id: int,
|
||||
userId: int,
|
||||
isCollected: bool = True,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""收藏/取消收藏草稿"""
|
||||
await db.execute(
|
||||
update(StoryDraft)
|
||||
.where(
|
||||
StoryDraft.id == draft_id,
|
||||
StoryDraft.user_id == userId
|
||||
)
|
||||
.values(is_collected=isCollected)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return {"code": 0, "message": "收藏成功" if isCollected else "取消收藏成功"}
|
||||
|
||||
|
||||
@router.get("/{draft_id}/collect-status")
|
||||
async def get_draft_collect_status(
|
||||
draft_id: int,
|
||||
userId: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取草稿收藏状态"""
|
||||
result = await db.execute(
|
||||
select(StoryDraft.is_collected)
|
||||
.where(
|
||||
StoryDraft.id == draft_id,
|
||||
StoryDraft.user_id == userId
|
||||
)
|
||||
)
|
||||
is_collected = result.scalar_one_or_none()
|
||||
|
||||
return {"code": 0, "data": {"isCollected": is_collected or False}}
|
||||
|
||||
@@ -52,6 +52,7 @@ class CollectRequest(BaseModel):
|
||||
class PlayRecordRequest(BaseModel):
|
||||
userId: int
|
||||
storyId: int
|
||||
draftId: Optional[int] = None # AI草稿ID,原故事为空
|
||||
endingName: str
|
||||
endingType: str = ""
|
||||
pathHistory: list
|
||||
@@ -364,24 +365,79 @@ async def toggle_collect(request: CollectRequest, db: AsyncSession = Depends(get
|
||||
|
||||
@router.get("/collections")
|
||||
async def get_collections(user_id: int = Query(..., alias="userId"), db: AsyncSession = Depends(get_db)):
|
||||
"""获取收藏列表"""
|
||||
result = await db.execute(
|
||||
select(Story)
|
||||
"""获取收藏列表(包含原故事和AI改写草稿)"""
|
||||
from app.models.story import StoryDraft, DraftStatus
|
||||
|
||||
# 查询收藏的原故事
|
||||
original_result = await db.execute(
|
||||
select(Story, UserProgress.updated_at)
|
||||
.join(UserProgress, Story.id == UserProgress.story_id)
|
||||
.where(UserProgress.user_id == user_id, UserProgress.is_collected == True)
|
||||
.order_by(UserProgress.updated_at.desc())
|
||||
)
|
||||
stories = result.scalars().all()
|
||||
original_stories = original_result.all()
|
||||
|
||||
data = [{
|
||||
"id": s.id,
|
||||
"title": s.title,
|
||||
"cover_url": s.cover_url,
|
||||
"description": s.description,
|
||||
"category": s.category,
|
||||
"play_count": s.play_count,
|
||||
"like_count": s.like_count
|
||||
} for s in stories]
|
||||
# 查询收藏的草稿
|
||||
draft_result = await db.execute(
|
||||
select(StoryDraft, Story.title.label('story_title'), Story.category, Story.cover_url)
|
||||
.join(Story, StoryDraft.story_id == Story.id)
|
||||
.where(
|
||||
StoryDraft.user_id == user_id,
|
||||
StoryDraft.is_collected == True,
|
||||
StoryDraft.status == DraftStatus.completed
|
||||
)
|
||||
.order_by(StoryDraft.created_at.desc())
|
||||
)
|
||||
drafts = draft_result.all()
|
||||
|
||||
# 按 story_id 分组
|
||||
collections = {}
|
||||
|
||||
# 处理原故事
|
||||
for story, updated_at in original_stories:
|
||||
story_id = story.id
|
||||
if story_id not in collections:
|
||||
collections[story_id] = {
|
||||
"storyId": story_id,
|
||||
"storyTitle": story.title,
|
||||
"category": story.category,
|
||||
"coverUrl": story.cover_url,
|
||||
"versions": []
|
||||
}
|
||||
collections[story_id]["versions"].append({
|
||||
"type": "original",
|
||||
"id": story_id,
|
||||
"title": story.title,
|
||||
"draftId": None
|
||||
})
|
||||
|
||||
# 处理草稿
|
||||
for row in drafts:
|
||||
draft = row[0]
|
||||
story_title = row[1]
|
||||
category = row[2]
|
||||
cover_url = row[3]
|
||||
story_id = draft.story_id
|
||||
|
||||
if story_id not in collections:
|
||||
collections[story_id] = {
|
||||
"storyId": story_id,
|
||||
"storyTitle": story_title,
|
||||
"category": category,
|
||||
"coverUrl": cover_url,
|
||||
"versions": []
|
||||
}
|
||||
collections[story_id]["versions"].append({
|
||||
"type": draft.draft_type or "rewrite",
|
||||
"id": draft.id,
|
||||
"title": draft.title or f"{story_title}-{draft.draft_type}",
|
||||
"draftId": draft.id
|
||||
})
|
||||
|
||||
# 转换为列表,添加版本数量
|
||||
data = []
|
||||
for item in collections.values():
|
||||
item["versionCount"] = len(item["versions"])
|
||||
data.append(item)
|
||||
|
||||
return {"code": 0, "data": data}
|
||||
|
||||
@@ -511,11 +567,17 @@ async def save_play_record(request: PlayRecordRequest, db: AsyncSession = Depend
|
||||
"""保存游玩记录(相同路径只保留最新)"""
|
||||
import json
|
||||
|
||||
# 查找该用户该故事的所有记录
|
||||
result = await db.execute(
|
||||
select(PlayRecord)
|
||||
.where(PlayRecord.user_id == request.userId, PlayRecord.story_id == request.storyId)
|
||||
# 查找该用户该故事的所有记录(区分原故事和草稿)
|
||||
query = select(PlayRecord).where(
|
||||
PlayRecord.user_id == request.userId,
|
||||
PlayRecord.story_id == request.storyId
|
||||
)
|
||||
if request.draftId:
|
||||
query = query.where(PlayRecord.draft_id == request.draftId)
|
||||
else:
|
||||
query = query.where(PlayRecord.draft_id == None)
|
||||
|
||||
result = await db.execute(query)
|
||||
existing_records = result.scalars().all()
|
||||
|
||||
# 检查是否有相同路径的记录
|
||||
@@ -530,6 +592,7 @@ async def save_play_record(request: PlayRecordRequest, db: AsyncSession = Depend
|
||||
record = PlayRecord(
|
||||
user_id=request.userId,
|
||||
story_id=request.storyId,
|
||||
draft_id=request.draftId,
|
||||
ending_name=request.endingName,
|
||||
ending_type=request.endingType,
|
||||
path_history=request.pathHistory
|
||||
@@ -554,8 +617,10 @@ async def get_play_records(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取游玩记录列表"""
|
||||
from app.models.story import StoryDraft
|
||||
|
||||
if story_id:
|
||||
# 获取指定故事的记录
|
||||
# 获取指定故事的记录(包含草稿发布状态)
|
||||
result = await db.execute(
|
||||
select(PlayRecord)
|
||||
.where(PlayRecord.user_id == user_id, PlayRecord.story_id == story_id)
|
||||
@@ -563,12 +628,33 @@ async def get_play_records(
|
||||
)
|
||||
records = result.scalars().all()
|
||||
|
||||
data = [{
|
||||
"id": r.id,
|
||||
"endingName": r.ending_name,
|
||||
"endingType": r.ending_type,
|
||||
"createdAt": r.created_at.strftime("%Y-%m-%d %H:%M") if r.created_at else ""
|
||||
} for r in records]
|
||||
# 获取相关草稿的发布状态
|
||||
draft_ids = [r.draft_id for r in records if r.draft_id]
|
||||
draft_status = {}
|
||||
if draft_ids:
|
||||
draft_result = await db.execute(
|
||||
select(StoryDraft.id, StoryDraft.published_to_center, StoryDraft.title)
|
||||
.where(StoryDraft.id.in_(draft_ids))
|
||||
)
|
||||
for d in draft_result.all():
|
||||
draft_status[d.id] = {"published": d.published_to_center, "title": d.title}
|
||||
|
||||
data = []
|
||||
for r in records:
|
||||
item = {
|
||||
"id": r.id,
|
||||
"draftId": r.draft_id,
|
||||
"endingName": r.ending_name,
|
||||
"endingType": r.ending_type,
|
||||
"createdAt": r.created_at.strftime("%Y-%m-%d %H:%M") if r.created_at else ""
|
||||
}
|
||||
# 如果是草稿记录,添加发布状态
|
||||
if r.draft_id and r.draft_id in draft_status:
|
||||
item["isPublished"] = draft_status[r.draft_id]["published"]
|
||||
item["draftTitle"] = draft_status[r.draft_id]["title"]
|
||||
else:
|
||||
item["isPublished"] = True # 原故事视为"已发布"
|
||||
data.append(item)
|
||||
else:
|
||||
# 获取所有玩过的故事(按故事分组,取最新一条)
|
||||
result = await db.execute(
|
||||
|
||||
@@ -148,6 +148,7 @@ CREATE TABLE IF NOT EXISTS `story_drafts` (
|
||||
`is_read` TINYINT(1) DEFAULT 0 COMMENT '用户是否已查看',
|
||||
`published_to_center` TINYINT(1) DEFAULT 0 COMMENT '是否发布到创作中心',
|
||||
`draft_type` VARCHAR(20) DEFAULT 'rewrite' COMMENT '草稿类型: rewrite/continue/create',
|
||||
`is_collected` TINYINT(1) DEFAULT 0 COMMENT '用户是否收藏',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`completed_at` TIMESTAMP NULL DEFAULT NULL COMMENT '完成时间',
|
||||
PRIMARY KEY (`id`),
|
||||
@@ -166,6 +167,7 @@ CREATE TABLE IF NOT EXISTS `play_records` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL COMMENT '用户ID',
|
||||
`story_id` INT NOT NULL COMMENT '故事ID',
|
||||
`draft_id` INT DEFAULT NULL COMMENT 'AI草稿ID,原故事为空',
|
||||
`ending_name` VARCHAR(100) NOT NULL COMMENT '结局名称',
|
||||
`ending_type` VARCHAR(20) DEFAULT '' COMMENT '结局类型',
|
||||
`path_history` JSON NOT NULL COMMENT '完整的选择路径',
|
||||
|
||||
Reference in New Issue
Block a user