library-picturebook-activity/frontend/src/stores/aicreate.ts

215 lines
7.8 KiB
TypeScript
Raw Normal View History

/**
* AI Pinia Store
*
* lesingle-aicreate-client/utils/store.js
* Pinia setup
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAicreateStore = defineStore('aicreate', () => {
// ─── 认证信息 ───
const phone = ref(localStorage.getItem('le_phone') || '')
const orgId = ref(localStorage.getItem('le_orgId') || '')
const appSecret = ref(localStorage.getItem('le_appSecret') || '')
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('')
const workDetail = ref<any>(null)
const authRedirectUrl = ref('')
// ─── Tab 切换状态保存 ───
const lastCreateRoute = ref('')
// ─── 方法 ───
function setPhone(val: string) {
phone.value = val
localStorage.setItem('le_phone', val)
}
function setOrg(id: string, secret: string) {
orgId.value = id
appSecret.value = secret
localStorage.setItem('le_orgId', id)
localStorage.setItem('le_appSecret', secret)
}
function setSession(id: string, token: string) {
orgId.value = id
sessionToken.value = token
localStorage.setItem('le_orgId', id)
sessionStorage.setItem('le_orgId', id)
sessionStorage.setItem('le_sessionToken', token)
}
function clearSession() {
sessionToken.value = ''
sessionStorage.removeItem('le_sessionToken')
}
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 = ''
workDetail.value = null
lastCreateRoute.value = ''
// 清除所有 localStorage 中的创作相关数据
localStorage.removeItem('le_workId')
localStorage.removeItem('le_phone')
localStorage.removeItem('le_orgId')
localStorage.removeItem('le_appSecret')
// 清除 sessionStorage 中的恢复数据
sessionStorage.removeItem('le_recovery')
}
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 {
// 认证
phone, orgId, appSecret, sessionToken, authRedirectUrl,
setPhone, setOrg, setSession, clearSession,
// 创作流程
imageUrl, extractId, characters, selectedCharacter,
selectedStyle, storyData, workId, workDetail,
reset, saveRecoveryState, restoreRecoveryState,
// 开发模式
fillMockData,
fillMockWorkDetail,
// Tab 切换状态
lastCreateRoute, setLastCreateRoute, clearLastCreateRoute,
}
})