library-picturebook-activity/frontend/src/stores/aicreate.ts
2026-04-10 14:07:40 +08:00

203 lines
7.5 KiB
TypeScript
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 创作全局状态Pinia Store
*
* 敏感信息phone/orgId/appSecret不再存储在 localStorage
* orgId 仅存 sessionStorage会话级sessionToken 判断是否已初始化
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { clearExtractDraft } from '@/utils/aicreate/extractDraft'
export const useAicreateStore = defineStore('aicreate', () => {
// ─── 认证信息(不再存储敏感信息到 localStorage ───
const orgId = ref(sessionStorage.getItem('le_orgId') || '')
const sessionToken = ref(sessionStorage.getItem('le_sessionToken') || '')
// ─── 创作流程数据 ───
const imageUrl = ref('')
const extractId = ref('')
const characters = ref<any[]>([])
const selectedCharacter = ref<any>(null)
const selectedStyle = ref('')
const storyData = ref<any>(null)
const workId = ref('')
/** extract 接口可能返回的 workId供下游使用 */
const originalWorkId = ref('')
const workDetail = ref<any>(null)
// ─── Tab 切换状态保存 ───
const lastCreateRoute = ref('')
// ─── 方法 ───
function setSession(id: string, token: string) {
orgId.value = id
sessionToken.value = token
sessionStorage.setItem('le_orgId', id)
sessionStorage.setItem('le_sessionToken', token)
}
function clearSession() {
sessionToken.value = ''
orgId.value = ''
sessionStorage.removeItem('le_sessionToken')
sessionStorage.removeItem('le_orgId')
}
function setLastCreateRoute(path: string) {
lastCreateRoute.value = path
}
function clearLastCreateRoute() {
lastCreateRoute.value = ''
}
function reset() {
imageUrl.value = ''
extractId.value = ''
characters.value = []
selectedCharacter.value = null
selectedStyle.value = ''
storyData.value = null
workId.value = ''
originalWorkId.value = ''
workDetail.value = null
lastCreateRoute.value = ''
// 只清除创作流程数据,保留认证信息
localStorage.removeItem('le_workId')
// 清除 sessionStorage 中的恢复数据
sessionStorage.removeItem('le_recovery')
clearExtractDraft()
}
function saveRecoveryState() {
const recovery = {
path: window.location.pathname || '/',
workId: workId.value || localStorage.getItem('le_workId') || '',
imageUrl: imageUrl.value || '',
extractId: extractId.value || '',
selectedStyle: selectedStyle.value || '',
savedAt: Date.now()
}
sessionStorage.setItem('le_recovery', JSON.stringify(recovery))
}
/**
* 开发模式:填充一份 mock 数据,用于跳过真实后端调用走通 UI 流程
* 仅供开发期 UI 调试使用,不要在生产逻辑中调用
* @param count 要 mock 的角色数量1-3默认 3
*/
function fillMockData(count: number = 3) {
// 纯渐变占位图(不带文字,模拟真实 AI 抠图返回的角色形象)
const mockSvg = (hue: number) =>
'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">` +
`<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">` +
`<stop offset="0" stop-color="hsl(${hue},75%,72%)"/>` +
`<stop offset="1" stop-color="hsl(${(hue + 35) % 360},80%,58%)"/>` +
`</linearGradient></defs>` +
`<rect width="240" height="240" fill="url(#g)"/>` +
`</svg>`
)
imageUrl.value = mockSvg(250)
extractId.value = 'mock-extract-' + Date.now()
selectedCharacter.value = null
// 注意:真实 AI 接口不返回 name 字段mock 数据也不写 name由用户在 StoryInputView 自己起名
const allChars = [
{ charId: 'mock-c1', type: 'HERO', originalCropUrl: mockSvg(280) },
{ charId: 'mock-c2', type: 'SIDEKICK', originalCropUrl: mockSvg(30) },
{ charId: 'mock-c3', type: 'SIDEKICK', originalCropUrl: mockSvg(100) },
]
const n = Math.max(1, Math.min(count, allChars.length))
characters.value = allChars.slice(0, n)
}
/**
* 开发模式:填充一份完整的 mock 作品数据,用于跳过真实 AI 生成走通预览/编辑/发布等下游 UI
* 仅供开发期 UI 调试使用
*/
function fillMockWorkDetail() {
// 16:9 渐变占位图800x450模拟真实绘本插画
const mockPage = (hue: number) =>
'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="800" height="450" viewBox="0 0 800 450">` +
`<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">` +
`<stop offset="0" stop-color="hsl(${hue},70%,75%)"/>` +
`<stop offset="1" stop-color="hsl(${(hue + 45) % 360},75%,55%)"/>` +
`</linearGradient></defs>` +
`<rect width="800" height="450" fill="url(#g)"/>` +
`</svg>`
)
// 13 页(封面 + 12 内页),模拟真实绘本规模,便于测试横向胶卷式缩略图
const pageTexts = [
'', // 封面
'一个阳光明媚的早晨,小主角推开窗,看见远处的森林闪着金光。',
'它沿着小路走啊走,遇到了一只迷路的小鸟,羽毛湿漉漉的。',
'小主角轻轻抱起小鸟,决定送它回家。',
'路过一片野花田时,一只小狐狸从草丛里跳出来打招呼。',
'小狐狸说它认识森林里所有的小路,愿意做大家的向导。',
'三个朋友来到一条清澈的溪水边,溪里有一群闪闪发光的小鱼。',
'小鱼们告诉他们,那棵会发光的大树就在前方不远处。',
'森林深处真的有一棵会发光的大树,挂满了亮晶晶的果实。',
'原来这就是小鸟的家,妈妈正在树枝上焦急地张望。',
'小鸟欢快地飞回妈妈身边,鸟妈妈给大家每人一颗果实。',
'夕阳下,小主角和小狐狸告别,小狐狸送他们到森林边缘。',
'小主角带着这份美好回到家,心里也开出了一朵花。',
]
const wid = 'mock-work-' + Date.now()
workId.value = wid
workDetail.value = {
workId: wid,
status: 3, // COMPLETED
title: storyData.value?.title || '森林大冒险',
subtitle: '',
author: '',
coverUrl: mockPage(280),
pageList: pageTexts.map((text, i) => ({
pageNum: i,
text,
imageUrl: mockPage((280 + i * 27) % 360),
})),
}
}
function restoreRecoveryState() {
const raw = sessionStorage.getItem('le_recovery')
if (!raw) return null
try {
const recovery = JSON.parse(raw)
if (Date.now() - recovery.savedAt > 30 * 60 * 1000) {
sessionStorage.removeItem('le_recovery')
return null
}
if (recovery.workId) workId.value = recovery.workId
if (recovery.imageUrl) imageUrl.value = recovery.imageUrl
if (recovery.extractId) extractId.value = recovery.extractId
if (recovery.selectedStyle) selectedStyle.value = recovery.selectedStyle
sessionStorage.removeItem('le_recovery')
return recovery
} catch {
sessionStorage.removeItem('le_recovery')
return null
}
}
return {
// 认证
orgId, sessionToken,
setSession, clearSession,
// 创作流程
imageUrl, extractId, characters, selectedCharacter,
selectedStyle, storyData, workId, originalWorkId, workDetail,
reset, saveRecoveryState, restoreRecoveryState,
// 开发模式
fillMockData,
fillMockWorkDetail,
// Tab 切换状态
lastCreateRoute, setLastCreateRoute, clearLastCreateRoute,
}
})