demo-state.service.ts 91 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475
  1. import { Injectable } from '@angular/core';
  2. import {
  3. AiSelectionStage,
  4. Activity,
  5. BriefScenario,
  6. Candidate,
  7. CandidateStatus,
  8. DeliveryVersion,
  9. FieldMapping,
  10. GapItem,
  11. InteractionLog,
  12. LearnedRule,
  13. NavItem,
  14. NoteDetail,
  15. NoteVerdict,
  16. Requirement,
  17. ResourceSummary,
  18. ResourceFile,
  19. ResourceIssue,
  20. Rule,
  21. RuleTestCase,
  22. SamplingTask,
  23. Step,
  24. TaskTimelineItem,
  25. TaskRecord,
  26. TemplateField,
  27. TemplateItem,
  28. ViewKey
  29. } from './models';
  30. function buildNotes(account: string, styles: string[], startMatch: number): NoteDetail[] {
  31. return Array.from({ length: 10 }, (_, index) => {
  32. const style = styles[index % styles.length];
  33. const match = Math.max(58, startMatch - index * 2 + (index % 3));
  34. return {
  35. id: `${account}-${index + 1}`,
  36. title: [
  37. '用了三周的空瓶真实复盘',
  38. '通勤包里的急救护肤三件套',
  39. '成分党也会喜欢的轻乳霜',
  40. '换季敏感肌别再乱叠加',
  41. '早八人的极简护肤流程',
  42. '这支面霜到底适合谁',
  43. '出差三天我只带这几样',
  44. '热门配方横评和真实感受',
  45. '编辑部最近在用什么',
  46. '本周值得看的护肤趋势'
  47. ][index],
  48. publishTime: `2026-04-${String(28 - index).padStart(2, '0')}`,
  49. format: index % 3 === 0 ? '视频' : '图文',
  50. style,
  51. match,
  52. verdict: match >= 82 ? '正向样本' : match >= 76 ? '需复核' : '负向样本',
  53. summary: `${account} 近10条内容样本之一,AI 用标题、正文关键词、图片场景和互动反馈判断是否贴合 Brief 的内容风格。`,
  54. visualCue:
  55. index % 3 === 0
  56. ? '封面为真人出镜或产品实拍,画面信息完整。'
  57. : '图片以生活场景和局部细节为主,弱化广告感。',
  58. textCue:
  59. match >= 80
  60. ? '正文中出现使用周期、肤感、场景、优缺点等真实体验词。'
  61. : '正文偏资讯清单或功效罗列,缺少连续使用体验。',
  62. mismatch: match >= 80 ? '暂无明显冲突' : '真实体验占比不足,可能和 Brief 的“不要硬广”要求冲突。',
  63. keywords: ['真实体验', style, index % 2 === 0 ? '通勤' : '成分', '非硬广'],
  64. metrics: {
  65. read: `${8 + index * 2}.${index}万`,
  66. like: `${1200 + index * 180}`,
  67. collect: `${620 + index * 90}`,
  68. comment: `${86 + index * 11}`
  69. },
  70. aiReason:
  71. match >= 80
  72. ? '内容表达自然,场景和产品体验占比高,可作为风格匹配证据。'
  73. : '资讯或清单感偏重,真实体验表达不足,建议进入人工复核。'
  74. };
  75. });
  76. }
  77. function getApiBaseUrl(): string {
  78. if (typeof window === 'undefined') {
  79. return '';
  80. }
  81. const isAngularDevServer =
  82. (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') &&
  83. window.location.port === '4200';
  84. return isAngularDevServer ? 'http://localhost:3000' : '';
  85. }
  86. function inferFanTier(fans: string): string {
  87. const value = Number.parseFloat(fans);
  88. if (fans.includes('万') && value >= 50) {
  89. return '50万以上';
  90. }
  91. if (fans.includes('万') && value >= 10) {
  92. return '10万-50万';
  93. }
  94. if (fans.includes('万') && value >= 1) {
  95. return '1万-10万';
  96. }
  97. return '1万以下';
  98. }
  99. function buildCandidate(input: {
  100. id: string;
  101. platform: string;
  102. name: string;
  103. category: string;
  104. fans: string;
  105. price: number;
  106. score: number;
  107. styleMatch: number;
  108. status: CandidateStatus;
  109. tags: string[];
  110. reason: string;
  111. risk: string;
  112. owner: string;
  113. supplier: string;
  114. dataFreshness: number;
  115. priceExpire: string;
  116. styles: string[];
  117. startMatch: number;
  118. readMedian: string;
  119. interactionMedian: string;
  120. viralRate: number;
  121. replyRate: number;
  122. fulfillmentRate: number;
  123. naturalTrafficRatio: number;
  124. cooperationNoteRatio: number;
  125. estimatedCpm: number;
  126. audienceProfile: string;
  127. contentForms: string[];
  128. recallSources: string[];
  129. serviceLevel: string;
  130. brandFit: string;
  131. selected?: boolean;
  132. }): Candidate {
  133. return {
  134. id: input.id,
  135. platform: input.platform,
  136. name: input.name,
  137. category: input.category,
  138. contentForms: input.contentForms,
  139. fanTier: inferFanTier(input.fans),
  140. fans: input.fans,
  141. price: input.price,
  142. score: input.score,
  143. styleMatch: input.styleMatch,
  144. status: input.status,
  145. tags: input.tags,
  146. reason: input.reason,
  147. risk: input.risk,
  148. quota: `${input.platform} 扩展池`,
  149. selected: input.selected ?? false,
  150. link: `https://demo.tihao.ai/${input.platform}/${input.id}`,
  151. owner: input.owner,
  152. supplier: input.supplier,
  153. dataFreshness: input.dataFreshness,
  154. priceExpire: input.priceExpire,
  155. audienceProfile: input.audienceProfile,
  156. readMedian: input.readMedian,
  157. interactionMedian: input.interactionMedian,
  158. viralRate: input.viralRate,
  159. replyRate: input.replyRate,
  160. fulfillmentRate: input.fulfillmentRate,
  161. naturalTrafficRatio: input.naturalTrafficRatio,
  162. cooperationNoteRatio: input.cooperationNoteRatio,
  163. estimatedCpm: input.estimatedCpm,
  164. cooperationHistory: '近 12 个月有 2-5 次同类客户合作记录',
  165. serviceLevel: input.serviceLevel,
  166. brandFit: input.brandFit,
  167. recallSources: input.recallSources,
  168. ruleHits: ['资源库召回', '内容标签匹配', input.serviceLevel, input.brandFit],
  169. scoreBreakdown: [
  170. { label: '需求匹配', value: Math.min(99, input.score + 1) },
  171. { label: '内容表现', value: Math.min(99, input.styleMatch + 2) },
  172. { label: '预算健康', value: input.price <= 8000 ? 92 : input.price <= 15000 ? 82 : 72 },
  173. { label: '服务履约', value: input.fulfillmentRate }
  174. ],
  175. recentNotes: buildNotes(input.name, input.styles, input.startMatch)
  176. };
  177. }
  178. @Injectable({ providedIn: 'root' })
  179. export class DemoStateService {
  180. activeView: ViewKey = 'workspaceFlow';
  181. currentStep = 1;
  182. simpleMode = true;
  183. showAdvancedWorkspace = false;
  184. uploadedFileName = '某品牌种草合作Brief.docx';
  185. resourcePath = 'D:\\公司优秀媒体资源库\\4月份资源库';
  186. feedbackText =
  187. '客户反馈:小红书希望减少泛美妆号,优先生活方式、通勤穿搭、真实测评风格;抖音达人预算再压低15%,保留爆文率高的账号。';
  188. exportState = '待生成';
  189. apiState = 'PostgreSQL/RAG 检索';
  190. tempRuleTitle = '母婴/护肤场景中,近30天评论含“真实有用”的账号加权';
  191. tempRuleImpact = '预计强推荐增加 6 个,需复核减少 2 个';
  192. selectedPlatform = '全部';
  193. selectedStatus: CandidateStatus | '全部' = '全部';
  194. budgetMax = 18000;
  195. creatorQuery = '护肤 真实测评 通勤生活';
  196. creatorPlatform = '全部';
  197. creatorCategory = '全部';
  198. creatorContentForm = '全部';
  199. creatorFanTier = '全部';
  200. creatorRisk = '全部';
  201. creatorBudgetMax = 15000;
  202. sortMode: '综合分' | '风格匹配' | '报价升序' = '综合分';
  203. selectedCandidateId = 'M-1087';
  204. selectedNoteId = '梨涡测评室-1';
  205. noteVerdictFilter: NoteVerdict | '全部' = '全部';
  206. compareOpen = false;
  207. previewAudience: '客户版' | '内部复核版' = '客户版';
  208. selectedResourcePlatform = '小红书';
  209. selectedTaskId = 'TH-202605-001';
  210. selectedRuleId = 'R-003';
  211. ruleSandboxText = '客户要求:真实体验、通勤生活,不要硬广,预算尽量控制在 1 万以内。';
  212. ruleSandboxWeight = 80;
  213. ruleSimulationState = '尚未试算';
  214. templatePreviewMode: '客户版' | '内部复核版' | '复盘版' = '客户版';
  215. briefingNote =
  216. '目标客户想看的是“工具能不能把经验留下来”,所以 Demo 里要突出可确认、可追溯、可学习,而不是只展示一张漂亮名单。';
  217. selectedLearningScope: '本任务' | '单客户' | '行业' | '长期规则' = '本任务';
  218. templateUploadState = '等待上传,当前使用系统内置推荐表模板';
  219. deliveryChecklist = [
  220. { label: '客户版隐藏供应商与内部风险', detail: '对客只展示平台、账号、报价、推荐理由和内容证据', done: true },
  221. { label: '需复核账号已处理', detail: '导出前强推荐和备选账号不能含未解释风险', done: false },
  222. { label: '报价与档期已由媒介确认', detail: '价格过期或未绑定供应商的账号只能进内部版', done: false },
  223. { label: '反馈学习入口已准备', detail: '客户驳回原因可回流到规则库,不丢失原文', done: true }
  224. ];
  225. learningScopes: Array<'本任务' | '单客户' | '行业' | '长期规则'> = ['本任务', '单客户', '行业', '长期规则'];
  226. noteVerdicts: Array<NoteVerdict | '全部'> = ['全部', '正向样本', '需复核', '负向样本'];
  227. feedbackPresets = [
  228. '客户临时要求:小红书主推生活方式,不要泛美妆资讯号。',
  229. '商务复核:报价超过预算上限的账号先放备选,除非爆文率明显高。',
  230. '媒介反馈:近期未更新价格的账号需要先联系供应商确认档期。',
  231. '客户偏好:保留真实空瓶、通勤场景、素人感强的内容样本。'
  232. ];
  233. creatorQuickPrompts = [
  234. '找小红书护肤生活方式博主,2k-8k,真实测评,不要硬广',
  235. '保留 B站专业背书,补 3 个长视频成分科普账号',
  236. '抖音预算压低 15%,只保留爆文率高且报价健康的账号',
  237. '参考梨涡测评室,扩展 10 个相似风格中腰部博主'
  238. ];
  239. aiSelectionStages: AiSelectionStage[] = [
  240. {
  241. title: '需求理解',
  242. subtitle: '从 Brief 和临时反馈抽取可执行条件',
  243. input: '客户、品牌、平台数量、预算、内容风格、排除项、参考博主',
  244. aiJudgement: '识别出“小红书主推真实测评/通勤生活,预算 2k-18k,硬广降级”。',
  245. humanControl: '商务可编辑需求点、关闭低置信度推断、追加临时需求。',
  246. output: '6 个确认需求点,2 个待复核推断',
  247. status: '完成',
  248. accounts: ['客户A / 新锐护肤品牌']
  249. },
  250. {
  251. title: '候选召回',
  252. subtitle: '像蒲公英一样从多入口找博主',
  253. input: '资源库、关键词、相似博主、历史采纳、平台筛选',
  254. aiJudgement: '合并 5 类召回来源,去重后形成 324 个候选账号。',
  255. humanControl: '媒介可指定参考账号、锁定平台比例、加入手工收藏账号。',
  256. output: '小红书 182、抖音 72、B站 28、微信 18、微博 24',
  257. status: '完成',
  258. accounts: ['梨涡测评室', '晚间护肤手账', '通勤包里有什么']
  259. },
  260. {
  261. title: '硬性过滤',
  262. subtitle: '先处理不能推荐或必须降级的账号',
  263. input: '黑名单、报价、供应商绑定、报价有效期、近期活跃、合作禁区',
  264. aiJudgement: '剔除黑名单和低活跃账号 31 个,报价过期账号进入备选。',
  265. humanControl: '媒介可临时放行,但必须填写放行理由。',
  266. output: '293 个账号进入评分池',
  267. status: '完成',
  268. accounts: ['美妆情报局', '护肤趋势观察']
  269. },
  270. {
  271. title: '内容采样',
  272. subtitle: '抽取近 10 条内容做风格和硬广判断',
  273. input: '标题、正文关键词、图片/视频场景、互动质量、评论反馈',
  274. aiJudgement: '真实体验、通勤、成分拆解命中率高的账号加权,资讯/硬广感降权。',
  275. humanControl: '单条笔记可标为正向、需复核或负向样本。',
  276. output: '已完成 14 个账号采样,4 个仍待媒介确认',
  277. status: '进行中',
  278. accounts: ['梨涡测评室', '敏感肌自救记录', '阿森护肤实验室']
  279. },
  280. {
  281. title: '多目标评分',
  282. subtitle: '把单账号质量变成可解释分数',
  283. input: '需求匹配、内容表现、粉丝画像、预算健康、数据新鲜度、服务履约、风险扣分',
  284. aiJudgement: '晚间护肤手账因风格和粉丝画像高匹配进入强推荐,微博资讯号降级。',
  285. humanControl: '可调整权重并记录为本次任务规则。',
  286. output: 'Top 20 推荐池和 Top 60 备选池',
  287. status: '进行中',
  288. accounts: ['晚间护肤手账', '配方师小简', '打工人变美清单']
  289. },
  290. {
  291. title: '组合配比',
  292. subtitle: '避免整份名单同质化',
  293. input: '平台比例、头腰尾比例、内容类目、价格带、主推/备选数量',
  294. aiJudgement: '控制小红书生活方式占比,保留 B站专业背书和低预算补充位。',
  295. humanControl: '可锁定账号、换一批、批量强推/备选/剔除。',
  296. output: '客户版 16 个主推 + 12 个备选',
  297. status: '待执行',
  298. accounts: ['通勤包里有什么', '配方师小简', '周末家居护肤']
  299. },
  300. {
  301. title: '交付与学习',
  302. subtitle: '输出客户版,并把反馈变成待确认规则',
  303. input: '最终名单、推荐理由、风险说明、客户反馈',
  304. aiJudgement: '客户反馈只生成待确认规则,不自动污染长期规则库。',
  305. humanControl: '商务确认后入库,可限制规则只对单客户生效。',
  306. output: '名单 v1.0 + 待确认规则 3 条',
  307. status: '待执行',
  308. accounts: ['反馈学习规则']
  309. }
  310. ];
  311. deliveryVersions: DeliveryVersion[] = [
  312. {
  313. version: 'v0.1',
  314. title: 'AI 初稿',
  315. owner: '系统',
  316. time: '2026-05-12 01:20',
  317. summary: '按 Brief 生成 104 个候选,输出 Top 20 初稿。',
  318. status: '已生成'
  319. },
  320. {
  321. version: 'v0.3',
  322. title: '媒介复核版',
  323. owner: '媒介-阿敏',
  324. time: '2026-05-12 02:05',
  325. summary: '剔除 4 个硬广风险账号,补入 B站专业背书账号。',
  326. status: '复核中'
  327. },
  328. {
  329. version: 'v1.0',
  330. title: '客户交付版',
  331. owner: '商务-王一',
  332. time: '待导出',
  333. summary: '隐藏内部风险字段,只保留推荐理由、报价、内容样本和排期建议。',
  334. status: '待交付'
  335. }
  336. ];
  337. deliveryFields = [
  338. { label: '平台', customer: true, internal: true, selected: true },
  339. { label: '账号昵称', customer: true, internal: true, selected: true },
  340. { label: '粉丝量', customer: true, internal: true, selected: true },
  341. { label: '报价', customer: true, internal: true, selected: true },
  342. { label: '推荐理由', customer: true, internal: true, selected: true },
  343. { label: '内容样本', customer: true, internal: true, selected: true },
  344. { label: '供应商', customer: false, internal: true, selected: this.previewAudience === '内部复核版' },
  345. { label: '风险提示', customer: false, internal: true, selected: this.previewAudience === '内部复核版' },
  346. { label: '规则命中', customer: false, internal: true, selected: this.previewAudience === '内部复核版' }
  347. ];
  348. resourceHealthItems = [
  349. { label: '报价有效率', value: 82, detail: '报价未过期且供应商可确认' },
  350. { label: '字段完整率', value: 78, detail: '昵称、链接、报价、互动、供应商字段齐全' },
  351. { label: '近30天更新率', value: 69, detail: '本地资源表最近一次更新时间' },
  352. { label: '可联系比例', value: 88, detail: '已绑定媒介负责人或供应商联系人' }
  353. ];
  354. resourceFiles: ResourceFile[] = [
  355. {
  356. platform: '小红书',
  357. name: '小红书4月优质种草资源.xlsx',
  358. updateTime: '2026-05-10 18:20',
  359. rows: 4286,
  360. owner: '媒介-阿敏',
  361. missingFields: ['报价有效期']
  362. },
  363. {
  364. platform: '抖音',
  365. name: '抖音星图护肤达人池.xlsx',
  366. updateTime: '2026-05-09 21:10',
  367. rows: 2130,
  368. owner: '媒介-阿宁',
  369. missingFields: ['供应商联系人', '档期']
  370. },
  371. {
  372. platform: 'B站',
  373. name: 'B站长视频评测UP主.xlsx',
  374. updateTime: '2026-05-08 11:30',
  375. rows: 684,
  376. owner: '媒介-六月',
  377. missingFields: ['近30天播放中位数']
  378. },
  379. {
  380. platform: '微信',
  381. name: '公众号图文资源库.xlsx',
  382. updateTime: '2026-04-29 16:45',
  383. rows: 516,
  384. owner: '媒介-小七',
  385. missingFields: ['主页链接', '供应商联系人']
  386. },
  387. {
  388. platform: '微博',
  389. name: '微博话题扩散账号.xlsx',
  390. updateTime: '2026-05-06 14:05',
  391. rows: 902,
  392. owner: '媒介-小满',
  393. missingFields: ['报价有效期', '历史配合度']
  394. }
  395. ];
  396. resourceIssues: ResourceIssue[] = [
  397. {
  398. id: 'RI-001',
  399. platform: '小红书',
  400. level: '中',
  401. title: '报价有效期缺失',
  402. detail: '部分资源只有图文报价,缺少报价有效期,导出前需要媒介确认。',
  403. action: '批量标为备选并提醒媒介确认',
  404. resolved: false
  405. },
  406. {
  407. id: 'RI-002',
  408. platform: '抖音',
  409. level: '高',
  410. title: '高报价账号需要预算复核',
  411. detail: '客户压预算后,部分抖音账号仍接近预算上限。',
  412. action: '进入预算沙盘,优先保留爆文率高账号',
  413. resolved: false
  414. },
  415. {
  416. id: 'RI-003',
  417. platform: '微信',
  418. level: '高',
  419. title: '主页链接不完整',
  420. detail: '缺主页链接会影响近10条图文采样和风格比对。',
  421. action: '要求资源库补齐链接后再进入强推荐',
  422. resolved: false
  423. }
  424. ];
  425. ruleConflicts = [
  426. {
  427. title: '低预算优先 vs 保留头部背书',
  428. detail: '客户压预算后,B站专业背书账号报价偏高,需要保留为备选并标注价值。',
  429. level: '中'
  430. },
  431. {
  432. title: '真实体验优先 vs 话题扩散',
  433. detail: '微博资讯号适合扩散,但和“不要硬广”存在冲突,客户版建议隐藏。',
  434. level: '高'
  435. }
  436. ];
  437. ruleTestCases: RuleTestCase[] = [
  438. {
  439. id: 'RTC-001',
  440. label: '命中真实体验',
  441. condition: '标题/正文出现空瓶、用了三周、真实反馈等词',
  442. action: '风格分 +8,推荐理由补充真实体验证据',
  443. enabled: true
  444. },
  445. {
  446. id: 'RTC-002',
  447. label: '命中硬广表达',
  448. condition: '正文偏优惠、强卖点、购买引导,缺少使用过程',
  449. action: '降级为需复核,客户版隐藏',
  450. enabled: true
  451. },
  452. {
  453. id: 'RTC-003',
  454. label: '预算保护',
  455. condition: '报价低于预算上限 60%,且互动中位数高于同价位',
  456. action: '综合分 +5,进入性价比候选',
  457. enabled: false
  458. }
  459. ];
  460. briefScenarios: BriefScenario[] = [
  461. {
  462. id: 'budget-down',
  463. label: '客户压预算 15%',
  464. description: '报价接近上限的账号降级,低预算高匹配账号加权。',
  465. selected: false,
  466. impact: '抖音高报价账号转备选,小红书中腰部账号上浮'
  467. },
  468. {
  469. id: 'life-style',
  470. label: '小红书偏生活方式',
  471. description: '通勤、生活方式、真实分享类账号优先。',
  472. selected: false,
  473. impact: '通勤/生活方式标签加权,泛美妆资讯降权'
  474. },
  475. {
  476. id: 'strict-style',
  477. label: '严格排除硬广',
  478. description: '近10条内容匹配低于80%的账号进入复核池。',
  479. selected: false,
  480. impact: '资讯感强、清单感强的账号不进入强推荐'
  481. },
  482. {
  483. id: 'keep-bilibili',
  484. label: '保留 B站专业背书',
  485. description: '即使数量少,也保留长视频成分科普账号。',
  486. selected: false,
  487. impact: 'B站成分科普账号进入备选或强推荐候选'
  488. }
  489. ];
  490. interactionLogs: InteractionLog[] = [
  491. { time: '10:12', action: 'Brief 解析', detail: '已识别平台、数量、预算和内容风格 6 个需求点。' },
  492. { time: '10:18', action: '规则确认', detail: '启用 5 条经验规则,保留 1 条同质化限制待确认。' },
  493. { time: '10:24', action: '账号复核', detail: '梨涡测评室被标记为强推荐,近10条样本 9 条符合。' }
  494. ];
  495. navItems: NavItem[] = [
  496. { key: 'workspaceFlow', label: '提号工作台', note: '上传Brief→导出推荐表' },
  497. { key: 'workspace', label: '提号流程(完整)', note: 'Brief 到推荐名单' },
  498. { key: 'creatorSearch', label: '找博主', note: '像蒲公英一样筛人' },
  499. { key: 'delivery', label: '名单交付', note: '客户版与内部版' },
  500. { key: 'tasks', label: '任务记录', note: '客户需求与版本' },
  501. { key: 'resources', label: '资源库', note: '本地媒体表管理' },
  502. { key: 'rules', label: '规则库', note: '经验与提炼规则' },
  503. { key: 'learning', label: '反馈学习', note: '复盘与自我提升' },
  504. { key: 'templates', label: '模板中心', note: 'Brief 与推荐表' },
  505. { key: 'progressBoard', label: '进度看板', note: '人×任务×进度实时' }
  506. ];
  507. simpleNavKeys: ViewKey[] = ['workspaceFlow', 'workspace', 'creatorSearch', 'delivery', 'tasks', 'progressBoard'];
  508. businessShortcuts = [
  509. {
  510. id: 'fast-list',
  511. title: '一键生成可看名单',
  512. detail: '自动确认需求、运行规则、完成采样,先拿到可讨论的名单。'
  513. },
  514. {
  515. id: 'review-risk',
  516. title: '只看待处理问题',
  517. detail: '跳到资源缺口和需复核账号,适合媒介补信息。'
  518. },
  519. {
  520. id: 'client-preview',
  521. title: '客户版预览',
  522. detail: '隐藏内部风险字段,查看对客话术和推荐理由。'
  523. },
  524. {
  525. id: 'learn-feedback',
  526. title: '反馈变规则',
  527. detail: '把客户反馈转成待确认规则,保留人工审核。'
  528. }
  529. ];
  530. businessChecklist = [
  531. { key: 'brief', label: 'Brief 已解析', detail: '客户、平台、预算、风格已抽取' },
  532. { key: 'demand', label: '需求已确认', detail: '低置信度内容可编辑' },
  533. { key: 'rule', label: '规则已选好', detail: '默认启用核心经验规则' },
  534. { key: 'sample', label: '内容已采样', detail: '近10条笔记用于风格比对' },
  535. { key: 'export', label: '名单可导出', detail: '客户版和内部版分开输出' }
  536. ];
  537. stepGuides = [
  538. {
  539. objective: '把客户 Brief 变成可解析任务',
  540. operator: '上传或替换 Brief 文件',
  541. output: '客户、平台、预算、风格字段'
  542. },
  543. {
  544. objective: '确认 AI 抽取是否准确',
  545. operator: '勾选、编辑、补充需求点',
  546. output: '可执行需求清单'
  547. },
  548. {
  549. objective: '决定本次提号怎么筛',
  550. operator: '启用经验规则或补临时规则',
  551. output: '本次任务规则组合'
  552. },
  553. {
  554. objective: '从资源库召回并评分',
  555. operator: '运行提号和近10条内容采样',
  556. output: '带证据的候选池'
  557. },
  558. {
  559. objective: '复核账号并形成名单',
  560. operator: '筛选、标记、查看账号详情',
  561. output: '客户版推荐名单'
  562. },
  563. {
  564. objective: '把反馈沉淀成能力',
  565. operator: '提炼、确认或驳回规则',
  566. output: '可复用规则与记忆'
  567. }
  568. ];
  569. steps: Step[] = [
  570. { label: '上传 Brief', status: 'active' },
  571. { label: '确认需求', status: 'todo' },
  572. { label: '选择规则', status: 'todo' },
  573. { label: '生成推荐', status: 'todo' },
  574. { label: '导出推荐表', status: 'todo' },
  575. { label: '反馈学习', status: 'todo' }
  576. ];
  577. templates: TemplateItem[] = [
  578. { name: '达人Brief.xlsx', type: 'Brief模板', useCase: '客户标准化需求录入', selected: false },
  579. { name: '某品牌种草合作Brief.docx', type: 'Brief模板', useCase: '非结构化需求解析', selected: false },
  580. { name: '某品牌达人推广规划.xlsx', type: '规划模板', useCase: '多平台组合拆解', selected: false },
  581. { name: '【机构名称】客户+品牌-需求推荐名单.xlsx', type: '推荐表模板', useCase: '对客输出名单', selected: true },
  582. { name: '内部复核名单-含风险字段.xlsx', type: '复核模板', useCase: '媒介确认报价、供应商、风险和档期', selected: false },
  583. { name: '客户反馈复盘表.xlsx', type: '复盘模板', useCase: '沉淀客户偏好、排除项和长期规则', selected: false }
  584. ];
  585. requirements: Requirement[] = [
  586. {
  587. label: '客户/品牌',
  588. value: '客户A / 新锐护肤品牌',
  589. confidence: 96,
  590. selected: true,
  591. editable: false,
  592. evidence: 'Brief 标题:新锐护肤品牌种草合作需求'
  593. },
  594. {
  595. label: '投放目标',
  596. value: '新品种草、真实测评、站内转化蓄水',
  597. confidence: 92,
  598. selected: true,
  599. editable: false,
  600. evidence: 'Brief 目标:新品首发,希望突出真实使用体验'
  601. },
  602. {
  603. label: '平台需求',
  604. value: '小红书 20人、抖音 8人、B站 4人',
  605. confidence: 90,
  606. selected: true,
  607. editable: false,
  608. evidence: '达人推广规划 Sheet:平台配比'
  609. },
  610. {
  611. label: '预算边界',
  612. value: '单号 2,000-18,000,优先性价比',
  613. confidence: 84,
  614. selected: true,
  615. editable: false,
  616. evidence: '预算列存在多个空值,AI 按已填区间推断'
  617. },
  618. {
  619. label: '内容风格',
  620. value: '真实体验、通勤生活、轻专业成分党',
  621. confidence: 88,
  622. selected: true,
  623. editable: false,
  624. evidence: 'Brief 原文:不要硬广,要像真实分享'
  625. },
  626. {
  627. label: '排除项',
  628. value: '硬广感强、近30天低活跃、黑名单供应商',
  629. confidence: 82,
  630. selected: true,
  631. editable: false,
  632. evidence: '规则库默认风控项 + Brief 排除描述'
  633. }
  634. ];
  635. rules: Rule[] = [
  636. {
  637. id: 'R-001',
  638. title: '按 Brief 平台优先提号',
  639. description: '先满足 Brief 明确指定的平台,再用相邻平台补足备选池。',
  640. category: '平台响应',
  641. source: '经验规则',
  642. selected: true,
  643. impact: '确保客户要求不被泛化'
  644. },
  645. {
  646. id: 'R-002',
  647. title: '默认推荐数量不少于需求 3 倍',
  648. description: '当 Brief 写明数量时,候选池按需求数量的 3 倍以上生成,便于商务二次筛选。',
  649. category: '数量策略',
  650. source: '经验规则',
  651. selected: true,
  652. impact: '小红书候选池 60 个,抖音 24 个,B站 12 个'
  653. },
  654. {
  655. id: 'R-003',
  656. title: '风格匹配低于 80% 自动降级',
  657. description: '需要风格比对时,采样账号近 10 篇内容,图文/标题/标签综合匹配不足 80% 标记为需复核。',
  658. category: '内容风格',
  659. source: '经验规则',
  660. selected: true,
  661. impact: '减少客户返工'
  662. },
  663. {
  664. id: 'R-004',
  665. title: '黑名单与低配合度剔除',
  666. description: '历史拉黑、配合度低、供应商关系未绑定的媒体默认不进入强推荐。',
  667. category: '风险控制',
  668. source: '经验规则',
  669. selected: true,
  670. impact: '降低执行风险'
  671. },
  672. {
  673. id: 'R-005',
  674. title: '优先近期采集有效价格',
  675. description: '报价有效期已过或采集超过 45 天的资源进入备选,并提示媒介复核。',
  676. category: '价格有效性',
  677. source: 'AI提炼',
  678. selected: true,
  679. impact: '避免报价失真'
  680. },
  681. {
  682. id: 'R-006',
  683. title: '同质化账号限额',
  684. description: '同一平台同一内容类目强推荐不超过 40%,避免名单看起来重复。',
  685. category: '组合均衡',
  686. source: '用户反馈',
  687. selected: false,
  688. impact: '提升名单丰富度'
  689. }
  690. ];
  691. candidates: Candidate[] = [
  692. {
  693. id: 'M-1087',
  694. platform: '小红书',
  695. name: '梨涡测评室',
  696. fans: '18.6万',
  697. price: 6800,
  698. score: 94,
  699. styleMatch: 91,
  700. status: '强推荐',
  701. tags: ['真实测评', '成分党', '图文强'],
  702. reason: '近10篇中 9 篇为真实体验或成分拆解,互动中位数高于同价位账号 28%。',
  703. risk: '视频内容偏少,可作为图文主推。',
  704. quota: '小红书 1/60',
  705. selected: true,
  706. link: 'https://www.xiaohongshu.com/user/profile/demo1087',
  707. owner: '媒介-阿敏',
  708. supplier: '杭州某MCN',
  709. dataFreshness: 92,
  710. priceExpire: '2026-05-31',
  711. ruleHits: ['平台优先', '风格>80%', '报价有效', '供应商已绑定'],
  712. scoreBreakdown: [
  713. { label: '需求匹配', value: 95 },
  714. { label: '历史表现', value: 89 },
  715. { label: '风险健康', value: 96 }
  716. ],
  717. recentNotes: buildNotes('梨涡测评室', ['真实测评', '通勤生活', '成分拆解'], 94)
  718. },
  719. {
  720. id: 'M-2214',
  721. platform: '小红书',
  722. name: '通勤包里有什么',
  723. fans: '9.4万',
  724. price: 3600,
  725. score: 89,
  726. styleMatch: 86,
  727. status: '强推荐',
  728. tags: ['生活方式', '通勤', '性价比'],
  729. reason: '内容场景和 Brief 的通勤生活需求贴合,历史合作反馈为配合度高。',
  730. risk: '粉丝量中腰部,需要搭配高粉账号。',
  731. quota: '小红书 2/60',
  732. selected: true,
  733. link: 'https://www.xiaohongshu.com/user/profile/demo2214',
  734. owner: '媒介-小徐',
  735. supplier: '上海生活方式MCN',
  736. dataFreshness: 87,
  737. priceExpire: '2026-05-25',
  738. ruleHits: ['数量池补足', '生活方式加权', '低预算友好'],
  739. scoreBreakdown: [
  740. { label: '需求匹配', value: 88 },
  741. { label: '历史表现', value: 82 },
  742. { label: '风险健康', value: 92 }
  743. ],
  744. recentNotes: buildNotes('通勤包里有什么', ['通勤生活', '真实分享', '生活方式'], 90)
  745. },
  746. {
  747. id: 'M-3309',
  748. platform: '抖音',
  749. name: '林小林认真护肤',
  750. fans: '42.1万',
  751. price: 12800,
  752. score: 87,
  753. styleMatch: 82,
  754. status: '备选',
  755. tags: ['口播', '测评', '转化'],
  756. reason: '爆文率和完播率表现稳定,预算接近上限,建议作为抖音头部备选。',
  757. risk: '单条报价较高,需确认档期。',
  758. quota: '抖音 1/24',
  759. selected: true,
  760. link: 'https://www.douyin.com/user/demo3309',
  761. owner: '媒介-阿宁',
  762. supplier: '星图服务商',
  763. dataFreshness: 78,
  764. priceExpire: '2026-05-18',
  765. ruleHits: ['平台优先', '爆文率保护', '预算接近上限'],
  766. scoreBreakdown: [
  767. { label: '需求匹配', value: 84 },
  768. { label: '历史表现', value: 91 },
  769. { label: '风险健康', value: 78 }
  770. ],
  771. recentNotes: buildNotes('林小林认真护肤', ['口播测评', '真实测评', '科普'], 84)
  772. },
  773. {
  774. id: 'M-4402',
  775. platform: 'B站',
  776. name: '阿澈的成分笔记',
  777. fans: '13.2万',
  778. price: 9800,
  779. score: 84,
  780. styleMatch: 88,
  781. status: '备选',
  782. tags: ['长视频', '成分科普', '深度测评'],
  783. reason: '长视频讲解能力强,适合承接品牌专业背书。',
  784. risk: '制作周期较长。',
  785. quota: 'B站 1/12',
  786. selected: true,
  787. link: 'https://space.bilibili.com/demo4402',
  788. owner: '媒介-六月',
  789. supplier: '独立UP主',
  790. dataFreshness: 74,
  791. priceExpire: '2026-06-05',
  792. ruleHits: ['B站补充背书', '成分党加权', '制作周期提示'],
  793. scoreBreakdown: [
  794. { label: '需求匹配', value: 85 },
  795. { label: '历史表现', value: 80 },
  796. { label: '风险健康', value: 84 }
  797. ],
  798. recentNotes: buildNotes('阿澈的成分笔记', ['成分科普', '深度测评', '科普'], 91)
  799. },
  800. {
  801. id: 'M-5520',
  802. platform: '微信',
  803. name: '美妆情报局',
  804. fans: '7.8万',
  805. price: 5200,
  806. score: 78,
  807. styleMatch: 73,
  808. status: '需复核',
  809. tags: ['图文', '资讯', '价格友好'],
  810. reason: '预算友好,但近10篇资讯感较重,和真实测评风格存在偏差。',
  811. risk: '风格匹配不足 80%,需人工确认。',
  812. quota: '补充平台',
  813. selected: false,
  814. link: 'https://mp.weixin.qq.com/demo5520',
  815. owner: '媒介-小七',
  816. supplier: '公众号资源组',
  817. dataFreshness: 61,
  818. priceExpire: '2026-05-12',
  819. ruleHits: ['价格友好', '风格不足80%', '需复核'],
  820. scoreBreakdown: [
  821. { label: '需求匹配', value: 72 },
  822. { label: '历史表现', value: 76 },
  823. { label: '风险健康', value: 69 }
  824. ],
  825. recentNotes: buildNotes('美妆情报局', ['资讯', '清单', '趋势'], 73)
  826. },
  827. buildCandidate({
  828. id: 'M-6101',
  829. platform: '小红书',
  830. name: '晚间护肤手账',
  831. category: '护肤',
  832. fans: '26.3万',
  833. price: 8200,
  834. score: 91,
  835. styleMatch: 93,
  836. status: '强推荐',
  837. tags: ['空瓶', '真实体验', '生活方式'],
  838. reason: '长期做空瓶和敏感肌日记,内容语气自然,适合新品真实种草。',
  839. risk: '档期较满,需要提前 10 天锁定。',
  840. owner: '媒介-小乔',
  841. supplier: '华东生活方式资源组',
  842. dataFreshness: 91,
  843. priceExpire: '2026-06-08',
  844. styles: ['空瓶复盘', '真实测评', '生活方式'],
  845. startMatch: 93,
  846. readMedian: '9.8万',
  847. interactionMedian: '4860',
  848. viralRate: 18,
  849. replyRate: 94,
  850. fulfillmentRate: 96,
  851. naturalTrafficRatio: 82,
  852. cooperationNoteRatio: 22,
  853. estimatedCpm: 84,
  854. audienceProfile: '女性 25-34 岁,一二线城市占比 58%,护肤消费兴趣高',
  855. contentForms: ['图文', '视频'],
  856. recallSources: ['关键词召回', '相似博主召回', '历史采纳召回'],
  857. serviceLevel: '高配合度',
  858. brandFit: '护肤新品高匹配',
  859. selected: true
  860. }),
  861. buildCandidate({
  862. id: 'M-6102',
  863. platform: '小红书',
  864. name: '打工人变美清单',
  865. category: '美妆',
  866. fans: '11.7万',
  867. price: 4800,
  868. score: 88,
  869. styleMatch: 87,
  870. status: '强推荐',
  871. tags: ['通勤', '清单', '性价比'],
  872. reason: '通勤场景稳定,报价健康,可补足中腰部生活方式账号。',
  873. risk: '清单型内容较多,需要脚本里强化单品体验。',
  874. owner: '媒介-小徐',
  875. supplier: '上海通勤内容组',
  876. dataFreshness: 89,
  877. priceExpire: '2026-06-01',
  878. styles: ['通勤生活', '清单', '真实分享'],
  879. startMatch: 88,
  880. readMedian: '6.2万',
  881. interactionMedian: '2510',
  882. viralRate: 14,
  883. replyRate: 91,
  884. fulfillmentRate: 94,
  885. naturalTrafficRatio: 76,
  886. cooperationNoteRatio: 28,
  887. estimatedCpm: 77,
  888. audienceProfile: '女性 22-30 岁,新一线/二线城市白领占比高',
  889. contentForms: ['图文'],
  890. recallSources: ['条件筛选召回', '关键词召回'],
  891. serviceLevel: '高配合度',
  892. brandFit: '预算友好'
  893. }),
  894. buildCandidate({
  895. id: 'M-6103',
  896. platform: '小红书',
  897. name: '敏感肌自救记录',
  898. category: '护肤',
  899. fans: '7.2万',
  900. price: 3900,
  901. score: 86,
  902. styleMatch: 90,
  903. status: '备选',
  904. tags: ['敏感肌', '真实测评', '素人感'],
  905. reason: '素人感强,评论区真实问题多,适合作为可信样本补充。',
  906. risk: '粉丝规模偏小,建议搭配高互动账号。',
  907. owner: '媒介-阿敏',
  908. supplier: '独立博主',
  909. dataFreshness: 84,
  910. priceExpire: '2026-05-29',
  911. styles: ['敏感肌', '真实测评', '素人分享'],
  912. startMatch: 89,
  913. readMedian: '4.1万',
  914. interactionMedian: '1790',
  915. viralRate: 12,
  916. replyRate: 86,
  917. fulfillmentRate: 88,
  918. naturalTrafficRatio: 88,
  919. cooperationNoteRatio: 12,
  920. estimatedCpm: 69,
  921. audienceProfile: '女性 20-32 岁,敏感肌与成分党标签集中',
  922. contentForms: ['图文', '视频'],
  923. recallSources: ['关键词召回', '内容样本召回'],
  924. serviceLevel: '稳定履约',
  925. brandFit: '风格高匹配'
  926. }),
  927. buildCandidate({
  928. id: 'M-6201',
  929. platform: '抖音',
  930. name: '七七成分开麦',
  931. category: '护肤',
  932. fans: '68.5万',
  933. price: 18500,
  934. score: 83,
  935. styleMatch: 85,
  936. status: '备选',
  937. tags: ['口播', '成分科普', '爆文率高'],
  938. reason: '完播和爆文率强,适合作为抖音声量位,但预算压力较大。',
  939. risk: '报价超过当前建议上限,需谈价或只保留备选。',
  940. owner: '媒介-阿宁',
  941. supplier: '星图服务商',
  942. dataFreshness: 86,
  943. priceExpire: '2026-05-26',
  944. styles: ['成分科普', '口播测评', '转化'],
  945. startMatch: 86,
  946. readMedian: '46万',
  947. interactionMedian: '1.8万',
  948. viralRate: 22,
  949. replyRate: 82,
  950. fulfillmentRate: 89,
  951. naturalTrafficRatio: 63,
  952. cooperationNoteRatio: 41,
  953. estimatedCpm: 146,
  954. audienceProfile: '女性 24-35 岁,护肤功效兴趣强,三线以上城市占比 64%',
  955. contentForms: ['视频'],
  956. recallSources: ['平台筛选召回', '历史表现召回'],
  957. serviceLevel: '稳定履约',
  958. brandFit: '专业背书'
  959. }),
  960. buildCandidate({
  961. id: 'M-6202',
  962. platform: '抖音',
  963. name: '小麦真实开箱',
  964. category: '美妆',
  965. fans: '23.9万',
  966. price: 7600,
  967. score: 85,
  968. styleMatch: 81,
  969. status: '备选',
  970. tags: ['开箱', '测评', '转化'],
  971. reason: '开箱测评节奏好,价格在预算中段,适合补转化素材。',
  972. risk: '护肤深度不足,需要提供产品体验脚本。',
  973. owner: '媒介-南南',
  974. supplier: '短视频资源组',
  975. dataFreshness: 79,
  976. priceExpire: '2026-05-20',
  977. styles: ['开箱', '口播测评', '真实分享'],
  978. startMatch: 82,
  979. readMedian: '18万',
  980. interactionMedian: '6900',
  981. viralRate: 16,
  982. replyRate: 84,
  983. fulfillmentRate: 91,
  984. naturalTrafficRatio: 68,
  985. cooperationNoteRatio: 35,
  986. estimatedCpm: 96,
  987. audienceProfile: '女性 18-28 岁,彩妆/护肤兴趣重叠',
  988. contentForms: ['视频'],
  989. recallSources: ['条件筛选召回', '预算策略召回'],
  990. serviceLevel: '稳定履约',
  991. brandFit: '转化补充'
  992. }),
  993. buildCandidate({
  994. id: 'M-6203',
  995. platform: '抖音',
  996. name: '阿森护肤实验室',
  997. category: '护肤',
  998. fans: '15.8万',
  999. price: 6800,
  1000. score: 82,
  1001. styleMatch: 78,
  1002. status: '需复核',
  1003. tags: ['实验', '功效', '男性视角'],
  1004. reason: '专业表达强,但画面偏实验室感,和生活方式 Brief 有偏差。',
  1005. risk: '风格匹配低于 80%,客户版需谨慎呈现。',
  1006. owner: '媒介-六月',
  1007. supplier: '独立达人',
  1008. dataFreshness: 72,
  1009. priceExpire: '2026-05-19',
  1010. styles: ['实验测评', '成分科普', '功效'],
  1011. startMatch: 79,
  1012. readMedian: '11万',
  1013. interactionMedian: '4200',
  1014. viralRate: 11,
  1015. replyRate: 78,
  1016. fulfillmentRate: 86,
  1017. naturalTrafficRatio: 71,
  1018. cooperationNoteRatio: 32,
  1019. estimatedCpm: 91,
  1020. audienceProfile: '男女比例接近,功效护肤兴趣高',
  1021. contentForms: ['视频'],
  1022. recallSources: ['内容标签召回'],
  1023. serviceLevel: '需沟通确认',
  1024. brandFit: '需风格复核'
  1025. }),
  1026. buildCandidate({
  1027. id: 'M-6301',
  1028. platform: 'B站',
  1029. name: '配方师小简',
  1030. category: '护肤',
  1031. fans: '31.4万',
  1032. price: 16800,
  1033. score: 90,
  1034. styleMatch: 92,
  1035. status: '强推荐',
  1036. tags: ['长视频', '成分科普', '专业背书'],
  1037. reason: '长视频解释能力强,适合承担品牌专业背书和搜索留存。',
  1038. risk: '制作周期 14 天,需先锁定脚本窗口。',
  1039. owner: '媒介-六月',
  1040. supplier: '独立UP主',
  1041. dataFreshness: 82,
  1042. priceExpire: '2026-06-12',
  1043. styles: ['成分科普', '长视频测评', '专业拆解'],
  1044. startMatch: 92,
  1045. readMedian: '13万',
  1046. interactionMedian: '5300',
  1047. viralRate: 13,
  1048. replyRate: 88,
  1049. fulfillmentRate: 92,
  1050. naturalTrafficRatio: 79,
  1051. cooperationNoteRatio: 24,
  1052. estimatedCpm: 126,
  1053. audienceProfile: '女性 23-35 岁,成分党、学生党与白领混合',
  1054. contentForms: ['长视频', '图文'],
  1055. recallSources: ['相似博主召回', '历史采纳召回'],
  1056. serviceLevel: '稳定履约',
  1057. brandFit: '专业背书高匹配',
  1058. selected: true
  1059. }),
  1060. buildCandidate({
  1061. id: 'M-6302',
  1062. platform: 'B站',
  1063. name: '护肤避坑研究员',
  1064. category: '护肤',
  1065. fans: '8.9万',
  1066. price: 7200,
  1067. score: 81,
  1068. styleMatch: 84,
  1069. status: '备选',
  1070. tags: ['避坑', '深度测评', '学生党'],
  1071. reason: '评论区信任度高,适合做备选口碑内容。',
  1072. risk: '更新频率一般,需要确认排期。',
  1073. owner: '媒介-小七',
  1074. supplier: '独立UP主',
  1075. dataFreshness: 69,
  1076. priceExpire: '2026-05-30',
  1077. styles: ['避坑', '深度测评', '成分科普'],
  1078. startMatch: 85,
  1079. readMedian: '5.7万',
  1080. interactionMedian: '2100',
  1081. viralRate: 9,
  1082. replyRate: 80,
  1083. fulfillmentRate: 84,
  1084. naturalTrafficRatio: 83,
  1085. cooperationNoteRatio: 18,
  1086. estimatedCpm: 102,
  1087. audienceProfile: '女性 18-26 岁,学生党与初入职场占比较高',
  1088. contentForms: ['长视频'],
  1089. recallSources: ['关键词召回', '平台策略召回'],
  1090. serviceLevel: '需沟通确认',
  1091. brandFit: '备选口碑'
  1092. }),
  1093. buildCandidate({
  1094. id: 'M-6401',
  1095. platform: '微信',
  1096. name: '成分慢读',
  1097. category: '护肤',
  1098. fans: '12.5万',
  1099. price: 9600,
  1100. score: 79,
  1101. styleMatch: 80,
  1102. status: '备选',
  1103. tags: ['公众号', '深度图文', '专业'],
  1104. reason: '适合承接长图文解释,补足私域和搜索沉淀。',
  1105. risk: '平台不在主需求中,需要客户确认是否保留。',
  1106. owner: '媒介-小七',
  1107. supplier: '公众号资源组',
  1108. dataFreshness: 65,
  1109. priceExpire: '2026-05-22',
  1110. styles: ['深度图文', '成分科普', '趋势'],
  1111. startMatch: 81,
  1112. readMedian: '3.4万',
  1113. interactionMedian: '980',
  1114. viralRate: 6,
  1115. replyRate: 76,
  1116. fulfillmentRate: 87,
  1117. naturalTrafficRatio: 74,
  1118. cooperationNoteRatio: 29,
  1119. estimatedCpm: 132,
  1120. audienceProfile: '女性 28-40 岁,高线城市和高消费力用户偏多',
  1121. contentForms: ['图文'],
  1122. recallSources: ['资源库召回'],
  1123. serviceLevel: '稳定履约',
  1124. brandFit: '补充平台'
  1125. }),
  1126. buildCandidate({
  1127. id: 'M-6501',
  1128. platform: '微博',
  1129. name: '护肤趋势观察',
  1130. category: '美妆',
  1131. fans: '54.6万',
  1132. price: 11800,
  1133. score: 76,
  1134. styleMatch: 72,
  1135. status: '需复核',
  1136. tags: ['趋势', '话题扩散', '资讯'],
  1137. reason: '适合话题扩散,但内容资讯感强,不适合作为主推真实测评。',
  1138. risk: '硬广和资讯感风险,需要从客户版隐藏或降级。',
  1139. owner: '媒介-阿宁',
  1140. supplier: '微博资源组',
  1141. dataFreshness: 70,
  1142. priceExpire: '2026-06-02',
  1143. styles: ['趋势', '资讯', '话题'],
  1144. startMatch: 74,
  1145. readMedian: '22万',
  1146. interactionMedian: '3600',
  1147. viralRate: 10,
  1148. replyRate: 73,
  1149. fulfillmentRate: 82,
  1150. naturalTrafficRatio: 58,
  1151. cooperationNoteRatio: 44,
  1152. estimatedCpm: 112,
  1153. audienceProfile: '泛美妆兴趣人群,话题扩散强但转化弱',
  1154. contentForms: ['图文', '短内容'],
  1155. recallSources: ['平台补充召回'],
  1156. serviceLevel: '需沟通确认',
  1157. brandFit: '话题扩散'
  1158. }),
  1159. buildCandidate({
  1160. id: 'M-6601',
  1161. platform: '小红书',
  1162. name: '妈妈的浴室柜',
  1163. category: '母婴',
  1164. fans: '16.9万',
  1165. price: 6200,
  1166. score: 84,
  1167. styleMatch: 83,
  1168. status: '备选',
  1169. tags: ['母婴', '家庭场景', '温和护肤'],
  1170. reason: '家庭和温和护肤场景自然,适合敏感肌线索扩展。',
  1171. risk: '主目标不是母婴,需要确认是否纳入多场景名单。',
  1172. owner: '媒介-小乔',
  1173. supplier: '母婴生活资源组',
  1174. dataFreshness: 88,
  1175. priceExpire: '2026-06-04',
  1176. styles: ['家庭场景', '温和护肤', '真实分享'],
  1177. startMatch: 84,
  1178. readMedian: '7.3万',
  1179. interactionMedian: '2900',
  1180. viralRate: 13,
  1181. replyRate: 89,
  1182. fulfillmentRate: 93,
  1183. naturalTrafficRatio: 81,
  1184. cooperationNoteRatio: 19,
  1185. estimatedCpm: 86,
  1186. audienceProfile: '女性 25-38 岁,妈妈人群和敏感肌标签重叠',
  1187. contentForms: ['图文', '视频'],
  1188. recallSources: ['场景扩展召回'],
  1189. serviceLevel: '高配合度',
  1190. brandFit: '场景扩展'
  1191. }),
  1192. buildCandidate({
  1193. id: 'M-6701',
  1194. platform: '小红书',
  1195. name: '周末家居护肤',
  1196. category: '家居',
  1197. fans: '6.4万',
  1198. price: 3100,
  1199. score: 80,
  1200. styleMatch: 82,
  1201. status: '备选',
  1202. tags: ['家居', '生活方式', '低预算'],
  1203. reason: '生活方式感强,价格友好,可作为低预算补充。',
  1204. risk: '护肤内容占比不足,需要限定选题。',
  1205. owner: '媒介-小徐',
  1206. supplier: '生活方式资源组',
  1207. dataFreshness: 83,
  1208. priceExpire: '2026-05-28',
  1209. styles: ['家居生活', '生活方式', '真实分享'],
  1210. startMatch: 83,
  1211. readMedian: '3.6万',
  1212. interactionMedian: '1320',
  1213. viralRate: 8,
  1214. replyRate: 87,
  1215. fulfillmentRate: 90,
  1216. naturalTrafficRatio: 84,
  1217. cooperationNoteRatio: 15,
  1218. estimatedCpm: 72,
  1219. audienceProfile: '女性 23-34 岁,家居和通勤生活兴趣高',
  1220. contentForms: ['图文'],
  1221. recallSources: ['相似场景召回'],
  1222. serviceLevel: '稳定履约',
  1223. brandFit: '低预算补充'
  1224. })
  1225. ];
  1226. private candidateBaseline = new Map(
  1227. this.candidates.map((candidate) => [
  1228. candidate.id,
  1229. {
  1230. score: candidate.score,
  1231. styleMatch: candidate.styleMatch,
  1232. status: candidate.status,
  1233. selected: candidate.selected,
  1234. risk: candidate.risk,
  1235. reason: candidate.reason,
  1236. ruleHits: [...candidate.ruleHits]
  1237. }
  1238. ])
  1239. );
  1240. resources: ResourceSummary[] = [
  1241. { platform: '小红书', total: 4286, fresh: 86, matched: 182, status: '主力平台' },
  1242. { platform: '抖音', total: 2130, fresh: 79, matched: 72, status: '预算复核' },
  1243. { platform: 'B站', total: 684, fresh: 73, matched: 28, status: '补充背书' },
  1244. { platform: '微信', total: 516, fresh: 61, matched: 18, status: '待人工确认' },
  1245. { platform: '微博', total: 902, fresh: 69, matched: 24, status: '话题扩散' }
  1246. ];
  1247. fieldMappings: FieldMapping[] = [
  1248. { source: '达人昵称', target: 'accountName', confidence: 98, confirmed: true },
  1249. { source: '平台ID/主页链接', target: 'platformId / link', confidence: 94, confirmed: true },
  1250. { source: '报价-图文', target: 'imagePrice', confidence: 91, confirmed: true },
  1251. { source: '近30天互动中位数', target: 'interactionMedian', confidence: 88, confirmed: false },
  1252. { source: '供应商联系人', target: 'supplierContact', confidence: 84, confirmed: false },
  1253. { source: '备注/特殊要求', target: 'riskNote', confidence: 71, confirmed: false }
  1254. ];
  1255. activities: Activity[] = [
  1256. { title: '解析 Brief 表格/文档', detail: '识别客户、品牌、平台、数量、预算、内容风格与排除项', status: '完成' },
  1257. { title: '读取本地媒体资源库', detail: '模拟扫描 D 盘资源库,多平台字段完成统一映射', status: '完成' },
  1258. { title: '执行规则与评分', detail: '数量、价格、风格、风险、供应商关系进入综合评分', status: '进行中' },
  1259. { title: '后台实时采样', detail: '需要登录平台采集近10篇内容时进入异步任务队列', status: '待执行' }
  1260. ];
  1261. samplingTasks: SamplingTask[] = [
  1262. { account: '梨涡测评室', platform: '小红书', progress: 100, status: '完成', result: '9/10 篇符合真实测评或成分拆解' },
  1263. { account: '通勤包里有什么', platform: '小红书', progress: 100, status: '完成', result: '8/10 篇符合通勤生活场景' },
  1264. { account: '林小林认真护肤', platform: '抖音', progress: 70, status: '进行中', result: '已采样 7 条视频,风格暂定 82%' },
  1265. { account: '美妆情报局', platform: '微信', progress: 40, status: '待执行', result: '等待公众号图文采样' }
  1266. ];
  1267. learnedRules: LearnedRule[] = [
  1268. {
  1269. title: '客户反馈中的明确偏好要先转成排除和加分项',
  1270. description: '当反馈出现“减少/不要/优先/保留”等词时,提炼为可确认规则,不直接静默生效。',
  1271. confidence: 91,
  1272. action: '生成待确认规则',
  1273. approved: false
  1274. }
  1275. ];
  1276. gapItems: GapItem[] = [
  1277. { title: '需求点需要可编辑和可追溯', current: '原 Demo 只有勾选,缺少原文证据和修正入口。', suggestion: '补充编辑状态、证据来源、手动新增需求。', done: true },
  1278. { title: '资源库需要展示字段映射', current: '只展示各平台数量,客户看不到本地表如何被 AI 理解。', suggestion: '增加字段映射确认、资源目录检测和同步动作。', done: true },
  1279. { title: '风格比对需要有过程', current: '只展示结果分,缺少近10篇内容采样的可解释过程。', suggestion: '增加后台采样队列、样本标题、风格命中率。', done: true },
  1280. { title: '推荐名单需要二次筛选', current: '只展示卡片,缺少平台/状态/预算过滤和账号详情。', suggestion: '增加筛选、排序、详情侧栏、状态调整。', done: true },
  1281. { title: '自我提升需要审核机制', current: '反馈提炼后直接加入规则,容易让客户担心 AI 乱学。', suggestion: '保留“待确认-入库”的人工审核链路。', done: true }
  1282. ];
  1283. taskRecords: TaskRecord[] = [
  1284. { id: 'TH-202605-001', brand: '客户A / 新锐护肤品牌', status: '提号中', owner: '商务-王一', version: '名单 v0.3', lastAction: '已生成候选池,等待商务复核' },
  1285. { id: 'TH-202604-028', brand: '客户B / 食饮新品', status: '已交付', owner: '商务-李想', version: '名单 v1.2', lastAction: '客户反馈已提炼 2 条规则' },
  1286. { id: 'TH-202604-019', brand: '客户C / 家清系列', status: '待确认', owner: '商务-陈晨', version: '需求 v0.1', lastAction: '等待确认平台数量' },
  1287. { id: 'TH-202604-033', brand: '客户D / 母婴洗护', status: '待复盘', owner: '商务-周周', version: '反馈 v0.2', lastAction: '客户要求减少硬广达人,规则待确认入库' },
  1288. { id: 'TH-202605-006', brand: '客户E / 家居香氛', status: '资源同步', owner: '商务-林夕', version: '资源 v0.1', lastAction: '正在映射小红书和微信资源字段' },
  1289. { id: 'TH-202605-009', brand: '客户F / 数码配件', status: '待采样', owner: '商务-阿树', version: '名单 v0.1', lastAction: 'B站长视频账号等待近10条内容采样' },
  1290. { id: 'TH-202605-012', brand: '客户G / 轻食新品', status: '已交付', owner: '商务-小满', version: '名单 v1.0', lastAction: '客户版名单已导出,等待投放结果回填' }
  1291. ];
  1292. taskTimeline: TaskTimelineItem[] = [
  1293. {
  1294. taskId: 'TH-202605-001',
  1295. time: '2026-05-12 09:18',
  1296. title: 'Brief 上传并解析',
  1297. detail: '识别客户、品牌、平台数量、预算和内容风格。'
  1298. },
  1299. {
  1300. taskId: 'TH-202605-001',
  1301. time: '2026-05-12 09:31',
  1302. title: '生成候选池',
  1303. detail: '从资源库召回 324 个账号,去重后 293 个进入评分池。'
  1304. },
  1305. {
  1306. taskId: 'TH-202605-001',
  1307. time: '2026-05-12 10:05',
  1308. title: '媒介复核',
  1309. detail: '剔除硬广风险账号 4 个,补入 B站专业背书账号。'
  1310. },
  1311. {
  1312. taskId: 'TH-202604-028',
  1313. time: '2026-04-28 16:40',
  1314. title: '客户反馈回收',
  1315. detail: '客户要求减少泛美食资讯号,已提炼 2 条行业偏好规则。'
  1316. },
  1317. {
  1318. taskId: 'TH-202605-006',
  1319. time: '2026-05-12 08:55',
  1320. title: '资源同步',
  1321. detail: '正在映射小红书和微信资源字段,等待供应商联系人补齐。'
  1322. }
  1323. ];
  1324. templateFields: TemplateField[] = [
  1325. { label: '平台', source: '资源库/Brief', customer: true, internal: true, required: true, selected: true },
  1326. { label: '账号昵称', source: '资源库', customer: true, internal: true, required: true, selected: true },
  1327. { label: '主页链接', source: '资源库', customer: true, internal: true, required: true, selected: true },
  1328. { label: '粉丝量', source: '资源库', customer: true, internal: true, required: true, selected: true },
  1329. { label: '图文/视频报价', source: '资源库', customer: true, internal: true, required: true, selected: true },
  1330. { label: '推荐理由', source: 'AI生成+人工复核', customer: true, internal: true, required: true, selected: true },
  1331. { label: '近10条样本', source: '后台采样', customer: true, internal: true, required: false, selected: true },
  1332. { label: '供应商/联系人', source: '资源库', customer: false, internal: true, required: true, selected: true },
  1333. { label: '风险提示', source: '规则库', customer: false, internal: true, required: false, selected: true },
  1334. { label: '客户采纳结果', source: '复盘回填', customer: false, internal: true, required: false, selected: false }
  1335. ];
  1336. get selectedRulesCount(): number {
  1337. return this.rules.filter((rule) => rule.selected).length;
  1338. }
  1339. get selectedCandidatesCount(): number {
  1340. return this.filteredCandidates.filter((candidate) => candidate.selected).length;
  1341. }
  1342. get visibleNavItems(): NavItem[] {
  1343. if (!this.simpleMode) {
  1344. return this.navItems;
  1345. }
  1346. const simpleKeys = new Set<ViewKey>([...this.simpleNavKeys, this.activeView]);
  1347. return this.navItems.filter((item) => simpleKeys.has(item.key));
  1348. }
  1349. get activeNavItem(): NavItem {
  1350. return this.navItems.find((item) => item.key === this.activeView) ?? this.navItems[0];
  1351. }
  1352. get taskDisplayName(): string {
  1353. return '客户A / 新锐护肤品牌种草合作';
  1354. }
  1355. get deliveryStatusLabel(): string {
  1356. if (this.currentStep < 4) {
  1357. return '名单待生成';
  1358. }
  1359. if (this.currentStep === 4) {
  1360. return '推荐生成中';
  1361. }
  1362. return `交付准备度 ${this.exportReadiness}%`;
  1363. }
  1364. get nextAction(): { title: string; detail: string } {
  1365. if (this.currentStep <= 1) {
  1366. return { title: '解析 Brief', detail: '识别客户、平台、数量、预算、内容风格和排除项。' };
  1367. }
  1368. if (this.currentStep === 2) {
  1369. return { title: '确认需求', detail: '保留已勾选需求,进入规则选择和提号。' };
  1370. }
  1371. if (this.currentStep === 3) {
  1372. return { title: '生成推荐池', detail: '按已选规则从资源库召回账号,进入采样和评分。' };
  1373. }
  1374. if (this.currentStep === 4 && this.samplingTasks.some((task) => task.status !== '完成')) {
  1375. return { title: '完成近10条采样', detail: '把风格比对证据补齐,降低客户返工风险。' };
  1376. }
  1377. if (this.currentStep === 4) {
  1378. return { title: '查看推荐名单', detail: '进入账号复核和最近10条内容证据。' };
  1379. }
  1380. if (this.currentStep === 5) {
  1381. return { title: '进入客户版交付', detail: '隐藏内部风险字段,预览对客版本并准备导出。' };
  1382. }
  1383. return { title: '提炼反馈规则', detail: '把客户反馈生成待确认规则,人工确认后入库。' };
  1384. }
  1385. get currentStepGuide(): { objective: string; operator: string; output: string } {
  1386. return this.stepGuides[this.currentStep - 1] ?? this.stepGuides[0];
  1387. }
  1388. get checklistDoneCount(): number {
  1389. return this.businessChecklist.filter((item) => this.isChecklistDone(item.key)).length;
  1390. }
  1391. get selectedCandidateList(): Candidate[] {
  1392. return this.candidates.filter((candidate) => candidate.selected && candidate.status !== '已剔除');
  1393. }
  1394. get reviewQueueCandidates(): Candidate[] {
  1395. return this.candidates.filter((candidate) => candidate.status === '需复核' || candidate.status === '已剔除').slice(0, 5);
  1396. }
  1397. get creatorShortlistCandidates(): Candidate[] {
  1398. return this.creatorFilteredCandidates.slice(0, 8);
  1399. }
  1400. get compareCandidates(): Candidate[] {
  1401. return this.selectedCandidateList.slice(0, 4);
  1402. }
  1403. get activeScenarioCount(): number {
  1404. return this.briefScenarios.filter((scenario) => scenario.selected).length;
  1405. }
  1406. get exportReadiness(): number {
  1407. const mapping = (this.confirmedMappingsCount / this.fieldMappings.length) * 25;
  1408. const rules = (this.selectedRulesCount / this.rules.length) * 25;
  1409. const candidates = Math.min(1, this.selectedCandidateList.length / 4) * 25;
  1410. const sampling = (this.samplingTasks.filter((task) => task.status === '完成').length / this.samplingTasks.length) * 25;
  1411. return Math.round(mapping + rules + candidates + sampling);
  1412. }
  1413. get strongRecommendationCount(): number {
  1414. return this.candidates.filter((candidate) => candidate.status === '强推荐').length;
  1415. }
  1416. get totalMatchedCandidates(): number {
  1417. return this.resources.reduce((sum, resource) => sum + resource.matched, 0);
  1418. }
  1419. get workspacePipelineStages(): AiSelectionStage[] {
  1420. return this.aiSelectionStages.slice(1, 6);
  1421. }
  1422. get platforms(): string[] {
  1423. return ['全部', ...Array.from(new Set(this.candidates.map((candidate) => candidate.platform)))];
  1424. }
  1425. get categories(): string[] {
  1426. return ['全部', ...Array.from(new Set(this.candidates.map((candidate) => candidate.category ?? candidate.tags[0] ?? '未分类')))];
  1427. }
  1428. get contentForms(): string[] {
  1429. const forms = this.candidates.flatMap((candidate) => candidate.contentForms ?? [candidate.platform === 'B站' ? '长视频' : '图文']);
  1430. return ['全部', ...Array.from(new Set(forms))];
  1431. }
  1432. get fanTiers(): string[] {
  1433. return ['全部', '1万以下', '1万-10万', '10万-50万', '50万以上'];
  1434. }
  1435. get creatorFilteredCandidates(): Candidate[] {
  1436. const queryTokens = this.creatorQuery
  1437. .split(/\s+/)
  1438. .map((token) => token.trim())
  1439. .filter(Boolean);
  1440. return this.candidates
  1441. .filter((candidate) => this.creatorPlatform === '全部' || candidate.platform === this.creatorPlatform)
  1442. .filter((candidate) => this.creatorCategory === '全部' || candidate.category === this.creatorCategory || candidate.tags.includes(this.creatorCategory))
  1443. .filter((candidate) => this.creatorContentForm === '全部' || (candidate.contentForms ?? []).includes(this.creatorContentForm))
  1444. .filter((candidate) => this.creatorFanTier === '全部' || (candidate.fanTier ?? inferFanTier(candidate.fans)) === this.creatorFanTier)
  1445. .filter((candidate) => candidate.price <= this.creatorBudgetMax)
  1446. .filter((candidate) => {
  1447. if (this.creatorRisk === '只看安全') {
  1448. return candidate.status !== '需复核' && candidate.status !== '已剔除';
  1449. }
  1450. if (this.creatorRisk === '只看需复核') {
  1451. return candidate.status === '需复核';
  1452. }
  1453. return true;
  1454. })
  1455. .filter((candidate) => {
  1456. if (queryTokens.length === 0) {
  1457. return true;
  1458. }
  1459. const haystack = [
  1460. candidate.name,
  1461. candidate.platform,
  1462. candidate.category ?? '',
  1463. candidate.reason,
  1464. candidate.risk,
  1465. candidate.audienceProfile ?? '',
  1466. ...(candidate.tags ?? []),
  1467. ...(candidate.recallSources ?? [])
  1468. ].join(' ');
  1469. return queryTokens.some((token) => haystack.includes(token));
  1470. })
  1471. .sort((a, b) => b.score - a.score);
  1472. }
  1473. get creatorAiSummary(): string {
  1474. const count = this.creatorFilteredCandidates.length;
  1475. const strong = this.creatorFilteredCandidates.filter((candidate) => candidate.status === '强推荐').length;
  1476. const review = this.creatorFilteredCandidates.filter((candidate) => candidate.status === '需复核').length;
  1477. return `命中 ${count} 个博主,其中强推荐 ${strong} 个、需复核 ${review} 个;AI 会优先保留风格匹配高、报价健康、服务履约稳定的账号。`;
  1478. }
  1479. get deliveryChecklistDoneCount(): number {
  1480. return this.deliveryChecklist.filter((item) => item.done).length;
  1481. }
  1482. get selectedDeliveryFields(): string[] {
  1483. return this.deliveryFields
  1484. .filter((field) => field.selected)
  1485. .filter((field) => this.previewAudience === '内部复核版' || field.customer)
  1486. .map((field) => field.label);
  1487. }
  1488. get deliveryGaps(): string[] {
  1489. const gaps: string[] = [];
  1490. if (this.confirmedMappingsCount < this.fieldMappings.length) {
  1491. gaps.push('还有字段映射未确认');
  1492. }
  1493. if (this.samplingTasks.some((task) => task.status !== '完成')) {
  1494. gaps.push('近10条内容采样未全部完成');
  1495. }
  1496. if (this.selectedCandidateList.some((candidate) => candidate.status === '需复核')) {
  1497. gaps.push('名单里仍有需复核账号');
  1498. }
  1499. if (this.selectedCandidateList.length < 8) {
  1500. gaps.push('客户版主推账号数量偏少');
  1501. }
  1502. return gaps;
  1503. }
  1504. get resourceFixQueue(): ResourceIssue[] {
  1505. return this.resourceIssues.filter((issue) => !issue.resolved).slice(0, 4);
  1506. }
  1507. get taskAttentionItems(): Array<{ title: string; detail: string; action: string; view: ViewKey }> {
  1508. return [
  1509. {
  1510. title: '补齐微信主页链接',
  1511. detail: '影响近10条内容采样,建议先到资源库处理高风险字段。',
  1512. action: '去资源库',
  1513. view: 'resources'
  1514. },
  1515. {
  1516. title: '确认需复核账号',
  1517. detail: `${this.reviewQueueCandidates.length} 个账号仍有风险或剔除原因,需要业务确认。`,
  1518. action: '打开名单',
  1519. view: 'workspace'
  1520. },
  1521. {
  1522. title: '客户反馈待学习',
  1523. detail: '把本次驳回原因转成待确认规则,后续同类客户可复用。',
  1524. action: '去学习',
  1525. view: 'learning'
  1526. }
  1527. ];
  1528. }
  1529. get learningScopeEffect(): string {
  1530. if (this.selectedLearningScope === '本任务') {
  1531. return '只影响当前推荐名单,适合临时口径或单次活动偏好。';
  1532. }
  1533. if (this.selectedLearningScope === '单客户') {
  1534. return '后续同一客户 Brief 会默认带入,但仍保留人工确认。';
  1535. }
  1536. if (this.selectedLearningScope === '行业') {
  1537. return '同品类客户会获得弱加权,避免把单个客户口味过度放大。';
  1538. }
  1539. return '进入全局规则库前需要二次确认,命中后会影响所有新任务。';
  1540. }
  1541. get filteredCandidates(): Candidate[] {
  1542. return this.candidates
  1543. .filter((candidate) => this.selectedPlatform === '全部' || candidate.platform === this.selectedPlatform)
  1544. .filter((candidate) => this.selectedStatus === '全部' || candidate.status === this.selectedStatus)
  1545. .filter((candidate) => candidate.price <= this.budgetMax)
  1546. .sort((a, b) => {
  1547. if (this.sortMode === '风格匹配') {
  1548. return b.styleMatch - a.styleMatch;
  1549. }
  1550. if (this.sortMode === '报价升序') {
  1551. return a.price - b.price;
  1552. }
  1553. return b.score - a.score;
  1554. });
  1555. }
  1556. get selectedCandidate(): Candidate {
  1557. return this.candidates.find((candidate) => candidate.id === this.selectedCandidateId) ?? this.candidates[0];
  1558. }
  1559. get selectedNote(): NoteDetail {
  1560. return (
  1561. this.selectedCandidate.recentNotes.find((note) => note.id === this.selectedNoteId) ??
  1562. this.selectedCandidate.recentNotes[0]
  1563. );
  1564. }
  1565. get filteredNotes(): NoteDetail[] {
  1566. return this.selectedCandidate.recentNotes.filter(
  1567. (note) => this.noteVerdictFilter === '全部' || note.verdict === this.noteVerdictFilter
  1568. );
  1569. }
  1570. get confirmedMappingsCount(): number {
  1571. return this.fieldMappings.filter((mapping) => mapping.confirmed).length;
  1572. }
  1573. get selectedResource(): ResourceSummary {
  1574. return this.resources.find((resource) => resource.platform === this.selectedResourcePlatform) ?? this.resources[0];
  1575. }
  1576. get selectedResourceFiles(): ResourceFile[] {
  1577. return this.resourceFiles.filter((file) => file.platform === this.selectedResourcePlatform);
  1578. }
  1579. get selectedResourceIssues(): ResourceIssue[] {
  1580. return this.resourceIssues.filter((issue) => issue.platform === this.selectedResourcePlatform);
  1581. }
  1582. get selectedResourceCandidates(): Candidate[] {
  1583. return this.candidates.filter((candidate) => candidate.platform === this.selectedResourcePlatform).slice(0, 6);
  1584. }
  1585. get selectedRule(): Rule {
  1586. return this.rules.find((rule) => rule.id === this.selectedRuleId) ?? this.rules[0];
  1587. }
  1588. get enabledRuleTestCases(): RuleTestCase[] {
  1589. return this.ruleTestCases.filter((item) => item.enabled);
  1590. }
  1591. get selectedTask(): TaskRecord {
  1592. return this.taskRecords.find((task) => task.id === this.selectedTaskId) ?? this.taskRecords[0];
  1593. }
  1594. get selectedTaskTimeline(): TaskTimelineItem[] {
  1595. const timeline = this.taskTimeline.filter((item) => item.taskId === this.selectedTaskId);
  1596. if (timeline.length > 0) {
  1597. return timeline;
  1598. }
  1599. return [
  1600. {
  1601. taskId: this.selectedTaskId,
  1602. time: '待补充',
  1603. title: '暂无详细时间线',
  1604. detail: '正式版会记录 Brief、资源同步、规则调整、名单导出和客户反馈。'
  1605. }
  1606. ];
  1607. }
  1608. get selectedTemplate(): TemplateItem {
  1609. return this.templates.find((template) => template.selected) ?? this.templates[0];
  1610. }
  1611. get visibleTemplateFields(): TemplateField[] {
  1612. return this.templateFields.filter((field) => {
  1613. if (this.templatePreviewMode === '客户版') {
  1614. return field.customer && field.selected;
  1615. }
  1616. if (this.templatePreviewMode === '复盘版') {
  1617. return field.selected || field.label.includes('采纳') || field.source.includes('复盘');
  1618. }
  1619. return field.internal && field.selected;
  1620. });
  1621. }
  1622. isChecklistDone(key: string): boolean {
  1623. if (key === 'brief') {
  1624. return this.currentStep > 1;
  1625. }
  1626. if (key === 'demand') {
  1627. return this.currentStep > 2 && this.requirements.some((item) => item.selected);
  1628. }
  1629. if (key === 'rule') {
  1630. return this.currentStep > 3 && this.selectedRulesCount >= 3;
  1631. }
  1632. if (key === 'sample') {
  1633. return this.currentStep > 4 && this.samplingTasks.every((task) => task.status === '完成');
  1634. }
  1635. if (key === 'export') {
  1636. return this.currentStep >= 5 && (this.exportReadiness >= 80 || this.exportState.includes('导出'));
  1637. }
  1638. return false;
  1639. }
  1640. toggleSimpleMode(): void {
  1641. this.simpleMode = !this.simpleMode;
  1642. if (this.simpleMode && !this.simpleNavKeys.includes(this.activeView)) {
  1643. this.activeView = 'workspace';
  1644. }
  1645. this.appendLog('视图模式', this.simpleMode ? '已切换为商务简洁模式。' : '已切换为完整业务模式。');
  1646. }
  1647. toggleAdvancedWorkspace(): void {
  1648. this.showAdvancedWorkspace = !this.showAdvancedWorkspace;
  1649. this.appendLog('工作台细节', this.showAdvancedWorkspace ? '已展开规则、采样和试算细节。' : '已收起高级业务细节。');
  1650. }
  1651. backToWorkflow(): void {
  1652. this.activeView = 'workspace';
  1653. this.appendLog('导航', `已返回提号流程 Step ${this.currentStep}。`);
  1654. }
  1655. runNextAction(): void {
  1656. if (this.currentStep <= 1) {
  1657. this.analyzeBrief();
  1658. this.appendLog('下一步', '已解析 Brief。');
  1659. return;
  1660. }
  1661. if (this.currentStep === 2) {
  1662. this.confirmDemand();
  1663. this.appendLog('下一步', '已确认需求。');
  1664. return;
  1665. }
  1666. if (this.currentStep === 3) {
  1667. this.generateRecommendations();
  1668. this.appendLog('下一步', '已生成推荐名单。');
  1669. return;
  1670. }
  1671. if (this.currentStep === 4 && this.samplingTasks.some((task) => task.status !== '完成')) {
  1672. this.runSampling();
  1673. this.currentStep = 5;
  1674. this.updateSteps();
  1675. this.appendLog('下一步', '已完成近10条内容采样。');
  1676. return;
  1677. }
  1678. if (this.currentStep === 4) {
  1679. this.currentStep = 5;
  1680. this.updateSteps();
  1681. this.appendLog('下一步', '已进入推荐名单复核。');
  1682. return;
  1683. }
  1684. if (this.currentStep === 5) {
  1685. this.previewAudience = '客户版';
  1686. this.activeView = 'delivery';
  1687. this.updateSteps();
  1688. this.appendLog('下一步', '已进入客户版交付预览。');
  1689. return;
  1690. }
  1691. this.extractRules();
  1692. this.appendLog('下一步', '已从反馈中提炼待确认规则。');
  1693. }
  1694. runBusinessShortcut(id: string): void {
  1695. if (id === 'fast-list') {
  1696. this.analyzeBrief();
  1697. this.confirmDemand();
  1698. this.generateRecommendations();
  1699. this.runSampling();
  1700. this.currentStep = 5;
  1701. this.previewAudience = '客户版';
  1702. this.exportState = '已生成可讨论名单,等待客户版预览';
  1703. this.showAdvancedWorkspace = false;
  1704. this.updateSteps();
  1705. this.appendLog('快捷操作', '已一键生成可看名单。');
  1706. return;
  1707. }
  1708. if (id === 'review-risk') {
  1709. this.selectedStatus = '需复核';
  1710. this.selectedResourcePlatform = '微信';
  1711. this.activeView = 'resources';
  1712. this.appendLog('快捷操作', '已切到待处理问题和需复核账号。');
  1713. return;
  1714. }
  1715. if (id === 'client-preview') {
  1716. this.previewAudience = '客户版';
  1717. this.templatePreviewMode = '客户版';
  1718. this.currentStep = 5;
  1719. this.activeView = 'delivery';
  1720. this.updateSteps();
  1721. this.appendLog('快捷操作', '已进入客户版名单预览。');
  1722. return;
  1723. }
  1724. if (id === 'learn-feedback') {
  1725. this.extractRules();
  1726. this.activeView = 'learning';
  1727. this.appendLog('快捷操作', '已进入反馈学习并提炼规则。');
  1728. }
  1729. }
  1730. addVisibleCreatorsToPool(): void {
  1731. const visibleIds = new Set(this.creatorShortlistCandidates.map((candidate) => candidate.id));
  1732. this.candidates = this.candidates.map((candidate) =>
  1733. visibleIds.has(candidate.id)
  1734. ? {
  1735. ...candidate,
  1736. selected: true,
  1737. status: candidate.status === '已剔除' ? '备选' : candidate.status
  1738. }
  1739. : candidate
  1740. );
  1741. this.exportState = `已将 ${visibleIds.size} 个当前筛选博主加入候选池`;
  1742. this.appendLog('找博主', `已批量加入 ${visibleIds.size} 个当前筛选账号。`);
  1743. }
  1744. openRecommendationReview(candidate?: Candidate): void {
  1745. if (candidate) {
  1746. this.openCandidate(candidate);
  1747. }
  1748. this.currentStep = 5;
  1749. this.activeView = 'workspace';
  1750. this.updateSteps();
  1751. this.appendLog('名单复核', candidate ? `已打开 ${candidate.name} 的内容证据。` : '已打开推荐名单复核视图。');
  1752. }
  1753. toggleDeliveryChecklist(item: { done: boolean; label: string }): void {
  1754. item.done = !item.done;
  1755. this.appendLog('交付检查', `${item.label} 已标记为${item.done ? '完成' : '待处理'}。`);
  1756. }
  1757. setLearningScope(scope: '本任务' | '单客户' | '行业' | '长期规则'): void {
  1758. this.selectedLearningScope = scope;
  1759. this.appendLog('学习范围', `反馈规则生效范围已设为${scope}。`);
  1760. }
  1761. uploadTemplateMock(): void {
  1762. this.templateUploadState = '已上传《客户A-推荐名单字段模板.xlsx》,字段预览已刷新';
  1763. this.templates = [
  1764. {
  1765. name: '客户A-推荐名单字段模板.xlsx',
  1766. type: '推荐表模板',
  1767. useCase: '客户A 专属字段顺序和对客话术',
  1768. selected: true
  1769. },
  1770. ...this.templates.map((template) => ({ ...template, selected: false }))
  1771. ];
  1772. this.appendLog('模板上传', '已模拟上传客户A专属推荐表模板。');
  1773. }
  1774. useCreatorPrompt(prompt: string): void {
  1775. this.creatorQuery = prompt;
  1776. this.appendLog('找博主', `已按提示词更新搜索:${prompt}`);
  1777. }
  1778. setCreatorFilters(platform: string, category = '全部'): void {
  1779. this.creatorPlatform = platform;
  1780. this.creatorCategory = category;
  1781. this.appendLog('找博主筛选', `平台 ${platform},类目 ${category}。`);
  1782. }
  1783. addCreatorToPool(candidate: Candidate): void {
  1784. candidate.selected = true;
  1785. candidate.status = candidate.status === '已剔除' ? '备选' : candidate.status;
  1786. this.selectedCandidateId = candidate.id;
  1787. this.selectedNoteId = candidate.recentNotes[0]?.id ?? '';
  1788. this.exportState = `${candidate.name} 已加入候选池`;
  1789. this.appendLog('候选池', `${candidate.name} 从找博主页加入本次名单。`);
  1790. }
  1791. toggleScenario(scenario: BriefScenario): void {
  1792. scenario.selected = !scenario.selected;
  1793. this.recalculateScenario();
  1794. this.appendLog('情景试算', `${scenario.label}:${scenario.selected ? '已启用' : '已关闭'}。`);
  1795. }
  1796. selectVisibleCandidates(): void {
  1797. const visibleIds = new Set(this.filteredCandidates.map((candidate) => candidate.id));
  1798. this.candidates = this.candidates.map((candidate) => ({
  1799. ...candidate,
  1800. selected: visibleIds.has(candidate.id) ? true : candidate.selected
  1801. }));
  1802. this.appendLog('名单筛选', `已勾选当前筛选下的 ${visibleIds.size} 个账号。`);
  1803. }
  1804. clearSelectedCandidates(): void {
  1805. this.candidates = this.candidates.map((candidate) => ({ ...candidate, selected: false }));
  1806. this.appendLog('名单筛选', '已清空当前推荐名单勾选。');
  1807. }
  1808. bulkStatus(status: CandidateStatus): void {
  1809. const count = this.selectedCandidateList.length;
  1810. this.candidates = this.candidates.map((candidate) =>
  1811. candidate.selected ? { ...candidate, status, selected: status !== '已剔除' } : candidate
  1812. );
  1813. this.appendLog('批量复核', `已将 ${count} 个勾选账号标记为${status}。`);
  1814. }
  1815. toggleCompare(): void {
  1816. this.compareOpen = !this.compareOpen;
  1817. this.appendLog('对比视图', this.compareOpen ? '已打开账号横向对比。' : '已收起账号横向对比。');
  1818. }
  1819. switchPreviewAudience(): void {
  1820. this.previewAudience = this.previewAudience === '客户版' ? '内部复核版' : '客户版';
  1821. this.appendLog('预览模式', `已切换为${this.previewAudience}。`);
  1822. }
  1823. setPreviewAudience(audience: '客户版' | '内部复核版'): void {
  1824. if (this.previewAudience === audience) {
  1825. return;
  1826. }
  1827. this.previewAudience = audience;
  1828. this.appendLog('预览模式', `已切换为${this.previewAudience}。`);
  1829. }
  1830. setNoteVerdictFilter(verdict: NoteVerdict | '全部'): void {
  1831. this.noteVerdictFilter = verdict;
  1832. const nextNote = this.filteredNotes[0] ?? this.selectedCandidate.recentNotes[0];
  1833. this.selectedNoteId = nextNote.id;
  1834. }
  1835. markNote(note: NoteDetail, verdict: NoteVerdict): void {
  1836. note.verdict = verdict;
  1837. if (verdict === '正向样本') {
  1838. note.match = Math.min(99, note.match + 3);
  1839. this.selectedCandidate.styleMatch = Math.min(99, this.selectedCandidate.styleMatch + 1);
  1840. }
  1841. if (verdict === '负向样本') {
  1842. note.match = Math.max(45, note.match - 5);
  1843. this.selectedCandidate.styleMatch = Math.max(50, this.selectedCandidate.styleMatch - 2);
  1844. this.selectedCandidate.status = '需复核';
  1845. }
  1846. this.appendLog('笔记标注', `${this.selectedCandidate.name}《${note.title}》被标记为${verdict}。`);
  1847. }
  1848. addCandidateReview(text: string): void {
  1849. this.feedbackText = `${this.feedbackText}\n商务复核:${this.selectedCandidate.name} - ${text}`;
  1850. this.appendLog('复核意见', `${this.selectedCandidate.name}:${text}`);
  1851. }
  1852. appendFeedbackPreset(text: string): void {
  1853. this.feedbackText = `${this.feedbackText}\n${text}`;
  1854. this.appendLog('反馈补充', text);
  1855. }
  1856. selectResourcePlatform(platform: string): void {
  1857. this.selectedResourcePlatform = platform;
  1858. this.appendLog('资源库', `已查看 ${platform} 资源详情。`);
  1859. }
  1860. rescanSelectedResource(): void {
  1861. this.resources = this.resources.map((resource) =>
  1862. resource.platform === this.selectedResourcePlatform
  1863. ? {
  1864. ...resource,
  1865. fresh: Math.min(99, resource.fresh + 6),
  1866. matched: resource.matched + 9,
  1867. status: '刚刚同步'
  1868. }
  1869. : resource
  1870. );
  1871. this.resourceHealthItems = this.resourceHealthItems.map((item, index) => ({
  1872. ...item,
  1873. value: Math.min(98, item.value + (index === 1 ? 5 : 3))
  1874. }));
  1875. this.appendLog('资源同步', `${this.selectedResourcePlatform} 已重新扫描,匹配数和字段完整率已更新。`);
  1876. }
  1877. resolveResourceIssue(issue: ResourceIssue): void {
  1878. issue.resolved = true;
  1879. this.appendLog('资源问题', `${issue.platform}:${issue.title} 已标记处理。`);
  1880. }
  1881. confirmMapping(mapping: FieldMapping): void {
  1882. mapping.confirmed = true;
  1883. mapping.confidence = Math.min(99, mapping.confidence + 4);
  1884. this.appendLog('字段映射', `${mapping.source} -> ${mapping.target} 已确认。`);
  1885. }
  1886. selectRule(rule: Rule): void {
  1887. this.selectedRuleId = rule.id;
  1888. this.appendLog('规则沙盘', `已选中 ${rule.title}。`);
  1889. }
  1890. toggleRuleTestCase(item: RuleTestCase): void {
  1891. item.enabled = !item.enabled;
  1892. }
  1893. runRuleSandbox(): void {
  1894. const enabledCount = this.enabledRuleTestCases.length;
  1895. const rule = this.selectedRule;
  1896. const impact = Math.round((this.ruleSandboxWeight / 100) * (enabledCount + 1) * 3);
  1897. this.ruleSimulationState = `${rule.title} 试算完成:预计强推荐 +${Math.max(1, impact)},需复核 -${Math.max(1, enabledCount)}。`;
  1898. this.candidates = this.candidates.map((candidate, index) =>
  1899. index < 4
  1900. ? {
  1901. ...candidate,
  1902. score: Math.min(99, candidate.score + Math.max(1, enabledCount)),
  1903. ruleHits: Array.from(new Set([...candidate.ruleHits, rule.category]))
  1904. }
  1905. : candidate
  1906. );
  1907. this.appendLog('规则试算', this.ruleSimulationState);
  1908. }
  1909. selectTask(task: TaskRecord): void {
  1910. this.selectedTaskId = task.id;
  1911. this.appendLog('任务查看', `已打开 ${task.id}:${task.brand}。`);
  1912. }
  1913. copyTask(task: TaskRecord): void {
  1914. const nextId = `TH-202605-${String(this.taskRecords.length + 20).padStart(3, '0')}`;
  1915. const copied: TaskRecord = {
  1916. ...task,
  1917. id: nextId,
  1918. status: '待确认',
  1919. owner: '商务-王一',
  1920. version: '需求 v0.1',
  1921. lastAction: `从 ${task.id} 复制行业规则和模板,等待上传新 Brief`
  1922. };
  1923. this.taskRecords = [copied, ...this.taskRecords];
  1924. this.selectedTaskId = nextId;
  1925. this.taskTimeline = [
  1926. {
  1927. taskId: nextId,
  1928. time: '刚刚',
  1929. title: '复制为新任务',
  1930. detail: `复用 ${task.brand} 的规则组合、导出模板和复盘偏好。`
  1931. },
  1932. ...this.taskTimeline
  1933. ];
  1934. this.appendLog('任务复制', `${task.id} 已复制为 ${nextId}。`);
  1935. }
  1936. sendTaskFeedbackToLearning(): void {
  1937. const text = `${this.selectedTask.brand}:${this.selectedTask.lastAction}`;
  1938. this.feedbackText = `${this.feedbackText}\n任务复盘:${text}`;
  1939. this.activeView = 'learning';
  1940. this.appendLog('任务复盘', `${this.selectedTask.id} 的最近动作已加入反馈学习。`);
  1941. }
  1942. setTemplatePreviewMode(mode: '客户版' | '内部复核版' | '复盘版'): void {
  1943. this.templatePreviewMode = mode;
  1944. this.appendLog('模板预览', `已切换为${mode}。`);
  1945. }
  1946. toggleTemplateField(field: TemplateField): void {
  1947. if (field.required) {
  1948. field.selected = true;
  1949. return;
  1950. }
  1951. field.selected = !field.selected;
  1952. }
  1953. applyTemplateFieldsToDelivery(): void {
  1954. const active = new Set(this.templateFields.filter((field) => field.selected).map((field) => field.label));
  1955. this.deliveryFields = this.deliveryFields.map((field) => ({
  1956. ...field,
  1957. selected: active.has(field.label) || field.selected
  1958. }));
  1959. this.exportState = `已应用 ${this.visibleTemplateFields.length} 个模板字段到导出配置`;
  1960. this.appendLog('模板应用', `${this.selectedTemplate.name} 的字段配置已同步到名单交付。`);
  1961. }
  1962. setView(view: ViewKey): void {
  1963. this.activeView = view;
  1964. }
  1965. goStep(step: number): void {
  1966. this.currentStep = step;
  1967. this.updateSteps();
  1968. }
  1969. onFileSelected(event: Event): void {
  1970. const input = event.target as HTMLInputElement;
  1971. const file = input.files?.[0];
  1972. if (!file) {
  1973. return;
  1974. }
  1975. this.uploadedFileName = file.name;
  1976. this.currentStep = 1;
  1977. this.exportState = 'Brief 已上传,等待解析';
  1978. this.updateSteps();
  1979. }
  1980. analyzeBrief(): void {
  1981. this.currentStep = 2;
  1982. this.requirements = this.requirements.map((item, index) => ({
  1983. ...item,
  1984. confidence: Math.min(99, item.confidence + (index % 2 === 0 ? 2 : 1))
  1985. }));
  1986. this.activities[0] = { ...this.activities[0], status: '完成' };
  1987. this.exportState = '已抽取 6 个关键需求点';
  1988. this.updateSteps();
  1989. }
  1990. confirmDemand(): void {
  1991. this.currentStep = 3;
  1992. this.exportState = '需求已确认,等待规则选择';
  1993. this.updateSteps();
  1994. }
  1995. toggleRequirementEdit(item: Requirement): void {
  1996. item.editable = !item.editable;
  1997. }
  1998. addRequirement(): void {
  1999. this.requirements = [
  2000. ...this.requirements,
  2001. {
  2002. label: '临时补充',
  2003. value: '优先选择历史合作中客户反馈好、执行配合度高的账号',
  2004. confidence: 100,
  2005. selected: true,
  2006. editable: true,
  2007. evidence: '商务手动补充'
  2008. }
  2009. ];
  2010. }
  2011. addTemporaryRule(): void {
  2012. const next = String(this.rules.length + 1).padStart(3, '0');
  2013. this.rules = [
  2014. ...this.rules,
  2015. {
  2016. id: `R-${next}`,
  2017. title: this.tempRuleTitle,
  2018. description: '本次任务临时生效,可在复盘后保存为长期规则。',
  2019. category: '临时策略',
  2020. source: '临时补充',
  2021. selected: true,
  2022. impact: this.tempRuleImpact
  2023. }
  2024. ];
  2025. this.exportState = '临时规则已加入本次提号';
  2026. }
  2027. runResourceSync(): void {
  2028. this.resources = this.resources.map((resource, index) => ({
  2029. ...resource,
  2030. fresh: Math.min(96, resource.fresh + 4 + index),
  2031. matched: resource.matched + 6 + index
  2032. }));
  2033. this.fieldMappings = this.fieldMappings.map((mapping, index) => ({
  2034. ...mapping,
  2035. confidence: Math.min(99, mapping.confidence + 3),
  2036. confirmed: index < 4 ? true : mapping.confirmed
  2037. }));
  2038. this.activities[1] = { ...this.activities[1], status: '完成' };
  2039. this.exportState = '资源库已重新同步并更新字段映射';
  2040. }
  2041. confirmAllMappings(): void {
  2042. this.fieldMappings = this.fieldMappings.map((mapping) => ({ ...mapping, confirmed: true }));
  2043. }
  2044. generateRecommendations(): void {
  2045. this.currentStep = 4;
  2046. this.exportState = `已生成 ${this.totalMatchedCandidates} 个候选,当前展示 Top ${Math.min(12, this.candidates.length)}`;
  2047. this.activities = this.activities.map((activity, index) => ({
  2048. ...activity,
  2049. status: index < 3 ? '完成' : '进行中'
  2050. }));
  2051. this.candidates = this.candidates.map((candidate, index) => ({
  2052. ...candidate,
  2053. score: Math.min(97, candidate.score + (index < 2 ? 2 : 0)),
  2054. selected: index < 4,
  2055. status: candidate.status === '已剔除' ? '备选' : candidate.status
  2056. }));
  2057. this.updateSteps();
  2058. }
  2059. runSampling(): void {
  2060. this.currentStep = 4;
  2061. this.samplingTasks = this.samplingTasks.map((task) => ({
  2062. ...task,
  2063. progress: 100,
  2064. status: '完成',
  2065. result: task.result.includes('等待') ? '已完成近10篇内容采样,等待商务复核' : task.result
  2066. }));
  2067. this.activities[3] = { ...this.activities[3], status: '完成' };
  2068. this.candidates = this.candidates.map((candidate) => ({
  2069. ...candidate,
  2070. styleMatch: candidate.status === '需复核' ? Math.min(79, candidate.styleMatch + 4) : Math.min(96, candidate.styleMatch + 2)
  2071. }));
  2072. this.exportState = '近10篇内容采样已完成';
  2073. this.updateSteps();
  2074. }
  2075. toggleRule(rule: Rule): void {
  2076. rule.selected = !rule.selected;
  2077. }
  2078. toggleCandidate(candidate: Candidate): void {
  2079. candidate.selected = !candidate.selected;
  2080. }
  2081. openCandidate(candidate: Candidate): void {
  2082. this.selectedCandidateId = candidate.id;
  2083. this.selectedNoteId = candidate.recentNotes[0]?.id ?? '';
  2084. }
  2085. selectNote(note: NoteDetail): void {
  2086. this.selectedNoteId = note.id;
  2087. }
  2088. updateCandidateStatus(candidate: Candidate, status: CandidateStatus): void {
  2089. candidate.status = status;
  2090. candidate.selected = status !== '已剔除';
  2091. }
  2092. rotateCandidate(): void {
  2093. this.candidates = this.candidates.map((candidate, index) => {
  2094. if (candidate.status === '需复核') {
  2095. return { ...candidate, status: '备选', score: candidate.score + 5, selected: true };
  2096. }
  2097. return index === 0 ? { ...candidate, score: Math.min(99, candidate.score + 1) } : candidate;
  2098. });
  2099. this.exportState = '已按当前规则换一批并补足备选';
  2100. }
  2101. extractRules(): void {
  2102. const suggestions: LearnedRule[] = [
  2103. {
  2104. title: '小红书减少泛美妆号,优先生活方式和通勤场景',
  2105. description: '当 Brief 或反馈中出现“真实测评、通勤、生活方式”时,生活方式标签加权 +12,泛美妆标签降权 -10。',
  2106. confidence: 93,
  2107. action: '建议加入长期规则',
  2108. approved: false
  2109. },
  2110. {
  2111. title: '抖音预算压低时保留爆文率高账号',
  2112. description: '抖音预算下调时,先按报价降序剔除,但爆文率、完播率进入保护分。',
  2113. confidence: 87,
  2114. action: '建议加入预算策略',
  2115. approved: false
  2116. },
  2117. {
  2118. title: '客户明确不要硬广时,营销资讯类账号自动进入复核池',
  2119. description: '当反馈出现“硬广、资讯感、种草不自然”等表述时,资讯类账号风格分扣 8 分。',
  2120. confidence: 82,
  2121. action: '建议加入风格风控',
  2122. approved: false
  2123. }
  2124. ];
  2125. this.learnedRules = suggestions;
  2126. this.currentStep = 6;
  2127. this.updateSteps();
  2128. }
  2129. approveLearnedRule(item: LearnedRule): void {
  2130. item.approved = true;
  2131. this.rules = [
  2132. ...this.rules,
  2133. {
  2134. id: `R-${String(this.rules.length + 1).padStart(3, '0')}`,
  2135. title: item.title,
  2136. description: item.description,
  2137. category: '反馈学习',
  2138. source: 'AI提炼',
  2139. selected: true,
  2140. impact: `置信度 ${item.confidence}%`
  2141. }
  2142. ];
  2143. this.exportState = '反馈规则已确认入库';
  2144. }
  2145. rejectLearnedRule(item: LearnedRule): void {
  2146. this.learnedRules = this.learnedRules.filter((rule) => rule.title !== item.title);
  2147. }
  2148. selectTemplate(template: TemplateItem): void {
  2149. this.templates = this.templates.map((item) => ({
  2150. ...item,
  2151. selected: item.name === template.name
  2152. }));
  2153. this.exportState = `已选择模板:${template.name}`;
  2154. }
  2155. async exportRecommendations(): Promise<void> {
  2156. this.currentStep = 5;
  2157. this.updateSteps();
  2158. const selected = this.candidates.filter((candidate) => candidate.selected && candidate.status !== '已剔除');
  2159. try {
  2160. const response = await fetch(`${getApiBaseUrl()}/api/recommendations/export`, {
  2161. method: 'POST',
  2162. headers: { 'Content-Type': 'application/json' },
  2163. body: JSON.stringify({ candidates: selected })
  2164. });
  2165. if (!response.ok) {
  2166. throw new Error('API unavailable');
  2167. }
  2168. const blob = await response.blob();
  2169. this.triggerDownload(blob, '客户A-新锐护肤品牌-需求推荐名单.xls');
  2170. this.apiState = 'Node API 已生成导出文件';
  2171. this.exportState = '推荐名单已导出';
  2172. return;
  2173. } catch {
  2174. const html = this.buildExcelHtml(selected);
  2175. const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' });
  2176. this.triggerDownload(blob, '客户A-新锐护肤品牌-需求推荐名单.xls');
  2177. this.apiState = '前端兜底生成导出文件';
  2178. this.exportState = '推荐名单已导出';
  2179. }
  2180. }
  2181. private recalculateScenario(): void {
  2182. const activeIds = new Set(this.briefScenarios.filter((scenario) => scenario.selected).map((scenario) => scenario.id));
  2183. this.candidates = this.candidates.map((candidate) => {
  2184. const base = this.candidateBaseline.get(candidate.id);
  2185. if (!base) {
  2186. return candidate;
  2187. }
  2188. let score = base.score;
  2189. let styleMatch = base.styleMatch;
  2190. let status = base.status;
  2191. const ruleHits = [...base.ruleHits];
  2192. let risk = base.risk;
  2193. let reason = base.reason;
  2194. let selected = candidate.selected;
  2195. if (activeIds.has('budget-down')) {
  2196. if (candidate.price > 12000) {
  2197. score -= 6;
  2198. status = '备选';
  2199. risk = `${base.risk} 客户压预算后需谈价或替换。`;
  2200. } else if (candidate.price <= 7000) {
  2201. score += 4;
  2202. ruleHits.push('预算压缩保护');
  2203. }
  2204. }
  2205. if (activeIds.has('life-style')) {
  2206. const lifestyleHit = candidate.tags.some((tag) => ['生活方式', '通勤', '真实测评'].includes(tag));
  2207. if (lifestyleHit) {
  2208. score += 5;
  2209. styleMatch += 4;
  2210. ruleHits.push('生活方式加权');
  2211. } else if (candidate.platform === '微信') {
  2212. score -= 5;
  2213. status = '需复核';
  2214. risk = '客户偏生活方式后,资讯号需人工复核。';
  2215. }
  2216. }
  2217. if (activeIds.has('strict-style')) {
  2218. if (styleMatch < 80) {
  2219. score -= 8;
  2220. status = '需复核';
  2221. ruleHits.push('硬广风格复核');
  2222. } else {
  2223. score += 2;
  2224. }
  2225. }
  2226. if (activeIds.has('keep-bilibili') && candidate.platform === 'B站') {
  2227. score += 5;
  2228. selected = true;
  2229. status = status === '已剔除' ? '备选' : status;
  2230. reason = `${base.reason} 当前情景要求保留专业背书账号。`;
  2231. ruleHits.push('B站背书保留');
  2232. }
  2233. return {
  2234. ...candidate,
  2235. score: Math.max(50, Math.min(99, score)),
  2236. styleMatch: Math.max(45, Math.min(99, styleMatch)),
  2237. status,
  2238. selected,
  2239. risk,
  2240. reason,
  2241. ruleHits: Array.from(new Set(ruleHits))
  2242. };
  2243. });
  2244. this.exportState =
  2245. this.activeScenarioCount > 0
  2246. ? `已按 ${this.activeScenarioCount} 个客户情景试算名单`
  2247. : '已恢复默认提号策略';
  2248. }
  2249. private appendLog(action: string, detail: string): void {
  2250. const now = new Date();
  2251. const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
  2252. this.interactionLogs = [{ time, action, detail }, ...this.interactionLogs].slice(0, 6);
  2253. }
  2254. private updateSteps(): void {
  2255. this.steps = this.steps.map((step, index) => {
  2256. const position = index + 1;
  2257. if (position < this.currentStep) {
  2258. return { ...step, status: 'done' };
  2259. }
  2260. if (position === this.currentStep) {
  2261. return { ...step, status: 'active' };
  2262. }
  2263. return { ...step, status: 'todo' };
  2264. });
  2265. }
  2266. private triggerDownload(blob: Blob, filename: string): void {
  2267. const url = URL.createObjectURL(blob);
  2268. const link = document.createElement('a');
  2269. link.href = url;
  2270. link.download = filename;
  2271. link.click();
  2272. URL.revokeObjectURL(url);
  2273. }
  2274. private buildExcelHtml(candidates: Candidate[]): string {
  2275. const rows = candidates
  2276. .map(
  2277. (candidate) => `
  2278. <tr>
  2279. <td>${candidate.platform}</td>
  2280. <td>${candidate.name}</td>
  2281. <td>${candidate.fans}</td>
  2282. <td>${candidate.price}</td>
  2283. <td>${candidate.score}</td>
  2284. <td>${candidate.styleMatch}%</td>
  2285. <td>${candidate.status}</td>
  2286. <td>${candidate.owner}</td>
  2287. <td>${candidate.supplier}</td>
  2288. <td>${candidate.reason}</td>
  2289. <td>${candidate.risk}</td>
  2290. </tr>`
  2291. )
  2292. .join('');
  2293. return `
  2294. <html>
  2295. <head><meta charset="UTF-8"></head>
  2296. <body>
  2297. <table border="1">
  2298. <tr style="background:#1f8a70;color:#ffffff;font-weight:bold">
  2299. <th>平台</th><th>账号</th><th>粉丝量</th><th>报价</th><th>综合分</th><th>风格匹配</th><th>推荐状态</th><th>责任媒介</th><th>供应商</th><th>推荐理由</th><th>风险提示</th>
  2300. </tr>
  2301. ${rows}
  2302. </table>
  2303. </body>
  2304. </html>`;
  2305. }
  2306. }