493d175e87
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 48s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 47s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m36s
CI/CD Pipeline / Integration Tests (push) Successful in 3m38s
CI/CD Pipeline / Validate - Style (push) Successful in 4m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m23s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m24s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m51s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m30s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m35s
CI/CD Pipeline / Validate - Security (push) Successful in 10m3s
CI/CD Pipeline / Unit Tests (push) Successful in 12m44s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Failing after 28h16m55s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 28h26m37s
CI/CD Pipeline / PR Build Web Image (push) Failing after 28h29m47s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 28h29m9s
CI/CD Pipeline / PR Build API Image (push) Failing after 28h29m9s
CI/CD Pipeline / Deploy Production (push) Failing after 28h16m15s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 28h16m17s
CI/CD Pipeline / Build Production Web Image (push) Failing after 28h16m17s
CI/CD Pipeline / Build Production API Image (push) Failing after 28h16m17s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 28h25m59s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 28h25m59s
CI/CD Pipeline / Frontend Lint (push) Failing after 28h29m3s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 28h29m10s
CI/CD Pipeline / Canary Release to Production (push) Failing after 28h16m15s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
196 lines
6.3 KiB
TypeScript
196 lines
6.3 KiB
TypeScript
/**
|
||
* 文案库 API(#1811 v2)
|
||
* CRUD + 搜索/分类/分页;后端未就绪时使用 mock 数据(SCRIPTS_API_MOCK=true)
|
||
*/
|
||
import apiClient from "../client"
|
||
import type {
|
||
ScriptItem,
|
||
ScriptListParams,
|
||
ScriptListResponse,
|
||
ScriptUpsertRequest,
|
||
ScriptCategory,
|
||
} from "./types"
|
||
|
||
/** 是否启用 mock(后端合入后改为 false) */
|
||
export const SCRIPTS_API_MOCK = true
|
||
|
||
// ==================== Mock 数据 ====================
|
||
|
||
const MOCK_CATEGORIES: ScriptCategory[] = [
|
||
"promo",
|
||
"vlog",
|
||
"knowledge",
|
||
"story",
|
||
"emotion",
|
||
"other",
|
||
]
|
||
|
||
const SAMPLE_TITLES: Record<ScriptCategory, string[]> = {
|
||
promo: ["新品上市限时特惠", "618大促开场", "品牌故事宣传片"],
|
||
vlog: ["周末citywalk记录", "打工人的一天", "探店vlog"],
|
||
knowledge: ["3分钟学会XX", "冷知识科普", "行业深度解读"],
|
||
story: ["励志小故事", "情感故事一则", "反转剧情"],
|
||
emotion: ["深夜emo时刻", "治愈系文案", "朋友圈金句"],
|
||
other: ["通用开场白", "节日祝福", "万能结尾"],
|
||
}
|
||
|
||
const SAMPLE_TAGS = ["热门", "新品", "节日", "情感", "干货", "搞笑", "治愈", "励志"]
|
||
|
||
function genMockScripts(): ScriptItem[] {
|
||
const items: ScriptItem[] = []
|
||
const now = Date.now()
|
||
let idx = 0
|
||
for (const cat of MOCK_CATEGORIES) {
|
||
const titles = SAMPLE_TITLES[cat]
|
||
for (let i = 0; i < titles.length; i++) {
|
||
idx++
|
||
const title = titles[i]
|
||
const content = `这是一条【${cat}】分类下的示例文案,标题为「${title}」。\n\n正文可以包含多段落,每段对应一个片段(segments)。\n\n此为 mock 数据,后端接口就绪后会自动切换为真实数据。`
|
||
const segments = content.split(/\n\n+/).filter(Boolean)
|
||
const tagCount = 1 + (idx % 3)
|
||
const tags: string[] = []
|
||
for (let t = 0; t < tagCount; t++) {
|
||
tags.push(SAMPLE_TAGS[(idx + t) % SAMPLE_TAGS.length])
|
||
}
|
||
items.push({
|
||
id: `mock_${idx}`,
|
||
title,
|
||
content,
|
||
segments,
|
||
tags,
|
||
title_text: title,
|
||
title_category: cat,
|
||
title_config: {
|
||
font: "default",
|
||
color: "#ffffff",
|
||
stroke: "#000000",
|
||
position: (["top", "center", "bottom"] as const)[idx % 3],
|
||
size: 48,
|
||
bold: idx % 2 === 0,
|
||
italic: false,
|
||
},
|
||
char_count: content.length,
|
||
use_count: Math.floor(Math.random() * 50),
|
||
created_at: new Date(now - idx * 86400_000 * 2).toISOString(),
|
||
updated_at: new Date(now - idx * 86400_000).toISOString(),
|
||
})
|
||
}
|
||
}
|
||
return items
|
||
}
|
||
|
||
const MOCK_SCRIPTS = genMockScripts()
|
||
|
||
// ==================== 真实 API ====================
|
||
|
||
/** 获取文案列表(支持分页/搜索/分类) */
|
||
export async function getScripts(params: ScriptListParams = {}): Promise<ScriptListResponse> {
|
||
if (SCRIPTS_API_MOCK) {
|
||
const page = params.page ?? 1
|
||
const pageSize = params.page_size ?? 20
|
||
let items = [...MOCK_SCRIPTS]
|
||
if (params.keyword) {
|
||
const kw = params.keyword.toLowerCase()
|
||
items = items.filter(
|
||
(s) => s.title.toLowerCase().includes(kw) || s.content.toLowerCase().includes(kw),
|
||
)
|
||
}
|
||
if (params.category && params.category !== "all") {
|
||
items = items.filter((s) => s.title_category === params.category)
|
||
}
|
||
if (params.tag) {
|
||
items = items.filter((s) => s.tags?.includes(params.tag as string))
|
||
}
|
||
const total = items.length
|
||
const start = (page - 1) * pageSize
|
||
const pageItems = items.slice(start, start + pageSize)
|
||
return new Promise((r) =>
|
||
setTimeout(() => r({ items: pageItems, total, page, page_size: pageSize }), 200),
|
||
)
|
||
}
|
||
const res = await apiClient.get<ScriptListResponse>("/scripts", { params })
|
||
return res.data
|
||
}
|
||
|
||
/** 获取单条文案详情 */
|
||
export async function getScript(id: string): Promise<ScriptItem> {
|
||
if (SCRIPTS_API_MOCK) {
|
||
const item = MOCK_SCRIPTS.find((s) => s.id === id)
|
||
return new Promise((r) => setTimeout(() => r(item ?? MOCK_SCRIPTS[0]), 120))
|
||
}
|
||
const res = await apiClient.get<ScriptItem>(`/scripts/${id}`)
|
||
return res.data
|
||
}
|
||
|
||
/** 新建文案 */
|
||
export async function createScript(data: ScriptUpsertRequest): Promise<ScriptItem> {
|
||
if (SCRIPTS_API_MOCK) {
|
||
const segments =
|
||
data.segments && data.segments.length > 0
|
||
? data.segments
|
||
: data.content.split(/\n\n+/).filter(Boolean)
|
||
const item: ScriptItem = {
|
||
id: `mock_${Date.now()}`,
|
||
...data,
|
||
segments,
|
||
char_count: data.content.length,
|
||
use_count: 0,
|
||
tags: data.tags ?? [],
|
||
created_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString(),
|
||
}
|
||
MOCK_SCRIPTS.unshift(item)
|
||
return new Promise((r) => setTimeout(() => r(item), 200))
|
||
}
|
||
const res = await apiClient.post<ScriptItem>("/scripts", data)
|
||
return res.data
|
||
}
|
||
|
||
/** 更新文案 */
|
||
export async function updateScript(id: string, data: ScriptUpsertRequest): Promise<ScriptItem> {
|
||
if (SCRIPTS_API_MOCK) {
|
||
const idx = MOCK_SCRIPTS.findIndex((s) => s.id === id)
|
||
const segments =
|
||
data.segments && data.segments.length > 0
|
||
? data.segments
|
||
: data.content.split(/\n\n+/).filter(Boolean)
|
||
const updated: ScriptItem = {
|
||
...MOCK_SCRIPTS[idx],
|
||
...data,
|
||
segments,
|
||
char_count: data.content.length,
|
||
tags: data.tags ?? MOCK_SCRIPTS[idx]?.tags ?? [],
|
||
updated_at: new Date().toISOString(),
|
||
}
|
||
if (idx >= 0) MOCK_SCRIPTS[idx] = updated
|
||
return new Promise((r) => setTimeout(() => r(updated), 200))
|
||
}
|
||
const res = await apiClient.put<ScriptItem>(`/scripts/${id}`, data)
|
||
return res.data
|
||
}
|
||
|
||
/** 删除文案 */
|
||
export async function deleteScript(id: string): Promise<void> {
|
||
if (SCRIPTS_API_MOCK) {
|
||
const idx = MOCK_SCRIPTS.findIndex((s) => s.id === id)
|
||
if (idx >= 0) MOCK_SCRIPTS.splice(idx, 1)
|
||
return new Promise((r) => setTimeout(r, 150))
|
||
}
|
||
await apiClient.delete(`/scripts/${id}`)
|
||
}
|
||
|
||
/** 复制文案(返回新副本) */
|
||
export async function duplicateScript(id: string): Promise<ScriptItem> {
|
||
const orig = await getScript(id)
|
||
const copy = await createScript({
|
||
title: `${orig.title}(副本)`,
|
||
content: orig.content,
|
||
segments: orig.segments,
|
||
tags: orig.tags,
|
||
title_text: orig.title_text,
|
||
title_category: orig.title_category,
|
||
title_config: orig.title_config,
|
||
})
|
||
return copy
|
||
}
|