|
|
@@ -3,11 +3,12 @@
|
|
|
<div class="dh-header-icon"><span class="icon">hub</span></div>
|
|
|
<div class="dh-header-text">
|
|
|
<h1>IP操盘工作台</h1>
|
|
|
- <p>从建档、轻问卷、对标账号,到定位诊断、选题脚本和 7 天测试计划,形成可继续打磨的起步方案。</p>
|
|
|
+ <p>围绕真实账号证据推进诊断、定位、任务、选题和发布复盘,低价值历史工具不再占用主流程。</p>
|
|
|
</div>
|
|
|
<div class="ip-header-actions">
|
|
|
- <button class="btn" type="button" (click)="startNewProfile()">新建档案</button>
|
|
|
- <button class="btn" type="button" (click)="reloadState()">刷新记录</button>
|
|
|
+ <button class="btn" type="button" [disabled]="workspaceRefreshing()" (click)="refreshWorkspaceState()">
|
|
|
+ {{ workspaceRefreshing() ? '同步中...' : '刷新记录' }}
|
|
|
+ </button>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
@@ -18,726 +19,1379 @@
|
|
|
<button class="btn btn-sm" type="button" (click)="clearTemporaryData()">清空临时资料</button>
|
|
|
</div>
|
|
|
|
|
|
- <div class="ip-shell">
|
|
|
- <aside class="ip-side">
|
|
|
- <h3>IP 档案</h3>
|
|
|
- <button class="ip-profile-item"
|
|
|
- type="button"
|
|
|
- *ngFor="let profile of profiles(); trackBy: trackById"
|
|
|
- (click)="loadProfile(profile)">
|
|
|
- <strong>{{ profile.name }}</strong>
|
|
|
- <span>{{ profile.identity }} / {{ profile.industry }}</span>
|
|
|
- </button>
|
|
|
+ <section class="ip-card account-workbench">
|
|
|
+ <div class="operator-cockpit">
|
|
|
+ <div class="operator-cockpit__main">
|
|
|
+ <p class="account-workbench-kicker">账号驱动</p>
|
|
|
+ <h2>账号运营驾驶舱</h2>
|
|
|
+ <p>先看当前账号状态、定位版本和今天动作,再进入诊断、定位、任务和选题工作区。</p>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="ip-audit">
|
|
|
- <h3>修改记录</h3>
|
|
|
- <p *ngIf="!auditTrail().length">暂无记录</p>
|
|
|
- <article *ngFor="let event of auditTrail() | slice:0:8; trackBy: trackById">
|
|
|
- <strong>{{ event.summary }}</strong>
|
|
|
- <span>{{ event.createdAt | date:'MM-dd HH:mm:ss' }}</span>
|
|
|
- </article>
|
|
|
+ <div class="operator-cockpit__status" *ngIf="activeAccount() as account">
|
|
|
+ <section>
|
|
|
+ <span>当前账号</span>
|
|
|
+ <strong>{{ account.displayName }}</strong>
|
|
|
+ <small>{{ account.homepageUrl || account.uniqueId || account.secUserId || account.accountId }}</small>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>刷新状态</span>
|
|
|
+ <strong>{{ accountRefreshStatusLabel(account.lastRefreshStatus) }}</strong>
|
|
|
+ <small>{{ account.lastRefreshedAt ? (account.lastRefreshedAt | date:'MM-dd HH:mm') : '尚未刷新' }}</small>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>诊断模式</span>
|
|
|
+ <strong>{{ activeSnapshot()?.dataMode === 'data_diagnosis' ? '数据诊断' : '起号假设' }}</strong>
|
|
|
+ <small>{{ activeSnapshot() ? '基于账号快照' : '等待刷新账号数据' }}</small>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>定位版本</span>
|
|
|
+ <strong>{{ activePositioningVersion()?.persona || '待确认' }}</strong>
|
|
|
+ <small>{{ activePositioningVersion() ? '已生效' : '请先确认定位提案' }}</small>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
- </aside>
|
|
|
-
|
|
|
- <main class="ip-main">
|
|
|
- <ol class="ip-steps">
|
|
|
- <li [class.is-active]="step() === 1">1 建档</li>
|
|
|
- <li [class.is-active]="step() === 2">2 轻问卷</li>
|
|
|
- <li [class.is-active]="step() === 3">3 对标</li>
|
|
|
- <li [class.is-active]="step() === 4">4 生成</li>
|
|
|
- <li [class.is-active]="step() === 5">5 方案</li>
|
|
|
- </ol>
|
|
|
-
|
|
|
- <section class="ip-card" *ngIf="step() === 1">
|
|
|
- <h2>创建 IP 档案</h2>
|
|
|
- <div class="ip-form-grid">
|
|
|
- <label>IP 名称<input [ngModel]="profileDraft().name" (ngModelChange)="updateProfileDraft({name: $event})"></label>
|
|
|
- <label>当前身份<input [ngModel]="profileDraft().identity" (ngModelChange)="updateProfileDraft({identity: $event})"></label>
|
|
|
- <label>所在行业<input [ngModel]="profileDraft().industry" (ngModelChange)="updateProfileDraft({industry: $event})"></label>
|
|
|
- <label>账号阶段
|
|
|
- <select [ngModel]="profileDraft().stage" (ngModelChange)="updateProfileDraft({stage: $event})">
|
|
|
- <option value="new">未开始</option>
|
|
|
- <option value="started">刚注册/少量内容</option>
|
|
|
- <option value="restart">老号重启</option>
|
|
|
- <option value="existing">已有稳定账号</option>
|
|
|
- </select>
|
|
|
- </label>
|
|
|
- <label>目标用户<input [ngModel]="profileDraft().targetAudience" (ngModelChange)="updateProfileDraft({targetAudience: $event})"></label>
|
|
|
- <label>产品/服务<input [ngModel]="profileDraft().productsOrServices" (ngModelChange)="updateProfileDraft({productsOrServices: $event})"></label>
|
|
|
- </div>
|
|
|
- <label>用户痛点<textarea rows="3" [ngModel]="profileDraft().audiencePainPointsText" (ngModelChange)="updateProfileDraft({audiencePainPointsText: $event})"></textarea></label>
|
|
|
- <label>个人经历<textarea rows="3" [ngModel]="profileDraft().personalStoriesText" (ngModelChange)="updateProfileDraft({personalStoriesText: $event})"></textarea></label>
|
|
|
- <label>专业能力<textarea rows="3" [ngModel]="profileDraft().expertiseText" (ngModelChange)="updateProfileDraft({expertiseText: $event})"></textarea></label>
|
|
|
- <label>表达风格<textarea rows="2" [ngModel]="profileDraft().expressionStyleText" (ngModelChange)="updateProfileDraft({expressionStyleText: $event})"></textarea></label>
|
|
|
- <label>不想讲的内容<textarea rows="2" [ngModel]="profileDraft().boundariesText" (ngModelChange)="updateProfileDraft({boundariesText: $event})"></textarea></label>
|
|
|
- <label>期望结果<textarea rows="2" [ngModel]="profileDraft().goalsText" (ngModelChange)="updateProfileDraft({goalsText: $event})"></textarea></label>
|
|
|
- <div class="ip-actions">
|
|
|
- <button class="btn btn-primary" type="button" (click)="step.set(2)">下一步:轻问卷</button>
|
|
|
+
|
|
|
+ <section class="account-lifecycle-card" *ngIf="accountLifecycleState() as state">
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">当前阶段</span>
|
|
|
+ <h3>{{ state.title }}</h3>
|
|
|
+ <p>{{ state.description }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="account-lifecycle-card__next">
|
|
|
+ <strong>下一步:{{ state.nextActionLabel }}</strong>
|
|
|
+ <small *ngIf="state.blockers.length">待补齐:{{ state.blockers.join('、') }}</small>
|
|
|
+ <small *ngIf="!state.blockers.length">当前没有关键阻塞,可以进入下一轮分析或执行。</small>
|
|
|
</div>
|
|
|
+ <ul *ngIf="state.dataSignals.length">
|
|
|
+ <li *ngFor="let signal of state.dataSignals">{{ signal }}</li>
|
|
|
+ </ul>
|
|
|
</section>
|
|
|
|
|
|
- <section class="ip-card" *ngIf="step() === 2">
|
|
|
- <h2>轻问卷</h2>
|
|
|
- <label *ngFor="let question of questionnaire; let i = index; trackBy: trackByIndex">
|
|
|
- {{ i + 1 }}. {{ question }}
|
|
|
- <textarea rows="2" [ngModel]="questionnaireAnswers()[i]" (ngModelChange)="updateQuestionAnswer(i, $event)"></textarea>
|
|
|
+ <div class="operator-cockpit__empty" *ngIf="!activeAccount()">
|
|
|
+ <strong>先绑定抖音账号</strong>
|
|
|
+ <span>绑定后才能刷新公开主页、作品和评论样本。</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="account-bind-row">
|
|
|
+ <label>
|
|
|
+ 绑定抖音账号
|
|
|
+ <input
|
|
|
+ [ngModel]="accountInput()"
|
|
|
+ (ngModelChange)="accountInput.set($event)"
|
|
|
+ placeholder="粘贴抖音主页链接或账号 ID">
|
|
|
</label>
|
|
|
- <div class="ip-actions">
|
|
|
- <button class="btn" type="button" (click)="step.set(1)">上一步</button>
|
|
|
- <button class="btn btn-primary" type="button" (click)="step.set(3)">下一步:对标账号</button>
|
|
|
+ <button class="btn btn-primary" type="button" [disabled]="primaryAction().disabled" (click)="runPrimaryAction()">
|
|
|
+ {{ primaryAction().label }}
|
|
|
+ </button>
|
|
|
+ <button class="btn" type="button" [disabled]="accountRefreshing() || !activeAccount()" (click)="refreshActiveAccount()">
|
|
|
+ {{ accountRefreshing() ? '刷新中...' : '刷新账号数据' }}
|
|
|
+ </button>
|
|
|
+ <button class="btn" type="button" [disabled]="workspaceRefreshing()" (click)="refreshWorkspaceState()">
|
|
|
+ {{ workspaceRefreshing() ? '同步中...' : '刷新工作台' }}
|
|
|
+ </button>
|
|
|
+ <button class="btn" type="button" [disabled]="!activeSnapshot() || accountDiagnosisRunning()" (click)="runAccountDiagnosis()">
|
|
|
+ {{ accountDiagnosisRunning() ? '正在分析Top3作品...' : '分析账号定位' }}
|
|
|
+ </button>
|
|
|
+ <button class="btn" type="button" [disabled]="!activeSnapshot() || accountDiagnosisRunning()" (click)="generateAccountReportFromCompletedWorkReports()">
|
|
|
+ 用已生成作品报告重算定位
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <section class="account-manager-panel" *ngIf="monitoredAccounts().length || removedAccounts().length || archivedAccounts().length || accountDeletionJobs().length">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">账号管理</span>
|
|
|
+ <h3>运营账号生命周期</h3>
|
|
|
+ </div>
|
|
|
+ <small>移出用于整理工作台,归档用于长期只读保留,彻底删除会先进入影响预览。</small>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="account-manager-tabs" role="tablist" aria-label="账号管理分组">
|
|
|
+ <button type="button" [class.is-active]="accountManagerView() === 'active'" (click)="accountManagerView.set('active')">
|
|
|
+ 当前运营 <strong>{{ monitoredAccounts().length }}</strong>
|
|
|
+ </button>
|
|
|
+ <button type="button" [class.is-active]="accountManagerView() === 'removed'" (click)="accountManagerView.set('removed')">
|
|
|
+ 已移出 <strong>{{ removedAccounts().length }}</strong>
|
|
|
+ </button>
|
|
|
+ <button type="button" [class.is-active]="accountManagerView() === 'archived'" (click)="accountManagerView.set('archived')">
|
|
|
+ 已归档 <strong>{{ archivedAccounts().length }}</strong>
|
|
|
+ </button>
|
|
|
+ <button type="button" [class.is-active]="accountManagerView() === 'deletions'" (click)="accountManagerView.set('deletions')">
|
|
|
+ 删除记录 <strong>{{ accountDeletionJobs().length }}</strong>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="account-switch-list" *ngIf="accountManagerView() === 'active'">
|
|
|
+ <article
|
|
|
+ class="account-switch-card"
|
|
|
+ *ngFor="let account of monitoredAccounts(); trackBy: trackById"
|
|
|
+ [class.is-active]="activeAccount()?.id === account.id">
|
|
|
+ <button
|
|
|
+ class="account-switch-card__main"
|
|
|
+ type="button"
|
|
|
+ [attr.aria-current]="activeAccount()?.id === account.id ? 'true' : null"
|
|
|
+ (click)="selectMonitoredAccount(account)">
|
|
|
+ <strong>{{ account.displayName }}</strong>
|
|
|
+ <span>{{ accountRefreshStatusLabel(account.lastRefreshStatus) }} / {{ account.lastRefreshedAt ? (account.lastRefreshedAt | date:'MM-dd HH:mm') : '未刷新' }}</span>
|
|
|
+ </button>
|
|
|
+ <div class="account-switch-card__actions">
|
|
|
+ <button class="btn btn-sm" type="button" (click)="archiveMonitoredAccount(account, $event)">归档</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="removeMonitoredAccount(account, $event)">移出</button>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ <p class="account-manager-empty" *ngIf="!monitoredAccounts().length">没有当前运营账号。</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="account-switch-list" *ngIf="accountManagerView() === 'removed'">
|
|
|
+ <article class="account-switch-card is-muted" *ngFor="let account of removedAccounts(); trackBy: trackById">
|
|
|
+ <div class="account-switch-card__main">
|
|
|
+ <strong>{{ account.displayName }}</strong>
|
|
|
+ <span>已移出 / {{ account.statusChangedAt ? (account.statusChangedAt | date:'MM-dd HH:mm') : '保留历史数据' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="account-switch-card__actions">
|
|
|
+ <button class="btn btn-sm" type="button" (click)="restoreMonitoredAccount(account, $event)">恢复</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="archiveMonitoredAccount(account, $event)">归档</button>
|
|
|
+ <button class="btn btn-sm btn-danger" type="button" (click)="openAccountDeletionPreview(account, $event)">彻底删除</button>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ <p class="account-manager-empty" *ngIf="!removedAccounts().length">没有已移出的账号。</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="account-switch-list" *ngIf="accountManagerView() === 'archived'">
|
|
|
+ <article class="account-switch-card is-muted" *ngFor="let account of archivedAccounts(); trackBy: trackById">
|
|
|
+ <div class="account-switch-card__main">
|
|
|
+ <strong>{{ account.displayName }}</strong>
|
|
|
+ <span>已归档 / 不参与刷新和任务生成</span>
|
|
|
+ </div>
|
|
|
+ <div class="account-switch-card__actions">
|
|
|
+ <button class="btn btn-sm" type="button" (click)="unarchiveMonitoredAccount(account, $event)">取消归档</button>
|
|
|
+ <button class="btn btn-sm btn-danger" type="button" (click)="openAccountDeletionPreview(account, $event)">彻底删除</button>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ <p class="account-manager-empty" *ngIf="!archivedAccounts().length">没有已归档的账号。</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="account-deletion-jobs" *ngIf="accountManagerView() === 'deletions'">
|
|
|
+ <article *ngFor="let job of accountDeletionJobs(); trackBy: trackById">
|
|
|
+ <div>
|
|
|
+ <strong>{{ job.accountName }}</strong>
|
|
|
+ <span>{{ job.status === 'completed' ? '已完成' : job.status === 'failed' ? '失败' : '处理中' }} / {{ job.updatedAt | date:'MM-dd HH:mm' }}</span>
|
|
|
+ </div>
|
|
|
+ <small>删除 {{ job.deletedCounts | json }}</small>
|
|
|
+ </article>
|
|
|
+ <p class="account-manager-empty" *ngIf="!accountDeletionJobs().length">暂无删除记录。</p>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
- <section class="ip-card" *ngIf="step() === 3">
|
|
|
- <p class="ip-step-help">只填对标线索即可。爆点、数据、评论痛点和转化观察由 AI 分析,人工观察为可选补充。</p>
|
|
|
- <h2>对标账号 / 爆款线索</h2>
|
|
|
- <article class="ip-benchmark" *ngFor="let item of benchmarkDrafts(); let i = index; trackBy: trackByIndex">
|
|
|
- <div class="ip-benchmark-head">
|
|
|
- <strong>对标 {{ i + 1 }}</strong>
|
|
|
- <button class="btn btn-sm" type="button" (click)="removeBenchmark(i)">删除</button>
|
|
|
- </div>
|
|
|
- <div class="ip-benchmark-intake">
|
|
|
- <strong>AI 分析所需线索</strong>
|
|
|
- <span>至少填账号、主页或爆款链接/ID中的一项。</span>
|
|
|
+ <section class="account-cleanup-panel" *ngIf="accountCleanupRecommendations().length">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">数据清理建议</span>
|
|
|
+ <h3>先处理低价值增长数据</h3>
|
|
|
</div>
|
|
|
- <div class="ip-form-grid">
|
|
|
- <label>账号名称<input [ngModel]="item.name" (ngModelChange)="updateBenchmark(i, {name: $event})" placeholder="例如:老张聊实体生意"></label>
|
|
|
- <label>账号链接<input [ngModel]="item.url" (ngModelChange)="updateBenchmark(i, {url: $event})" placeholder="主页链接,可选"></label>
|
|
|
+ <small>当前只做建议,不自动删除定位、任务和发布绑定。</small>
|
|
|
+ </header>
|
|
|
+ <article *ngFor="let item of accountCleanupRecommendations(); trackBy: trackByAccountId" class="account-cleanup-item">
|
|
|
+ <div>
|
|
|
+ <strong>{{ item.accountName }}</strong>
|
|
|
+ <span>快照 {{ item.snapshotCount }} / 作品 {{ item.workCount }} / 评论样本 {{ item.commentSampleCount }}</span>
|
|
|
+ <small>{{ item.reasons.join(';') }}</small>
|
|
|
</div>
|
|
|
- <label>爆款作品链接 / awemeId<input [ngModel]="item.viralUrl" (ngModelChange)="updateBenchmark(i, {viralUrl: $event})" placeholder="推荐填写,用于后续证据分析"></label>
|
|
|
- <label>值得借鉴原因(可选)<textarea rows="2" [ngModel]="item.reasonToBenchmark" (ngModelChange)="updateBenchmark(i, {reasonToBenchmark: $event})" placeholder="你的主观判断,可留空"></textarea></label>
|
|
|
- <details class="ip-benchmark-optional">
|
|
|
- <summary>人工补充观察(可选,不填也可以生成)</summary>
|
|
|
- <label>账号简介<textarea rows="2" [ngModel]="item.bio" (ngModelChange)="updateBenchmark(i, {bio: $event})" placeholder="可留空"></textarea></label>
|
|
|
- <div class="ip-form-grid">
|
|
|
- <label>已知爆款标题/描述<input [ngModel]="item.viralTitle" (ngModelChange)="updateBenchmark(i, {viralTitle: $event})" placeholder="可留空"></label>
|
|
|
- <label>已知数据表现<input [ngModel]="item.performance" (ngModelChange)="updateBenchmark(i, {performance: $event})" placeholder="可留空"></label>
|
|
|
- </div>
|
|
|
- <label>你观察到的爆点<textarea rows="2" [ngModel]="item.perceivedHook" (ngModelChange)="updateBenchmark(i, {perceivedHook: $event})" placeholder="可留空"></textarea></label>
|
|
|
- <label>你观察到的转化线索<textarea rows="2" [ngModel]="item.conversionNotes" (ngModelChange)="updateBenchmark(i, {conversionNotes: $event})" placeholder="可留空"></textarea></label>
|
|
|
- </details>
|
|
|
</article>
|
|
|
- <div class="ip-actions">
|
|
|
- <button class="btn" type="button" (click)="addBenchmark()">添加对标</button>
|
|
|
- <span class="spacer"></span>
|
|
|
- <button class="btn" type="button" (click)="step.set(2)">上一步</button>
|
|
|
- <button class="btn btn-primary" type="button" [disabled]="!hasBenchmarkInput()" (click)="step.set(4)">下一步:确认生成</button>
|
|
|
- </div>
|
|
|
</section>
|
|
|
|
|
|
- <section class="ip-card" *ngIf="step() === 4">
|
|
|
- <h2>确认生成范围</h2>
|
|
|
- <div class="ip-generate-scope">
|
|
|
- <span>IP 定位报告</span>
|
|
|
- <span>对标拆解</span>
|
|
|
- <span>机会地图</span>
|
|
|
- <span>12 个核心选题</span>
|
|
|
- <span>9 条脚本大纲</span>
|
|
|
- <span>3 条完整口播</span>
|
|
|
- <span>7 天测试计划</span>
|
|
|
- <span>30 天方向</span>
|
|
|
- <span>质量检查</span>
|
|
|
- </div>
|
|
|
- <div class="ip-progress" *ngIf="activePlan() as plan">
|
|
|
- <div *ngFor="let item of plan.generationProgress; trackBy: trackByStage" [class]="'ip-progress-item is-' + item.status">
|
|
|
- <strong>{{ item.label }}</strong>
|
|
|
- <span>{{ item.status }}</span>
|
|
|
- <small *ngIf="item.durationMs">{{ item.durationMs }}ms</small>
|
|
|
+ <section class="account-deletion-modal" *ngIf="accountDeletionPreview() as preview" role="dialog" aria-modal="true" aria-label="删除影响预览">
|
|
|
+ <div class="account-deletion-modal__panel">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">删除影响预览</span>
|
|
|
+ <h3>彻底删除「{{ preview.accountName }}」</h3>
|
|
|
+ </div>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="closeAccountDeletionPreview()">关闭</button>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="account-deletion-summary">
|
|
|
+ <section>
|
|
|
+ <span>总影响数据</span>
|
|
|
+ <strong>{{ preview.totalRows }}</strong>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>高价值资产</span>
|
|
|
+ <strong>{{ preview.highValueRows }}</strong>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <ul class="account-deletion-warnings">
|
|
|
+ <li *ngFor="let warning of preview.warnings">{{ warning }}</li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <div class="account-deletion-table">
|
|
|
+ <article *ngFor="let item of preview.items" [class.is-high-value]="item.highValue">
|
|
|
+ <span>{{ item.label }}</span>
|
|
|
+ <strong>{{ item.count }}</strong>
|
|
|
+ <small>{{ item.purgeInMvp ? '本次删除' : '本次保留' }}</small>
|
|
|
+ </article>
|
|
|
</div>
|
|
|
+
|
|
|
+ <label class="account-deletion-confirm">
|
|
|
+ 请输入「{{ preview.confirmationText }}」确认删除
|
|
|
+ <input [ngModel]="accountDeletionConfirmation()" (ngModelChange)="accountDeletionConfirmation.set($event)" [placeholder]="preview.confirmationText">
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <footer>
|
|
|
+ <button class="btn" type="button" (click)="closeAccountDeletionPreview()">取消</button>
|
|
|
+ <button
|
|
|
+ class="btn btn-danger"
|
|
|
+ type="button"
|
|
|
+ [disabled]="accountDeletionRunning() || accountDeletionConfirmation().trim() !== preview.confirmationText"
|
|
|
+ (click)="confirmAccountDeletion()">
|
|
|
+ {{ accountDeletionRunning() ? '删除中...' : '确认彻底删除' }}
|
|
|
+ </button>
|
|
|
+ </footer>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <div class="operator-cockpit__next">
|
|
|
+ <section>
|
|
|
+ <span>今天动作</span>
|
|
|
+ <strong>{{ todayTaskCount() }} 项</strong>
|
|
|
+ <small>{{ todayTaskCount() ? '优先处理今天必须做' : '暂无今日任务' }}</small>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>发布复盘</span>
|
|
|
+ <strong>{{ pendingPublishCount() }} / {{ pendingBindingCount() }}</strong>
|
|
|
+ <small>待发布包 / 待绑定</small>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>证据样本</span>
|
|
|
+ <strong>{{ activeSnapshot()?.works?.length || 0 }}</strong>
|
|
|
+ <small>默认展示重点作品</small>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p class="ip-success" *ngIf="workspaceRefreshMessage()">{{ workspaceRefreshMessage() }}</p>
|
|
|
+ <p class="ip-success" *ngIf="accountRefreshMessage()">{{ accountRefreshMessage() }}</p>
|
|
|
+
|
|
|
+ <section class="assistant-confirm-panel" *ngIf="assistantConfirmations().length || assistantBlockedMessage()">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">AI 运营副驾</span>
|
|
|
+ <h3>动作确认与风控边界</h3>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+ <p *ngIf="assistantBlockedMessage()" class="assistant-confirm-panel__blocked">{{ assistantBlockedMessage() }}</p>
|
|
|
+ <article *ngFor="let item of assistantConfirmations(); trackBy: trackById" class="assistant-confirm-panel__item" [class.is-resolved]="item.status !== 'pending'">
|
|
|
+ <div>
|
|
|
+ <strong>{{ item.action.label }}</strong>
|
|
|
+ <p>{{ item.action.reason }}</p>
|
|
|
+ <small>风险等级:{{ item.action.risk === 'medium' ? '需要确认' : item.action.risk }}</small>
|
|
|
+ </div>
|
|
|
+ <div class="ip-actions" *ngIf="item.status === 'pending'">
|
|
|
+ <button class="btn btn-primary btn-sm" type="button" (click)="approveAssistantConfirmation(item.id)">确认执行</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="rejectAssistantConfirmation(item.id)">拒绝</button>
|
|
|
+ </div>
|
|
|
+ <span *ngIf="item.status !== 'pending'">{{ item.status === 'approved' ? '已确认' : '已拒绝' }}</span>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <div class="account-flow-mini">
|
|
|
+ <span [class.is-active]="!activeAccount()">绑定</span>
|
|
|
+ <span [class.is-active]="activeAccount() && !activeSnapshot()">刷新</span>
|
|
|
+ <span [class.is-active]="activeSnapshot() && !activeAccountDiagnosis()">诊断</span>
|
|
|
+ <span [class.is-active]="activeAccountDiagnosis()">执行</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <nav class="workspace-tabs" aria-label="IP 操盘工作区">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ *ngFor="let tab of workspaceTabs(); trackBy: trackById"
|
|
|
+ [class.is-active]="activeWorkspace() === tab.id"
|
|
|
+ (click)="setWorkspace(tab.id)">
|
|
|
+ <strong>{{ tab.label }}</strong>
|
|
|
+ <span>{{ tab.hint }}</span>
|
|
|
+ </button>
|
|
|
+ </nav>
|
|
|
+
|
|
|
+ <section class="account-monitor-panel evidence-summary-panel" [hidden]="activeWorkspace() !== 'overview' && activeWorkspace() !== 'evidence'" *ngIf="activeSnapshot() as snapshot">
|
|
|
+ <header class="account-monitor-panel__head">
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">{{ activeWorkspace() === 'evidence' ? '账号作品库' : '重点诊断作品' }}</span>
|
|
|
+ <h3>{{ activeWorkspace() === 'evidence' ? '账号历史作品与分析' : '账号内容与数据' }}</h3>
|
|
|
+ <p>
|
|
|
+ {{ accountWorkLibraryProgress(snapshot) }},已采集 {{ snapshotCommentCount(snapshot) }} 条重点评论。
|
|
|
+ {{ activeWorkspace() === 'evidence' ? '这里可以加载更多历史作品,并对任意作品生成或重新生成作品报告。' : '默认优先展示互动和采样价值更高的作品。' }}
|
|
|
+ </p>
|
|
|
</div>
|
|
|
- <p class="ip-error" *ngIf="generationError()">{{ generationError() }}</p>
|
|
|
- <div class="ip-actions">
|
|
|
- <button class="btn" type="button" (click)="step.set(3)">上一步</button>
|
|
|
- <button class="btn btn-primary" type="button" [disabled]="!canGenerate()" (click)="generatePlan()">
|
|
|
- {{ isGenerating() ? '生成中...' : '生成 IP 起步方案' }}
|
|
|
+ <div class="account-monitor-panel__actions">
|
|
|
+ <button class="btn btn-sm" type="button" *ngIf="activeWorkspace() !== 'evidence'" (click)="setWorkspace('evidence')">进入账号作品库</button>
|
|
|
+ <button
|
|
|
+ class="btn btn-sm"
|
|
|
+ type="button"
|
|
|
+ *ngIf="activeWorkspace() === 'evidence'"
|
|
|
+ [disabled]="accountWorksLoadingMore()"
|
|
|
+ (click)="loadMoreAccountWorks()">
|
|
|
+ {{ accountWorkLibraryMoreHint(snapshot) }}
|
|
|
</button>
|
|
|
+ <span class="account-monitor-source" *ngIf="activeWorkspace() === 'evidence'">
|
|
|
+ 数据来源:抖音公开主页<br>
|
|
|
+ 刷新时间:{{ snapshot.capturedAt | date:'MM-dd HH:mm' }}
|
|
|
+ </span>
|
|
|
</div>
|
|
|
- </section>
|
|
|
+ </header>
|
|
|
+ <p class="account-monitor-page-note" *ngIf="activeWorkspace() === 'evidence' && snapshot.workPageInfo?.pageStateReliable === false">
|
|
|
+ 本次作品列表还在补齐中,已先展示当前可同步到的公开作品;后续可刷新账号或继续加载更多作品。
|
|
|
+ </p>
|
|
|
+ <div class="account-work-library-toolbar" *ngIf="activeWorkspace() === 'evidence'">
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">排序依据</span>
|
|
|
+ <strong>{{ accountWorkSortLabel() }}</strong>
|
|
|
+ </div>
|
|
|
+ <div class="account-work-sort-buttons" role="group" aria-label="账号作品排序">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ *ngFor="let option of accountWorkSortOptions; trackBy: trackById"
|
|
|
+ [class.is-active]="accountWorkSortMode() === option.id"
|
|
|
+ (click)="setAccountWorkSortMode(option.id)">
|
|
|
+ <strong>{{ option.label }}</strong>
|
|
|
+ <span>{{ option.hint }}</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <section class="ip-result" *ngIf="step() === 5 && activePlan() as plan">
|
|
|
- <div class="ip-result-head ip-result-hero">
|
|
|
- <div>
|
|
|
- <span class="ip-kicker">IP 起步方案</span>
|
|
|
- <h2>{{ plan.diagnosis?.positioningStatement || '定位一句话待补充' }}</h2>
|
|
|
- <p>{{ planStatusHint(plan) }}</p>
|
|
|
+ <div class="account-monitor-profile">
|
|
|
+ <div class="account-monitor-avatar">
|
|
|
+ <img
|
|
|
+ *ngIf="snapshot.profile.avatarUrl"
|
|
|
+ [src]="snapshot.profile.avatarUrl"
|
|
|
+ [alt]="snapshot.profile.nickname"
|
|
|
+ referrerpolicy="no-referrer"
|
|
|
+ (error)="handleAccountProfileAvatarError(snapshot)"
|
|
|
+ >
|
|
|
+ <span *ngIf="!snapshot.profile.avatarUrl">{{ (snapshot.profile.nickname || '账').slice(0, 1) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="account-monitor-profile__identity">
|
|
|
+ <strong>{{ snapshot.profile.nickname || '未识别账号昵称' }}</strong>
|
|
|
+ <p>{{ snapshot.profile.signature || '主页暂未提供简介' }}</p>
|
|
|
+ </div>
|
|
|
+ <dl class="account-monitor-stats">
|
|
|
+ <div><dt>粉丝</dt><dd>{{ profileMetricLabel(snapshot.profile.followerCount) }}</dd></div>
|
|
|
+ <div><dt>获赞</dt><dd>{{ profileMetricLabel(snapshot.profile.totalFavorited) }}</dd></div>
|
|
|
+ <div><dt>关注</dt><dd>{{ profileMetricLabel(snapshot.profile.followingCount) }}</dd></div>
|
|
|
+ <div><dt>作品</dt><dd>{{ profileMetricLabel(snapshot.profile.awemeCount, snapshot.works.length) }}</dd></div>
|
|
|
+ </dl>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ class="account-monitor-work-list evidence-work-grid"
|
|
|
+ [class.evidence-work-grid--full]="activeWorkspace() === 'evidence'">
|
|
|
+ <article class="evidence-work-card" *ngFor="let work of accountWorksForWorkspace(snapshot); trackBy: trackById">
|
|
|
+ <div class="account-monitor-work__summary">
|
|
|
+ <div class="account-monitor-work__cover" aria-hidden="true">
|
|
|
+ <span>{{ workCoverLabel(work) }}</span>
|
|
|
+ <img
|
|
|
+ *ngIf="work.coverUrl"
|
|
|
+ [src]="work.coverUrl"
|
|
|
+ alt=""
|
|
|
+ width="72"
|
|
|
+ height="96"
|
|
|
+ loading="lazy"
|
|
|
+ referrerpolicy="no-referrer"
|
|
|
+ (error)="handleWorkCoverError(work)">
|
|
|
+ </div>
|
|
|
+ <div class="account-monitor-work__title">
|
|
|
+ <span>{{ work.publishTime ? (work.publishTime | date:'MM-dd HH:mm') : '发布时间未知' }}</span>
|
|
|
+ <strong>{{ work.title || work.desc || '未识别作品文案' }}</strong>
|
|
|
+ <small *ngIf="work.isDeepSampled">已优先深挖 {{ work.comments.length }} 条评论</small>
|
|
|
+ <small *ngIf="!work.isDeepSampled">未进入本次重点评论采样</small>
|
|
|
+ </div>
|
|
|
+ <dl>
|
|
|
+ <div><dt>播放</dt><dd>{{ work.metrics.playCount || 0 | number }}</dd></div>
|
|
|
+ <div><dt>点赞</dt><dd>{{ work.metrics.likeCount || 0 | number }}</dd></div>
|
|
|
+ <div><dt>评论</dt><dd>{{ work.metrics.commentCount || 0 | number }}</dd></div>
|
|
|
+ <div><dt>收藏</dt><dd>{{ work.metrics.collectCount || 0 | number }}</dd></div>
|
|
|
+ <div><dt>分享</dt><dd>{{ work.metrics.shareCount || 0 | number }}</dd></div>
|
|
|
+ </dl>
|
|
|
</div>
|
|
|
- <div class="ip-actions">
|
|
|
- <button class="btn" type="button" (click)="step.set(1)">补充资料</button>
|
|
|
- <button class="btn" type="button" [disabled]="plan.status !== 'ready'" (click)="navigateToTopicPool.emit()">去选题池</button>
|
|
|
- <button class="btn" type="button" (click)="syncMissingMaterials()">同步素材待办</button>
|
|
|
- <button class="btn" type="button" (click)="openIpMaterialLibrary()">去素材库</button>
|
|
|
- <div class="ip-export">
|
|
|
- <button class="btn" type="button" (click)="exportMenuOpen.set(!exportMenuOpen())">导出 Markdown</button>
|
|
|
- <div class="ip-export-menu" *ngIf="exportMenuOpen()">
|
|
|
- <button type="button" (click)="exportMarkdown('full')">完整方案</button>
|
|
|
- <button type="button" (click)="exportMarkdown('client')">客户沟通版</button>
|
|
|
- <button type="button" (click)="exportMarkdown('execution')">执行版</button>
|
|
|
- </div>
|
|
|
+ <div class="work-evidence-strip" *ngIf="showWorkEvidenceStrip(work) && workEvidenceSummary(work) as summary">
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">{{ workEvidenceKicker(work) }}</span>
|
|
|
+ <strong>{{ summary.title }}</strong>
|
|
|
+ <small>{{ summary.meta }}</small>
|
|
|
+ </div>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="openWorkEvidence(work)">
|
|
|
+ {{ workEvidenceButtonLabel(work) }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="work-evidence-strip work-analysis-strip" *ngIf="!workAnalysisRunForWork(work)">
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">作品分析报告</span>
|
|
|
+ <strong>{{ isAccountWorkAnalyzing(work) ? '正在生成作品报告' : '尚未分析此作品' }}</strong>
|
|
|
+ <small>{{ work.awemeId ? '可单独提取文字稿并结合评论生成 LLM 作品报告。' : '缺少视频 ID,暂不能分析。' }}</small>
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ class="btn btn-sm btn-primary"
|
|
|
+ type="button"
|
|
|
+ [disabled]="!canStartAccountWorkAnalysis(work)"
|
|
|
+ (click)="analyzeAccountWorkFromLibrary(work)">
|
|
|
+ {{ isAccountWorkAnalyzing(work) ? '分析中...' : '分析此作品' }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="work-evidence-strip work-analysis-strip" *ngIf="workAnalysisRunForWork(work) as run">
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">作品分析报告</span>
|
|
|
+ <strong>{{ workAnalysisStatusLabel(run.status) }}</strong>
|
|
|
+ <small>{{ workAnalysisRunDetail(run) }}</small>
|
|
|
+ </div>
|
|
|
+ <div class="work-analysis-actions">
|
|
|
+ <button class="btn btn-sm" type="button" *ngIf="run.report" (click)="openWorkReport(work)">
|
|
|
+ 查看作品报告
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="btn btn-sm"
|
|
|
+ type="button"
|
|
|
+ [disabled]="isWorkAnalysisRunActive(run) || !canStartAccountWorkAnalysis(work)"
|
|
|
+ (click)="analyzeAccountWorkFromLibrary(work, 'retry')">
|
|
|
+ {{ isWorkAnalysisRunActive(run) || isAccountWorkAnalyzing(work) ? '分析中...' : '重新分析' }}
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- <button class="btn btn-primary" type="button" [disabled]="plan.status !== 'ready' || !productionReadyTopics().length" (click)="syncHighPriorityTopics()">同步可制作选题</button>
|
|
|
</div>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
+ <div class="account-monitor-load-more" *ngIf="activeWorkspace() === 'evidence'">
|
|
|
+ <button
|
|
|
+ class="btn btn-sm"
|
|
|
+ type="button"
|
|
|
+ [disabled]="accountWorksLoadingMore()"
|
|
|
+ (click)="loadMoreAccountWorks()">
|
|
|
+ {{ accountWorkLibraryMoreHint(snapshot) }}
|
|
|
+ </button>
|
|
|
+ <small>{{ accountWorkLibraryMoreNote(snapshot) }}</small>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <article class="account-positioning-report" [hidden]="activeWorkspace() !== 'overview' && activeWorkspace() !== 'positioning'" *ngIf="accountPositioningReport() as report">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">账号定位报告</span>
|
|
|
+ <h3>{{ report.title }}</h3>
|
|
|
+ <p>
|
|
|
+ <strong [class]="'account-positioning-report__badge account-positioning-report__badge--' + report.sourceMode">{{ report.sourceBadge }}</strong>
|
|
|
+ {{ report.source }}{{ report.updatedAt ? ' / ' + (report.updatedAt | date:'MM-dd HH:mm') : '' }}
|
|
|
+ </p>
|
|
|
</div>
|
|
|
- <p class="ip-error" *ngIf="generationError()">{{ generationError() }}</p>
|
|
|
- <p class="ip-success" *ngIf="materialSyncMessage()">{{ materialSyncMessage() }}</p>
|
|
|
- <p class="ip-success" *ngIf="productionEntryMessage()">{{ productionEntryMessage() }}</p>
|
|
|
+ <button class="btn btn-sm" type="button" *ngIf="activePositioningProposal() as proposal" (click)="openEvidenceDrawer(proposal.evidenceItemIds, '账号定位依据')">
|
|
|
+ 查看定位依据
|
|
|
+ </button>
|
|
|
+ </header>
|
|
|
+ <p class="account-positioning-report__fallback" *ngIf="report.fallbackNote">{{ report.fallbackNote }}</p>
|
|
|
+ <div class="account-positioning-report__meta">
|
|
|
+ <span>数据范围 <strong>{{ report.dataScopeLabel }}</strong></span>
|
|
|
+ <span>作品报告 <strong>{{ report.workReportLabel }}</strong></span>
|
|
|
+ <span>判断置信度 <strong>{{ report.confidenceLabel }}</strong></span>
|
|
|
+ </div>
|
|
|
+ <div class="account-positioning-report__grid">
|
|
|
+ <section>
|
|
|
+ <span>当前账号像什么 IP</span>
|
|
|
+ <strong>{{ report.currentIpGuess }}</strong>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>实际吸引的人群</span>
|
|
|
+ <strong>{{ report.targetAudience }}</strong>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>用户为什么持续关注</span>
|
|
|
+ <strong>{{ report.followReason }}</strong>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>当前最大阻力</span>
|
|
|
+ <strong>{{ report.blocker }}</strong>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <div class="account-positioning-report__llm">
|
|
|
+ <section>
|
|
|
+ <h4>高互动内容共性</h4>
|
|
|
+ <p>{{ report.highInteractionPattern }}</p>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <h4>评论痛点归纳</h4>
|
|
|
+ <p>{{ report.commentPainSummary }}</p>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <h4>定位偏差判断</h4>
|
|
|
+ <p>{{ report.positioningMismatch }}</p>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <div class="account-positioning-report__directions" *ngIf="report.directionSignals.length">
|
|
|
+ <section *ngFor="let item of report.directionSignals">
|
|
|
+ <span>{{ item.roleLabel }}</span>
|
|
|
+ <strong>{{ item.title }}</strong>
|
|
|
+ <p>{{ item.body }}</p>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <div class="account-positioning-report__lists">
|
|
|
+ <section>
|
|
|
+ <h4>内容支柱</h4>
|
|
|
+ <ul>
|
|
|
+ <li *ngFor="let item of report.contentPillars">{{ item }}</li>
|
|
|
+ <li *ngIf="!report.contentPillars.length">等待账号诊断生成</li>
|
|
|
+ </ul>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <h4>表达边界</h4>
|
|
|
+ <ul>
|
|
|
+ <li *ngFor="let item of report.boundaries">{{ item }}</li>
|
|
|
+ <li *ngIf="!report.boundaries.length">暂无明确禁区,建议确认定位后补充。</li>
|
|
|
+ </ul>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <h4>证据与缺口</h4>
|
|
|
+ <ul>
|
|
|
+ <li *ngFor="let item of report.evidenceSummary">{{ item }}</li>
|
|
|
+ <li *ngFor="let item of report.evidenceGaps">{{ item }}</li>
|
|
|
+ <li *ngIf="!report.evidenceSummary.length">等待作品报告和评论样本进入诊断。</li>
|
|
|
+ </ul>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <p class="account-positioning-report__next">下一步:{{ report.nextAction }}</p>
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <details class="account-intent-panel" [hidden]="activeWorkspace() !== 'overview'" [open]="!activeAccount()?.intendedTrack">
|
|
|
+ <summary>补充想做的赛道、人设和目标受众</summary>
|
|
|
+ <p>新号或作品样本不足时,这些信息会作为待验证假设;老号则用于判断“想做的定位”和真实内容是否一致。</p>
|
|
|
+ <div class="ip-form-grid">
|
|
|
+ <label>想做的赛道
|
|
|
+ <input [ngModel]="accountIntentDraft().intendedTrack" (ngModelChange)="updateAccountIntentDraft({intendedTrack: $event})" placeholder="例如:传统行业老板 IP">
|
|
|
+ </label>
|
|
|
+ <label>期望人设
|
|
|
+ <input [ngModel]="accountIntentDraft().intendedPersona" (ngModelChange)="updateAccountIntentDraft({intendedPersona: $event})" placeholder="例如:懂业务的增长顾问">
|
|
|
+ </label>
|
|
|
+ <label>目标受众
|
|
|
+ <input [ngModel]="accountIntentDraft().intendedAudience" (ngModelChange)="updateAccountIntentDraft({intendedAudience: $event})" placeholder="例如:传统行业中小企业老板">
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <button class="btn btn-sm btn-primary" type="button" [disabled]="!activeAccount()" (click)="saveAccountIntent()">保存账号运营假设</button>
|
|
|
+ </details>
|
|
|
+
|
|
|
+ <article class="account-diagnosis-panel" [hidden]="activeWorkspace() !== 'overview'" *ngIf="activeAccountDiagnosis() as diagnosis">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">账号诊断摘要</span>
|
|
|
+ <h3>{{ diagnosis.mode === 'data_diagnosis' ? '基于真实作品与评论的账号判断' : '样本不足时的起号假设' }}</h3>
|
|
|
+ </div>
|
|
|
+ <small>{{ diagnosis.updatedAt | date:'MM-dd HH:mm' }} / 评论样本 {{ diagnosis.commentSampleCount }} 条</small>
|
|
|
+ </header>
|
|
|
+ <div class="account-score-grid">
|
|
|
+ <span>定位清晰度 <strong>{{ diagnosis.scores.positioningClarity }}</strong></span>
|
|
|
+ <span>内容结构力 <strong>{{ diagnosis.scores.contentStructure }}</strong></span>
|
|
|
+ <span>互动转化力 <strong>{{ diagnosis.scores.interactionConversion }}</strong></span>
|
|
|
+ <span>账号辨识度 <strong>{{ diagnosis.scores.recognizability }}</strong></span>
|
|
|
+ <span>增长可持续性 <strong>{{ diagnosis.scores.sustainability }}</strong></span>
|
|
|
+ </div>
|
|
|
+ <div class="strategy-report-meta" *ngIf="latestStrategyReport() as report">
|
|
|
+ <section>
|
|
|
+ <span>本轮数据</span>
|
|
|
+ <strong>{{ report.dataScopeSummary?.workCount || 0 }} 条作品 / {{ report.dataScopeSummary?.commentCount || 0 }} 条评论</strong>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>判断置信度</span>
|
|
|
+ <strong>{{ confidenceLabel(report.confidenceLevel) }}</strong>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>已采用校准</span>
|
|
|
+ <strong>{{ report.appliedCalibrationSummary?.length || 0 }} 条</strong>
|
|
|
+ </section>
|
|
|
+ <section>
|
|
|
+ <span>仍缺</span>
|
|
|
+ <strong>{{ report.evidenceGaps?.length ? (report.evidenceGaps || []).join('、') : '暂无关键缺口' }}</strong>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <div class="account-report-cards">
|
|
|
+ <section
|
|
|
+ *ngFor="let item of diagnosisReportCards(diagnosis)"
|
|
|
+ [class]="'account-report-card account-report-card--' + item.tone">
|
|
|
+ <span>{{ item.label }}</span>
|
|
|
+ <strong>{{ item.title }}</strong>
|
|
|
+ <p>{{ item.body }}</p>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
|
|
|
- <div class="ip-failure-panel" *ngIf="plan.status === 'failed' || planFailureCode(plan)">
|
|
|
+ <section class="account-calibration-panel" [hidden]="activeWorkspace() !== 'overview'" *ngIf="activePlan() as plan">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">账号校准</span>
|
|
|
+ <h3>把你的判断写回下一轮分析</h3>
|
|
|
+ <p>这里保存的是运营判断:哪些定位准确、哪些话题不要再做、哪些表达要避开。下一次重新分析会继承这些约束。</p>
|
|
|
+ </div>
|
|
|
+ <small *ngIf="latestAccountCalibration() as calibration">最近校准:{{ calibration.updatedAt | date:'MM-dd HH:mm' }}</small>
|
|
|
+ </header>
|
|
|
+ <div class="calibration-summary" *ngIf="latestAccountCalibration() as calibration">
|
|
|
+ <strong>{{ calibration.accuracy === 'accurate' ? '判断准确' : calibration.accuracy === 'partial' ? '部分准确' : '判断不准确' }}</strong>
|
|
|
+ <span>{{ calibration.positioningNotes || calibration.operatorExperienceNotes || '已作为下一轮分析约束。' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="ip-form-grid">
|
|
|
+ <label>诊断准确度
|
|
|
+ <select [ngModel]="accountCalibrationDraft().accuracy" (ngModelChange)="updateAccountCalibrationDraft({accuracy: $event})">
|
|
|
+ <option value="accurate">准确</option>
|
|
|
+ <option value="partial">部分准确</option>
|
|
|
+ <option value="wrong">不准确</option>
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
+ <label>目标受众修正
|
|
|
+ <input [ngModel]="accountCalibrationDraft().targetAudienceNotes" (ngModelChange)="updateAccountCalibrationDraft({targetAudienceNotes: $event})" placeholder="例如:更偏传统行业老板">
|
|
|
+ </label>
|
|
|
+ <label>定位/关注理由修正
|
|
|
+ <input [ngModel]="accountCalibrationDraft().positioningNotes" (ngModelChange)="updateAccountCalibrationDraft({positioningNotes: $event})" placeholder="例如:强调落地增长,而不是工具教程">
|
|
|
+ </label>
|
|
|
+ <label>人设修正
|
|
|
+ <input [ngModel]="accountCalibrationDraft().personaNotes" (ngModelChange)="updateAccountCalibrationDraft({personaNotes: $event})" placeholder="例如:懂业务的增长顾问">
|
|
|
+ </label>
|
|
|
+ <label>不要再推荐的话题
|
|
|
+ <input [ngModel]="accountCalibrationDraft().forbiddenTopicsText" (ngModelChange)="updateAccountCalibrationDraft({forbiddenTopicsText: $event})" placeholder="用逗号或换行分隔">
|
|
|
+ </label>
|
|
|
+ <label>不要使用的表达
|
|
|
+ <input [ngModel]="accountCalibrationDraft().forbiddenExpressionsText" (ngModelChange)="updateAccountCalibrationDraft({forbiddenExpressionsText: $event})" placeholder="用逗号或换行分隔">
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <label class="calibration-notes">老号经验补充
|
|
|
+ <textarea rows="3" [ngModel]="accountCalibrationDraft().operatorExperienceNotes" (ngModelChange)="updateAccountCalibrationDraft({operatorExperienceNotes: $event})" placeholder="例如:这个号过去方法论内容容易收藏,但纯工具清单转粉差。"></textarea>
|
|
|
+ </label>
|
|
|
+ <button class="btn btn-sm btn-primary" type="button" [disabled]="!activeAccount()" (click)="saveManualAccountCalibration()">保存账号校准</button>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <div class="positioning-proposal-panel positioning-proposal-panel--compact" [hidden]="activeWorkspace() !== 'overview' && activeWorkspace() !== 'positioning'" *ngIf="activePositioningProposal() as proposal">
|
|
|
+ <div class="positioning-proposal-summary">
|
|
|
+ <span class="ip-kicker">定位提案摘要</span>
|
|
|
+ <h3>{{ positioningProposalDraft().persona || proposal.proposedVersion.persona }}</h3>
|
|
|
+ <dl>
|
|
|
+ <div>
|
|
|
+ <dt>目标人群</dt>
|
|
|
+ <dd>{{ positioningProposalDraft().targetAudience || proposal.proposedVersion.targetAudience }}</dd>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <dt>关注理由</dt>
|
|
|
+ <dd>{{ positioningProposalDraft().followReason || proposal.proposedVersion.followReason }}</dd>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <dt>证据数量</dt>
|
|
|
+ <dd>{{ proposal.evidenceItemIds.length }} 条</dd>
|
|
|
+ </div>
|
|
|
+ </dl>
|
|
|
+ <p>{{ proposal.reason }}</p>
|
|
|
+ <p class="positioning-evidence-note" *ngIf="activeSnapshot() as snapshot">
|
|
|
+ 定位依据:最近 {{ snapshot.works.length }} 条作品、{{ activeAccountDiagnosis().commentSampleCount || 0 }} 条重点评论。
|
|
|
+ 作品文案和评论仅作为证据,系统会先归纳主题与痛点,不会直接复制为定位结论。
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <div class="positioning-edit-panel" *ngIf="positioningEditOpen()">
|
|
|
+ <div class="ip-form-grid">
|
|
|
+ <label>当前推断目标人群
|
|
|
+ <input [ngModel]="positioningProposalDraft().targetAudience" (ngModelChange)="updatePositioningProposalDraft({targetAudience: $event})">
|
|
|
+ </label>
|
|
|
+ <label>建议账号角色
|
|
|
+ <input [ngModel]="positioningProposalDraft().persona" (ngModelChange)="updatePositioningProposalDraft({persona: $event})">
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <label class="positioning-follow-reason">用户为什么值得持续关注
|
|
|
+ <span>这里应是一句简洁的账号价值承诺,不是视频文案或文字稿。</span>
|
|
|
+ <textarea rows="4" maxlength="160" [ngModel]="positioningProposalDraft().followReason" (ngModelChange)="updatePositioningProposalDraft({followReason: $event})"></textarea>
|
|
|
+ </label>
|
|
|
+ <div class="ip-form-grid">
|
|
|
+ <label>内容支柱
|
|
|
+ <textarea rows="3" [ngModel]="positioningProposalDraft().contentPillarsText" (ngModelChange)="updatePositioningProposalDraft({contentPillarsText: $event})"></textarea>
|
|
|
+ </label>
|
|
|
+ <label>表达风格
|
|
|
+ <textarea rows="3" [ngModel]="positioningProposalDraft().expressionStyleText" (ngModelChange)="updatePositioningProposalDraft({expressionStyleText: $event})"></textarea>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <label>明确不做什么
|
|
|
+ <textarea rows="2" [ngModel]="positioningProposalDraft().boundariesText" (ngModelChange)="updatePositioningProposalDraft({boundariesText: $event})"></textarea>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <div class="ip-actions">
|
|
|
+ <button class="btn btn-sm" type="button" (click)="openEvidenceDrawer(proposal.evidenceItemIds, 'IP定位提案证据')">查看定位证据</button>
|
|
|
+ <button class="btn" type="button" (click)="positioningEditOpen.set(!positioningEditOpen())">{{ positioningEditOpen() ? '收起编辑' : '编辑定位' }}</button>
|
|
|
+ <button class="btn" type="button" *ngIf="positioningEditOpen()" (click)="savePositioningProposalRevision(proposal)">保存修改稿</button>
|
|
|
+ <button class="btn btn-primary" type="button" (click)="acceptPositioningProposal(proposal)">确认生效</button>
|
|
|
+ <button class="btn" type="button" (click)="rejectPositioningProposal(proposal)">拒绝提案</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="priority-task-board" [hidden]="activeWorkspace() !== 'overview' && activeWorkspace() !== 'tasks'" *ngIf="operationTasks().length">
|
|
|
+ <section class="priority-task-column">
|
|
|
+ <h3>今天必须做</h3>
|
|
|
+ <article *ngFor="let task of tasksInColumn('today'); trackBy: trackById">
|
|
|
+ <strong>{{ task.title }}</strong>
|
|
|
+ <p>{{ task.reason }}</p>
|
|
|
+ <small>增长影响 {{ task.growthImpactScore }}</small>
|
|
|
+ <button class="btn btn-sm" type="button" *ngIf="task.relatedEvidenceIds.length" (click)="openEvidenceDrawer(task.relatedEvidenceIds, '任务来源证据')">查看证据</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="moveTask(task, 'done')">完成</button>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+ <section class="priority-task-column">
|
|
|
+ <h3>本周要做</h3>
|
|
|
+ <article *ngFor="let task of tasksInColumn('this_week'); trackBy: trackById">
|
|
|
+ <strong>{{ task.title }}</strong>
|
|
|
+ <p>{{ task.reason }}</p>
|
|
|
+ <small>增长影响 {{ task.growthImpactScore }}</small>
|
|
|
+ <button class="btn btn-sm" type="button" *ngIf="task.relatedEvidenceIds.length" (click)="openEvidenceDrawer(task.relatedEvidenceIds, '任务来源证据')">查看证据</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="moveTask(task, 'today')">提到今天</button>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+ <section class="priority-task-column">
|
|
|
+ <h3>后续优化</h3>
|
|
|
+ <article *ngFor="let task of tasksInColumn('later'); trackBy: trackById">
|
|
|
+ <strong>{{ task.title }}</strong>
|
|
|
+ <p>{{ task.reason }}</p>
|
|
|
+ <small>增长影响 {{ task.growthImpactScore }}</small>
|
|
|
+ <button class="btn btn-sm" type="button" *ngIf="task.relatedEvidenceIds.length" (click)="openEvidenceDrawer(task.relatedEvidenceIds, '任务来源证据')">查看证据</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="moveTask(task, 'this_week')">排到本周</button>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+ <section class="priority-task-column">
|
|
|
+ <h3>已完成</h3>
|
|
|
+ <article *ngFor="let task of tasksInColumn('done'); trackBy: trackById">
|
|
|
+ <strong>{{ task.title }}</strong>
|
|
|
+ <p>{{ task.reason }}</p>
|
|
|
+ <small>增长影响 {{ task.growthImpactScore }}</small>
|
|
|
+ <button class="btn btn-sm" type="button" *ngIf="task.relatedEvidenceIds.length" (click)="openEvidenceDrawer(task.relatedEvidenceIds, '任务来源证据')">查看证据</button>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p class="content-direction-note" [hidden]="activeWorkspace() !== 'directions'" *ngIf="contentDirections().length">
|
|
|
+ 当前内容方向来自 {{ accountStrategySourceLabel() }}:账号作品、主页信息、重点评论和爆款/对标证据会先被归纳为 IP 定位,再生成方向和选题。主页简介、评论和爆款文案只作为证据来源,不应直接复制成方向。
|
|
|
+ </p>
|
|
|
+
|
|
|
+ <section class="ip-positioning-bridge-panel" [hidden]="activeWorkspace() !== 'directions'">
|
|
|
+ <div class="ip-positioning-result-card" [class.is-missing]="currentIpPositioning().status === 'missing'">
|
|
|
+ <span class="ip-kicker">当前IP定位结果</span>
|
|
|
+ <h3>{{ currentIpPositioning().label }}</h3>
|
|
|
+ <dl>
|
|
|
+ <div>
|
|
|
+ <dt>账号角色</dt>
|
|
|
+ <dd>{{ currentIpPositioning().persona }}</dd>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <dt>目标人群</dt>
|
|
|
+ <dd>{{ currentIpPositioning().targetAudience }}</dd>
|
|
|
+ </div>
|
|
|
<div>
|
|
|
- <span class="ip-kicker">失败原因</span>
|
|
|
- <strong>{{ planFailureCode(plan) || 'unknown' }}</strong>
|
|
|
- <p>{{ failureMessage(planFailureCode(plan)) }}</p>
|
|
|
+ <dt>关注理由</dt>
|
|
|
+ <dd>{{ currentIpPositioning().followReason }}</dd>
|
|
|
</div>
|
|
|
+ </dl>
|
|
|
+ <p>{{ currentIpPositioning().source }}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="ip-viral-fit-panel">
|
|
|
+ <div class="ip-viral-fit-panel__head">
|
|
|
<div>
|
|
|
- <span>失败阶段</span>
|
|
|
- <strong>{{ planFailureStage(plan) }}</strong>
|
|
|
- <p>{{ failureNextAction(planFailureCode(plan)) }}</p>
|
|
|
+ <span class="ip-kicker">博主监测 / 爆款分析适配</span>
|
|
|
+ <h3>结合当前IP后的方向和建议</h3>
|
|
|
+ <p>这里不直接复用爆款文案,只判断它的结构、评论触发点和用户问题,是否适合迁移到当前IP。</p>
|
|
|
</div>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="refreshSavedAnalyses()" [disabled]="savedAnalysisLoading()">
|
|
|
+ {{ savedAnalysisLoading() ? '加载中...' : '刷新爆款分析' }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="ip-viral-fit-empty" *ngIf="!ipViralAdaptationSuggestions().length">
|
|
|
+ <strong>暂无可适配的爆款/监测记录</strong>
|
|
|
+ <p>{{ currentIpPositioning().status === 'missing' ? '请先刷新账号并生成定位提案。' : '可先在博主监测中分析对标作品,或在爆款分析中保存记录,再回到这里查看与当前IP的适配建议。' }}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="ip-viral-fit-grid" *ngIf="ipViralAdaptationSuggestions().length">
|
|
|
+ <article *ngFor="let suggestion of ipViralAdaptationSuggestions(); trackBy: trackById">
|
|
|
+ <span>{{ suggestion.sourceLabel }} / {{ suggestion.fitDirection }}</span>
|
|
|
+ <h4>{{ suggestion.title }}</h4>
|
|
|
+ <p><strong>可借结构:</strong>{{ suggestion.reusablePattern }}</p>
|
|
|
+ <p><strong>IP适配:</strong>{{ suggestion.ipAdaptation }}</p>
|
|
|
+ <p><strong>建议选题:</strong>{{ suggestion.suggestedTopic }}</p>
|
|
|
+ <small>风险:{{ suggestion.risk }}</small>
|
|
|
+ <button
|
|
|
+ class="btn btn-sm evidence-link-button"
|
|
|
+ type="button"
|
|
|
+ *ngIf="suggestion.analysisId"
|
|
|
+ [disabled]="savedAnalysisLoading()"
|
|
|
+ (click)="bindViralSuggestionEvidence(suggestion)">
|
|
|
+ 沉淀为当前IP证据
|
|
|
+ </button>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
+ <p class="ip-success" *ngIf="savedAnalysisMessage()">{{ savedAnalysisMessage() }}</p>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="direction-workspace" [hidden]="activeWorkspace() !== 'directions'" *ngIf="contentDirections().length">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">方向选题</span>
|
|
|
+ <h3>方向摘要</h3>
|
|
|
+ <p>先选择一个方向,再查看该方向下的选题、证据和制作动作。</p>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="direction-workspace__layout">
|
|
|
+ <div class="direction-summary-list" role="list">
|
|
|
+ <article
|
|
|
+ class="direction-summary-card"
|
|
|
+ *ngFor="let direction of contentDirections(); trackBy: trackById"
|
|
|
+ [class.is-active]="selectedDirection().id === direction.id">
|
|
|
+ <button
|
|
|
+ class="direction-summary-card__select"
|
|
|
+ type="button"
|
|
|
+ (click)="selectDirection(direction)">
|
|
|
+ <span>{{ directionRoleLabel(direction.role) }}方向</span>
|
|
|
+ <strong>{{ direction.title }}</strong>
|
|
|
+ <small>{{ topicsForDirection(direction).length }} 个选题 / {{ evidenceCountLabel(directionEvidenceIds(direction)) }}</small>
|
|
|
+ </button>
|
|
|
+ </article>
|
|
|
</div>
|
|
|
|
|
|
- <div class="ip-summary-grid">
|
|
|
- <section class="ip-summary-card">
|
|
|
- <span>状态</span>
|
|
|
- <strong>{{ planStatusLabel(plan.status) }}</strong>
|
|
|
- <small>{{ plan.updatedAt | date:'yyyy-MM-dd HH:mm' }}</small>
|
|
|
+ <article class="selected-direction-panel" *ngIf="selectedDirection() as direction">
|
|
|
+ <span class="ip-kicker">选中方向</span>
|
|
|
+ <h3>{{ direction.title }}</h3>
|
|
|
+ <p>{{ direction.purpose }}</p>
|
|
|
+ <dl>
|
|
|
+ <div><dt>目标人群</dt><dd>{{ direction.targetAudience }}</dd></div>
|
|
|
+ <div><dt>更新建议</dt><dd>{{ direction.cadenceSuggestion }}</dd></div>
|
|
|
+ <div><dt>观察指标</dt><dd>{{ direction.observeMetrics.join('、') }}</dd></div>
|
|
|
+ </dl>
|
|
|
+ <small>来源证据:{{ evidenceCountLabel(directionEvidenceIds(direction)) }}</small>
|
|
|
+ <small *ngIf="direction.fitReason">适配当前IP:{{ direction.fitReason }}</small>
|
|
|
+ <small *ngIf="direction.risks?.length">避免照搬:{{ direction.risks?.join('、') }}</small>
|
|
|
+ <button
|
|
|
+ class="btn btn-sm evidence-link-button"
|
|
|
+ type="button"
|
|
|
+ [disabled]="!directionEvidenceIds(direction).length"
|
|
|
+ (click)="openEvidenceDrawer(directionEvidenceIds(direction), direction.title + ' · 来源证据')">
|
|
|
+ 查看方向依据
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div class="selected-topic-list" *ngIf="topicsForDirection(direction).length">
|
|
|
+ <article
|
|
|
+ class="direction-topic-card"
|
|
|
+ [class.is-generating]="isTopicScriptGenerating(topic)"
|
|
|
+ [class.has-script]="!!topicScript(topic)"
|
|
|
+ *ngFor="let topic of topicsForDirection(direction); trackBy: trackById">
|
|
|
+ <strong>{{ topic.title }}</strong>
|
|
|
+ <p>{{ topic.userProblem || topic.contentGoal }}</p>
|
|
|
+ <small>目标人群:{{ topic.targetAudience || direction.targetAudience }}</small>
|
|
|
+ <small *ngIf="topicDirectEvidenceIds(topic).length; else inheritedDirectionEvidence">
|
|
|
+ 本题专属依据:已关联 {{ topicDirectEvidenceIds(topic).length }} 条
|
|
|
+ </small>
|
|
|
+ <ng-template #inheritedDirectionEvidence>
|
|
|
+ <small>本题暂无专属依据,当前沿用所属方向的判断。</small>
|
|
|
+ </ng-template>
|
|
|
+ <small>可借结构:{{ displayText(topic.viralPatternRefs) }}</small>
|
|
|
+ <small>避免照搬:{{ topic.riskNote || '只借结构和用户问题,不复制原视频表达。' }}</small>
|
|
|
+ <div class="topic-actions">
|
|
|
+ <button
|
|
|
+ *ngIf="topicDirectEvidenceIds(topic).length"
|
|
|
+ class="btn btn-sm"
|
|
|
+ type="button"
|
|
|
+ (click)="openEvidenceDrawer(topicDirectEvidenceIds(topic), topic.title + ' · 本题依据')">
|
|
|
+ 查看本题依据
|
|
|
+ </button>
|
|
|
+ <button class="btn btn-sm" type="button" [disabled]="isTopicScriptGenerating(topic)" (click)="generateShotScriptForTopic(topic)">
|
|
|
+ {{ topicScriptActionLabel(topic) }}
|
|
|
+ </button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="addTopicToCalendar(topic)">
|
|
|
+ 加入内容日历
|
|
|
+ </button>
|
|
|
+ <button class="btn btn-sm btn-primary" type="button" (click)="createPublishPackageForTopic(topic)">
|
|
|
+ {{ publishPackageActionLabel(topic) }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
+ <p class="direction-topic-empty" *ngIf="!topicsForDirection(direction).length">
|
|
|
+ 当前方向还没有可执行选题,请先重新分析账号定位或刷新工作台。
|
|
|
+ </p>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <div class="shot-script-modal-backdrop" *ngIf="activeShotScriptModal() as script" (click)="closeShotScriptModal()">
|
|
|
+ <section class="shot-script-modal" role="dialog" aria-modal="true" aria-label="分镜脚本内容" (click)="$event.stopPropagation()">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">分镜脚本</span>
|
|
|
+ <h3>{{ script.title || '分镜脚本' }}</h3>
|
|
|
+ <p>{{ script.scriptSummary || script.durationReason || '已生成可拍摄脚本,请人工复核后再进入发布包。' }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="shot-script-modal__actions">
|
|
|
+ <button class="btn btn-sm" type="button" [disabled]="isShotScriptRegenerating(script)" (click)="regenerateShotScript(script)">
|
|
|
+ {{ isShotScriptRegenerating(script) ? '重新生成中...' : '重新生成' }}
|
|
|
+ </button>
|
|
|
+ <button class="shot-script-modal__close" type="button" aria-label="关闭分镜脚本弹窗" title="关闭" (click)="closeShotScriptModal()">×</button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="shot-script-modal__summary">
|
|
|
+ <section>
|
|
|
+ <span>建议时长</span>
|
|
|
+ <strong>{{ script.recommendedDurationSeconds || 60 }} 秒</strong>
|
|
|
</section>
|
|
|
- <section class="ip-summary-card" *ngIf="plan.qualityCheck as q">
|
|
|
- <span>质量分</span>
|
|
|
- <strong>{{ q.overallScore }}/100</strong>
|
|
|
- <small>{{ q.passed ? '已过质量门槛' : '未过质量门槛' }}</small>
|
|
|
+ <section>
|
|
|
+ <span>质量状态</span>
|
|
|
+ <strong>{{ scriptQualityLabel(script.qualityLevel) }}</strong>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="shot-script-modal__core">
|
|
|
+ <section>
|
|
|
+ <span>开头钩子</span>
|
|
|
+ <p>{{ script.hook }}</p>
|
|
|
</section>
|
|
|
- <section class="ip-summary-card">
|
|
|
- <span>选题</span>
|
|
|
- <strong>{{ plan.topics.length }}</strong>
|
|
|
- <small>可制作 {{ productionReadyTopics().length }} / 需打磨 {{ polishFirstTopics().length }} / 需素材 {{ materialNeededTopics().length }}</small>
|
|
|
+ <section>
|
|
|
+ <span>核心观点</span>
|
|
|
+ <p>{{ script.viewpoint }}</p>
|
|
|
</section>
|
|
|
- <section class="ip-summary-card">
|
|
|
- <span>脚本</span>
|
|
|
- <strong>{{ fullScripts().length }} 完整 / {{ outlineScripts().length }} 大纲</strong>
|
|
|
- <small>可拍摄 {{ recordableScripts().length }}</small>
|
|
|
+ <section>
|
|
|
+ <span>结尾引导</span>
|
|
|
+ <p>{{ script.closingCta }}</p>
|
|
|
</section>
|
|
|
</div>
|
|
|
|
|
|
- <div class="ip-quality-audit" *ngIf="qualityAudit(plan) as audit">
|
|
|
- <header>
|
|
|
- <div>
|
|
|
- <span class="ip-kicker">P2A 核心质量审计</span>
|
|
|
- <strong>{{ audit.overallScore }}/100</strong>
|
|
|
- <small>{{ audit.productionGate.reason }}</small>
|
|
|
- </div>
|
|
|
- <div class="ip-gate-tags">
|
|
|
- <span [class.is-pass]="audit.productionGate.canEnterTopicPool">选题池</span>
|
|
|
- <span [class.is-pass]="audit.productionGate.canCreateDraft">内容制作</span>
|
|
|
- <span [class.is-pass]="audit.productionGate.canExportExecution">执行导出</span>
|
|
|
- </div>
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="!plan.benchmarkEvidence?.length || evidenceLoading()" (click)="generateEvidenceBasedSuggestions()">
|
|
|
- {{ evidenceLoading() ? '生成中...' : '生成增强建议' }}
|
|
|
- </button>
|
|
|
- </header>
|
|
|
- <div class="ip-quality-dimensions">
|
|
|
- <section *ngFor="let item of audit.dimensions; trackBy: trackByIndex" [class.is-blocked]="item.blockingIssues.length">
|
|
|
- <strong>{{ item.label }} <span>{{ item.score }}</span></strong>
|
|
|
- <p>{{ item.conclusion }}</p>
|
|
|
- <small *ngIf="item.improvementActions.length">{{ displayText(item.improvementActions) }}</small>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
+ <div class="shot-script-modal__shots" *ngIf="script.shotList?.length">
|
|
|
+ <article *ngFor="let shot of script.shotList; trackBy: trackById">
|
|
|
+ <strong>{{ shot.startSecond }}-{{ shot.endSecond }} 秒|{{ shot.segmentTitle }}</strong>
|
|
|
+ <p>{{ shot.visualDesign }}</p>
|
|
|
+ <small><strong>口播台词:</strong>{{ shot.narration }}</small>
|
|
|
+ <small><strong>画面重点字:</strong>{{ shot.subtitle }}</small>
|
|
|
+ <small>动作:{{ shot.actionNotes }}</small>
|
|
|
+ <small>音效:{{ shot.audioNotes }}</small>
|
|
|
+ </article>
|
|
|
</div>
|
|
|
|
|
|
- <div class="ip-readiness-panel" [class.is-blocked]="plan.status !== 'ready'">
|
|
|
- <div class="ip-readiness-main">
|
|
|
- <span class="ip-kicker">{{ plan.status === 'ready' ? '可进入生产链路' : '当前仍是待完善草稿' }}</span>
|
|
|
- <strong>{{ plan.status === 'ready' ? '方案已通过质量门槛,可以继续同步可制作选题。' : '先补齐关键资料,再判断是否进入选题池和生产 Pipeline。' }}</strong>
|
|
|
- <p>{{ plan.status === 'ready' ? '建议先同步可制作选题,进入选题池继续打磨脚本和制作。' : '页面下方内容可以阅读和编辑,但不能当成最终执行方案;优先处理阻断问题和待补充资料。' }}</p>
|
|
|
- </div>
|
|
|
- <div class="ip-readiness-list" *ngIf="plan.status !== 'ready'">
|
|
|
- <section *ngIf="plan.qualityCheck?.blockingIssues?.length">
|
|
|
- <strong>阻断问题</strong>
|
|
|
- <p *ngFor="let issue of firstItems(plan.qualityCheck?.blockingIssues, 3)">{{ displayText(issue) }}</p>
|
|
|
- </section>
|
|
|
- <section *ngIf="plan.missingInputs.length">
|
|
|
- <strong>待补资料</strong>
|
|
|
- <p *ngFor="let item of firstItems(plan.missingInputs, 3)">{{ displayText(item.type) }}:{{ displayText(item.description) }}</p>
|
|
|
- </section>
|
|
|
+ <footer>
|
|
|
+ <p class="shot-script-modal__hint">“画面重点字”不是逐字字幕,而是建议打在视频画面上的重点文案;逐字字幕仍以口播台词为准。</p>
|
|
|
+ <p><strong>风险提醒:</strong>{{ script.riskNote || '不要照搬对标账号话术,只迁移结构和用户问题。' }}</p>
|
|
|
+ </footer>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="publish-package-modal-backdrop" *ngIf="activePublishPackageModal() as pkg" (click)="closePublishPackageModal()">
|
|
|
+ <section class="publish-package-modal" role="dialog" aria-modal="true" aria-label="发布包详情" (click)="$event.stopPropagation()">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">人工发布包</span>
|
|
|
+ <h3>{{ pkg.titleOptions[0] || '发布包详情' }}</h3>
|
|
|
+ <p>{{ evidencePlatformLabel(pkg.platform) }} / {{ pkg.accountName }} / {{ publishStatusLabel(pkg.status) }}</p>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ <button class="publish-package-modal__close" type="button" aria-label="关闭发布包" title="关闭" (click)="closePublishPackageModal()">×</button>
|
|
|
+ </header>
|
|
|
|
|
|
- <article class="ip-card ip-section-card ip-supplement-panel">
|
|
|
- <header>
|
|
|
+ <div class="publish-package-modal__body">
|
|
|
+ <section class="publish-package-modal__overview">
|
|
|
+ <div><span>建议排期</span><strong>{{ pkg.scheduleSuggestion || '待人工确定' }}</strong></div>
|
|
|
+ <div><span>时长与节奏</span><strong>{{ pkg.durationAndRhythm || '以执行脚本为准' }}</strong></div>
|
|
|
+ <div><span>发布方式</span><strong>人工确认后手动发布</strong></div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="publish-package-modal__polish" [class.is-failed]="pkg.polishSource === 'local_fallback'">
|
|
|
<div>
|
|
|
- <span class="ip-kicker">补充资料闭环</span>
|
|
|
- <h3>把待补资料变成下一次质量检查的依据</h3>
|
|
|
- <small class="ip-regeneration-meta">{{ supplementMessage() || '补充资料不会自动覆盖正文,保存后请手动重新质量检查或重生成相关模块。' }}</small>
|
|
|
+ <span>{{ pkg.polishSource === 'llm' ? 'AI 精修结果' : '发布包草稿' }}</span>
|
|
|
+ <strong>{{ pkg.polishSummary || '已根据选题和执行脚本生成规则草稿,正在等待 AI 结合账号证据精修。' }}</strong>
|
|
|
</div>
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="!!regeneratingStage()" (click)="regenerateSection('quality_check')">
|
|
|
- {{ regeneratingStage() === 'quality_check' ? '检查中...' : '重新质量检查' }}
|
|
|
- </button>
|
|
|
- <button class="btn btn-sm" type="button" (click)="importFilledMaterials()">导入已补素材</button>
|
|
|
- </header>
|
|
|
+ <small *ngIf="pkg.polishRationale?.length">{{ pkg.polishRationale?.join(';') }}</small>
|
|
|
+ </section>
|
|
|
|
|
|
- <div class="ip-supplement-grid">
|
|
|
+ <div class="publish-package-modal__grid">
|
|
|
<section>
|
|
|
- <h4>新增补充资料</h4>
|
|
|
- <div class="ip-form-grid">
|
|
|
- <label>资料类型
|
|
|
- <select [ngModel]="supplementDraft().type" (ngModelChange)="updateSupplementDraft({type: $event})">
|
|
|
- <option *ngFor="let type of supplementTypes" [value]="type">{{ type }}</option>
|
|
|
- </select>
|
|
|
- </label>
|
|
|
- <label>关联待补资料
|
|
|
- <select [ngModel]="supplementDraft().relatedMissingInputId" (ngModelChange)="updateSupplementDraft({relatedMissingInputId: $event})">
|
|
|
- <option value="">不关联</option>
|
|
|
- <option *ngFor="let item of plan.missingInputs; let i = index" [value]="i + ':' + displayText(item.type)">
|
|
|
- {{ displayText(item.type) }}:{{ displayText(item.description) }}
|
|
|
- </option>
|
|
|
- </select>
|
|
|
- </label>
|
|
|
- </div>
|
|
|
- <label>标题<input [ngModel]="supplementDraft().title" (ngModelChange)="updateSupplementDraft({title: $event})"></label>
|
|
|
- <label>内容<textarea rows="4" [ngModel]="supplementDraft().content" (ngModelChange)="updateSupplementDraft({content: $event})"></textarea></label>
|
|
|
- <div class="ip-actions">
|
|
|
- <button class="btn btn-primary" type="button" [disabled]="!supplementDraft().title.trim() || !supplementDraft().content.trim()" (click)="addSupplementInput()">保存补充资料</button>
|
|
|
- </div>
|
|
|
+ <h4>标题候选</h4>
|
|
|
+ <ol><li *ngFor="let title of pkg.titleOptions">{{ title }}</li></ol>
|
|
|
</section>
|
|
|
-
|
|
|
<section>
|
|
|
- <h4>已补充资料</h4>
|
|
|
- <p class="ip-empty" *ngIf="!plan.supplementInputs?.length">还没有补充资料。优先补客户案例、客户问题、服务前后对比和对标数据。</p>
|
|
|
- <div class="ip-supplement-list" *ngIf="plan.supplementInputs?.length">
|
|
|
- <article *ngFor="let item of plan.supplementInputs; trackBy: trackById">
|
|
|
- <label>类型
|
|
|
- <select [(ngModel)]="item.type" (change)="saveSupplementInput(item, {type: item.type})">
|
|
|
- <option *ngFor="let type of supplementTypes" [value]="type">{{ type }}</option>
|
|
|
- </select>
|
|
|
- </label>
|
|
|
- <label>标题<input [(ngModel)]="item.title" (change)="saveSupplementInput(item, {title: item.title})"></label>
|
|
|
- <label>内容<textarea rows="3" [(ngModel)]="item.content" (change)="saveSupplementInput(item, {content: item.content})"></textarea></label>
|
|
|
- <div class="ip-actions">
|
|
|
- <small>{{ item.updatedAt | date:'MM-dd HH:mm:ss' }}</small>
|
|
|
- <button class="btn btn-sm" type="button" (click)="deleteSupplementInput(item)">删除</button>
|
|
|
+ <h4>封面建议</h4>
|
|
|
+ <ul><li *ngFor="let cover of pkg.coverSuggestions">{{ cover }}</li></ul>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <section class="publish-package-modal__section">
|
|
|
+ <h4>发布文案</h4>
|
|
|
+ <p class="publish-package-modal__copy">{{ pkg.caption }}</p>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <div class="publish-package-modal__script-grid">
|
|
|
+ <section class="publish-package-modal__section">
|
|
|
+ <h4>分段口播稿</h4>
|
|
|
+ <div class="publish-package-modal__timed-script" *ngIf="publishPackageScriptShots(pkg) as shots">
|
|
|
+ <article *ngFor="let shot of shots; trackBy: trackById">
|
|
|
+ <span class="publish-package-modal__time">{{ shot.startSecond }}–{{ shot.endSecond }} 秒</span>
|
|
|
+ <div>
|
|
|
+ <strong>{{ shot.segmentTitle }}</strong>
|
|
|
+ <p>{{ shot.narration }}</p>
|
|
|
+ <small>画面:{{ shot.visualDesign }}</small>
|
|
|
</div>
|
|
|
</article>
|
|
|
+ <div class="publish-package-modal__untimed" *ngIf="!shots.length">
|
|
|
+ <p class="publish-package-modal__copy">{{ pkg.scriptText || '暂无完整口播稿,请先重新生成分镜脚本。' }}</p>
|
|
|
+ <small>当前脚本没有分段时间。重新生成分镜脚本后,这里会按秒展示对应口播。</small>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</section>
|
|
|
+ <section class="publish-package-modal__section">
|
|
|
+ <h4>拍摄清单</h4>
|
|
|
+ <ul class="publish-package-modal__shooting-list"><li *ngFor="let item of pkg.shootingChecklist || []">{{ item }}</li></ul>
|
|
|
+ <p *ngIf="!pkg.shootingChecklist?.length">暂无拍摄清单,请先补齐脚本和素材。</p>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
- </article>
|
|
|
|
|
|
- <article class="ip-card ip-section-card ip-evidence-panel">
|
|
|
- <header>
|
|
|
- <div>
|
|
|
- <span class="ip-kicker">P2C 对标证据</span>
|
|
|
- <h3>抓取单条对标作品,回到机会地图、选题和脚本判断</h3>
|
|
|
- <small class="ip-regeneration-meta">{{ evidenceMessage() || '当前只做用户触发的单条作品证据采集,不做无人值守批量抓取。短链需要先展开为 douyin.com/video/{id}。' }}</small>
|
|
|
- </div>
|
|
|
- </header>
|
|
|
- <div class="ip-evidence-form">
|
|
|
- <label>对标账号
|
|
|
- <select [ngModel]="evidenceBenchmarkId()" (ngModelChange)="evidenceBenchmarkId.set($event)">
|
|
|
- <option value="">不绑定账号</option>
|
|
|
- <option *ngFor="let benchmark of benchmarkOptions(plan); trackBy: trackById" [value]="benchmark.id">{{ benchmark.name }}</option>
|
|
|
- </select>
|
|
|
- </label>
|
|
|
- <label>作品链接或 awemeId
|
|
|
- <input [ngModel]="evidenceUrl()" (ngModelChange)="evidenceUrl.set($event)" placeholder="https://www.douyin.com/video/..." />
|
|
|
- </label>
|
|
|
- <button class="btn btn-primary" type="button" [disabled]="!evidenceUrl().trim() || evidenceLoading()" (click)="fetchBenchmarkEvidence()">
|
|
|
- {{ evidenceLoading() ? '采集中...' : '采集证据' }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <div class="ip-evidence-form ip-evidence-form--saved">
|
|
|
- <label>已保存爆款分析
|
|
|
- <select [ngModel]="savedAnalysisId()" (ngModelChange)="savedAnalysisId.set($event)">
|
|
|
- <option value="">请选择分析记录</option>
|
|
|
- <option *ngFor="let analysis of savedAnalyses(); trackBy: trackById" [value]="analysis.id">
|
|
|
- {{ displayText(analysis.videoSnapshot.desc || analysis.awemeId) }}
|
|
|
- </option>
|
|
|
- </select>
|
|
|
- </label>
|
|
|
- <button class="btn" type="button" [disabled]="savedAnalysisLoading()" (click)="refreshSavedAnalyses()">
|
|
|
- {{ savedAnalysisLoading() ? '加载中...' : '刷新分析记录' }}
|
|
|
- </button>
|
|
|
- <button class="btn btn-primary" type="button" [disabled]="!savedAnalysisId() || savedAnalysisLoading()" (click)="bindSavedAnalysisEvidence()">
|
|
|
- 绑定为证据
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <p class="ip-success" *ngIf="savedAnalysisMessage()">{{ savedAnalysisMessage() }}</p>
|
|
|
- <div class="ip-evidence-grid" *ngIf="plan.benchmarkEvidence?.length || plan.evidenceFetchJobs?.length">
|
|
|
+ <div class="publish-package-modal__grid">
|
|
|
<section>
|
|
|
- <h4>最近证据</h4>
|
|
|
- <p class="ip-empty" *ngIf="!plan.benchmarkEvidence?.length">暂无已保存对标证据。</p>
|
|
|
- <article *ngFor="let item of recentBenchmarkEvidence(plan); trackBy: trackById">
|
|
|
- <strong>{{ displayText(item.title) }}</strong>
|
|
|
- <span>{{ item.confidence }} / {{ displayText(item.authorName) }}</span>
|
|
|
- <p>{{ displayText(item.summary) }}</p>
|
|
|
- <small>{{ displayText(item.migrationSuggestion) }}</small>
|
|
|
- <button class="btn btn-sm" type="button" (click)="generateEvidenceSuggestions(item)">生成打磨建议</button>
|
|
|
- </article>
|
|
|
+ <h4>话题与评论引导</h4>
|
|
|
+ <p>{{ pkg.hashtags.join(' ') }}</p>
|
|
|
+ <p><strong>互动问题:</strong>{{ pkg.commentGuidance || '待人工补充' }}</p>
|
|
|
+ <p><strong>置顶评论:</strong>{{ pkg.pinnedCommentSuggestion || '待人工补充' }}</p>
|
|
|
</section>
|
|
|
<section>
|
|
|
- <h4>采集记录</h4>
|
|
|
- <p class="ip-empty" *ngIf="!plan.evidenceFetchJobs?.length">暂无采集记录。</p>
|
|
|
- <article *ngFor="let job of recentEvidenceJobs(plan); trackBy: trackById" [class.is-failed]="job.status === 'failed'">
|
|
|
- <strong>{{ evidenceJobStatusLabel(job.status) }} / {{ evidenceStrengthLabel(job.evidenceStrength) }}</strong>
|
|
|
- <span>{{ job.awemeId || job.inputUrl }}</span>
|
|
|
- <small>{{ job.updatedAt | date:'MM-dd HH:mm:ss' }}</small>
|
|
|
- <p *ngIf="job.failureMessage">{{ job.failureMessage }}</p>
|
|
|
- </article>
|
|
|
+ <h4>字幕与声音</h4>
|
|
|
+ <p><strong>画面重点字:</strong>{{ (pkg.subtitleHighlights || []).join('、') || '以执行脚本为准' }}</p>
|
|
|
+ <p><strong>声音建议:</strong>{{ pkg.audioSuggestion || '口播清晰优先' }}</p>
|
|
|
</section>
|
|
|
</div>
|
|
|
- <div class="ip-evidence-suggestions" *ngIf="plan.evidenceSuggestions?.length">
|
|
|
- <h4>证据打磨建议</h4>
|
|
|
- <article *ngFor="let suggestion of recentEvidenceSuggestions(plan); trackBy: trackById" [class.is-applied]="suggestion.status === 'applied'" [class.is-dismissed]="suggestion.status === 'dismissed'">
|
|
|
- <div>
|
|
|
- <strong>{{ displayText(suggestion.title) }}</strong>
|
|
|
- <span>{{ suggestionTargetLabel(suggestion.targetType) }} / {{ suggestion.status }}</span>
|
|
|
- </div>
|
|
|
- <p>{{ displayText(suggestion.rationale) }}</p>
|
|
|
- <small>{{ displayText(suggestion.suggestedChange) }}</small>
|
|
|
- <div class="ip-actions" *ngIf="suggestion.status === 'draft'">
|
|
|
- <button class="btn btn-sm btn-primary" type="button" (click)="applyEvidenceSuggestion(suggestion)">应用</button>
|
|
|
- <button class="btn btn-sm" type="button" (click)="dismissEvidenceSuggestion(suggestion)">忽略</button>
|
|
|
- </div>
|
|
|
- </article>
|
|
|
+
|
|
|
+ <div class="publish-package-modal__grid publish-package-modal__checks">
|
|
|
+ <section class="publish-package-modal__check-group">
|
|
|
+ <header><h4>物料检查</h4><span>发布前逐项确认</span></header>
|
|
|
+ <label class="publish-package-modal__check-row" *ngFor="let item of pkg.materialChecklist">
|
|
|
+ <input type="checkbox" [checked]="item.checked" (change)="updatePublishChecklist(pkg, 'materialChecklist', item.id, $any($event.target).checked)">
|
|
|
+ <span>{{ item.label }}</span>
|
|
|
+ </label>
|
|
|
+ </section>
|
|
|
+ <section class="publish-package-modal__check-group">
|
|
|
+ <header><h4>风险检查</h4><span>确认后再手动发布</span></header>
|
|
|
+ <label class="publish-package-modal__check-row" *ngFor="let item of pkg.riskChecklist">
|
|
|
+ <input type="checkbox" [checked]="item.checked" (change)="updatePublishChecklist(pkg, 'riskChecklist', item.id, $any($event.target).checked)">
|
|
|
+ <span>{{ item.label }}</span>
|
|
|
+ </label>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
- </article>
|
|
|
|
|
|
- <div class="ip-stage-note">
|
|
|
- <strong>如何判断当前问题</strong>
|
|
|
- <span>质量分、待补充资料、定位字段为空、同步按钮禁用,属于当前 P0a 必须继续优化的问题;局部重新生成、素材库联动、自动对标数据和更细的脚本版本管理,属于后续 P1/P2 增强。</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="ip-debug-panel ip-bridge-panel" *ngIf="plan.bridgeRecords?.length">
|
|
|
- <details open>
|
|
|
- <summary>
|
|
|
- <strong>制作/回填记录</strong>
|
|
|
- <span>最近 {{ recentBridgeRecords(plan).length }} 条动作,可用于追踪生产闭环</span>
|
|
|
- </summary>
|
|
|
- <div class="ip-debug-grid ip-bridge-grid">
|
|
|
- <section *ngFor="let record of recentBridgeRecords(plan); trackBy: trackById" [class.is-failed]="record.status === 'failed'" [class.is-skipped]="record.status === 'skipped'">
|
|
|
- <strong>{{ displayText(record.title) }}</strong>
|
|
|
- <span>{{ bridgeTargetLabel(record.target) }} / {{ bridgeStatusLabel(record.status) }}</span>
|
|
|
- <small>{{ record.createdAt | date:'MM-dd HH:mm:ss' }}</small>
|
|
|
- <small>{{ record.sourceType }} -> {{ record.targetId }}</small>
|
|
|
- <p>{{ displayText(record.message) }}</p>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
- </details>
|
|
|
+ <section class="publish-package-modal__section publish-package-modal__monitoring">
|
|
|
+ <h4>发布后复盘指标</h4>
|
|
|
+ <p>{{ (pkg.postPublishMonitoringTargets || []).join('、') || '至少记录播放、点赞、评论、收藏、分享和人工判断。' }}</p>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
|
|
|
- <div class="ip-debug-panel" *ngIf="plan.regenerationNotes?.length">
|
|
|
- <details>
|
|
|
- <summary>
|
|
|
- <strong>局部重生成记录</strong>
|
|
|
- <span>同一阶段最多允许手动重试 2 次</span>
|
|
|
- </summary>
|
|
|
- <div class="ip-debug-grid">
|
|
|
- <section *ngFor="let note of recentRegenerationNotes(plan); trackBy: trackById" [class.is-failed]="note.status === 'failed'">
|
|
|
- <strong>{{ stageLabel(note.stage) }}</strong>
|
|
|
- <span>第 {{ note.attempt }} 次 / {{ note.status === 'failed' ? '失败' : '完成' }}</span>
|
|
|
- <small>{{ note.createdAt | date:'MM-dd HH:mm:ss' }}</small>
|
|
|
- <small *ngIf="note.preservedUserEdits">保留 {{ note.preservedUserEdits }} 处手动编辑</small>
|
|
|
- <small *ngIf="note.failureCode">{{ note.failureCode }}</small>
|
|
|
- <p>{{ note.summary }}</p>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
- </details>
|
|
|
- </div>
|
|
|
+ <footer>
|
|
|
+ <button class="btn btn-sm" type="button" [disabled]="isPublishPackagePolishing(pkg)" (click)="polishPublishPackage(pkg)">{{ publishPackagePolishLabel(pkg) }}</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="copyPublishPackageMarkdown(pkg)">复制发布包</button>
|
|
|
+ <button class="btn btn-sm" type="button" *ngIf="pkg.status === 'draft'" (click)="updatePublishPackageStatus(pkg.id, 'ready_for_review')">标记待审核</button>
|
|
|
+ <button class="btn btn-sm btn-primary" type="button" *ngIf="pkg.status === 'ready_for_review'" (click)="updatePublishPackageStatus(pkg.id, 'approved')">确认可发布</button>
|
|
|
+ </footer>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="ip-debug-panel" *ngIf="plan.status === 'failed' || plan.errorMessage || plan.generationProgress.length">
|
|
|
- <details>
|
|
|
- <summary>
|
|
|
- <strong>生成过程与调试信息</strong>
|
|
|
- <span>{{ copyMessage() || '用于定位接口、JSON 和质量门槛问题' }}</span>
|
|
|
- </summary>
|
|
|
- <div class="ip-debug-actions">
|
|
|
- <button class="btn btn-sm" type="button" (click)="copyPlanDebugInfo(plan)">复制调试信息</button>
|
|
|
- </div>
|
|
|
- <div class="ip-debug-grid">
|
|
|
- <section *ngFor="let item of plan.generationProgress; trackBy: trackByStage">
|
|
|
- <strong>{{ item.label }}</strong>
|
|
|
- <span>{{ item.status }}</span>
|
|
|
- <small *ngIf="item.durationMs">{{ item.durationMs }}ms</small>
|
|
|
- <small *ngIf="item.failureCode">{{ item.failureCode }}</small>
|
|
|
- <p *ngIf="item.errorMessage">{{ item.errorMessage }}</p>
|
|
|
- <pre *ngIf="item.rawPreview">{{ item.rawPreview }}</pre>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
- </details>
|
|
|
+ <div class="publish-binding-panel" [hidden]="activeWorkspace() !== 'overview' && activeWorkspace() !== 'publish'" *ngIf="activePlan() as plan">
|
|
|
+ <header class="publish-binding-panel__head">
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">发布复盘</span>
|
|
|
+ <h3>发布复盘:把真实表现反哺下一轮</h3>
|
|
|
+ <p *ngIf="plan.publishPackages?.length; else noPublishPackageYet">人工发布后绑定真实作品,补齐可得数据和判断,再把复盘结论转成下一轮可执行的候选选题。</p>
|
|
|
+ <ng-template #noPublishPackageYet>
|
|
|
+ <p>当前还没有发布包。先从内容方向选择选题、生成分镜脚本,再生成发布包。</p>
|
|
|
+ </ng-template>
|
|
|
</div>
|
|
|
+ <div class="publish-retrospective-summary" *ngIf="latestPublishRetrospective() as review">
|
|
|
+ <span>最近复盘</span>
|
|
|
+ <strong>{{ retrospectiveOutcomeLabel(review.outcome) }}</strong>
|
|
|
+ <small>下一步:{{ review.nextAction === 'continue' ? '继续放大' : review.nextAction === 'adjust' ? '调整后再测' : review.nextAction === 'pause' ? '暂停该打法' : '更换方向' }}</small>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
|
|
|
- <nav class="ip-result-tabs" aria-label="方案内容">
|
|
|
- <button type="button" [class.is-active]="resultView() === 'overview'" (click)="setResultView('overview')">总览判断</button>
|
|
|
- <button type="button" [class.is-active]="resultView() === 'topics'" (click)="setResultView('topics')">选题打磨</button>
|
|
|
- <button type="button" [class.is-active]="resultView() === 'scripts'" (click)="setResultView('scripts')">脚本稿</button>
|
|
|
- <button type="button" [class.is-active]="resultView() === 'plan'" (click)="setResultView('plan')">测试计划</button>
|
|
|
- </nav>
|
|
|
-
|
|
|
- <ng-container *ngIf="resultView() === 'overview'">
|
|
|
- <div class="ip-quality ip-quality--panel" *ngIf="plan.qualityCheck as q">
|
|
|
+ <div class="publish-binding-list" *ngIf="plan.publishPackages?.length">
|
|
|
+ <article class="publish-binding-card" *ngFor="let pkg of plan.publishPackages; trackBy: trackById">
|
|
|
+ <div class="publish-binding-card__head">
|
|
|
<div>
|
|
|
- <strong>质量检查</strong>
|
|
|
- <span [class.is-pass]="q.passed">{{ q.passed ? '可进入生产链路' : '需要补充资料' }}</span>
|
|
|
+ <span>{{ evidencePlatformLabel(pkg.platform) }} / {{ publishStatusLabel(pkg.status) }}</span>
|
|
|
+ <strong>{{ pkg.titleOptions[0] || '未命名发布包' }}</strong>
|
|
|
+ <small>{{ publishReviewStageLabel(pkg) }}:{{ publishReviewNextStep(pkg) }}</small>
|
|
|
</div>
|
|
|
- <div class="ip-quality-scores">
|
|
|
- <span>定位 {{ q.positioningClarityScore }}</span>
|
|
|
- <span>用户 {{ q.audienceClarityScore }}</span>
|
|
|
- <span>素材 {{ q.evidenceSupportScore }}</span>
|
|
|
- <span>迁移 {{ q.benchmarkTransferScore }}</span>
|
|
|
- <span>选题 {{ q.topicUsabilityScore }}</span>
|
|
|
- </div>
|
|
|
- <div class="ip-warning-list" *ngIf="q.blockingIssues.length || q.improvementSuggestions.length">
|
|
|
- <p *ngFor="let issue of q.blockingIssues">阻断:{{ displayText(issue) }}</p>
|
|
|
- <p *ngFor="let suggestion of q.improvementSuggestions">建议:{{ displayText(suggestion) }}</p>
|
|
|
+ <div class="ip-actions">
|
|
|
+ <button class="btn btn-sm" type="button" (click)="openPublishPackageModal(pkg)">查看发布包</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="autoMatchPublishPackage(pkg)">自动匹配作品</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <article class="ip-card ip-section-card">
|
|
|
- <header>
|
|
|
- <div>
|
|
|
- <span class="ip-kicker">定位诊断</span>
|
|
|
- <h3>先判断这个 IP 是否成立</h3>
|
|
|
- <small class="ip-regeneration-meta">{{ lastRegenerationText(plan, 'diagnosis') }}</small>
|
|
|
- </div>
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="!!regeneratingStage()" (click)="regenerateSection('diagnosis')">
|
|
|
- {{ regeneratingStage() === 'diagnosis' ? '重生成中...' : '重生成定位' }}
|
|
|
- </button>
|
|
|
- </header>
|
|
|
- <ng-container *ngIf="plan.diagnosis; else diagnosisPending">
|
|
|
- <div class="ip-diagnosis-grid">
|
|
|
- <p><strong>目标用户</strong>{{ displayText(plan.diagnosis.targetAudience.summary) }}</p>
|
|
|
- <p><strong>市场机会</strong>{{ displayText(plan.diagnosis.marketOpportunity) }}</p>
|
|
|
- <p><strong>差异化</strong>{{ displayText(plan.diagnosis.differentiation) }}</p>
|
|
|
- <p><strong>变现路径</strong>{{ displayText(plan.diagnosis.monetizationPath) }}</p>
|
|
|
- </div>
|
|
|
- <div class="ip-chip-row">
|
|
|
- <span *ngFor="let tag of plan.diagnosis.personaTags || []">{{ displayText(tag) }}</span>
|
|
|
- </div>
|
|
|
- </ng-container>
|
|
|
- <ng-template #diagnosisPending>
|
|
|
- <p class="ip-empty">定位诊断尚未生成完成,请等待生成流程结束或查看错误信息。</p>
|
|
|
- </ng-template>
|
|
|
- </article>
|
|
|
+ <div class="publish-binding-form">
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ [ngModel]="publishBindingUrlDrafts()[pkg.id] || ''"
|
|
|
+ (ngModelChange)="updatePublishBindingUrlDraft(pkg.id, $event)"
|
|
|
+ placeholder="粘贴已发布作品链接,用于手动绑定">
|
|
|
+ <button class="btn btn-sm btn-primary" type="button" (click)="manualBindPublishPackage(pkg)">手动绑定</button>
|
|
|
+ </div>
|
|
|
|
|
|
- <article class="ip-card ip-section-card">
|
|
|
- <header>
|
|
|
- <div>
|
|
|
- <span class="ip-kicker">对标迁移</span>
|
|
|
- <h3>看哪些能学,哪些不能照搬</h3>
|
|
|
- <small class="ip-regeneration-meta">{{ lastRegenerationText(plan, 'benchmark') }}</small>
|
|
|
+ <ng-container *ngIf="bindingForPackage(pkg.id) as binding; else noPublishBinding">
|
|
|
+ <div
|
|
|
+ class="publish-binding-status"
|
|
|
+ [ngClass]="bindingDataStatusClass(pkg, binding)">
|
|
|
+ <div class="publish-binding-status__body">
|
|
|
+ <span>{{ bindingDataStatusLabel(pkg, binding) }}</span>
|
|
|
+ <strong>{{ binding.awemeId || binding.workUrl || '已绑定作品' }}</strong>
|
|
|
+ <small>{{ bindingDataStatusDescription(pkg, binding) }}</small>
|
|
|
</div>
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="!!regeneratingStage()" (click)="regenerateSection('benchmark')">
|
|
|
- {{ regeneratingStage() === 'benchmark' ? '重生成中...' : '重生成对标' }}
|
|
|
- </button>
|
|
|
- </header>
|
|
|
- <p class="ip-empty" *ngIf="!plan.benchmarkAnalyses.length">对标拆解尚未生成完成。</p>
|
|
|
- <div class="ip-analysis-grid">
|
|
|
- <section *ngFor="let item of plan.benchmarkAnalyses; trackBy: trackByIndex">
|
|
|
- <h4>{{ displayText(item.accountName) }} <small>{{ item.fitScore }}分</small></h4>
|
|
|
- <p>{{ displayText(item.positioning) }}</p>
|
|
|
- <p><strong>可借鉴:</strong>{{ displayText(item.borrowablePoints) }}</p>
|
|
|
- <p><strong>不可照搬:</strong>{{ displayText(item.nonCopyablePoints) }}</p>
|
|
|
- <p><strong>迁移方向:</strong>{{ displayText(item.migrationDirection) }}</p>
|
|
|
- </section>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="rejectPublishBinding(binding)">取消绑定</button>
|
|
|
</div>
|
|
|
- </article>
|
|
|
|
|
|
- <article class="ip-card ip-section-card">
|
|
|
- <header>
|
|
|
- <div>
|
|
|
- <span class="ip-kicker">机会地图</span>
|
|
|
- <h3>从爆款点转成自己的内容方向</h3>
|
|
|
- <small class="ip-regeneration-meta">{{ lastRegenerationText(plan, 'opportunities') }}</small>
|
|
|
- </div>
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="!!regeneratingStage()" (click)="regenerateSection('opportunities')">
|
|
|
- {{ regeneratingStage() === 'opportunities' ? '重生成中...' : '重生成机会地图' }}
|
|
|
+ <div class="publication-review-form">
|
|
|
+ <label>
|
|
|
+ <span>真实链接</span>
|
|
|
+ <input type="text" [ngModel]="reviewDraft(pkg.id).url" (ngModelChange)="updateReviewDraft(pkg.id, { url: $event })" placeholder="可选,留空则使用绑定链接">
|
|
|
+ </label>
|
|
|
+ <label>
|
|
|
+ <span>发布时间</span>
|
|
|
+ <input type="datetime-local" [ngModel]="reviewDraft(pkg.id).publishedAt" (ngModelChange)="updateReviewDraft(pkg.id, { publishedAt: $event })">
|
|
|
+ </label>
|
|
|
+ <label><span>播放</span><input type="number" min="0" [ngModel]="reviewDraft(pkg.id).views" (ngModelChange)="updateReviewDraft(pkg.id, { views: $event })"></label>
|
|
|
+ <label><span>点赞</span><input type="number" min="0" [ngModel]="reviewDraft(pkg.id).likes" (ngModelChange)="updateReviewDraft(pkg.id, { likes: $event })"></label>
|
|
|
+ <label><span>评论</span><input type="number" min="0" [ngModel]="reviewDraft(pkg.id).comments" (ngModelChange)="updateReviewDraft(pkg.id, { comments: $event })"></label>
|
|
|
+ <label><span>收藏</span><input type="number" min="0" [ngModel]="reviewDraft(pkg.id).favorites" (ngModelChange)="updateReviewDraft(pkg.id, { favorites: $event })"></label>
|
|
|
+ <label><span>分享</span><input type="number" min="0" [ngModel]="reviewDraft(pkg.id).shares" (ngModelChange)="updateReviewDraft(pkg.id, { shares: $event })"></label>
|
|
|
+ <label><span>人工评分</span><input type="number" min="0" max="5" [ngModel]="reviewDraft(pkg.id).rating" (ngModelChange)="updateReviewDraft(pkg.id, { rating: $event })"></label>
|
|
|
+ <label class="publication-review-form__notes">
|
|
|
+ <span>复盘判断</span>
|
|
|
+ <textarea [ngModel]="reviewDraft(pkg.id).notes" (ngModelChange)="updateReviewDraft(pkg.id, { notes: $event })" placeholder="这条作品是否符合预期?哪些定位、选题或表达需要调整?"></textarea>
|
|
|
+ </label>
|
|
|
+ <button class="btn btn-sm btn-primary" type="button" [disabled]="publishRetrospectiveStates()[pkg.id] === 'analyzing'" (click)="recordPublicationReview(pkg.id)">
|
|
|
+ {{ publishRetrospectiveStates()[pkg.id] === 'analyzing' ? '分析复盘中...' : '记录复盘' }}
|
|
|
</button>
|
|
|
- </header>
|
|
|
- <p class="ip-empty" *ngIf="!plan.opportunityMap.length">机会地图尚未生成完成。</p>
|
|
|
- <div class="ip-opportunity-list">
|
|
|
- <section *ngFor="let item of plan.opportunityMap; trackBy: trackById">
|
|
|
- <div>
|
|
|
- <strong>{{ displayText(item.type) }}</strong>
|
|
|
- <span>{{ item.fitScore }}分</span>
|
|
|
- </div>
|
|
|
- <p>{{ displayText(item.reason) }}</p>
|
|
|
- <small>风险边界:{{ displayText(item.riskBoundary) }}</small>
|
|
|
- </section>
|
|
|
</div>
|
|
|
- </article>
|
|
|
- </ng-container>
|
|
|
-
|
|
|
- <ng-container *ngIf="resultView() === 'topics'">
|
|
|
- <article class="ip-card ip-section-card">
|
|
|
- <header>
|
|
|
+ <div class="publish-review-result" *ngIf="retrospectiveForPackage(pkg.id) as review">
|
|
|
<div>
|
|
|
- <span class="ip-kicker">P0b 可编辑</span>
|
|
|
- <h3>优先打磨高优先级选题</h3>
|
|
|
- <small class="ip-regeneration-meta">{{ lastRegenerationText(plan, 'topics_scripts_plan') }}</small>
|
|
|
+ <span>复盘报告已生成</span>
|
|
|
+ <strong>{{ review.reportTitle || retrospectiveOutcomeLabel(review.outcome) }} / 下一步:{{ retrospectiveNextActionLabel(review.nextAction) }}</strong>
|
|
|
+ <p>{{ review.insight?.nextRoundBrief || review.diagnosis }}</p>
|
|
|
</div>
|
|
|
<div class="ip-actions">
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="!!regeneratingStage()" (click)="regenerateSection('topics_scripts_plan')">
|
|
|
- {{ regeneratingStage() === 'topics_scripts_plan' ? '重生成中...' : '重生成选题脚本' }}
|
|
|
- </button>
|
|
|
- <button class="btn btn-primary" type="button" [disabled]="plan.status !== 'ready' || !productionReadyTopics().length" (click)="syncHighPriorityTopics()">同步可制作</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="openPublishRetrospectiveModal(pkg, review)">查看复盘报告</button>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="setWorkspace('directions')">查看方向选题</button>
|
|
|
</div>
|
|
|
- </header>
|
|
|
- <p class="ip-empty" *ngIf="!plan.topics.length">选题尚未生成完成。</p>
|
|
|
- <div class="ip-topic-cards">
|
|
|
- <section *ngFor="let topic of sortedTopics(); trackBy: trackById" class="ip-topic-card" [class.is-high]="topic.priority === 'high'">
|
|
|
- <div class="ip-topic-card__head">
|
|
|
- <span>{{ priorityLabel(topic.priority) }}优先级</span>
|
|
|
- <small>{{ trafficLayerLabel(topic.trafficLayer) }} / {{ topic.column }}</small>
|
|
|
- </div>
|
|
|
- <div class="ip-topic-quality" [ngClass]="topicQualityClass(topic.qualityLevel)">
|
|
|
- <strong>{{ topicQualityLabel(topic.qualityLevel) }}</strong>
|
|
|
- <span>{{ topic.productionReadinessScore || 0 }}/100</span>
|
|
|
- <small *ngIf="topic.polishSuggestions?.length">{{ displayText(topic.polishSuggestions) }}</small>
|
|
|
- </div>
|
|
|
- <label>标题<input [(ngModel)]="topic.title" (change)="saveTopicField(topic, {title: topic.title})"></label>
|
|
|
- <label>适配理由<textarea rows="3" [(ngModel)]="topic.fitReason" (change)="saveTopicField(topic, {fitReason: topic.fitReason})"></textarea></label>
|
|
|
- <p><strong>内容目标:</strong>{{ displayText(topic.contentGoal) }}</p>
|
|
|
- <p><strong>所需素材:</strong>{{ displayText(topic.requiredMaterials) }}</p>
|
|
|
- <p><strong>风险:</strong>{{ displayText(topic.riskNote) }}</p>
|
|
|
- <div class="ip-topic-card__actions">
|
|
|
- <span *ngIf="topic.userEdited">已手动编辑</span>
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="plan.status !== 'ready' || topic.qualityLevel !== 'make_now' || !!topic.syncedTopicId" (click)="syncTopic(topic)">
|
|
|
- {{ topic.syncedTopicId ? '已同步选题池' : '同步选题池' }}
|
|
|
- </button>
|
|
|
- <button class="btn btn-sm btn-primary" type="button" [disabled]="plan.status !== 'ready' || topic.qualityLevel !== 'make_now'" (click)="createTopicVideoDraft(topic)">
|
|
|
- 生成主题视频草稿
|
|
|
- </button>
|
|
|
+ <section class="publish-next-round" *ngIf="followUpTopicsForReview(review) as followUps">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span>下一轮候选选题</span>
|
|
|
+ <strong>{{ followUps.length ? '选择要进入下一轮测试的题' : '本次复盘没有生成候选选题' }}</strong>
|
|
|
+ </div>
|
|
|
+ <small>{{ followUps.length ? '来源可追踪,纳入后进入方向选题。' : '可以查看报告后手动调整方向和选题。' }}</small>
|
|
|
+ </header>
|
|
|
+ <div class="publish-next-round__list" *ngIf="followUps.length; else noFollowUpTopics">
|
|
|
+ <article
|
|
|
+ class="publish-followup-topic"
|
|
|
+ [ngClass]="followUpTopicStatusClass(topic)"
|
|
|
+ *ngFor="let topic of followUps; trackBy: trackById">
|
|
|
+ <div>
|
|
|
+ <span>{{ followUpTopicStatusLabel(topic) }}</span>
|
|
|
+ <strong>{{ topic.title }}</strong>
|
|
|
+ <p>{{ topic.reason }}</p>
|
|
|
+ <small>来源:{{ followUpTopicSourceLabel(topic, pkg, review) }}</small>
|
|
|
+ </div>
|
|
|
+ <ul *ngIf="topic.sourceSuggestions.length">
|
|
|
+ <li *ngFor="let signal of topic.sourceSuggestions.slice(0, 3)">{{ signal }}</li>
|
|
|
+ </ul>
|
|
|
+ <div class="ip-actions">
|
|
|
+ <button
|
|
|
+ class="btn btn-sm btn-primary"
|
|
|
+ type="button"
|
|
|
+ *ngIf="!topic.planTopicId && !topic.syncedTopicId && topic.status !== 'accepted' && topic.status !== 'dismissed'"
|
|
|
+ (click)="acceptFollowUpTopic(topic.id)">
|
|
|
+ 纳入方向选题
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="btn btn-sm"
|
|
|
+ type="button"
|
|
|
+ *ngIf="!topic.planTopicId && !topic.syncedTopicId && topic.status !== 'accepted' && topic.status !== 'dismissed'"
|
|
|
+ (click)="dismissFollowUpTopic(topic.id)">
|
|
|
+ 暂不处理
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="btn btn-sm"
|
|
|
+ type="button"
|
|
|
+ *ngIf="topic.planTopicId || topic.syncedTopicId || topic.status === 'accepted'"
|
|
|
+ (click)="goToDirections()">
|
|
|
+ 去方向选题
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
</div>
|
|
|
+ <ng-template #noFollowUpTopics>
|
|
|
+ <p class="publish-next-round__empty">LLM 本次没有给出可直接测试的下一轮选题。建议先查看复盘报告,必要时回到方向选题手动补充。</p>
|
|
|
+ </ng-template>
|
|
|
</section>
|
|
|
</div>
|
|
|
- </article>
|
|
|
- </ng-container>
|
|
|
+ </ng-container>
|
|
|
|
|
|
- <ng-container *ngIf="resultView() === 'scripts'">
|
|
|
- <article class="ip-card ip-section-card">
|
|
|
- <header>
|
|
|
- <div>
|
|
|
- <span class="ip-kicker">脚本稿</span>
|
|
|
- <h3>先看完整口播,再看大纲</h3>
|
|
|
- <small class="ip-regeneration-meta">{{ lastRegenerationText(plan, 'topics_scripts_plan') }}</small>
|
|
|
- </div>
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="!!regeneratingStage()" (click)="regenerateSection('topics_scripts_plan')">
|
|
|
- {{ regeneratingStage() === 'topics_scripts_plan' ? '重生成中...' : '重生成脚本' }}
|
|
|
- </button>
|
|
|
- </header>
|
|
|
- <p class="ip-empty" *ngIf="!plan.scripts.length">脚本尚未生成完成。</p>
|
|
|
-
|
|
|
- <section class="ip-script-group" *ngIf="fullScripts().length">
|
|
|
- <h4>完整口播(优先测试)</h4>
|
|
|
- <p class="ip-script-help">默认先看钩子、观点和可拍摄性;完整口播折叠查看,避免长文本干扰判断。</p>
|
|
|
- <details class="ip-script-detail" *ngFor="let script of fullScripts(); let i = index; trackBy: trackById" [open]="i === 0">
|
|
|
- <summary>
|
|
|
- <strong>{{ script.title || script.id }}</strong>
|
|
|
- <span>{{ scriptQualityLabel(script.qualityLevel) }} / {{ script.userEdited ? '已手动编辑' : 'AI 草稿' }}</span>
|
|
|
- </summary>
|
|
|
- <div class="ip-script-quality" [ngClass]="scriptQualityClass(script.qualityLevel)">
|
|
|
- <strong>{{ displayText(script.scriptSummary) }}</strong>
|
|
|
- <small *ngIf="script.productionNotes?.length">{{ displayText(script.productionNotes) }}</small>
|
|
|
- </div>
|
|
|
- <div class="ip-script-priority">
|
|
|
- <section>
|
|
|
- <span>开头钩子</span>
|
|
|
- <strong>{{ displayText(script.hook) }}</strong>
|
|
|
- </section>
|
|
|
- <section>
|
|
|
- <span>核心观点</span>
|
|
|
- <strong>{{ displayText(script.viewpoint) }}</strong>
|
|
|
- </section>
|
|
|
- <section>
|
|
|
- <span>风险提示</span>
|
|
|
- <strong>{{ displayText(script.riskNote) }}</strong>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
- <div class="ip-script-edit">
|
|
|
- <label>开头钩子<input [(ngModel)]="script.hook" (change)="saveScriptField(script, {hook: script.hook})"></label>
|
|
|
- <label>观点<textarea rows="2" [(ngModel)]="script.viewpoint" (change)="saveScriptField(script, {viewpoint: script.viewpoint})"></textarea></label>
|
|
|
- <details class="ip-script-fulltext">
|
|
|
- <summary>展开完整口播</summary>
|
|
|
- <label>完整口播<textarea rows="6" [(ngModel)]="script.fullScript" (change)="saveScriptField(script, {fullScript: script.fullScript})"></textarea></label>
|
|
|
- </details>
|
|
|
- <p><strong>拍摄建议:</strong>{{ displayText(script.shootingTips) }}</p>
|
|
|
- <p><strong>可替换素材位:</strong>{{ displayText(script.replaceableSlots) }}</p>
|
|
|
- <div class="ip-actions">
|
|
|
- <button class="btn btn-sm btn-primary" type="button" [disabled]="plan.status !== 'ready' || script.qualityLevel !== 'ready_to_record'" (click)="createDigitalHumanDraft(script)">
|
|
|
- 生成数字人口播草稿
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </details>
|
|
|
- </section>
|
|
|
+ <ng-template #noPublishBinding>
|
|
|
+ <p class="publish-binding-card__empty">尚未绑定真实作品。人工发布后刷新账号自动匹配,或直接粘贴作品链接。</p>
|
|
|
+ </ng-template>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
|
|
|
- <section class="ip-script-group" *ngIf="outlineScripts().length">
|
|
|
- <h4>脚本大纲(后续打磨)</h4>
|
|
|
- <details class="ip-script-detail" *ngFor="let script of outlineScripts(); trackBy: trackById">
|
|
|
- <summary>
|
|
|
- <strong>{{ script.title || script.id }}</strong>
|
|
|
- <span>{{ scriptQualityLabel(script.qualityLevel) }}</span>
|
|
|
- </summary>
|
|
|
- <div class="ip-script-quality" [ngClass]="scriptQualityClass(script.qualityLevel)">
|
|
|
- <strong>{{ displayText(script.scriptSummary) }}</strong>
|
|
|
- <small *ngIf="script.productionNotes?.length">{{ displayText(script.productionNotes) }}</small>
|
|
|
- </div>
|
|
|
- <div class="ip-script-outline">
|
|
|
- <p><strong>钩子:</strong>{{ displayText(script.hook) }}</p>
|
|
|
- <p><strong>痛点:</strong>{{ displayText(script.painPoint) }}</p>
|
|
|
- <p><strong>观点:</strong>{{ displayText(script.viewpoint) }}</p>
|
|
|
- <p><strong>案例/方法:</strong>{{ displayText(script.caseOrMethod) }}</p>
|
|
|
- <p><strong>结尾:</strong>{{ displayText(script.closingCta) }}</p>
|
|
|
- </div>
|
|
|
- </details>
|
|
|
- </section>
|
|
|
+ <div class="publish-retrospective-modal-backdrop" *ngIf="activePublishRetrospectiveModal() as modal" (click)="closePublishRetrospectiveModal()">
|
|
|
+ <section class="publish-retrospective-modal" role="dialog" aria-modal="true" aria-label="发布复盘报告" (click)="$event.stopPropagation()">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">发布复盘报告</span>
|
|
|
+ <h2>{{ modal.record.reportTitle || modal.pkg.titleOptions[0] || '发布复盘报告' }}</h2>
|
|
|
+ <p>{{ retrospectiveOutcomeLabel(modal.record.outcome) }} / 下一步:{{ retrospectiveNextActionLabel(modal.record.nextAction) }} / {{ modal.record.updatedAt | date:'yyyy-MM-dd HH:mm' }}</p>
|
|
|
+ </div>
|
|
|
+ <button class="ip-work-report-modal__close" type="button" aria-label="关闭发布复盘报告" title="关闭" (click)="closePublishRetrospectiveModal()">×</button>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="publish-retrospective-modal__body">
|
|
|
+ <section class="publish-retrospective-modal__summary">
|
|
|
+ <div class="publish-retrospective-modal__summary-card">
|
|
|
+ <span>绑定作品</span>
|
|
|
+ <strong>{{ modal.work?.title || modal.work?.desc || modal.record.awemeId || modal.record.workId || '未识别作品标题' }}</strong>
|
|
|
+ </div>
|
|
|
+ <div class="publish-retrospective-modal__summary-card">
|
|
|
+ <span>结果判断</span>
|
|
|
+ <strong>{{ modal.record.diagnosis }}</strong>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section>
|
|
|
+ <h3>下一轮判断</h3>
|
|
|
+ <p>{{ modal.record.insight?.nextRoundBrief || modal.record.diagnosis }}</p>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section *ngIf="modal.record.insight?.causeSignals?.length">
|
|
|
+ <h3>表现信号</h3>
|
|
|
+ <article class="publish-retrospective-modal__item" *ngFor="let signal of modal.record.insight?.causeSignals">
|
|
|
+ <strong>{{ signal.label }} · {{ signal.score }}</strong>
|
|
|
+ <p>{{ signal.judgment }}</p>
|
|
|
+ <small *ngIf="signal.evidence.length">{{ signal.evidence.join(' / ') }}</small>
|
|
|
</article>
|
|
|
- </ng-container>
|
|
|
+ </section>
|
|
|
|
|
|
- <ng-container *ngIf="resultView() === 'plan'">
|
|
|
- <article class="ip-card ip-section-card">
|
|
|
- <header>
|
|
|
- <div>
|
|
|
- <span class="ip-kicker">内容测试</span>
|
|
|
- <h3>7 天详细测试 + 30 天方向</h3>
|
|
|
- <small class="ip-regeneration-meta">{{ lastRegenerationText(plan, 'quality_check') }}</small>
|
|
|
- </div>
|
|
|
- <div class="ip-actions">
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="!!regeneratingStage()" (click)="regenerateSection('topics_scripts_plan')">
|
|
|
- {{ regeneratingStage() === 'topics_scripts_plan' ? '重生成中...' : '重生成测试计划' }}
|
|
|
- </button>
|
|
|
- <button class="btn btn-sm" type="button" [disabled]="!!regeneratingStage()" (click)="regenerateSection('quality_check')">
|
|
|
- {{ regeneratingStage() === 'quality_check' ? '检查中...' : '重新质量检查' }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </header>
|
|
|
- <p class="ip-empty" *ngIf="!plan.sevenDayTestPlan.length">7 天测试计划尚未生成完成。</p>
|
|
|
- <div class="ip-plan-days">
|
|
|
- <section *ngFor="let day of plan.sevenDayTestPlan; trackBy: trackByIndex">
|
|
|
- <strong>第 {{ day.day }} 天:{{ displayText(day.topicTitle) }}</strong>
|
|
|
- <p>{{ displayText(day.testGoal) }}</p>
|
|
|
- <p><strong>拍摄重点:</strong>{{ displayText(day.shootingFocus) }}</p>
|
|
|
- <p><strong>观察:</strong>{{ displayText(day.observeMetrics) }}</p>
|
|
|
- <p><strong>复盘问题:</strong>{{ displayText(day.reviewQuestions) }}</p>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
+ <section *ngIf="modal.record.insight?.directionAdjustments?.length">
|
|
|
+ <h3>方向调整</h3>
|
|
|
+ <article class="publish-retrospective-modal__item" *ngFor="let item of modal.record.insight?.directionAdjustments">
|
|
|
+ <strong>{{ item.directionTitle || '未命名方向' }}</strong>
|
|
|
+ <p>{{ item.reason }}</p>
|
|
|
+ <small>建议动作:{{ retrospectiveNextActionLabel(item.action === 'increase' || item.action === 'keep' ? 'continue' : item.action === 'pause' ? 'pause' : 'adjust') }}</small>
|
|
|
</article>
|
|
|
+ </section>
|
|
|
|
|
|
- <article class="ip-card ip-section-card" *ngIf="plan.thirtyDayDirection as direction">
|
|
|
- <h3>30 天栏目测试方向</h3>
|
|
|
- <div class="ip-chip-row">
|
|
|
- <span *ngFor="let column of direction.columnStructure">{{ displayText(column.column) }} {{ column.ratio }}%</span>
|
|
|
- </div>
|
|
|
- <p><strong>每周重点:</strong>{{ displayText(direction.weeklyFocus) }}</p>
|
|
|
- <p><strong>素材方向:</strong>{{ displayText(direction.materialDirections) }}</p>
|
|
|
- <p><strong>迭代路径:</strong>{{ displayText(direction.iterationPath) }}</p>
|
|
|
+ <section *ngIf="modal.record.insight?.nextTopicSignals?.length">
|
|
|
+ <h3>下一轮选题信号</h3>
|
|
|
+ <article class="publish-retrospective-modal__item" *ngFor="let signal of modal.record.insight?.nextTopicSignals">
|
|
|
+ <strong>{{ signal.title }}</strong>
|
|
|
+ <p>{{ signal.userProblem }} · {{ signal.reason }}</p>
|
|
|
+ <small>{{ signal.targetAudience }}</small>
|
|
|
</article>
|
|
|
+ </section>
|
|
|
|
|
|
- <article class="ip-card ip-section-card" *ngIf="plan.missingInputs.length">
|
|
|
- <h3>待补充资料</h3>
|
|
|
- <div class="ip-missing-list">
|
|
|
- <p *ngFor="let item of plan.missingInputs">{{ displayText(item.type) }}:{{ displayText(item.description) }}</p>
|
|
|
- </div>
|
|
|
+ <section *ngIf="modal.record.insight?.scriptConstraints?.length">
|
|
|
+ <h3>脚本约束</h3>
|
|
|
+ <ul>
|
|
|
+ <li *ngFor="let item of modal.record.insight?.scriptConstraints">{{ item.instruction }}<span *ngIf="item.avoid">;避免:{{ item.avoid }}</span></li>
|
|
|
+ </ul>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section>
|
|
|
+ <h3>真实表现</h3>
|
|
|
+ <ul>
|
|
|
+ <li *ngFor="let signal of modal.record.actualSignals">{{ signal }}</li>
|
|
|
+ </ul>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <footer>
|
|
|
+ <button class="btn btn-sm" type="button" (click)="closePublishRetrospectiveModal()">关闭</button>
|
|
|
+ <button class="btn btn-sm btn-primary" type="button" (click)="setWorkspace('directions'); closePublishRetrospectiveModal()">查看下一轮选题</button>
|
|
|
+ </footer>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="ip-work-report-modal-backdrop" *ngIf="activeWorkReportModal() as modal" (click)="closeWorkReportModal()">
|
|
|
+ <section class="ip-work-report-modal" role="dialog" aria-modal="true" aria-labelledby="ip-work-report-title" (click)="$event.stopPropagation()">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">作品分析报告</span>
|
|
|
+ <h2 id="ip-work-report-title">{{ modal.report.title || modal.work.title || modal.work.desc || '作品分析报告' }}</h2>
|
|
|
+ <p>{{ workReportSourceLabel(modal.report) }} / 已保存 / {{ modal.report.createdAt | date:'yyyy-MM-dd HH:mm' }}</p>
|
|
|
+ </div>
|
|
|
+ <button class="ip-work-report-modal__close" type="button" aria-label="关闭作品分析报告" title="关闭" (click)="closeWorkReportModal()">×</button>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="ip-work-report-modal__body">
|
|
|
+ <section class="ip-work-report-modal__sections" *ngIf="modal.sections.length; else emptyWorkReport">
|
|
|
+ <article class="ip-work-report-modal__section" *ngFor="let section of modal.sections">
|
|
|
+ <span>{{ section.tone === 'structure' ? '内容结构' : '运营迁移' }} / 置信度 {{ confidenceLabel(section.confidence) }}</span>
|
|
|
+ <h3>{{ section.title }}</h3>
|
|
|
+ <p>{{ section.body }}</p>
|
|
|
+ <p *ngIf="section.judgment"><strong>运营判断:</strong>{{ section.judgment }}</p>
|
|
|
+ <ul *ngIf="section.signals.length">
|
|
|
+ <li *ngFor="let signal of section.signals">{{ signal }}</li>
|
|
|
+ </ul>
|
|
|
+ <small *ngIf="section.gaps.length">待补:{{ section.gaps.join('、') }}</small>
|
|
|
</article>
|
|
|
- </ng-container>
|
|
|
- </section>
|
|
|
- </main>
|
|
|
+ </section>
|
|
|
+ <ng-template #emptyWorkReport>
|
|
|
+ <p class="ip-work-report-modal__empty">当前作品已有运行记录,但还没有可阅读的报告内容。</p>
|
|
|
+ </ng-template>
|
|
|
+
|
|
|
+ <details class="ip-work-report-modal__comments">
|
|
|
+ <summary>查看对应评论样本({{ modal.comments.length }} 条)</summary>
|
|
|
+ <ol *ngIf="modal.comments.length; else emptyWorkReportComments">
|
|
|
+ <li *ngFor="let comment of modal.comments">
|
|
|
+ <p>{{ comment.text }}</p>
|
|
|
+ <small>{{ workReportCommentMeta(comment) }}</small>
|
|
|
+ </li>
|
|
|
+ </ol>
|
|
|
+ </details>
|
|
|
+ <ng-template #emptyWorkReportComments>
|
|
|
+ <p class="ip-work-report-modal__empty">当前作品报告没有可追溯的评论样本。</p>
|
|
|
+ </ng-template>
|
|
|
+
|
|
|
+ <details *ngIf="modal.transcriptText">
|
|
|
+ <summary>查看视频文字稿</summary>
|
|
|
+ <p>{{ modal.transcriptText }}</p>
|
|
|
+ </details>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
+
|
|
|
+
|
|
|
+ <div class="evidence-drawer-backdrop" *ngIf="evidenceDrawerIds().length" (click)="closeEvidenceDrawer()"></div>
|
|
|
+ <section class="evidence-drawer" *ngIf="evidenceDrawerIds().length" role="dialog" aria-modal="true" aria-label="证据来源">
|
|
|
+ <header>
|
|
|
+ <div>
|
|
|
+ <span class="ip-kicker">可追溯依据</span>
|
|
|
+ <h3>{{ evidenceDrawerTitle() }}</h3>
|
|
|
+ <p>{{ selectedEvidenceSources().length }} 条依据,原始长文本默认收起。</p>
|
|
|
+ </div>
|
|
|
+ <button class="evidence-drawer__close" type="button" aria-label="关闭证据弹窗" title="关闭" (click)="closeEvidenceDrawer()">×</button>
|
|
|
+ </header>
|
|
|
+ <div class="evidence-drawer__body">
|
|
|
+ <p class="ip-empty" *ngIf="!selectedEvidenceSources().length">当前引用未匹配到本地证据,请刷新账号或重新分析。</p>
|
|
|
+ <section class="evidence-drawer__group" *ngFor="let group of selectedEvidenceGroups(); trackBy: trackById">
|
|
|
+ <div class="evidence-drawer__group-heading">
|
|
|
+ <div><h4>{{ group.label }}</h4><p>{{ group.description }}</p></div>
|
|
|
+ <span>{{ group.items.length }} 条</span>
|
|
|
+ </div>
|
|
|
+ <article class="evidence-drawer__item" *ngFor="let evidence of group.items; trackBy: trackById">
|
|
|
+ <div class="evidence-drawer__meta">
|
|
|
+ <span>{{ evidence.typeLabel }}</span>
|
|
|
+ <small *ngIf="evidence.meta">{{ evidence.meta }}</small>
|
|
|
+ </div>
|
|
|
+ <h4>{{ evidence.title }}</h4>
|
|
|
+ <p>{{ evidencePreview(evidence) }}</p>
|
|
|
+ <details *ngIf="evidenceHasDetails(evidence)">
|
|
|
+ <summary>查看完整材料</summary>
|
|
|
+ <p>{{ evidence.summary }}</p>
|
|
|
+ </details>
|
|
|
+ <small class="evidence-gap-note" *ngIf="evidence.gaps?.length">仍缺:{{ evidence.gaps?.join('、') }}</small>
|
|
|
+ <a *ngIf="evidence.url" [href]="evidence.url" target="_blank" rel="noopener noreferrer">打开原始来源</a>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
</section>
|