Files
ai_game/client/js/scenes/AICreateScene.js

1261 lines
39 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.

/**
* AI创作中心场景
*/
import BaseScene from './BaseScene';
import { get, post } from '../utils/http';
export default class AICreateScene extends BaseScene {
constructor(main, params) {
super(main, params);
this.currentTab = 0; // 0:我的改写 1:我的续写 2:AI创作
this.tabs = ['我的改写', '我的续写', 'AI创作'];
// 滚动
this.scrollY = 0;
this.maxScrollY = 0;
this.isDragging = false;
this.lastTouchY = 0;
this.hasMoved = false;
// 用户数据
this.publishedRewrites = []; // 已发布的改写作品
this.publishedContinues = []; // 已发布的续写作品
this.quota = { daily: 3, used: 0, purchased: 0 };
// 创作表单
this.createForm = {
genre: '',
keywords: '',
protagonist: '',
conflict: ''
};
// 快捷标签
this.genreTags = ['都市言情', '古风宫廷', '悬疑推理', '校园青春', '修仙玄幻', '职场商战'];
// 创作确认面板
this.showCreatePanel = false;
this.createPanelBtns = {};
}
async init() {
await this.loadData();
}
async loadData() {
try {
const userId = this.main.userManager.userId;
if (!userId) return;
// 加载已发布的改写作品
const rewriteRes = await this.main.userManager.getPublishedDrafts('rewrite');
this.publishedRewrites = rewriteRes || [];
// 加载已发布的续写作品
const continueRes = await this.main.userManager.getPublishedDrafts('continue');
this.publishedContinues = continueRes || [];
// 加载配额
const quotaData = await this.main.userManager.getAIQuota();
if (quotaData) this.quota = quotaData;
} catch (e) {
console.error('加载数据失败:', e);
}
this.calculateMaxScroll();
}
calculateMaxScroll() {
let contentHeight = 400;
if (this.currentTab === 0) {
contentHeight = 300 + this.publishedRewrites.length * 90;
} else if (this.currentTab === 1) {
contentHeight = 300 + this.publishedContinues.length * 90;
} else {
// AI创作Tab表单高度 + 已创作列表高度
const formHeight = 500;
const listHeight = this.createdStories ? this.createdStories.length * 90 : 0;
contentHeight = formHeight + listHeight + 100;
}
this.maxScrollY = Math.max(0, contentHeight - this.screenHeight + 200);
}
update() {}
render(ctx) {
this.renderBackground(ctx);
this.renderHeader(ctx);
this.renderQuotaBar(ctx);
this.renderTabs(ctx);
this.renderContent(ctx);
// 创作确认面板(最上层)
if (this.showCreatePanel) {
this.renderCreatePanel(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);
// 装饰光效
const glow = ctx.createRadialGradient(this.screenWidth / 2, 150, 0, this.screenWidth / 2, 150, 200);
glow.addColorStop(0, 'rgba(168, 85, 247, 0.15)');
glow.addColorStop(1, 'transparent');
ctx.fillStyle = glow;
ctx.fillRect(0, 0, this.screenWidth, 300);
}
renderHeader(ctx) {
// 顶部遮罩
const headerGradient = ctx.createLinearGradient(0, 0, 0, 80);
headerGradient.addColorStop(0, 'rgba(15,12,41,1)');
headerGradient.addColorStop(1, 'rgba(15,12,41,0)');
ctx.fillStyle = headerGradient;
ctx.fillRect(0, 0, this.screenWidth, 80);
// 返回按钮
ctx.fillStyle = '#ffffff';
ctx.font = '16px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(' 返回', 15, 40);
// 标题
ctx.textAlign = 'center';
ctx.font = 'bold 18px sans-serif';
const titleGradient = ctx.createLinearGradient(this.screenWidth / 2 - 50, 0, this.screenWidth / 2 + 50, 0);
titleGradient.addColorStop(0, '#a855f7');
titleGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = titleGradient;
ctx.fillText('✨ AI创作中心', this.screenWidth / 2, 40);
}
renderQuotaBar(ctx) {
const barY = 60;
const remaining = this.quota.daily - this.quota.used + this.quota.purchased;
// 配额背景
ctx.fillStyle = 'rgba(255,255,255,0.08)';
this.roundRect(ctx, 15, barY, this.screenWidth - 30, 36, 18);
ctx.fill();
// 配额文字
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`今日剩余: ${remaining}`, 30, barY + 23);
// 充值按钮
const btnWidth = 70;
const btnX = this.screenWidth - 30 - btnWidth;
const btnGradient = ctx.createLinearGradient(btnX, barY + 5, btnX + btnWidth, barY + 5);
btnGradient.addColorStop(0, '#a855f7');
btnGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, btnX, barY + 5, btnWidth, 26, 13);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('获取更多', btnX + btnWidth / 2, barY + 22);
this.quotaBtnRect = { x: btnX, y: barY + 5, width: btnWidth, height: 26 };
}
renderTabs(ctx) {
const tabY = 110;
const tabWidth = (this.screenWidth - 40) / 3;
const padding = 15;
this.tabRects = [];
this.tabs.forEach((tab, index) => {
const x = padding + index * (tabWidth + 5);
const isActive = index === this.currentTab;
if (isActive) {
const gradient = ctx.createLinearGradient(x, tabY, x + tabWidth, tabY);
gradient.addColorStop(0, '#a855f7');
gradient.addColorStop(1, '#ec4899');
ctx.fillStyle = gradient;
} else {
ctx.fillStyle = 'rgba(255,255,255,0.1)';
}
this.roundRect(ctx, x, tabY, tabWidth, 36, 18);
ctx.fill();
ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.6)';
ctx.font = isActive ? 'bold 13px sans-serif' : '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(tab, x + tabWidth / 2, tabY + 23);
this.tabRects.push({ x, y: tabY, width: tabWidth, height: 36, index });
});
}
renderContent(ctx) {
const contentY = 160;
ctx.save();
ctx.beginPath();
ctx.rect(0, contentY, this.screenWidth, this.screenHeight - contentY);
ctx.clip();
switch (this.currentTab) {
case 0:
this.renderRewriteTab(ctx, contentY);
break;
case 1:
this.renderContinueTab(ctx, contentY);
break;
case 2:
this.renderCreateTab(ctx, contentY);
break;
}
ctx.restore();
}
renderRewriteTab(ctx, startY) {
const y = startY - this.scrollY;
const padding = 15;
// 说明文字
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('展示你从草稿箱发布的改写作品', this.screenWidth / 2, y + 25);
// 作品列表
this.renderPublishedList(ctx, y + 50, this.publishedRewrites, 'rewrite');
}
renderContinueTab(ctx, startY) {
const y = startY - this.scrollY;
const padding = 15;
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('展示你从草稿箱发布的续写作品', this.screenWidth / 2, y + 25);
// 作品列表
this.renderPublishedList(ctx, y + 50, this.publishedContinues, 'continue');
}
renderCreateTab(ctx, startY) {
const y = startY - this.scrollY;
const padding = 15;
const inputWidth = this.screenWidth - padding * 2;
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('输入关键词AI为你创作全新故事', this.screenWidth / 2, y + 25);
// 题材选择
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('选择题材:', padding, y + 55);
const tagEndY = this.renderTags(ctx, this.genreTags, padding, y + 70, 'genre');
// 关键词输入
let currentY = tagEndY + 25;
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '13px sans-serif';
ctx.fillText('故事关键词:', padding, currentY);
this.renderInputBox(ctx, padding, currentY + 15, inputWidth, 45, this.createForm.keywords || '例如:霸总、契约婚姻、追妻火葬场', 'keywords');
// 主角设定
currentY += 80;
ctx.textAlign = 'left';
ctx.fillText('主角设定:', padding, currentY);
this.renderInputBox(ctx, padding, currentY + 15, inputWidth, 45, this.createForm.protagonist || '例如:独立女性设计师', 'protagonist');
// 核心冲突
currentY += 80;
ctx.textAlign = 'left';
ctx.fillText('核心冲突:', padding, currentY);
this.renderInputBox(ctx, padding, currentY + 15, inputWidth, 45, this.createForm.conflict || '例如:假结婚变真爱', 'conflict');
// 开始创作按钮
const btnY = currentY + 85;
const btnGradient = ctx.createLinearGradient(padding, btnY, this.screenWidth - padding, btnY);
btnGradient.addColorStop(0, '#a855f7');
btnGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, padding, btnY, inputWidth, 50, 25);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('✨ 开始AI创作', this.screenWidth / 2, btnY + 32);
this.createBtnRect = { x: padding, y: btnY + this.scrollY, width: inputWidth, height: 50 };
// 提示文字
const tipY = btnY + 75;
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('创作完成后可在「个人中心 > 草稿箱」查看', this.screenWidth / 2, tipY);
}
renderCreatedList(ctx, startY, list) {
const padding = 15;
const cardWidth = this.screenWidth - padding * 2;
const cardHeight = 80;
const cardGap = 10;
this.createdItemRects = [];
list.forEach((item, index) => {
const y = startY + index * (cardHeight + cardGap);
// 卡片背景
ctx.fillStyle = 'rgba(255,255,255,0.08)';
this.roundRect(ctx, padding, y, cardWidth, cardHeight, 12);
ctx.fill();
// 标题
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
const title = item.title || '未命名故事';
ctx.fillText(title.length > 15 ? title.substring(0, 15) + '...' : title, padding + 15, y + 25);
// 状态标签
const isPending = item.status === 'pending';
const isFailed = item.status === 'failed';
const isCompleted = item.status === 'completed';
const isPublished = item.published_to_center;
let statusText = '草稿';
let statusColor = '#fbbf24';
if (isPublished) {
statusText = '已发布';
statusColor = '#10b981';
} else if (isCompleted) {
statusText = '已完成';
statusColor = '#60a5fa';
} else if (isPending) {
statusText = '创作中...';
statusColor = '#a855f7';
} else if (isFailed) {
statusText = '失败';
statusColor = '#ef4444';
}
ctx.fillStyle = statusColor;
ctx.font = '12px sans-serif';
ctx.fillText(statusText, padding + 15, y + 50);
// 按钮(只有完成状态才能操作)
if (isCompleted) {
const btnWidth = 50;
const btnHeight = 28;
const btnGap = 8;
let btnX = this.screenWidth - padding - btnWidth - 10;
const btnY = y + (cardHeight - btnHeight) / 2;
// 阅读按钮
const readGradient = ctx.createLinearGradient(btnX, btnY, btnX + btnWidth, btnY);
readGradient.addColorStop(0, '#a855f7');
readGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = readGradient;
this.roundRect(ctx, btnX, btnY, btnWidth, btnHeight, 14);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('阅读', btnX + btnWidth / 2, btnY + 18);
this.createdItemRects.push({
x: btnX,
y: btnY + this.scrollY,
width: btnWidth,
height: btnHeight,
action: 'preview',
item: item
});
// 发布按钮(未发布时显示)
if (!isPublished) {
btnX = btnX - btnWidth - btnGap;
const pubGradient = ctx.createLinearGradient(btnX, btnY, btnX + btnWidth, btnY);
pubGradient.addColorStop(0, '#10b981');
pubGradient.addColorStop(1, '#059669');
ctx.fillStyle = pubGradient;
this.roundRect(ctx, btnX, btnY, btnWidth, btnHeight, 14);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('发布', btnX + btnWidth / 2, btnY + 18);
this.createdItemRects.push({
x: btnX,
y: btnY + this.scrollY,
width: btnWidth,
height: btnHeight,
action: 'publish',
item: item
});
}
}
});
}
renderTags(ctx, tags, startX, startY, type) {
const tagHeight = 30;
const tagGap = 8;
let currentX = startX;
let currentY = startY;
let maxY = startY + tagHeight; // 记录最大Y坐标
if (!this.tagRects) this.tagRects = {};
this.tagRects[type] = [];
tags.forEach((tag, index) => {
ctx.font = '12px sans-serif';
const tagWidth = ctx.measureText(tag).width + 20;
if (currentX + tagWidth > this.screenWidth - 15) {
currentX = startX;
currentY += tagHeight + tagGap;
maxY = currentY + tagHeight;
}
const isSelected = (type === 'genre' && this.createForm.genre === tag) ||
(type === 'rewrite' && this.selectedRewriteTag === index) ||
(type === 'continue' && this.selectedContinueTag === index);
if (isSelected) {
const gradient = ctx.createLinearGradient(currentX, currentY, currentX + tagWidth, currentY);
gradient.addColorStop(0, '#a855f7');
gradient.addColorStop(1, '#ec4899');
ctx.fillStyle = gradient;
} else {
ctx.fillStyle = 'rgba(255,255,255,0.1)';
}
this.roundRect(ctx, currentX, currentY, tagWidth, tagHeight, 15);
ctx.fill();
if (!isSelected) {
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 1;
this.roundRect(ctx, currentX, currentY, tagWidth, tagHeight, 15);
ctx.stroke();
}
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.fillText(tag, currentX + tagWidth / 2, currentY + 20);
this.tagRects[type].push({
x: currentX,
y: currentY + this.scrollY,
width: tagWidth,
height: tagHeight,
index,
value: tag
});
currentX += tagWidth + tagGap;
});
return maxY; // 返回标签区域结束的Y坐标
}
renderInputBox(ctx, x, y, width, height, placeholder, field) {
ctx.fillStyle = 'rgba(255,255,255,0.08)';
this.roundRect(ctx, x, y, width, height, 12);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 1;
this.roundRect(ctx, x, y, width, height, 12);
ctx.stroke();
ctx.font = '13px sans-serif';
ctx.textAlign = 'left';
if (this.createForm[field]) {
ctx.fillStyle = '#ffffff';
ctx.fillText(this.createForm[field], x + 15, y + height / 2 + 5);
} else {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.fillText(placeholder, x + 15, y + height / 2 + 5);
}
if (!this.inputRects) this.inputRects = {};
this.inputRects[field] = { x, y: y + this.scrollY, width, height, field };
}
renderPublishedList(ctx, startY, items, type) {
const padding = 15;
const cardHeight = 80;
const cardGap = 12;
if (!this.publishedRects) this.publishedRects = {};
this.publishedRects[type] = [];
if (!items || items.length === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
const tipText = type === 'rewrite'
? '暂无改写作品,去草稿箱发布吧'
: '暂无续写作品,去草稿箱发布吧';
ctx.fillText(tipText, this.screenWidth / 2, startY + 40);
// 跳转草稿箱按钮
const btnY = startY + 70;
const btnWidth = 120;
const btnX = (this.screenWidth - btnWidth) / 2;
ctx.fillStyle = 'rgba(168, 85, 247, 0.3)';
this.roundRect(ctx, btnX, btnY, btnWidth, 36, 18);
ctx.fill();
ctx.fillStyle = '#a855f7';
ctx.font = '13px sans-serif';
ctx.fillText('前往草稿箱', this.screenWidth / 2, btnY + 24);
this.gotoDraftsBtnRect = { x: btnX, y: btnY + this.scrollY, width: btnWidth, height: 36 };
return;
}
items.forEach((item, index) => {
const y = startY + index * (cardHeight + cardGap);
// 卡片背景
ctx.fillStyle = 'rgba(255,255,255,0.06)';
this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, cardHeight, 12);
ctx.fill();
// 标题
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
const title = item.title?.length > 15 ? item.title.substring(0, 15) + '...' : (item.title || '未命名作品');
ctx.fillText(title, padding + 15, y + 25);
// 原故事
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '11px sans-serif';
ctx.fillText(`原故事:${item.storyTitle || '未知'}`, padding + 15, y + 45);
// 创作时间
ctx.fillText(item.createdAt || '', padding + 15, y + 65);
// 阅读按钮
const btnX = this.screenWidth - padding - 70;
const btnGradient = ctx.createLinearGradient(btnX, y + 25, btnX + 60, y + 25);
btnGradient.addColorStop(0, '#a855f7');
btnGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, btnX, y + 25, 60, 30, 15);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('阅读', btnX + 30, y + 45);
this.publishedRects[type].push({
x: padding,
y: y + this.scrollY,
width: this.screenWidth - padding * 2,
height: cardHeight,
item,
btnRect: { x: btnX, y: y + 25 + this.scrollY, width: 60, height: 30 }
});
});
}
renderStoryList(ctx, startY, type) {
const padding = 15;
const cardHeight = 70;
const cardGap = 10;
if (!this.storyRects) this.storyRects = {};
this.storyRects[type] = [];
if (this.recentStories.length === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('暂无游玩记录,去首页体验故事吧', this.screenWidth / 2, startY + 40);
return;
}
this.recentStories.forEach((story, index) => {
const y = startY + index * (cardHeight + cardGap);
const isSelected = this.selectedStory?.id === story.id;
// 卡片背景
if (isSelected) {
ctx.fillStyle = 'rgba(168, 85, 247, 0.2)';
} else {
ctx.fillStyle = 'rgba(255,255,255,0.06)';
}
this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, cardHeight, 12);
ctx.fill();
if (isSelected) {
ctx.strokeStyle = '#a855f7';
ctx.lineWidth = 2;
this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, cardHeight, 12);
ctx.stroke();
}
// 封面占位
const coverSize = 50;
ctx.fillStyle = 'rgba(255,255,255,0.1)';
this.roundRect(ctx, padding + 10, y + 10, coverSize, coverSize, 8);
ctx.fill();
// 故事标题
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
const title = story.title?.length > 12 ? story.title.substring(0, 12) + '...' : story.title;
ctx.fillText(title || '未知故事', padding + 70, y + 28);
// 分类和进度
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '11px sans-serif';
ctx.fillText(`${story.category || '未分类'} · ${story.progress || '进行中'}`, padding + 70, y + 50);
// 选择按钮
const btnX = this.screenWidth - padding - 60;
ctx.fillStyle = isSelected ? '#a855f7' : 'rgba(255,255,255,0.2)';
this.roundRect(ctx, btnX, y + 20, 50, 30, 15);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(isSelected ? '已选' : '选择', btnX + 25, y + 40);
this.storyRects[type].push({
x: padding,
y: y + this.scrollY,
width: this.screenWidth - padding * 2,
height: cardHeight,
story
});
});
// 开始按钮
if (this.selectedStory) {
const btnY = startY + this.recentStories.length * (cardHeight + cardGap) + 20;
const btnGradient = ctx.createLinearGradient(padding, btnY, this.screenWidth - padding, btnY);
btnGradient.addColorStop(0, '#a855f7');
btnGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, padding, btnY, this.screenWidth - padding * 2, 48, 24);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 15px sans-serif';
ctx.textAlign = 'center';
const btnText = type === 'rewrite' ? '✨ 开始AI改写' : '✨ 开始AI续写';
ctx.fillText(btnText, this.screenWidth / 2, btnY + 30);
this.actionBtnRect = {
x: padding,
y: btnY + this.scrollY,
width: this.screenWidth - padding * 2,
height: 48,
type
};
}
}
renderCreatePanel(ctx) {
const padding = 20;
const panelWidth = this.screenWidth - padding * 2;
const panelHeight = 380;
const panelX = padding;
const panelY = (this.screenHeight - panelHeight) / 2;
// 遮罩层
ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
// 面板背景渐变
const panelGradient = ctx.createLinearGradient(panelX, panelY, panelX, panelY + panelHeight);
panelGradient.addColorStop(0, '#1a1a3e');
panelGradient.addColorStop(1, '#0d0d1a');
ctx.fillStyle = panelGradient;
this.roundRect(ctx, panelX, panelY, panelWidth, panelHeight, 20);
ctx.fill();
// 面板边框渐变
const borderGradient = ctx.createLinearGradient(panelX, panelY, panelX + panelWidth, panelY);
borderGradient.addColorStop(0, '#a855f7');
borderGradient.addColorStop(1, '#ec4899');
ctx.strokeStyle = borderGradient;
ctx.lineWidth = 2;
this.roundRect(ctx, panelX, panelY, panelWidth, panelHeight, 20);
ctx.stroke();
// 标题栏
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('✨ 确认创作', this.screenWidth / 2, panelY + 35);
// 配额提示
const remaining = this.quota.daily - this.quota.used + this.quota.purchased;
ctx.fillStyle = remaining > 0 ? 'rgba(255,255,255,0.6)' : 'rgba(255,100,100,0.8)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(`剩余次数:${remaining}`, panelX + panelWidth - 15, panelY + 35);
// 分隔线
const lineGradient = ctx.createLinearGradient(panelX + 20, panelY + 55, panelX + panelWidth - 20, panelY + 55);
lineGradient.addColorStop(0, 'transparent');
lineGradient.addColorStop(0.5, 'rgba(168,85,247,0.5)');
lineGradient.addColorStop(1, 'transparent');
ctx.strokeStyle = lineGradient;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(panelX + 20, panelY + 55);
ctx.lineTo(panelX + panelWidth - 20, panelY + 55);
ctx.stroke();
// 创作信息展示
let infoY = panelY + 85;
const lineHeight = 45;
const items = [
{ label: '题材', value: this.createForm.genre || '未选择' },
{ label: '关键词', value: this.createForm.keywords || '未填写' },
{ label: '主角设定', value: this.createForm.protagonist || '未填写' },
{ label: '核心冲突', value: this.createForm.conflict || '未填写' }
];
items.forEach((item, index) => {
const y = infoY + index * lineHeight;
// 标签
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '13px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(item.label + '', panelX + 20, y);
// 值
ctx.fillStyle = '#ffffff';
ctx.font = '14px sans-serif';
let displayValue = item.value;
if (displayValue.length > 18) {
displayValue = displayValue.substring(0, 18) + '...';
}
ctx.fillText(displayValue, panelX + 85, y);
});
// 消耗提示
ctx.fillStyle = 'rgba(255,200,100,0.8)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('将消耗 1 次 AI 次数', this.screenWidth / 2, panelY + panelHeight - 85);
// 按钮区域
const btnWidth = (panelWidth - 50) / 2;
const btnHeight = 42;
const btnY = panelY + panelHeight - 60;
// 取消按钮
const cancelX = panelX + 15;
ctx.fillStyle = 'rgba(255,255,255,0.1)';
this.roundRect(ctx, cancelX, btnY, btnWidth, btnHeight, 21);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
this.roundRect(ctx, cancelX, btnY, btnWidth, btnHeight, 21);
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('取消', cancelX + btnWidth / 2, btnY + 27);
this.createPanelBtns.cancel = { x: cancelX, y: btnY, width: btnWidth, height: btnHeight };
// 确认按钮
const confirmX = panelX + panelWidth - btnWidth - 15;
const confirmGradient = ctx.createLinearGradient(confirmX, btnY, confirmX + btnWidth, btnY);
confirmGradient.addColorStop(0, '#a855f7');
confirmGradient.addColorStop(1, '#ec4899');
ctx.fillStyle = confirmGradient;
this.roundRect(ctx, confirmX, btnY, btnWidth, btnHeight, 21);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('开始创作', confirmX + btnWidth / 2, btnY + 27);
this.createPanelBtns.confirm = { x: confirmX, y: btnY, width: btnWidth, height: btnHeight };
}
roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
onTouchStart(e) {
const touch = e.touches[0];
this.lastTouchY = touch.clientY;
this.touchStartY = touch.clientY;
this.hasMoved = false;
if (touch.clientY > 160) {
this.isDragging = true;
}
}
onTouchMove(e) {
if (!this.isDragging) return;
const touch = e.touches[0];
const deltaY = this.lastTouchY - touch.clientY;
if (Math.abs(deltaY) > 3) {
this.hasMoved = true;
}
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 (this.showCreatePanel) {
this.handleCreatePanelTouch(x, y);
return;
}
// 返回按钮
if (y < 60 && x < 80) {
this.main.sceneManager.switchScene('home');
return;
}
// 配额按钮
if (this.quotaBtnRect && this.isInRect(x, y, this.quotaBtnRect)) {
this.showQuotaModal();
return;
}
// Tab切换
if (this.tabRects) {
for (const tab of this.tabRects) {
if (this.isInRect(x, y, tab)) {
if (this.currentTab !== tab.index) {
this.currentTab = tab.index;
this.scrollY = 0;
this.calculateMaxScroll();
}
return;
}
}
}
// 调整y坐标考虑滚动
const scrolledY = y + this.scrollY;
// 前往草稿箱按钮
if (this.gotoDraftsBtnRect && this.isInRect(x, scrolledY, this.gotoDraftsBtnRect)) {
this.main.sceneManager.switchScene('drafts');
return;
}
// 已发布作品点击(改写/续写Tab
if (this.currentTab < 2 && this.publishedRects) {
const type = this.currentTab === 0 ? 'rewrite' : 'continue';
const items = this.publishedRects[type];
if (items) {
for (const rect of items) {
// 阅读按钮点击
if (this.isInRect(x, scrolledY, rect.btnRect)) {
this.handleReadPublished(rect.item);
return;
}
}
}
}
// 标签点击只有创作Tab有标签
if (this.currentTab === 2 && this.tagRects) {
const tags = this.tagRects['genre'];
if (tags) {
for (const tag of tags) {
if (this.isInRect(x, scrolledY, tag)) {
this.handleTagSelect('genre', tag);
return;
}
}
}
}
// 输入框点击创作Tab
if (this.currentTab === 2 && this.inputRects) {
for (const key in this.inputRects) {
const rect = this.inputRects[key];
if (this.isInRect(x, scrolledY, rect)) {
this.showInputModal(rect.field);
return;
}
}
}
// 创作按钮
if (this.currentTab === 2 && this.createBtnRect && this.isInRect(x, scrolledY, this.createBtnRect)) {
this.handleCreate();
return;
}
}
handleReadPublished(item) {
// 跳转到故事场景播放AI改写/续写的内容
this.main.sceneManager.switchScene('story', {
storyId: item.storyId,
draftId: item.id,
fromDrafts: true
});
}
handlePreviewCreated(item) {
// 跳转到故事场景播放AI创作的故事使用 draftId
this.main.sceneManager.switchScene('story', {
storyId: item.story_id,
draftId: item.id,
fromDrafts: true,
draftType: 'create' // 标记为AI创作类型
});
}
async handlePublishCreated(item) {
wx.showModal({
title: '确认发布',
content: `确定要发布《${item.title || '未命名故事'}》吗?\n发布后可在"我的作品"中查看`,
success: async (res) => {
if (res.confirm) {
wx.showLoading({ title: '发布中...', mask: true });
try {
const result = await post(`/stories/ai-create/${item.id}/publish`);
wx.hideLoading();
if (result && result.code === 0) {
wx.showToast({ title: '发布成功!', icon: 'success' });
// 刷新列表
this.loadData();
this.render();
} else {
wx.showToast({ title: result?.data?.message || '发布失败', icon: 'none' });
}
} catch (e) {
wx.hideLoading();
wx.showToast({ title: '发布失败', icon: 'none' });
}
}
}
});
}
isInRect(x, y, rect) {
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
}
handleTagSelect(type, tag) {
if (type === 'genre') {
this.createForm.genre = tag.value;
}
}
showInputModal(field) {
const titles = {
keywords: '输入故事关键词',
protagonist: '输入主角设定',
conflict: '输入核心冲突'
};
wx.showModal({
title: titles[field],
editable: true,
placeholderText: '请输入...',
content: this.createForm[field] || '',
success: (res) => {
if (res.confirm && res.content) {
this.createForm[field] = res.content;
}
}
});
}
showQuotaModal() {
wx.showModal({
title: 'AI创作次数',
content: `今日剩余${this.quota.daily - this.quota.used}\n购买次数${this.quota.purchased}\n\n观看广告可获得1次`,
confirmText: '看广告',
success: (res) => {
if (res.confirm) {
this.watchAdForQuota();
}
}
});
}
watchAdForQuota() {
wx.showToast({ title: '获得1次AI次数', icon: 'success' });
this.quota.purchased += 1;
}
handleAction(type) {
if (!this.selectedStory) {
wx.showToast({ title: '请先选择故事', icon: 'none' });
return;
}
const remaining = this.quota.daily - this.quota.used + this.quota.purchased;
if (remaining <= 0) {
this.showQuotaModal();
return;
}
if (type === 'rewrite') {
this.startRewrite();
} else {
this.startContinue();
}
}
startRewrite() {
const tag = this.selectedRewriteTag !== undefined ? this.rewriteTags[this.selectedRewriteTag] : '';
wx.showModal({
title: 'AI改写',
content: '确定要改写这个故事的结局吗?',
editable: true,
placeholderText: tag || '输入改写方向(可选)',
success: async (res) => {
if (res.confirm) {
wx.showLoading({ title: 'AI创作中...' });
try {
const result = await this.main.storyManager.rewriteEnding(
this.selectedStory.id,
{ name: '当前结局', content: '' },
res.content || tag || '改写结局'
);
wx.hideLoading();
if (result) {
this.quota.used += 1;
this.main.sceneManager.switchScene('story', {
storyId: this.selectedStory.id,
aiContent: result
});
}
} catch (e) {
wx.hideLoading();
wx.showToast({ title: '创作失败', icon: 'none' });
}
}
}
});
}
startContinue() {
const tag = this.selectedContinueTag !== undefined ? this.continueTags[this.selectedContinueTag] : '';
wx.showModal({
title: 'AI续写',
content: '确定要让AI续写这个故事吗',
editable: true,
placeholderText: tag || '输入续写方向(可选)',
success: async (res) => {
if (res.confirm) {
wx.showLoading({ title: 'AI创作中...' });
try {
// TODO: 实现续写API
const result = await this.main.storyManager.continueStory(
this.selectedStory.id,
res.content || tag || '续写剧情'
);
wx.hideLoading();
if (result) {
this.quota.used += 1;
this.main.sceneManager.switchScene('story', {
storyId: this.selectedStory.id,
aiContent: result
});
}
} catch (e) {
wx.hideLoading();
wx.showToast({ title: '创作失败', icon: 'none' });
}
}
}
});
}
handleCreate() {
if (!this.createForm.genre) {
wx.showToast({ title: '请选择题材', icon: 'none' });
return;
}
if (!this.createForm.keywords) {
wx.showToast({ title: '请输入关键词', icon: 'none' });
return;
}
const remaining = this.quota.daily - this.quota.used + this.quota.purchased;
if (remaining < 1) {
wx.showModal({
title: '次数不足',
content: 'AI创作需要1次配额当前剩余' + remaining + '次',
confirmText: '获取更多',
success: (res) => {
if (res.confirm) this.showQuotaModal();
}
});
return;
}
// 显示创作确认面板
this.showCreatePanel = true;
}
handleCreatePanelTouch(x, y) {
// 点击取消
if (this.createPanelBtns.cancel && this.isInRect(x, y, this.createPanelBtns.cancel)) {
this.showCreatePanel = false;
return;
}
// 点击确认
if (this.createPanelBtns.confirm && this.isInRect(x, y, this.createPanelBtns.confirm)) {
this.showCreatePanel = false;
this.confirmCreate();
return;
}
}
async confirmCreate() {
wx.showLoading({ title: '提交中...', mask: true });
try {
const userId = this.main?.userManager?.userId;
if (!userId) {
wx.hideLoading();
wx.showToast({ title: '请先登录', icon: 'none' });
return;
}
const result = await this.main.storyManager.createStory({
...this.createForm,
userId: userId
});
wx.hideLoading();
const draftId = result?.data?.draftId || result?.draftId;
if (draftId) {
this.quota.used += 1;
// 显示提示框,用户可以选择等待或返回
wx.showModal({
title: '创作已提交',
content: 'AI正在创作故事预计需要1-2分钟完成后可在草稿箱查看',
confirmText: '等待结果',
cancelText: '返回',
success: (modalRes) => {
if (modalRes.confirm) {
// 用户选择等待显示loading并轮询
wx.showLoading({ title: 'AI创作中...', mask: true });
this.pollCreateStatus(draftId);
}
// 用户选择返回,后台继续创作,稍后可在草稿箱查看
}
});
} else {
wx.showToast({ title: '创作失败', icon: 'none' });
}
} catch (e) {
wx.hideLoading();
wx.showToast({ title: '创作失败', icon: 'none' });
}
}
/**
* 轮询AI创作状态
*/
async pollCreateStatus(draftId, retries = 0) {
const maxRetries = 60; // 最多等待5分钟每5秒检查一次
if (retries >= maxRetries) {
wx.hideLoading();
wx.showModal({
title: '创作超时',
content: '故事创作时间较长,请稍后在"AI创作"中查看',
showCancel: false
});
return;
}
try {
const res = await get(`/stories/ai-create/${draftId}/status`);
const status = res?.data || res; // 兼容两种格式
if (status && status.isCompleted) {
wx.hideLoading();
wx.showModal({
title: '创作成功!',
content: `故事《${status.title}》已保存到草稿箱`,
confirmText: '去查看',
cancelText: '继续创作',
success: (res) => {
if (res.confirm) {
// 刷新当前页面数据
this.loadData();
this.render();
}
}
});
} else if (status && (status.status === -1 || status.isFailed)) {
wx.hideLoading();
wx.showModal({
title: '创作失败',
content: status.errorMessage || '故事创作失败,请检查输入后重试',
showCancel: false
});
} else {
// 继续轮询
setTimeout(() => {
this.pollCreateStatus(draftId, retries + 1);
}, 5000);
}
} catch (e) {
console.error('轮询状态失败:', e);
// 请求失败,继续重试
setTimeout(() => {
this.pollCreateStatus(draftId, retries + 1);
}, 5000);
}
}
}