feat: AI创作流程优化,添加userId参数和轮询状态

This commit is contained in:
wangwuww111
2026-03-16 16:41:33 +08:00
parent d111f1a2cf
commit 95b6348029
2 changed files with 422 additions and 27 deletions

View File

@@ -2,6 +2,7 @@
* AI创作中心场景
*/
import BaseScene from './BaseScene';
import { get, post } from '../utils/http';
export default class AICreateScene extends BaseScene {
constructor(main, params) {
@@ -31,6 +32,10 @@ export default class AICreateScene extends BaseScene {
// 快捷标签
this.genreTags = ['都市言情', '古风宫廷', '悬疑推理', '校园青春', '修仙玄幻', '职场商战'];
// 创作确认面板
this.showCreatePanel = false;
this.createPanelBtns = {};
}
async init() {
@@ -66,7 +71,10 @@ export default class AICreateScene extends BaseScene {
} else if (this.currentTab === 1) {
contentHeight = 300 + this.publishedContinues.length * 90;
} else {
contentHeight = 600;
// 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);
}
@@ -79,6 +87,11 @@ export default class AICreateScene extends BaseScene {
this.renderQuotaBar(ctx);
this.renderTabs(ctx);
this.renderContent(ctx);
// 创作确认面板(最上层)
if (this.showCreatePanel) {
this.renderCreatePanel(ctx);
}
}
renderBackground(ctx) {
@@ -286,6 +299,120 @@ export default class AICreateScene extends BaseScene {
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) {
@@ -550,6 +677,134 @@ export default class AICreateScene extends BaseScene {
}
}
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);
@@ -594,6 +849,12 @@ export default class AICreateScene extends BaseScene {
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');
@@ -684,6 +945,43 @@ export default class AICreateScene extends BaseScene {
});
}
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;
}
@@ -829,10 +1127,10 @@ export default class AICreateScene extends BaseScene {
}
const remaining = this.quota.daily - this.quota.used + this.quota.purchased;
if (remaining < 5) {
if (remaining < 1) {
wx.showModal({
title: '次数不足',
content: 'AI创作需要5次配额,当前剩余' + remaining + '次',
content: 'AI创作需要1次配额,当前剩余' + remaining + '次',
confirmText: '获取更多',
success: (res) => {
if (res.confirm) this.showQuotaModal();
@@ -841,30 +1139,122 @@ export default class AICreateScene extends BaseScene {
return;
}
wx.showModal({
title: '确认创作',
content: `题材:${this.createForm.genre}\n关键词:${this.createForm.keywords}\n\n将消耗5次AI次数`,
success: async (res) => {
if (res.confirm) {
wx.showLoading({ title: 'AI创作中...', mask: true });
try {
// TODO: 实现完整创作API
const result = await this.main.storyManager.createStory(this.createForm);
wx.hideLoading();
if (result) {
this.quota.used += 5;
wx.showToast({ title: '创作成功!', icon: 'success' });
// 跳转到新故事
setTimeout(() => {
this.main.sceneManager.switchScene('story', { storyId: result.storyId });
}, 1500);
}
} catch (e) {
wx.hideLoading();
wx.showToast({ title: '创作失败', icon: 'none' });
}
}
// 显示创作确认面板
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);
}
}
}