- aicreate.scss 主题变量紫粉化,对齐 PublicLayout 设计语言
- 11 个创作流程 view 清理 emoji 改 antd 图标,文案去除"孩子/家长"等第三人称
- 路由调整:编排故事改到选画风之前(更顺的产品逻辑)
- WelcomeView 浮动 CTA + 完整 7 步流程引导
- CharactersView 单角色大图 / 多角色网格自适应
- StyleSelectView 预设路径 /aicreate/styles/{styleId}.jpg + SVG fallback
- CreatingView 改为异步任务式说明 + 去作品库入口
- PreviewView / DubbingView 缩略图统一为横向胶卷
- EditInfoView 底部三按钮(保存草稿 / 去配音 / 发布作品),配音改为可选
- BookReaderView 修复 dev 模式数据加载 + 紫粉封面
- DubbingView / BookReaderView 改用 page-fullscreen 布局类避免被 tabbar 遮挡
- store 新增 fillMockData / fillMockWorkDetail,支持 dev 无后端走通完整流程
- works/Index.vue 加 query.tab 双向同步,支持跳转携带 tab 参数
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
215 lines
7.8 KiB
TypeScript
215 lines
7.8 KiB
TypeScript
/**
|
||
* 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,
|
||
}
|
||
})
|