From 6365dd8dd0633d54058eea6eeb0ad6bda47e3db6 Mon Sep 17 00:00:00 2001 From: En Date: Wed, 8 Apr 2026 18:09:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=E7=BB=98=E6=9C=AC=E5=88=9B=E4=BD=9CH?= =?UTF-8?q?5=E6=95=B4=E5=90=88=E2=80=94=E2=80=94=E5=BC=95=E5=85=A5aicreate?= =?UTF-8?q?.scss=E4=BF=AE=E5=A4=8D=E6=A0=B7=E5=BC=8F=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DcheckQuota=E7=B1=BB=E5=9E=8B=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 main.ts 中引入 aicreate.scss,解决所有 CSS 变量和共享样式类缺失的根因问题 - Index.vue 从 iframe 嵌入模式重构为壳组件+子路由渲染模式 - 修复 aicreate.scss 布局适配:height:100% 填充 PublicLayout,page-fullscreen 使用 100% 而非 100dvh - 修复 checkQuota() 的 type 参数:'A' → 'A3',对齐乐读派后端 V4.0 接口要求 - 迁移 lesingle-aicreate-client 全部 11 个视图、2 个组件、API 层、Store、工具函数 Co-Authored-By: Claude Opus 4.6 --- frontend/src/api/aicreate/index.ts | 288 ++++++ frontend/src/api/aicreate/types.ts | 56 + frontend/src/assets/styles/aicreate.scss | 121 +++ .../src/components/aicreate/PageHeader.vue | 67 ++ frontend/src/components/aicreate/StepBar.vue | 38 + frontend/src/main.ts | 1 + frontend/src/stores/aicreate.ts | 108 ++ frontend/src/utils/aicreate/config.ts | 24 + frontend/src/utils/aicreate/hmac.ts | 26 + frontend/src/utils/aicreate/status.ts | 38 + .../src/views/public/create/Generating.vue | 156 --- frontend/src/views/public/create/Index.vue | 206 ++-- .../public/create/views/BookReaderView.vue | 393 +++++++ .../public/create/views/CharactersView.vue | 374 +++++++ .../public/create/views/CreatingView.vue | 480 +++++++++ .../views/public/create/views/DubbingView.vue | 957 ++++++++++++++++++ .../public/create/views/EditInfoView.vue | 371 +++++++ .../views/public/create/views/PreviewView.vue | 332 ++++++ .../public/create/views/SaveSuccessView.vue | 200 ++++ .../public/create/views/StoryInputView.vue | 287 ++++++ .../public/create/views/StyleSelectView.vue | 210 ++++ .../views/public/create/views/UploadView.vue | 433 ++++++++ .../views/public/create/views/WelcomeView.vue | 353 +++++++ 23 files changed, 5227 insertions(+), 292 deletions(-) create mode 100644 frontend/src/api/aicreate/index.ts create mode 100644 frontend/src/api/aicreate/types.ts create mode 100644 frontend/src/assets/styles/aicreate.scss create mode 100644 frontend/src/components/aicreate/PageHeader.vue create mode 100644 frontend/src/components/aicreate/StepBar.vue create mode 100644 frontend/src/stores/aicreate.ts create mode 100644 frontend/src/utils/aicreate/config.ts create mode 100644 frontend/src/utils/aicreate/hmac.ts create mode 100644 frontend/src/utils/aicreate/status.ts delete mode 100644 frontend/src/views/public/create/Generating.vue create mode 100644 frontend/src/views/public/create/views/BookReaderView.vue create mode 100644 frontend/src/views/public/create/views/CharactersView.vue create mode 100644 frontend/src/views/public/create/views/CreatingView.vue create mode 100644 frontend/src/views/public/create/views/DubbingView.vue create mode 100644 frontend/src/views/public/create/views/EditInfoView.vue create mode 100644 frontend/src/views/public/create/views/PreviewView.vue create mode 100644 frontend/src/views/public/create/views/SaveSuccessView.vue create mode 100644 frontend/src/views/public/create/views/StoryInputView.vue create mode 100644 frontend/src/views/public/create/views/StyleSelectView.vue create mode 100644 frontend/src/views/public/create/views/UploadView.vue create mode 100644 frontend/src/views/public/create/views/WelcomeView.vue diff --git a/frontend/src/api/aicreate/index.ts b/frontend/src/api/aicreate/index.ts new file mode 100644 index 0000000..3b45bb1 --- /dev/null +++ b/frontend/src/api/aicreate/index.ts @@ -0,0 +1,288 @@ +/** + * AI 创作 API 层 + * 从 lesingle-aicreate-client/src/api/index.js 迁移 + * + * 独立 axios 实例,直连乐读派后端(VITE_LEAI_API_URL + /api/v1) + * 认证:Bearer sessionToken(企业模式) + */ +import axios from 'axios' +import OSS from 'ali-oss' +import { signRequest } from '@/utils/aicreate/hmac' +import { useAicreateStore } from '@/stores/aicreate' +import { leaiApi } from '@/api/public' +import type { StsTokenData, CreateStoryParams } from './types' + +// 乐读派后端地址(从环境变量读取,直连,不走代理) +const leaiBaseUrl = import.meta.env.VITE_LEAI_API_URL || '' + +const api = axios.create({ + baseURL: leaiBaseUrl ? leaiBaseUrl + '/api/v1' : '/api/v1', + timeout: 120000 +}) + +// ─── 请求拦截器:双模式认证 ─── +api.interceptors.request.use((config) => { + // 需要从 store 获取最新状态,不能用模块级缓存 + const store = useAicreateStore() + if (store.sessionToken) { + config.headers['Authorization'] = 'Bearer ' + store.sessionToken + } else if (store.orgId && store.appSecret) { + const queryParams: Record = {} + if (config.params) { + Object.entries(config.params).forEach(([k, v]) => { + if (v != null) queryParams[k] = String(v) + }) + } + const headers = signRequest(store.orgId, store.appSecret, queryParams) + Object.assign(config.headers, headers) + } + return config +}) + +// ─── Token 刷新状态管理 ─── +let isRefreshing = false +let pendingRequests: Array<(token: string | null) => void> = [] + +async function handleTokenExpired(failedConfig: any): Promise { + if (!isRefreshing) { + isRefreshing = true + try { + const data = await leaiApi.refreshToken() + const store = useAicreateStore() + store.setSession(data.orgId || store.orgId, data.token) + if (data.phone) store.setPhone(data.phone) + isRefreshing = false + pendingRequests.forEach(cb => cb(data.token)) + pendingRequests = [] + } catch { + isRefreshing = false + pendingRequests.forEach(cb => cb(null)) + pendingRequests = [] + } + } + return new Promise((resolve, reject) => { + if (pendingRequests.length >= 20) { + reject(new Error('TOO_MANY_PENDING_REQUESTS')) + return + } + pendingRequests.push((newToken) => { + if (newToken) { + if (failedConfig.__retried) { + reject(new Error('TOKEN_REFRESH_FAILED')) + return + } + failedConfig.__retried = true + failedConfig.headers['Authorization'] = 'Bearer ' + newToken + delete failedConfig.headers['X-App-Key'] + delete failedConfig.headers['X-Timestamp'] + delete failedConfig.headers['X-Nonce'] + delete failedConfig.headers['X-Signature'] + resolve(api(failedConfig)) + } else { + reject(new Error('TOKEN_REFRESH_FAILED')) + } + }) + }) +} + +// ─── 响应拦截器 ─── +api.interceptors.response.use( + (res) => { + const d = res.data + if (d?.code !== 0 && d?.code !== 200) { + const store = useAicreateStore() + // Token 过期 + if (store.sessionToken && (d?.code === 20010 || d?.code === 20009)) { + return handleTokenExpired(res.config) + } + return Promise.reject(new Error(d?.msg || '请求失败')) + } + return d + }, + (err) => { + const store = useAicreateStore() + if (store.sessionToken && err.response?.status === 401) { + return handleTokenExpired(err.config) + } + return Promise.reject(err) + } +) + +// ═══════════════════════════════════════════════════════════ +// API 函数 +// ═══════════════════════════════════════════════════════════ + +/** 图片上传 */ +export function uploadImage(file: File) { + const form = new FormData() + form.append('file', file) + return api.post('/creation/upload', form, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 30000 + }) +} + +/** 角色提取 */ +export function extractCharacters(imageUrl: string, opts: { saveOriginal?: boolean; title?: string } = {}) { + const store = useAicreateStore() + const body: Record = { + orgId: store.orgId, + phone: store.phone, + imageUrl + } + if (opts.saveOriginal) body.saveOriginal = true + if (opts.title) body.title = opts.title + return api.post('/creation/extract-original', body, { timeout: 120000 }) +} + +/** 图片故事创作 */ +export function createStory(params: CreateStoryParams) { + const store = useAicreateStore() + const body: Record = { + orgId: store.orgId, + phone: store.phone, + imageUrl: params.imageUrl, + storyHint: params.storyHint, + style: params.style, + refAdaptMode: params.refAdaptMode || 'enhanced', + enableVoice: false + } + if (params.title) body.title = params.title + if (params.author) body.author = params.author + if (params.heroCharId) body.heroCharId = params.heroCharId + if (params.extractId) body.extractId = params.extractId + return api.post('/creation/image-story', body) +} + +/** 查询作品详情 */ +export function getWorkDetail(workId: string) { + const store = useAicreateStore() + return api.get(`/query/work/${workId}`, { + params: { orgId: store.orgId, phone: store.phone } + }) +} + +/** 额度校验 */ +export function checkQuota() { + const store = useAicreateStore() + return api.post('/query/validate', { + orgId: store.orgId, + phone: store.phone, + apiType: 'A3' + }) +} + +/** 编辑绘本信息 */ +export function updateWork(workId: string, data: Record) { + return api.put(`/update/work/${workId}`, data) +} + +/** 批量更新配音 URL */ +export function batchUpdateAudio(workId: string, pages: any[]) { + const store = useAicreateStore() + return api.post('/update/batch-audio', { + orgId: store.orgId, + phone: store.phone, + workId, + pages + }) +} + +/** 完成配音 */ +export function finishDubbing(workId: string) { + return batchUpdateAudio(workId, []) +} + +/** AI 配音 */ +export function voicePage(data: Record) { + const store = useAicreateStore() + return api.post('/creation/voice', { + orgId: store.orgId, + phone: store.phone, + ...data + }, { timeout: 120000 }) +} + +/** STS 临时凭证 */ +export function getStsToken() { + const store = useAicreateStore() + return api.post('/oss/sts-token', { + orgId: store.orgId, + phone: store.phone + }) +} + +// ─── OSS 直传 ─── +let _ossClient: OSS | null = null +let _stsData: StsTokenData | null = null + +async function getOssClient() { + if (_ossClient && _stsData) { + const expireTime = new Date(_stsData.expiration).getTime() + if (Date.now() < expireTime - 5 * 60 * 1000) { + return { client: _ossClient, prefix: _stsData.uploadPrefix } + } + } + const res = await getStsToken() + _stsData = res.data + _ossClient = new OSS({ + region: _stsData.region, + accessKeyId: _stsData.accessKeyId, + accessKeySecret: _stsData.accessKeySecret, + stsToken: _stsData.securityToken, + bucket: _stsData.bucket, + endpoint: _stsData.endpoint, + refreshSTSToken: async () => { + const r = await getStsToken() + _stsData = r.data + return { + accessKeyId: _stsData.accessKeyId, + accessKeySecret: _stsData.accessKeySecret, + stsToken: _stsData.securityToken + } + }, + refreshSTSTokenInterval: 300000 + }) + return { client: _ossClient, prefix: _stsData.uploadPrefix } +} + +/** + * STS 直传文件到 OSS + * @param file 要上传的文件 + * @param opts 选项 + * @returns 文件的完整 OSS URL + */ +export async function ossUpload(file: File | Blob, opts: { + type?: string + onProgress?: (pct: number) => void + ext?: string +} = {}): Promise { + const { type = 'img', onProgress, ext: forceExt } = opts + const { client, prefix } = await getOssClient() + const ext = forceExt || ((file as File).name ? (file as File).name.split('.').pop() : 'bin').toLowerCase() + const date = new Date().toISOString().slice(0, 10) + const rand = Math.random().toString(36).slice(2, 10) + const key = `${prefix}${date}/${type}_${Date.now()}_${rand}.${ext}` + await (client as OSS).put(key, file, { + headers: { 'x-oss-object-acl': 'public-read' }, + progress: (p: number) => { if (onProgress) onProgress(Math.round(p * 100)) } + }) + if (_stsData!.cdnDomain) { + return `${_stsData!.cdnDomain}/${key}` + } + return `https://${_stsData!.bucket}.${_stsData!.endpoint.replace('https://', '')}/${key}` +} + +/** STS 列举用户目录下的文件 */ +export async function ossListFiles() { + const { client, prefix } = await getOssClient() + const result = await (client as OSS).list({ prefix, 'max-keys': 100 }) + return (result.objects || []).map((obj: any) => ({ + name: obj.name.replace(prefix, ''), + size: obj.size, + lastModified: obj.lastModified, + url: obj.url + })) +} + +export default api diff --git a/frontend/src/api/aicreate/types.ts b/frontend/src/api/aicreate/types.ts new file mode 100644 index 0000000..de60e6f --- /dev/null +++ b/frontend/src/api/aicreate/types.ts @@ -0,0 +1,56 @@ +/** + * AI 创作相关类型定义 + */ + +/** STS 临时凭证 */ +export interface StsTokenData { + region: string + accessKeyId: string + accessKeySecret: string + securityToken: string + bucket: string + endpoint: string + uploadPrefix: string + cdnDomain?: string + expiration: string +} + +/** 角色提取结果 */ +export interface CharacterItem { + charId: string + name: string + imageUrl: string +} + +/** 作品详情 */ +export interface WorkDetail { + workId: string + orgId: string + phone: string + status: number + title: string + author: string + coverUrl: string + pages: WorkPage[] + style?: string +} + +/** 作品分页 */ +export interface WorkPage { + pageNo: number + imageUrl: string + text: string + audioUrl?: string +} + +/** 创建故事请求参数 */ +export interface CreateStoryParams { + imageUrl: string + storyHint: string + style: string + title?: string + author?: string + heroCharId?: string + extractId?: string + refAdaptMode?: string +} diff --git a/frontend/src/assets/styles/aicreate.scss b/frontend/src/assets/styles/aicreate.scss new file mode 100644 index 0000000..c629bcf --- /dev/null +++ b/frontend/src/assets/styles/aicreate.scss @@ -0,0 +1,121 @@ +// 乐读派 C端 — AI 创作专用样式(隔离在 .ai-create-shell 容器内) +// 暖橙 + 奶油白 儿童绘本风格 +// 所有 CSS 变量使用 --ai- 前缀,避免与主前端冲突 + +.ai-create-shell { + --ai-primary: #FF6B35; + --ai-primary-light: #FFF0E8; + --ai-secondary: #6C63FF; + --ai-accent: #FFD166; + --ai-success: #2EC4B6; + --ai-bg: #FFFDF7; + --ai-card: #FFFFFF; + --ai-text: #2D2D3F; + --ai-text-sub: #8E8EA0; + --ai-border: #F0EDE8; + --ai-radius: 20px; + --ai-radius-sm: 14px; + --ai-shadow: 0 8px 32px rgba(255, 107, 53, 0.12); + --ai-shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.06); + --ai-gradient: linear-gradient(135deg, #FF6B35 0%, #FF8F65 50%, #FFB088 100%); + --ai-gradient-purple: linear-gradient(135deg, #6C63FF 0%, #9B93FF 100%); + --ai-font: 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', -apple-system, sans-serif; + + font-family: var(--ai-font); + background: var(--ai-bg); + color: var(--ai-text); + -webkit-font-smoothing: antialiased; + max-width: 430px; + margin: 0 auto; + position: relative; + overflow: hidden; + height: 100%; + + // 通用按钮 + .btn-primary { + display: block; + width: 100%; + padding: 16px 0; + border: none; + border-radius: 50px; + background: var(--ai-gradient); + color: #fff; + font-size: 17px; + font-weight: 700; + letter-spacing: 1px; + cursor: pointer; + box-shadow: var(--ai-shadow); + transition: all 0.3s ease; + + &:disabled { + opacity: 0.45; + cursor: not-allowed; + } + + &:active:not(:disabled) { + transform: scale(0.97); + } + } + + .btn-ghost { + @extend .btn-primary; + background: var(--ai-primary-light); + color: var(--ai-primary); + box-shadow: none; + } + + // 通用卡片 + .card { + background: var(--ai-card); + border-radius: var(--ai-radius); + box-shadow: var(--ai-shadow-soft); + } + + // 安全区底部 + .safe-bottom { + padding-bottom: env(safe-area-inset-bottom, 20px); + } + + // 全屏自适应布局框架 + // 嵌入 PublicLayout 后,使用 100% 而非 100dvh,以适配头部和底栏 + .page-fullscreen { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0 !important; + overflow: hidden; + + > .page-content { + flex: 1; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + + > .page-bottom { + flex-shrink: 0; + padding: 12px 20px; + padding-bottom: max(12px, env(safe-area-inset-bottom)); + background: var(--ai-bg, #FFFDF7); + } + } + + // 动画 + @keyframes fadeInUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } + } + + @keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } + } + + @keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } + } + + @keyframes spin { + to { transform: rotate(360deg); } + } +} diff --git a/frontend/src/components/aicreate/PageHeader.vue b/frontend/src/components/aicreate/PageHeader.vue new file mode 100644 index 0000000..bbb933b --- /dev/null +++ b/frontend/src/components/aicreate/PageHeader.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/frontend/src/components/aicreate/StepBar.vue b/frontend/src/components/aicreate/StepBar.vue new file mode 100644 index 0000000..df84ebb --- /dev/null +++ b/frontend/src/components/aicreate/StepBar.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 92d03f1..2aadf8b 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -4,6 +4,7 @@ import Antd from "ant-design-vue" import "ant-design-vue/dist/reset.css" import "./styles/global.scss" import "./styles/theme.scss" +import "./assets/styles/aicreate.scss" import App from "./App.vue" import router from "./router" import { useAuthStore } from "./stores/auth" diff --git a/frontend/src/stores/aicreate.ts b/frontend/src/stores/aicreate.ts new file mode 100644 index 0000000..ac57f0c --- /dev/null +++ b/frontend/src/stores/aicreate.ts @@ -0,0 +1,108 @@ +/** + * AI 创作全局状态(Pinia Store) + * + * 从 lesingle-aicreate-client/utils/store.js 迁移 + * 保留原有字段和方法,适配 Pinia setup 语法 + */ +import { defineStore } from 'pinia' +import { ref, reactive } 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([]) + const selectedCharacter = ref(null) + const selectedStyle = ref('') + const storyData = ref(null) + const workId = ref('') + const workDetail = ref(null) + const authRedirectUrl = 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 reset() { + imageUrl.value = '' + extractId.value = '' + characters.value = [] + selectedCharacter.value = null + selectedStyle.value = '' + storyData.value = null + workId.value = '' + workDetail.value = null + localStorage.removeItem('le_workId') + } + + 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)) + } + + 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, + } +}) diff --git a/frontend/src/utils/aicreate/config.ts b/frontend/src/utils/aicreate/config.ts new file mode 100644 index 0000000..20906a2 --- /dev/null +++ b/frontend/src/utils/aicreate/config.ts @@ -0,0 +1,24 @@ +/** + * AI 创作运行时配置 + * 从 lesingle-aicreate-client/utils/config.js 迁移并重写 + * + * 整合后从环境变量读取乐读派后端地址 + */ + +// 乐读派后端地址(直连) +const leaiApiUrl = import.meta.env.VITE_LEAI_API_URL || '' + +const config = { + // API 基础路径(完整 URL,用于 axios baseURL 已在 api/aicreate 中使用) + apiBaseUrl: leaiApiUrl, + // WebSocket 基础路径(完整 URL) + wsBaseUrl: leaiApiUrl ? leaiApiUrl.replace(/^http/, 'ws') : '', + // 品牌信息 + brand: { + title: '乐读派', + subtitle: 'AI智能儿童绘本创作', + slogan: '让想象力飞翔' + }, +} + +export default config diff --git a/frontend/src/utils/aicreate/hmac.ts b/frontend/src/utils/aicreate/hmac.ts new file mode 100644 index 0000000..a2885bd --- /dev/null +++ b/frontend/src/utils/aicreate/hmac.ts @@ -0,0 +1,26 @@ +/** + * HMAC-SHA256 签名工具 + * 从 lesingle-aicreate-client/utils/hmac.js 迁移 + * + * 签名规则:排序的 query params + nonce + timestamp,用 & 拼接 + * POST JSON body 不参与签名 + */ +import CryptoJS from 'crypto-js' + +export function signRequest(orgId: string, appSecret: string, queryParams: Record = {}) { + const timestamp = String(Date.now()) + const nonce = Math.random().toString(36).substring(2, 15) + Date.now().toString(36) + + const allParams: Record = { ...queryParams, nonce, timestamp } + const sorted = Object.keys(allParams).sort() + const signStr = sorted.map(k => `${k}=${allParams[k]}`).join('&') + + const signature = CryptoJS.HmacSHA256(signStr, appSecret).toString(CryptoJS.enc.Hex) + + return { + 'X-App-Key': orgId, + 'X-Timestamp': timestamp, + 'X-Nonce': nonce, + 'X-Signature': signature + } +} diff --git a/frontend/src/utils/aicreate/status.ts b/frontend/src/utils/aicreate/status.ts new file mode 100644 index 0000000..10527a1 --- /dev/null +++ b/frontend/src/utils/aicreate/status.ts @@ -0,0 +1,38 @@ +/** + * V4.0 作品状态常量(数值型) + * 从 lesingle-aicreate-client/utils/status.js 迁移 + * + * 状态流转: PENDING(1) -> PROCESSING(2) -> COMPLETED(3) -> CATALOGED(4) -> DUBBED(5) + * 任意阶段可能 -> FAILED(-1) + */ +export const STATUS = { + FAILED: -1, + PENDING: 1, + PROCESSING: 2, + COMPLETED: 3, + CATALOGED: 4, + DUBBED: 5 +} as const + +export type StatusValue = typeof STATUS[keyof typeof STATUS] + +/** + * 根据作品状态决定应导航到的路由 + */ +export function getRouteByStatus(status: StatusValue, workId: string): { name: string; params?: Record } | null { + switch (status) { + case STATUS.PENDING: + case STATUS.PROCESSING: + return { name: 'PublicCreateCreating' } + case STATUS.COMPLETED: + return { name: 'PublicCreatePreview', params: { workId } } + case STATUS.CATALOGED: + return { name: 'PublicCreateDubbing', params: { workId } } + case STATUS.DUBBED: + return { name: 'PublicCreateRead', params: { workId } } + case STATUS.FAILED: + return null + default: + return null + } +} diff --git a/frontend/src/views/public/create/Generating.vue b/frontend/src/views/public/create/Generating.vue deleted file mode 100644 index c11bad6..0000000 --- a/frontend/src/views/public/create/Generating.vue +++ /dev/null @@ -1,156 +0,0 @@ - - - - - diff --git a/frontend/src/views/public/create/Index.vue b/frontend/src/views/public/create/Index.vue index e364d02..6df7ed2 100644 --- a/frontend/src/views/public/create/Index.vue +++ b/frontend/src/views/public/create/Index.vue @@ -1,45 +1,41 @@