import { Injectable } from '@angular/core'; import { AuthCreditService } from './auth-credit.service'; import { GenerationTemplate, GenerationTemplateInput } from '../models/template.model'; import { PipelineRoute } from '../pipelines/pipeline-registry'; const STORAGE_KEY = 'videoWorkflow.templates.user.v1'; @Injectable({ providedIn: 'root' }) export class TemplateService { private readonly systemTemplates: GenerationTemplate[] = this.buildSystemTemplates(); constructor(private auth: AuthCreditService) {} listTemplates(): GenerationTemplate[] { const userId = this.auth.currentUser?.objectId || ''; const userTemplates = this.readUserTemplates().filter((item) => item.userId === userId); return [...this.systemTemplates, ...userTemplates].sort((a, b) => { if (a.scope !== b.scope) return a.scope === 'system' ? -1 : 1; return Date.parse(b.updatedAt) - Date.parse(a.updatedAt); }); } listByPipeline(pipelineId: PipelineRoute): GenerationTemplate[] { return this.listTemplates().filter((item) => item.pipelineId === pipelineId); } createUserTemplate(input: GenerationTemplateInput): GenerationTemplate { const userId = this.requireUserId(); const now = new Date().toISOString(); const template: GenerationTemplate = { ...input, id: input.id || this.createId(), userId, scope: 'user', tags: this.uniqueTags(input.tags || []), config: this.cloneSerializable(input.config || {}), version: input.version || 1, usageCount: 0, createdAt: now, updatedAt: now, }; const items = this.readUserTemplates(); items.unshift(template); this.writeUserTemplates(items); return template; } cloneToUser(templateId: string): GenerationTemplate { const source = this.listTemplates().find((item) => item.id === templateId); if (!source) throw new Error('未找到模板'); return this.createUserTemplate({ pipelineId: source.pipelineId, name: `${source.name} 副本`, description: source.description || '', category: source.category, tags: source.tags || [], config: this.cloneSerializable(source.config || {}), preview: source.preview, version: source.version, }); } updateUserTemplate(id: string, patch: Partial): GenerationTemplate { const userId = this.requireUserId(); const items = this.readUserTemplates(); const index = items.findIndex((item) => item.id === id && item.userId === userId); if (index < 0) throw new Error('未找到可编辑的模板'); const updated: GenerationTemplate = { ...items[index], ...patch, id: items[index].id, userId, scope: 'user', tags: patch.tags ? this.uniqueTags(patch.tags) : items[index].tags, config: patch.config ? this.cloneSerializable(patch.config) : items[index].config, updatedAt: new Date().toISOString(), }; items[index] = updated; this.writeUserTemplates(items); return updated; } deleteUserTemplate(id: string): void { const userId = this.requireUserId(); this.writeUserTemplates(this.readUserTemplates().filter((item) => !(item.id === id && item.userId === userId))); } markUsed(id: string): void { const userId = this.auth.currentUser?.objectId || ''; const items = this.readUserTemplates(); const index = items.findIndex((item) => item.id === id && item.userId === userId); if (index < 0) return; items[index] = { ...items[index], usageCount: Number(items[index].usageCount || 0) + 1, updatedAt: new Date().toISOString(), }; this.writeUserTemplates(items); } private requireUserId(): string { const userId = this.auth.currentUser?.objectId; if (!userId) { this.auth.requestLogin('模板库'); throw new Error('请先登录后保存模板'); } return userId; } private readUserTemplates(): GenerationTemplate[] { try { const raw = localStorage.getItem(STORAGE_KEY); const parsed = raw ? JSON.parse(raw) : []; return Array.isArray(parsed) ? parsed : []; } catch { return []; } } private writeUserTemplates(items: GenerationTemplate[]): void { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); } private createId(): string { return `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } private uniqueTags(tags: string[]): string[] { return tags.map((item) => String(item || '').trim()).filter((item, index, list) => !!item && list.indexOf(item) === index).slice(0, 12); } private cloneSerializable(value: T): T { return JSON.parse(JSON.stringify(value || {})); } private buildSystemTemplates(): GenerationTemplate[] { const now = '2026-06-01T00:00:00.000Z'; return [ { id: 'sys_topic_knowledge_short', scope: 'system', pipelineId: 'topic-to-video', name: '知识口播短平快', description: '适合把一个观点拆成 3-5 个短分镜,节奏清晰,适合日更知识内容。', category: '知识科普', tags: ['口播', '知识', '日更'], config: { nScenes: 4, aspect: 'portrait', quickMode: true, mediaMode: 'image', clipFrames: 121, clipQuality: '720p', continuityEnabled: true, visualBible: { subject: '自信讲述者或干净的知识符号主体', product: '', location: '极简演播室或清爽信息图背景', palette: '白色、蓝色、高对比强调色', lighting: '柔和棚拍光', camera: '中近景,构图干净', styleNotes: '轻量信息图风格,大号关键词,层级清晰', constraints: '保持统一配色和讲述者风格', }, scriptFrame: '开头提出反常识观点 -> 三点解释 -> 结尾抛出行动建议', }, version: 1, usageCount: 0, createdAt: now, updatedAt: now, }, { id: 'sys_topic_product_seed', scope: 'system', pipelineId: 'topic-to-video', name: '产品种草结构', description: '从用户痛点切入,突出使用场景、差异点和购买理由。', category: '产品种草', tags: ['种草', '产品', '转化'], config: { nScenes: 5, aspect: 'portrait', quickMode: true, mediaMode: 'video', clipFrames: 121, clipQuality: '720p', continuityEnabled: true, visualBible: { subject: '日常使用场景中的目标用户', product: '细节清晰的主推产品', location: '明亮的生活方式场景', palette: '温暖中性色,搭配清新的强调色', lighting: '自然日光', camera: '产品细节特写与平滑运镜', styleNotes: '真实产品广告质感,构图干净', constraints: '保持产品颜色和形状一致', }, scriptFrame: '痛点场景 -> 产品出现 -> 三个卖点 -> 使用前后对比 -> 轻 CTA', }, version: 1, usageCount: 0, createdAt: now, updatedAt: now, }, { id: 'sys_image_cover_clean', scope: 'system', pipelineId: 'image-generation', name: '清爽封面图', description: '适合生成视频封面、知识卡片和素材库配图。', category: '其他', tags: ['封面', '信息图', '浅色'], config: { width: 1440, height: 1920, stylePrompt: '清爽浅色背景,主体明确,大标题留白,少量高对比强调色', }, version: 1, usageCount: 0, createdAt: now, updatedAt: now, }, { id: 'sys_dh_explainer', scope: 'system', pipelineId: 'digital-human', name: '数字人讲解', description: '适合课程介绍、产品说明、短口播讲解。', category: '口播', tags: ['数字人', '讲解', '口播'], config: { resolution: '1080p', scriptFrame: '问候 -> 背景问题 -> 核心说明 -> 适用人群 -> 行动建议', voiceMood: '自然、清晰、可信', }, version: 1, usageCount: 0, createdAt: now, updatedAt: now, }, ]; } }