library-picturebook-activity/lesingle-creation-frontend/src/composables/useContestWorkPreviewPages.ts
En 98e9ad1d28 feat(前端): 测试环境登录框支持自动填充测试账号
通过 VITE_AUTO_FILL_TEST 环境变量控制,在 .env.test 中启用,
使测试环境构建后登录框也能自动填充测试账号,方便测试人员使用。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 17:03:22 +08:00

230 lines
7.4 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.

import type { Ref } from "vue"
import { computed } from "vue"
import type { ContestWork } from "@/api/contests"
/** 单页预览:图片、故事文案、配音(与后端 UGC 快照字段对齐) */
export interface ContestWorkPreviewPage {
imageUrl: string
text?: string
/** 单页配音 URL后端 files 快照中的 audioUrl无则省略 */
audioUrl?: string
pageNo?: number
}
export interface ContestWorkPreviewResult {
pages: ContestWorkPreviewPage[]
/** 从 files 中解析出的 3D 等模型资源 URL已规范化顺序与出现顺序一致 */
modelFileUrls: string[]
}
/** 将 OSS 相对路径等规范化为可展示的绝对 URL与 ReviewWorkModal 行为一致) */
export function normalizeContestWorkAssetUrl(fileUrl: string): string {
if (!fileUrl) return ""
if (fileUrl.startsWith("http://") || fileUrl.startsWith("https://")) {
return fileUrl
}
const baseURL = import.meta.env.VITE_API_BASE_URL || ""
if (fileUrl.startsWith("/api") && baseURL.includes("/api")) {
const urlWithoutApi = baseURL.replace(/\/api$/, "")
return `${urlWithoutApi}${fileUrl}`
}
return `${baseURL}${fileUrl.startsWith("/") ? "" : "/"}${fileUrl}`
}
function parseJsonArray<T>(raw: unknown): T[] {
if (raw == null) return []
if (typeof raw === "string") {
try {
const p = JSON.parse(raw)
return Array.isArray(p) ? p : []
} catch {
return []
}
}
return Array.isArray(raw) ? raw : []
}
/** 是否为 3D 模型等资源(与 WorkDetailModal 扩展名约定一致) */
export function isContestWorkModelFile(urlOrFileName: string): boolean {
const pathWithoutQuery = urlOrFileName.split("?")[0]
return /\.(glb|gltf|obj|fbx|stl|zip)$/i.test(pathWithoutQuery)
}
function isImageFileName(url: string): boolean {
const pathWithoutQuery = url.split("?")[0]
return /\.(jpg|jpeg|png|gif|webp)$/i.test(pathWithoutQuery)
}
function firstImageAttachmentUrl(work: ContestWork): string {
const att = work.attachments?.find(
(a) =>
a.fileType?.startsWith("image/") ||
/\.(jpg|jpeg|png|gif|webp)$/i.test(a.fileName || "")
)
return att?.fileUrl ? normalizeContestWorkAssetUrl(att.fileUrl) : ""
}
function collectModelUrlsFromFiles(rawFiles: unknown[]): string[] {
const out: string[] = []
for (const item of rawFiles) {
if (typeof item === "string") {
if (isContestWorkModelFile(item)) {
out.push(normalizeContestWorkAssetUrl(item))
}
continue
}
if (typeof item === "object" && item !== null) {
const o = item as Record<string, unknown>
const fu = typeof o.fileUrl === "string" ? o.fileUrl : ""
if (fu && isContestWorkModelFile(fu)) {
out.push(normalizeContestWorkAssetUrl(fu))
}
}
}
return out
}
function readPageMetaFromFileEntry(fileAtIndex: unknown): {
text?: string
audioUrl?: string
pageNo?: number
} {
if (typeof fileAtIndex !== "object" || fileAtIndex === null) {
return {}
}
const o = fileAtIndex as Record<string, unknown>
const text = typeof o.text === "string" && o.text.trim() ? o.text : undefined
const audioUrl =
typeof o.audioUrl === "string" && o.audioUrl.trim()
? normalizeContestWorkAssetUrl(o.audioUrl)
: undefined
const pageNo =
typeof o.pageNo === "number"
? o.pageNo
: typeof o.pageNo === "string" && o.pageNo.trim()
? Number(o.pageNo)
: undefined
return { text, audioUrl, pageNo }
}
function findFileMetaByPageNo(
rawFiles: unknown[],
pageNo1: number
): { text?: string; audioUrl?: string } {
for (const f of rawFiles) {
if (typeof f !== "object" || f === null) continue
const o = f as Record<string, unknown>
const pn = o.pageNo
const n = typeof pn === "number" ? pn : typeof pn === "string" ? Number(pn) : NaN
if (n === pageNo1) {
const text = typeof o.text === "string" && o.text.trim() ? o.text : undefined
const audioUrl =
typeof o.audioUrl === "string" && o.audioUrl.trim()
? normalizeContestWorkAssetUrl(o.audioUrl)
: undefined
return { text, audioUrl }
}
}
return {}
}
/**
* 解析赛事作品的多页预览:合并 previewUrls 与结构化 filespageNo / imageUrl / text / audioUrl
* 单图、仅附件、仅 3D 等场景通过 pages 数量与 modelFileUrls 组合表达。
*/
export function buildContestWorkPreviewPages(
work: ContestWork | null | undefined
): ContestWorkPreviewResult {
const empty: ContestWorkPreviewResult = { pages: [], modelFileUrls: [] }
if (!work) return empty
const rawFiles = parseJsonArray<unknown>(work.files as unknown)
const modelFileUrls = collectModelUrlsFromFiles(rawFiles)
let previewUrls = parseJsonArray<string>(work.previewUrls as unknown).map((u) =>
String(u).trim()
)
if (previewUrls.length === 0 && work.previewUrl) {
previewUrls = [work.previewUrl]
}
previewUrls = previewUrls.filter(Boolean).map((u) => normalizeContestWorkAssetUrl(u))
if (previewUrls.length > 0) {
const pages: ContestWorkPreviewPage[] = previewUrls.map((imageUrl, i) => {
const fromIndex = readPageMetaFromFileEntry(rawFiles[i])
let text = fromIndex.text
let audioUrl = fromIndex.audioUrl
let pageNo = fromIndex.pageNo ?? i + 1
if (!text || !audioUrl) {
const byNo = findFileMetaByPageNo(rawFiles, i + 1)
if (!text && byNo.text) text = byNo.text
if (!audioUrl && byNo.audioUrl) audioUrl = byNo.audioUrl
}
return { imageUrl, text, audioUrl, pageNo }
})
return { pages, modelFileUrls }
}
const structured = rawFiles.filter((f): f is Record<string, unknown> => {
if (typeof f !== "object" || f === null) return false
const o = f as Record<string, unknown>
return typeof o.imageUrl === "string" && String(o.imageUrl).trim() !== ""
})
if (structured.length > 0) {
const sorted = [...structured].sort((a, b) => {
const pa = Number(a.pageNo) || 0
const pb = Number(b.pageNo) || 0
return pa - pb
})
const pages: ContestWorkPreviewPage[] = sorted.map((o) => {
const imageUrl = normalizeContestWorkAssetUrl(String(o.imageUrl))
const text = typeof o.text === "string" && o.text.trim() ? o.text : undefined
const audioUrl =
typeof o.audioUrl === "string" && o.audioUrl.trim()
? normalizeContestWorkAssetUrl(o.audioUrl)
: undefined
const pageNo =
typeof o.pageNo === "number"
? o.pageNo
: typeof o.pageNo === "string"
? Number(o.pageNo) || undefined
: undefined
return { imageUrl, text, audioUrl, pageNo }
})
return { pages, modelFileUrls }
}
for (const s of rawFiles) {
if (typeof s === "string" && isImageFileName(s)) {
return {
pages: [{ imageUrl: normalizeContestWorkAssetUrl(s) }],
modelFileUrls,
}
}
}
if (work.previewUrl) {
return {
pages: [{ imageUrl: normalizeContestWorkAssetUrl(work.previewUrl) }],
modelFileUrls,
}
}
const firstImg = firstImageAttachmentUrl(work)
if (firstImg) {
return { pages: [{ imageUrl: firstImg }], modelFileUrls }
}
return { pages: [], modelFileUrls }
}
export function useContestWorkPreviewPages(workRef: Ref<ContestWork | null | undefined>) {
const previewResult = computed(() => buildContestWorkPreviewPages(workRef.value))
const pages = computed(() => previewResult.value.pages)
const modelFileUrls = computed(() => previewResult.value.modelFileUrls)
return { pages, modelFileUrls, previewResult }
}