From 95b63480290ce059915c5fae6e4b595f2c0172c6 Mon Sep 17 00:00:00 2001 From: wangwuww111 <2816108629@qq.com> Date: Mon, 16 Mar 2026 16:41:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=E5=88=9B=E4=BD=9C=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=8C=E6=B7=BB=E5=8A=A0userId=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=92=8C=E8=BD=AE=E8=AF=A2=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/js/data/StoryManager.js | 5 + client/js/scenes/AICreateScene.js | 444 ++++++++++++++++++++++++++++-- 2 files changed, 422 insertions(+), 27 deletions(-) diff --git a/client/js/data/StoryManager.js b/client/js/data/StoryManager.js index e145f1f..b6bc0db 100644 --- a/client/js/data/StoryManager.js +++ b/client/js/data/StoryManager.js @@ -345,7 +345,12 @@ export default class StoryManager { */ async createStory(params) { try { + if (!params.userId) { + console.error('AI创作失败: 缺少userId'); + return null; + } const result = await post('/stories/ai-create', { + userId: params.userId, genre: params.genre, keywords: params.keywords, protagonist: params.protagonist, diff --git a/client/js/scenes/AICreateScene.js b/client/js/scenes/AICreateScene.js index 19ac0e8..fda06c2 100644 --- a/client/js/scenes/AICreateScene.js +++ b/client/js/scenes/AICreateScene.js @@ -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); + } } }