/** * 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); } } }