operations-special-training.component.html 9.3 KB

12345678
  1. <header class="page-header"><div><p>CONTENT OPERATIONS</p><h1>专项训练</h1><span>维护自然拼读、阅读理解、完形填空和语法专项的图文与视频内容</span></div><div class="header-actions"><button class="ui-button secondary" type="button" (click)="refresh()" [disabled]="loading()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="loading()"/>刷新</button><button class="ui-button primary" type="button" (click)="openCreate()"><lucide-icon [img]="icons.Plus" [size]="17"/>新增专项</button></div></header>
  2. <section class="summary-grid"><article class="summary-card"><span>专项内容</span><strong>{{page()?.total||0}}</strong><small>栏目 3 · 图文/视频内容</small></article><article class="summary-card"><span>已发布</span><strong>{{publishedCount()}}</strong><small>C 端当前可见</small></article><article class="summary-card"><span>草稿</span><strong>{{draftCount()}}</strong><small>仅运营后台可见</small></article><article class="summary-card"><span>内容待补</span><strong>{{mediaMissingCount()}}</strong><small>正文和视频均为空</small></article></section>
  3. @if(page()?.sourceStatus;as source){<p class="sync-status" [class.error]="!source.available" role="status"><span></span>{{source.message}} · 旧后台 {{source.legacyTotal}} 条 · 新增 {{source.created}} 条 · 更新 {{source.updated}} 条</p>}
  4. <section class="toolbar"><label class="grow"><span>搜索内容</span><input [ngModel]="query()" (ngModelChange)="query.set($event)" (keyup.enter)="load()" placeholder="标题或简介"/></label><label><span>发布状态</span><select [ngModel]="status()" (ngModelChange)="status.set($event)"><option value="">全部</option><option value="99">已发布</option><option value="0">草稿</option><option value="-2">回收站</option></select></label><button class="ui-button primary" type="button" (click)="load()"><lucide-icon [img]="icons.Search" [size]="17"/>查询</button><div class="segmented"><button type="button" [class.active]="view()==='cards'" (click)="view.set('cards')" title="卡片视图"><lucide-icon [img]="icons.LayoutGrid" [size]="17"/></button><button type="button" [class.active]="view()==='list'" (click)="view.set('list')" title="列表视图"><lucide-icon [img]="icons.List" [size]="17"/></button></div></section>
  5. @if(message()){<p class="notice" role="status">{{message()}}</p>}@if(error()&&!editorOpen()){<p class="notice error" role="alert">{{error()}}</p>}
  6. @if(view()==='cards'){<section class="special-grid">@for(row of page()?.items||[];track row.targetId){<article class="special-card"><div class="cover">@if(row.coverUrl){<img [src]="row.coverUrl" [alt]="row.title"/>}@else{<lucide-icon [img]="icons.BookOpen" [size]="36"/>}<span class="badge" [class.warn]="row.status===0" [class.muted]="row.status===-2">{{row.statusLabel}}</span></div><div class="card-body"><small>ID {{row.generalId}}</small><h2>{{row.title}}</h2><p>{{row.synopsis||'尚未填写简介'}}</p><div class="tags">@for(tag of row.tags;track tag){<span>{{tag}}</span>}@if(row.videoUrl){<span class="media-tag"><lucide-icon [img]="icons.Video" [size]="12"/>视频</span>}@if(row.contentHtml){<span class="media-tag"><lucide-icon [img]="icons.BookOpen" [size]="12"/>图文</span>}</div><footer><span>{{row.updatedAt|date:'yyyy-MM-dd HH:mm'}}</span><div class="row-actions"><button type="button" (click)="preview(row)" title="预览"><lucide-icon [img]="icons.Eye" [size]="15"/></button><button type="button" (click)="openEdit(row)" title="编辑"><lucide-icon [img]="icons.Pencil" [size]="15"/></button>@if(row.status===0){<button type="button" class="publish" (click)="lifecycle(row,'publish')">发布</button>}@if(row.status===99){<button type="button" (click)="lifecycle(row,'unpublish')">下架</button>}@if(row.status===-2){<button type="button" (click)="lifecycle(row,'restore')"><lucide-icon [img]="icons.RotateCcw" [size]="15"/></button>}@else{<button type="button" class="danger" (click)="lifecycle(row,'recycle')"><lucide-icon [img]="icons.Trash2" [size]="15"/></button>}</div></footer></div></article>}@empty{<div class="empty empty-card">没有符合条件的专项训练内容</div>}</section>}@else{<section class="content-card"><div class="table-scroll"><table class="data-table"><thead><tr><th>专项</th><th>简介</th><th>内容</th><th>状态</th><th>排序</th><th>更新时间</th><th>操作</th></tr></thead><tbody>@for(row of page()?.items||[];track row.targetId){<tr><td><strong>{{row.title}}</strong><small>ID {{row.generalId}}</small></td><td>{{row.synopsis||'--'}}</td><td><span class="content-flags"><i [class.ready]="row.contentHtml"><lucide-icon [img]="icons.BookOpen" [size]="14"/>正文</i><i [class.ready]="row.videoUrl"><lucide-icon [img]="icons.Video" [size]="14"/>视频</i></span></td><td><span class="badge" [class.warn]="row.status===0" [class.muted]="row.status===-2">{{row.statusLabel}}</span></td><td>{{row.orderId}}</td><td>{{row.updatedAt|date:'yyyy-MM-dd HH:mm'}}</td><td><div class="row-actions"><button type="button" (click)="preview(row)"><lucide-icon [img]="icons.Eye" [size]="15"/></button><button type="button" (click)="openEdit(row)"><lucide-icon [img]="icons.Pencil" [size]="15"/></button></div></td></tr>}@empty{<tr><td colspan="7"><div class="empty">没有符合条件的专项训练内容</div></td></tr>}</tbody></table></div></section>}
  7. @if(editorOpen()){<div class="backdrop" (click)="closeEditor()"></div><aside class="drawer editor-drawer" role="dialog" aria-modal="true"><header><div><p>SPECIAL TRAINING</p><h2>{{draft().targetId?'编辑专项训练':'新增专项训练'}}</h2><span>保存草稿后,再由运营经理预览并发布</span></div><button class="icon-action" type="button" (click)="closeEditor()"><lucide-icon [img]="icons.X" [size]="19"/></button></header>@if(error()){<p class="notice error">{{error()}}</p>}<div class="form-grid"><label class="wide"><span>标题 *</span><input [ngModel]="draft().title" (ngModelChange)="updateDraft('title',$event)" maxlength="200"/></label><label class="wide"><span>简介</span><textarea rows="3" [ngModel]="draft().synopsis" (ngModelChange)="updateDraft('synopsis',$event)" maxlength="4000"></textarea></label><label class="wide"><span>封面(统一 4:3)</span><div class="media-input"><input [ngModel]="draft().coverUrl" (ngModelChange)="updateDraft('coverUrl',$event)" placeholder="旧 /UploadFiles/... 或新 CDN 地址"/><label class="ui-button secondary file-button"><lucide-icon [img]="icons.Image" [size]="16"/>上传并裁切<input type="file" accept="image/jpeg,image/png,image/webp,image/gif" (change)="upload($event,'image')"/></label></div>@if(draft().coverUrl){<div class="cover-preview"><img [src]="draft().coverUrl" [alt]="draft().title||'封面预览'"/><small>4:3 居中裁切预览</small></div>}@else{<small>未设置时会自动读取正文第一张图片,并按 4:3 居中裁切显示。</small>}</label><label class="wide"><span>标签</span><input [ngModel]="tagsText()" (ngModelChange)="tagsText.set($event)" placeholder="用逗号分隔"/></label><label><span>作者</span><input [ngModel]="draft().author" (ngModelChange)="updateDraft('author',$event)"/></label><label><span>来源</span><input [ngModel]="draft().source" (ngModelChange)="updateDraft('source',$event)"/></label><label class="wide"><span>图文正文</span><textarea class="html-editor" rows="14" [ngModel]="draft().contentHtml" (ngModelChange)="updateDraft('contentHtml',$event)" placeholder="支持安全 HTML;脚本、事件属性和危险链接会由服务端清理"></textarea><small>发布前会执行 HTML 白名单清理。</small></label><label class="wide"><span>视频</span><div class="media-input"><input [ngModel]="draft().videoUrl" (ngModelChange)="updateDraft('videoUrl',$event)" placeholder="视频 URL"/><label class="ui-button secondary file-button"><lucide-icon [img]="icons.Upload" [size]="16"/>上传视频<input type="file" accept="video/mp4,video/webm" (change)="upload($event,'video')"/></label></div></label><label><span>排序</span><input type="number" [ngModel]="draft().orderId" (ngModelChange)="updateDraft('orderId',+$event)"/></label><label class="wide"><span>操作原因 *</span><textarea rows="3" [ngModel]="reason()" (ngModelChange)="reason.set($event)" placeholder="用于操作审计"></textarea></label></div><footer><button class="ui-button secondary push-left" type="button" (click)="preview()"><lucide-icon [img]="icons.Eye" [size]="16"/>预览</button><button class="ui-button secondary" type="button" (click)="closeEditor()">取消</button><button class="ui-button primary" type="button" (click)="save()" [disabled]="saving()">{{saving()?'保存中…':'保存草稿'}}</button></footer></aside>}
  8. @if(previewOpen()&&previewItem();as item){<div class="backdrop preview-backdrop" (click)="previewOpen.set(false)"></div><aside class="drawer preview-drawer" role="dialog" aria-modal="true"><header><div><p>CONTENT PREVIEW</p><h2>{{item.title||'未命名专项'}}</h2></div><button class="icon-action" type="button" (click)="previewOpen.set(false)"><lucide-icon [img]="icons.X" [size]="19"/></button></header>@if(item.coverUrl){<img class="preview-cover" [src]="item.coverUrl" [alt]="item.title"/>}<p class="preview-synopsis">{{item.synopsis}}</p>@if(item.videoUrl){<video class="preview-video" [src]="item.videoUrl" controls preload="metadata"></video>}<article class="preview-content" [innerHTML]="item.contentHtml"></article></aside>}