index.html 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>搜索评估 · 案例总览</title>
  7. <style>
  8. :root {
  9. --ink: #24211d;
  10. --muted: #6f6961;
  11. --line: #ded8ce;
  12. --paper: #fbfaf7;
  13. --panel: #ffffff;
  14. --mint: #1f8a70;
  15. --rose: #b24b63;
  16. --amber: #b87918;
  17. --cyan: #2a6f8f;
  18. --soft-mint: #e9f5f0;
  19. --soft-rose: #f8e9ee;
  20. --soft-amber: #fff2d9;
  21. --soft-cyan: #e7f2f7;
  22. --shadow: 0 18px 45px rgba(41, 35, 28, .08);
  23. }
  24. * { box-sizing: border-box; }
  25. body {
  26. margin: 0;
  27. color: var(--ink);
  28. background: var(--paper);
  29. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  30. line-height: 1.55;
  31. }
  32. header {
  33. padding: 40px 32px 22px;
  34. border-bottom: 1px solid var(--line);
  35. background: linear-gradient(180deg, #fff 0%, #fbfaf7 100%);
  36. }
  37. .wrap { max-width: 1220px; margin: 0 auto; }
  38. .eyebrow {
  39. display: flex;
  40. gap: 8px;
  41. align-items: center;
  42. color: var(--mint);
  43. font-size: 13px;
  44. font-weight: 700;
  45. text-transform: uppercase;
  46. letter-spacing: 0;
  47. }
  48. h1 {
  49. margin: 10px 0 10px;
  50. font-size: clamp(32px, 5vw, 58px);
  51. line-height: 1.06;
  52. letter-spacing: 0;
  53. }
  54. .lede {
  55. max-width: 860px;
  56. margin: 0;
  57. color: var(--muted);
  58. font-size: 17px;
  59. }
  60. .stats {
  61. display: grid;
  62. grid-template-columns: repeat(4, minmax(0, 1fr));
  63. gap: 12px;
  64. margin-top: 24px;
  65. }
  66. .stat {
  67. background: var(--panel);
  68. border: 1px solid var(--line);
  69. border-radius: 8px;
  70. padding: 14px;
  71. box-shadow: var(--shadow);
  72. min-height: 86px;
  73. }
  74. .stat strong { display: block; font-size: 26px; line-height: 1.1; }
  75. .stat span { color: var(--muted); font-size: 13px; }
  76. main { padding: 24px 32px 48px; }
  77. .toolbar {
  78. display: flex;
  79. gap: 10px;
  80. align-items: center;
  81. justify-content: space-between;
  82. margin-bottom: 18px;
  83. flex-wrap: wrap;
  84. }
  85. .filters { display: flex; gap: 8px; flex-wrap: wrap; }
  86. button, select {
  87. border: 1px solid var(--line);
  88. background: #fff;
  89. color: var(--ink);
  90. border-radius: 8px;
  91. padding: 9px 12px;
  92. font: inherit;
  93. }
  94. button { cursor: pointer; }
  95. button.active {
  96. color: #fff;
  97. background: var(--ink);
  98. border-color: var(--ink);
  99. }
  100. select { min-width: 190px; }
  101. .grid {
  102. display: grid;
  103. grid-template-columns: repeat(3, minmax(0, 1fr));
  104. gap: 16px;
  105. }
  106. .result {
  107. min-height: 500px;
  108. background: var(--panel);
  109. border: 1px solid var(--line);
  110. border-radius: 8px;
  111. overflow: hidden;
  112. box-shadow: var(--shadow);
  113. display: flex;
  114. flex-direction: column;
  115. }
  116. .thumbs {
  117. display: grid;
  118. grid-template-columns: repeat(3, 1fr);
  119. gap: 2px;
  120. height: 150px;
  121. background: #eee7dc;
  122. overflow: hidden;
  123. }
  124. .thumbs img {
  125. width: 100%;
  126. height: 100%;
  127. object-fit: cover;
  128. display: block;
  129. background: #eee7dc;
  130. }
  131. .thumbs img:first-child:nth-last-child(1) { grid-column: 1 / -1; }
  132. .body { padding: 15px; flex: 1; display: flex; flex-direction: column; }
  133. .meta {
  134. display: flex;
  135. justify-content: space-between;
  136. gap: 10px;
  137. color: var(--muted);
  138. font-size: 12px;
  139. margin-bottom: 8px;
  140. }
  141. .platform {
  142. color: #fff;
  143. border-radius: 999px;
  144. padding: 2px 8px;
  145. font-weight: 700;
  146. white-space: nowrap;
  147. }
  148. .p-xhs { background: var(--rose); }
  149. .p-gzh { background: var(--mint); }
  150. .p-x { background: var(--cyan); }
  151. h2 { margin: 0 0 8px; font-size: 18px; line-height: 1.25; letter-spacing: 0; }
  152. .excerpt {
  153. color: var(--muted);
  154. font-size: 13px;
  155. display: -webkit-box;
  156. -webkit-line-clamp: 5;
  157. -webkit-box-orient: vertical;
  158. overflow: hidden;
  159. }
  160. .tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
  161. .tag {
  162. background: #f2eee7;
  163. border-radius: 999px;
  164. padding: 3px 8px;
  165. font-size: 12px;
  166. color: #514a42;
  167. }
  168. .scorebar { margin-top: auto; }
  169. .overall {
  170. display: flex;
  171. align-items: end;
  172. justify-content: space-between;
  173. border-top: 1px solid var(--line);
  174. padding-top: 12px;
  175. }
  176. .score {
  177. font-size: 36px;
  178. line-height: .9;
  179. font-weight: 800;
  180. }
  181. .decision { color: var(--mint); font-weight: 800; }
  182. .decision.discard { color: var(--amber); }
  183. .mini-bars {
  184. display: grid;
  185. grid-template-columns: repeat(5, 1fr);
  186. gap: 4px;
  187. margin-top: 10px;
  188. }
  189. .mini-bars span {
  190. height: 7px;
  191. border-radius: 999px;
  192. background: #eee;
  193. overflow: hidden;
  194. position: relative;
  195. }
  196. .mini-bars span::before {
  197. content: "";
  198. display: block;
  199. width: calc(var(--v) * 20%);
  200. height: 100%;
  201. background: var(--mint);
  202. }
  203. .group-snapshot {
  204. display: grid;
  205. grid-template-columns: repeat(4, minmax(0, 1fr));
  206. gap: 5px;
  207. margin-top: 10px;
  208. }
  209. .group-pill {
  210. border: 1px solid var(--line);
  211. border-radius: 8px;
  212. padding: 5px 6px;
  213. background: #fff;
  214. min-width: 0;
  215. }
  216. .group-pill span {
  217. display: block;
  218. color: var(--muted);
  219. font-size: 11px;
  220. white-space: nowrap;
  221. overflow: hidden;
  222. text-overflow: ellipsis;
  223. }
  224. .group-pill strong {
  225. display: block;
  226. font-size: 14px;
  227. line-height: 1.1;
  228. }
  229. .actions { display: flex; gap: 8px; margin-top: 12px; }
  230. .actions a, .actions button {
  231. flex: 1;
  232. text-align: center;
  233. text-decoration: none;
  234. color: var(--ink);
  235. background: #fff;
  236. border: 1px solid var(--line);
  237. border-radius: 8px;
  238. padding: 8px 10px;
  239. font-size: 13px;
  240. }
  241. dialog {
  242. width: min(980px, calc(100vw - 28px));
  243. max-height: calc(100vh - 32px);
  244. border: 1px solid var(--line);
  245. border-radius: 8px;
  246. padding: 0;
  247. box-shadow: 0 28px 90px rgba(0,0,0,.25);
  248. }
  249. dialog::backdrop { background: rgba(38, 33, 27, .42); }
  250. .modal-head {
  251. position: sticky;
  252. top: 0;
  253. background: #fff;
  254. border-bottom: 1px solid var(--line);
  255. padding: 16px;
  256. z-index: 2;
  257. display: flex;
  258. justify-content: space-between;
  259. gap: 14px;
  260. align-items: start;
  261. }
  262. .modal-head h3 { margin: 0; font-size: 20px; line-height: 1.25; }
  263. .modal-content {
  264. display: grid;
  265. grid-template-columns: 1.1fr .9fr;
  266. gap: 18px;
  267. padding: 16px;
  268. }
  269. .section-title { margin: 18px 0 8px; font-weight: 800; }
  270. .raw {
  271. white-space: pre-wrap;
  272. background: #faf7f1;
  273. border: 1px solid var(--line);
  274. border-radius: 8px;
  275. padding: 12px;
  276. max-height: 330px;
  277. overflow: auto;
  278. color: #3d3831;
  279. font-size: 13px;
  280. }
  281. .images {
  282. display: grid;
  283. grid-template-columns: repeat(2, minmax(0, 1fr));
  284. gap: 8px;
  285. }
  286. .images img {
  287. width: 100%;
  288. max-height: 260px;
  289. object-fit: contain;
  290. border: 1px solid var(--line);
  291. border-radius: 8px;
  292. background: #f1ece4;
  293. }
  294. .scores {
  295. display: grid;
  296. gap: 12px;
  297. }
  298. .score-group {
  299. border: 1px solid var(--line);
  300. border-radius: 8px;
  301. background: #fff;
  302. overflow: hidden;
  303. }
  304. .score-group-head {
  305. display: flex;
  306. justify-content: space-between;
  307. gap: 10px;
  308. align-items: center;
  309. padding: 10px 11px;
  310. background: #faf7f1;
  311. border-bottom: 1px solid var(--line);
  312. font-size: 13px;
  313. font-weight: 800;
  314. }
  315. .score-group-head small {
  316. color: var(--muted);
  317. font-weight: 700;
  318. white-space: nowrap;
  319. }
  320. .score-group-body {
  321. display: grid;
  322. gap: 8px;
  323. padding: 10px;
  324. }
  325. .score-row {
  326. display: grid;
  327. grid-template-columns: 128px 1fr 34px;
  328. gap: 10px;
  329. align-items: center;
  330. font-size: 13px;
  331. }
  332. .score-row.missing {
  333. color: #a39b91;
  334. }
  335. .score-row.missing .meter span {
  336. display: none;
  337. }
  338. .meter {
  339. height: 9px;
  340. border-radius: 999px;
  341. background: #eee7dc;
  342. overflow: hidden;
  343. }
  344. .meter span { display: block; height: 100%; width: calc(var(--v) * 20%); background: var(--rose); }
  345. .rubric-note {
  346. background: var(--soft-cyan);
  347. border-left: 4px solid var(--cyan);
  348. padding: 10px 12px;
  349. color: #254c5d;
  350. border-radius: 4px;
  351. font-size: 13px;
  352. }
  353. @media (max-width: 980px) {
  354. .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  355. .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  356. .modal-content { grid-template-columns: 1fr; }
  357. }
  358. @media (max-width: 640px) {
  359. header, main { padding-left: 16px; padding-right: 16px; }
  360. .grid, .stats { grid-template-columns: 1fr; }
  361. .toolbar { align-items: stretch; }
  362. select { width: 100%; }
  363. .result { min-height: auto; }
  364. .group-snapshot { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  365. }
  366. /* Extra styles for interactive navigation & matrix */
  367. .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;}
  368. .stats .stat{min-width:0;}
  369. .p-zhihu{background:#2a6f8f;} .p-x{background:#2a6f8f;} .p-bili{background:#b24b63;} .p-douyin{background:#24211d;}
  370. .p-sph{background:#07c160;} .p-youtube{background:#c4302b;} .p-github{background:#24292e;} .p-toutiao{background:#f04142;} .p-weibo{background:#e6162d;}
  371. .nav{display:flex;flex-direction:column;gap:8px;margin-bottom:14px;}
  372. .navrow{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
  373. .navlab{width:54px;flex:0 0 54px;color:var(--muted);font-size:12px;font-weight:700;text-transform:uppercase;}
  374. .navrow .tab{border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:999px;padding:6px 14px;font-size:13px;cursor:pointer;}
  375. .navrow .tab.on{background:var(--ink);color:#fff;border-color:var(--ink);}
  376. .navrow .tab .q{font-family:ui-monospace,Menlo,monospace;} .navrow .tab small{color:var(--muted);margin-left:4px;} .navrow .tab.on small{color:#cfd8dc;}
  377. #navQ .qtab{max-width:100%;white-space:normal;text-align:left;line-height:1.4;}
  378. .navrow .tab{white-space:normal;}
  379. #refresh{cursor:pointer;}
  380. /* 1:1 Morandi Matrix Replicated Style Rules */
  381. .btn{padding:3px 10px;border-radius:14px;border:1px solid #d1d5db;background:#fff;cursor:pointer;font-size:.74rem;color:#444;}
  382. .g-form .btn.on{background:#4f46e5;border-color:#4f46e5;color:#fff;}
  383. .g-lens .btn.on{background:#be185d;border-color:#be185d;color:#fff;}
  384. .g-mod .btn.on{background:#2e7d32;border-color:#2e7d32;color:#fff;}
  385. .g-tool .btn.on{background:#c2410c;border-color:#c2410c;color:#fff;}
  386. .g-tier .btn.on{background:#374151;border-color:#374151;color:#fff;}
  387. .mxwrap{max-height:55vh;overflow:auto;border:1px solid var(--line);border-radius:0 0 8px 8px;margin-bottom:12px;background:#fff;position:relative;}
  388. table#comboMx{border-collapse:separate;border-spacing:0;background:#fff;font-size:.65rem;width:100%;}
  389. table#comboMx th, table#comboMx td{border-right:1px solid #f0f1f5;border-bottom:1px solid #f0f1f5;text-align:center;}
  390. table#comboMx thead th{position:sticky;background:#fff;}
  391. table#comboMx thead tr.l1 th{top:0;z-index:11;background:#eef2ff;color:#4338ca;font-weight:700;font-size:.72rem;height:20px;border-bottom:1px solid #c7d2fe;}
  392. table#comboMx thead tr.l2 th{top:20px;z-index:11;background:#f5f7ff;color:#6366f1;font-weight:600;font-size:.66rem;height:18px;border-bottom:1px solid #e0e7ff;}
  393. table#comboMx thead tr.leaf th{top:38px;z-index:9;background:#fff;color:#1f2937;font-size:.66rem;min-width:120px;max-width:120px;height:24px;padding:3px;}
  394. table#comboMx thead .corner{left:0;top:0;z-index:13;background:#fff;min-width:96px;}
  395. .l1div{border-left:3px solid #818cf8!important;} .l2div{border-left:1.5px solid #cbd5e8!important;}
  396. table#comboMx tbody th.rh{position:sticky;left:0;z-index:8;background:#fffaf0;color:#92580c;text-align:left;padding:4px 8px;font-weight:600;white-space:nowrap;min-width:96px;border-right:1px solid #e6e8ef;}
  397. table#comboMx tbody tr.l1row td{background:#eef2ff;color:#4338ca;font-weight:700;font-size:.7rem;text-align:left;padding:3px 8px;position:sticky;left:0;}
  398. td.cell{min-width:120px;max-width:120px;height:30px;padding:2px 5px;cursor:pointer;font-family:ui-monospace,Menlo,monospace;font-size:.65rem;color:#1f2937;text-align:left;line-height:1.25;border-left:4px solid transparent;overflow:hidden;position:relative;}
  399. td.cell:hover{box-shadow:inset 0 0 0 2px #f59e0b;}
  400. td.cell.t0{border-left-color:#e5e7eb;background:#fbfbfc;color:#aeb3bd;}
  401. td.cell.t1{border-left-color:#bbf7d0;}
  402. td.cell.t2{border-left-color:#4ade80;background:#f3fdf6;}
  403. td.cell.t3{border-left-color:#15803d;background:#ecfdf3;}
  404. td.cell.tNA{border-left-color:#fcd34d;background:repeating-linear-gradient(45deg,#fff,#fff 4px,#fef9ec 4px,#fef9ec 8px);color:#b6bac4;}
  405. td.cell.gq-cell{color:#15803d;font-weight:600;}
  406. td.cell.rowdim{opacity:.26;}
  407. td.cell.hide{visibility:hidden;}
  408. td.cell.sel{outline:2px solid var(--ink);outline-offset:-2px;font-weight:bold;}
  409. td.cell.rowsel,td.cell.colsel{box-shadow:inset 0 0 0 999px rgba(255,193,7,.12);}
  410. td.cell.rowsel.colsel{box-shadow:inset 0 0 0 999px rgba(255,193,7,.22);}
  411. th.rh.rowsel{background:rgba(255,193,7,.22);color:var(--ink);font-weight:700;}
  412. thead th.colsel{background:rgba(255,193,7,.22);color:var(--ink);font-weight:700;}
  413. /* Database hit badge inside matrix cells */
  414. td.cell .hit-badge{
  415. position:absolute; right:2px; top:2px; background:#10b981; color:#fff; font-size:9px; font-weight:bold; border-radius:3px; padding:0 3px; line-height:1.2; box-shadow:0 1px 2px rgba(0,0,0,0.1);
  416. }
  417. /* Pop-up floating panel styling */
  418. .pop{position:fixed;background:#fff;border:1px solid #d1d5db;border-radius:9px;box-shadow:0 10px 30px rgba(0,0,0,.18);padding:13px 15px;max-width:450px;z-index:200;display:none;font-size:.82rem;line-height:1.5;}
  419. .pop .pt{font-weight:700;color:#1f2937;margin-bottom:7px;display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding-right:18px;}
  420. .pop .path{font-size:.66rem;padding:1px 6px;border-radius:3px;} .pop .path.a{background:#eef2ff;color:#4338ca;} .pop .path.t{background:#fef3c7;color:#92400e;}
  421. .pop .tier{font-size:.64rem;padding:1px 7px;border-radius:3px;font-weight:600;}
  422. .tb0{background:#e5e7eb;color:#6b7280;} .tb1{background:#dcfce7;color:#166534;} .tb2{background:#bbf7d0;color:#14532d;} .tb3{background:#15803d;color:#fff;} .tbNA{background:#fef3c7;color:#92400e;}
  423. .pop .reason{background:#fffbeb;border-left:3px solid #f59e0b;padding:7px 11px;border-radius:3px;color:#78350f;margin:7px 0;font-size:.8rem;}
  424. .pop .reason.inv{background:#fef2f2;border-left-color:#ef4444;color:#991b1b;}
  425. .pop .gq{background:#ecfdf3;border:1px solid #a7f3d0;border-radius:6px;padding:7px 11px;margin:7px 0;font-family:ui-monospace,Menlo,monospace;font-size:.84rem;color:#15803d;}
  426. .pop .gq .t{font-family:inherit;font-size:.64rem;color:#6b7280;display:block;margin-bottom:2px;}
  427. .pop .small{font-size:.7rem;color:#9aa0ad;margin:4px 0;}
  428. .pop .lbl{font-size:.66rem;color:#6b7280;font-weight:600;margin:9px 0 3px;}
  429. .pop ul{margin:0;padding:0;list-style:none;} .pop li{padding:3px 8px;background:#f9fafb;border-radius:4px;margin-bottom:3px;font-family:ui-monospace,Menlo,monospace;font-size:.78rem;color:#1f2937;}
  430. .pop li.gen{background:#eef0ff;color:#3730a3;} .pop li.cur{box-shadow:inset 0 0 0 2px #818cf8;} .pop li .fm{font-size:.62rem;color:#4f46e5;margin-right:6px;}
  431. .pop .note{font-size:.7rem;color:#2e7d32;margin-top:6px;}
  432. .pop .close{position:absolute;top:6px;right:9px;cursor:pointer;color:#9ca3af;font-size:1.15rem;}
  433. .fac{border:1px solid var(--line);background:#fff;border-radius:999px;padding:4px 11px;font-size:12px;cursor:pointer;color:var(--ink);}
  434. .fac.on{background:var(--mint);color:#fff;border-color:var(--mint);}
  435. .fac small{opacity:.65;margin-left:3px;}
  436. #navQ .qtab .hit{display:inline-block;margin-left:7px;background:var(--soft-mint);color:var(--mint);border-radius:999px;padding:0 8px;font-size:11px;font-weight:700;}
  437. #navQ .qtab.on .hit{background:rgba(255,255,255,.22);color:#fff;}
  438. </style>
  439. </head>
  440. <body>
  441. <header>
  442. <div class="wrap">
  443. <div class="eyebrow">Content Search · runs/ 实时 · query → 形式 → 渠道</div>
  444. <h1>搜索评估 · 案例总览</h1>
  445. <p class="lede" id="lede" style="margin:0">加载中…</p>
  446. <div class="stats" id="stats"></div>
  447. </div>
  448. </header>
  449. <main>
  450. <div class="wrap">
  451. <div class="nav">
  452. <div class="navrow" style="margin-bottom:4px;">
  453. <span class="navlab">组合矩阵</span>
  454. <span style="color:var(--muted);font-size:12px;flex:1">行=类型 列=动作 格子色=评分,角标=命中数。点击选组合且弹出详情。</span>
  455. <label style="font-size:12px;cursor:pointer;display:flex;align-items:center;gap:4px;color:var(--muted);user-select:none;margin-right:12px;">
  456. <input type="checkbox" id="showFullMx" onchange="renderMatrix()" checked> 显示完整矩阵(未打勾只看已命中)
  457. </label>
  458. <button id="refresh" onclick="loadData(true)">↻ 刷新 runs</button>
  459. </div>
  460. <div class="mx-header" style="background:#fff; border: 1px solid var(--line); border-radius: 8px 8px 0 0; padding:12px 16px 8px; border-bottom: none;">
  461. <h4 style="margin:0 0 4px; font-size:13px; color:#1f2937; display:flex; justify-content:space-between; align-items:center; font-weight:700;">
  462. <span>动作 × 类型 · 组合矩阵 <span style="font-weight:400;font-size:11px;color:#9aa0ad">基于 <b id="gm">gemini-3.1-flash-lite</b></span></span>
  463. <span class="legend" style="display:flex;gap:9px;align-items:center;color:#6b7280;font-size:11px;">
  464. <span><span class="sw" style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#15803d;vertical-align:middle;margin-right:3px;"></span>高 · <b id="s3" style="color:#15803d;">0</b></span>
  465. <span><span class="sw" style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#4ade80;vertical-align:middle;margin-right:3px;"></span>中 · <b id="s2" style="color:#4ade80;">0</b></span>
  466. <span><span class="sw" style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#bbf7d0;vertical-align:middle;margin-right:3px;"></span>低 · <b id="s1" style="color:#10b981;">0</b></span>
  467. <span><span class="sw" style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#e5e7eb;vertical-align:middle;margin-right:3px;"></span>无效 · <b id="s0" style="color:#9ca3af;">0</b></span>
  468. <span><span class="sw" style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#fcd34d;vertical-align:middle;margin-right:3px;"></span>未判 · <b id="sna" style="color:#b87918;">0</b></span>
  469. </span>
  470. </h4>
  471. <div class="sub" style="font-size:11px;color:#6b7280;margin-bottom:8px;">
  472. 共 1350 格 · 形式①原词不替换 · ②句子套模板 · ③同义池替换。
  473. </div>
  474. <div class="ctl" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;font-size:11px;border-top:1px solid #f3f4f6;padding-top:8px;">
  475. <span class="lab" style="color:#9aa0ad;font-weight:600;">query生成方式</span>
  476. <span class="g-form">
  477. <button class="btn on" data-k="form" data-v="A">①原词</button>
  478. <button class="btn" data-k="form" data-v="B">②句子</button>
  479. <button class="btn" data-k="form" data-v="C">③同义</button>
  480. </span>
  481. <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">知识类型</span>
  482. <span class="g-lens">
  483. <button class="btn on" data-k="lens" data-v="工序">工序</button>
  484. <button class="btn" data-k="lens" data-v="工具">工具</button>
  485. <button class="btn" data-k="lens" data-v="能力">能力</button>
  486. </span>
  487. <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">约束·工具类型</span>
  488. <span class="g-tool">
  489. <button class="btn on" data-k="tool" data-v="">无</button>
  490. <span id="toolBtns"></span>
  491. </span>
  492. <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">搜索优先级</span>
  493. <span class="g-tier">
  494. <button class="btn on" data-k="tier" data-v="0">全部</button>
  495. <button class="btn" data-k="tier" data-v="2">≥中</button>
  496. <button class="btn" data-k="tier" data-v="3">仅高</button>
  497. </span>
  498. </div>
  499. </div>
  500. <div class="mxwrap" style="max-height:55vh; overflow:auto; border:1px solid var(--line); border-radius: 0 0 8px 8px; margin-bottom:12px; background:#fff; position:relative;">
  501. <table id="comboMx"></table>
  502. </div>
  503. <div class="pop" id="pop"></div>
  504. <div class="navrow"><span class="navlab">渠道</span><div id="navC" style="display:flex;gap:8px;flex-wrap:wrap"></div></div>
  505. </div>
  506. <div class="toolbar">
  507. <div class="filters"></div>
  508. <button id="reevalBtn" onclick="reevalCurrentQuery()" title="只对当前 query 的所有 form/帖子复评(不重新搜索)">♻️ 重评当前 query</button>
  509. <select id="sort">
  510. <option value="score">按综合分排序</option>
  511. <option value="date">按发布时间排序</option>
  512. <option value="platform">按平台排序</option>
  513. </select>
  514. </div>
  515. <div class="grid" id="grid"></div>
  516. </div>
  517. </main>
  518. <dialog id="detailDialog">
  519. <div class="modal-head">
  520. <div>
  521. <div id="modalMeta" class="meta"></div>
  522. <h3 id="modalTitle"></h3>
  523. </div>
  524. <button onclick="detailDialog.close()">关闭</button>
  525. </div>
  526. <div class="modal-content">
  527. <section>
  528. <div class="rubric-note" id="modalReason"></div>
  529. <div class="section-title">抓取文本节选</div>
  530. <div class="raw" id="modalText"></div>
  531. <div class="section-title">图片预览</div>
  532. <div class="images" id="modalImages"></div>
  533. </section>
  534. <aside>
  535. <div class="section-title">评分详情</div>
  536. <div class="scores" id="modalScores"></div>
  537. <div class="section-title">类型 / 命中 query</div>
  538. <div class="tags" id="modalTags"></div>
  539. </aside>
  540. </div>
  541. </dialog>
  542. <script>
  543. let DATA={queries:[],actions:[],types:[],matrix:[]}, st={form:'A',lens:'工序',tools:[],tier:0,qi:0,fi:0,channel:"all"}, VIEW=[];
  544. function esc(s) { return (s === undefined || s === null ? "" : String(s)).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;"); }
  545. const ACTIONS=[{"name": "检索", "l1": "获取", "l2": "搜索"}, {"name": "下载", "l1": "获取", "l2": "搜索"}, {"name": "调取", "l1": "获取", "l2": "查询"}, {"name": "上传", "l1": "获取", "l2": "录入"}, {"name": "拍摄", "l1": "获取", "l2": "录入"}, {"name": "录音", "l1": "获取", "l2": "录入"}, {"name": "键入", "l1": "获取", "l2": "录入"}, {"name": "选取", "l1": "获取", "l2": "引用"}, {"name": "裁切", "l1": "提取", "l2": "物理提取"}, {"name": "抠取", "l1": "提取", "l2": "物理提取"}, {"name": "抽帧", "l1": "提取", "l2": "物理提取"}, {"name": "识别", "l1": "提取", "l2": "化学提取"}, {"name": "反推", "l1": "提取", "l2": "化学提取"}, {"name": "解构", "l1": "提取", "l2": "化学提取"}, {"name": "元素生成", "l1": "生成", "l2": "元素生成"}, {"name": "数组生成", "l1": "生成", "l2": "关系生成"}, {"name": "结构生成", "l1": "生成", "l2": "关系生成"}, {"name": "添加", "l1": "修改", "l2": "增"}, {"name": "叠加", "l1": "修改", "l2": "增"}, {"name": "抹除", "l1": "修改", "l2": "删"}, {"name": "剪除", "l1": "修改", "l2": "删"}, {"name": "重述", "l1": "修改", "l2": "变"}, {"name": "风格化", "l1": "修改", "l2": "变"}, {"name": "转换", "l1": "修改", "l2": "变"}, {"name": "替换", "l1": "修改", "l2": "变"}, {"name": "调整", "l1": "修改", "l2": "变"}, {"name": "增强", "l1": "修改", "l2": "变"}];
  546. const TYPES=[{"name": "提示词", "l1": "程序控制类型", "l2": "指令"}, {"name": "负向提示词", "l1": "程序控制类型", "l2": "指令"}, {"name": "描述", "l1": "程序控制类型", "l2": "指令"}, {"name": "生成参数", "l1": "程序控制类型", "l2": "参数"}, {"name": "规格参数", "l1": "程序控制类型", "l2": "参数"}, {"name": "模型权重", "l1": "程序控制类型", "l2": "参数"}, {"name": "评分", "l1": "程序控制类型", "l2": "评估"}, {"name": "评语", "l1": "程序控制类型", "l2": "评估"}, {"name": "工作流", "l1": "程序控制类型", "l2": "流程"}, {"name": "批处理", "l1": "程序控制类型", "l2": "流程"}, {"name": "数字人", "l1": "数据复用类型", "l2": "原子"}, {"name": "版式", "l1": "数据复用类型", "l2": "原子"}, {"name": "模板", "l1": "数据复用类型", "l2": "序列"}, {"name": "参考图", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "参考视频", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "参考音频", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "对标内容", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "分镜图", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "转场", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "蒙版", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "控制图", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "运动轨迹", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "滤镜", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "构图布局", "l1": "内容类型", "l2": "素材/化学变化"}, {"name": "截图", "l1": "内容类型", "l2": "素材/物理变化"}, {"name": "视频片段", "l1": "内容类型", "l2": "素材/物理变化"}, {"name": "转场片段", "l1": "内容类型", "l2": "素材/物理变化"}, {"name": "关键帧", "l1": "内容类型", "l2": "素材/物理变化"}, {"name": "音效", "l1": "内容类型", "l2": "素材/物理变化"}, {"name": "特效", "l1": "内容类型", "l2": "素材/物理变化"}, {"name": "大纲", "l1": "内容类型", "l2": "半成品/序列"}, {"name": "脚本", "l1": "内容类型", "l2": "半成品/序列"}, {"name": "分镜脚本", "l1": "内容类型", "l2": "半成品/序列"}, {"name": "剪辑脚本", "l1": "内容类型", "l2": "半成品/序列"}, {"name": "配音文案", "l1": "内容类型", "l2": "半成品/序列"}, {"name": "底图", "l1": "内容类型", "l2": "半成品/原子"}, {"name": "样图", "l1": "内容类型", "l2": "半成品/原子"}, {"name": "分镜视频", "l1": "内容类型", "l2": "半成品/原子"}, {"name": "图层组合", "l1": "内容类型", "l2": "半成品/组合"}, {"name": "拼图", "l1": "内容类型", "l2": "半成品/组合"}, {"name": "歌词", "l1": "内容类型", "l2": "准成品"}, {"name": "配音", "l1": "内容类型", "l2": "准成品"}, {"name": "BGM", "l1": "内容类型", "l2": "准成品"}, {"name": "字幕", "l1": "内容类型", "l2": "准成品"}, {"name": "标题", "l1": "内容类型", "l2": "准成品"}, {"name": "正文", "l1": "内容类型", "l2": "准成品"}, {"name": "成品图", "l1": "内容类型", "l2": "成品"}, {"name": "视频成品", "l1": "内容类型", "l2": "成品"}, {"name": "合成图", "l1": "内容类型", "l2": "成品"}, {"name": "知识库", "l1": "知识类型", "l2": "知识库"}];
  547. const POOLS={"action_leaves": {"检索": ["找", "搜", "哪里找", "在哪找", "搜索"], "下载": ["下载", "怎么下载", "获取", "导入"], "调取": ["复用", "调用", "套用", "加载"], "上传": ["上传", "导入", "怎么导入", "用自己的"], "拍摄": ["拍", "拍摄", "录制", "录屏", "怎么拍"], "录音": ["录音", "录", "怎么录", "录制"], "键入": ["写", "输入", "怎么写", "编写"], "选取": ["挑选", "怎么选", "筛选", "选"], "裁切": ["裁剪", "截取", "怎么裁", "切片"], "抠取": ["抠图", "抠", "分割", "怎么抠"], "抽帧": ["抽帧", "提取帧", "怎么抽帧", "导出帧"], "识别": ["识别", "检测", "OCR", "提取文字", "转录"], "反推": ["反推", "分析", "推断", "怎么反推"], "解构": ["拆解", "解构", "分析结构"], "元素生成": ["生成", "制作", "怎么做", "做", "创作"], "数组生成": ["批量生成", "生成多张", "怎么批量", "批量做"], "结构生成": ["生成", "搭建", "怎么生成", "构建"], "添加": ["添加", "加", "怎么加", "加上"], "叠加": ["叠加", "合成", "叠", "加"], "抹除": ["去除", "去掉", "怎么去掉", "抹除", "消除"], "剪除": ["剪掉", "删掉", "怎么剪", "截短"], "重述": ["改写", "润色", "怎么改", "重写"], "风格化": ["风格化", "转风格", "风格迁移", "怎么转风格"], "转换": ["转换", "转", "怎么转", "转格式"], "替换": ["替换", "换", "怎么换", "替换成"], "调整": ["调整", "调", "怎么调", "优化"], "增强": ["增强", "提升画质", "怎么增强", "修复", "超分"]}, "types": {"提示词": ["提示词", "prompt", "咒语"], "负向提示词": ["负向提示词", "反向提示词", "negative prompt"], "描述": ["描述", "描述词", "提示描述"], "生成参数": ["参数", "出图参数", "生成参数", "seed"], "规格参数": ["尺寸", "分辨率", "画幅", "规格"], "模型权重": ["模型", "LoRA", "底模", "checkpoint"], "评分": ["评分", "打分", "评级"], "评语": ["评语", "点评", "反馈", "修改意见"], "工作流": ["工作流", "workflow", "流程图", "节点"], "批处理": ["批处理", "批量", "batch"], "数字人": ["数字人", "虚拟人", "数字分身", "AI分身"], "版式": ["版式", "排版", "版面", "layout"], "模板": ["模板", "template", "套版"], "参考图": ["参考图", "参考", "垫图", "ref"], "参考视频": ["参考视频", "参考片", "参考素材"], "参考音频": ["参考音频", "参考音色", "音色样本"], "对标内容": ["对标", "爆款参考", "竞品", "标杆案例"], "分镜图": ["分镜图", "分镜", "故事板", "storyboard"], "转场": ["转场", "转场效果", "transition"], "蒙版": ["蒙版", "抠像", "遮罩", "mask"], "控制图": ["控制图", "ControlNet", "结构图", "线稿"], "运动轨迹": ["运镜", "运动轨迹", "运动笔刷", "motion brush"], "滤镜": ["滤镜", "filter", "调色", "LUT"], "构图布局": ["构图", "布局", "构图布局"], "截图": ["截图", "屏幕截图", "截屏"], "视频片段": ["视频片段", "片段", "素材", "clip"], "转场片段": ["转场片段", "转场素材"], "关键帧": ["关键帧", "帧", "视频帧"], "音效": ["音效", "声音", "SFX", "音效素材"], "特效": ["特效", "视频特效", "VFX", "effect"], "大纲": ["大纲", "内容大纲", "提纲"], "脚本": ["脚本", "文案脚本", "剧本"], "分镜脚本": ["分镜脚本", "分镜表", "分镜"], "剪辑脚本": ["剪辑脚本", "剪辑表", "卡点表", "时间轴"], "配音文案": ["配音文案", "口播文案", "旁白文案"], "底图": ["底图", "背景图", "打底图"], "样图": ["样图", "效果图", "候选图", "draft"], "分镜视频": ["分镜视频", "分镜片段"], "图层组合": ["图层组合", "图层", "图层合成"], "拼图": ["拼图", "九宫格", "长图", "对比图"], "歌词": ["歌词", "词", "lyrics"], "配音": ["配音", "旁白", "解说", "人声"], "BGM": ["BGM", "背景音乐", "配乐"], "字幕": ["字幕", "视频字幕", "字幕条"], "标题": ["标题", "文案标题", "爆款标题"], "正文": ["正文", "文案", "内容正文"], "成品图": ["成品图", "出图", "图", "海报"], "视频成品": ["视频成品", "成片", "视频"], "合成图": ["合成图", "拼合图", "融合图", "合成"], "知识库": ["知识库", "资料库", "knowledge base"]}, "knowledge": {"工序": {"单步": ["教程", "流程", "步骤", "怎么做", "方法", "教学"], "全程": ["完整流程", "全流程", "pipeline", "SOP"]}, "能力": {"标记": ["一键", "自动", "直出", "秒出"], "达成": ["同款", "复刻", "效果"], "载体": ["提示词", "参数", "预设", "公式"]}, "工具": {"发现": ["用什么软件", "用什么工具", "工具推荐", "哪个好用", "有哪些工具"]}}, "tool_type": {"AI 模型": ["AI"], "桌面 APP": ["软件", "电脑端"], "云端 Web": ["在线", "网页版"], "API·CLI": ["代码", "命令行"], "插件扩展": ["插件"]}};
  548. const TOOL_TYPES=["AI 模型", "桌面 APP", "云端 Web", "API·CLI", "插件扩展"];
  549. const GMODEL="gemini-3.1-flash-lite";
  550. const AL=POOLS.action_leaves, TP=POOLS.types, KN=POOLS.knowledge, TQ=POOLS.tool_type;
  551. function aPool(a){return AL[a]||[a];}
  552. function tPool(t){return TP[t]||[t];}
  553. function pick(arr,i){return arr[Math.min(i,arr.length-1)];}
  554. function toolPrefix(lens){ return (st.tools.length && lens!=='工具') ? st.tools.map(t=>TQ[t][0]).join('/')+' ' : ''; }
  555. function genForms(leaf,ty,lens,tq){
  556. tq=tq||''; const aP=aPool(leaf), tP=tPool(ty);
  557. const aNat=aP[0], tNat=tP[0], aSyn=pick(aP,1), tSyn=pick(tP,1), K=KN[lens];
  558. if(lens==='工序'){ const s=K['单步'];
  559. return [['①原词',`${tq}${leaf} ${ty} 流程`],['②句子',`${tq}怎么${aNat}${tNat}`],['③同义',`${tq}${aSyn} ${tSyn} ${pick(s,2)}`]]; }
  560. if(lens==='能力'){ const mk=K['标记'];
  561. return [['①原词',`${tq}${leaf} ${ty} 技巧`],['②句子',`${tq}有没有能${mk[0]}${aNat}${tNat}的功能`],['③同义',`${tq}${pick(mk,1)} ${aSyn}${tSyn}`]]; }
  562. const fd=K['发现'];
  563. return [['①原词',`${leaf} ${ty} 工具`],['②句子',`${aNat}${tNat}用什么软件好`],['③同义',`${aSyn} ${tSyn} ${pick(fd,2)}`]];
  564. }
  565. const FIDX={A:0,B:1,C:2};
  566. function genQ(leaf,ty){return genForms(leaf,ty,st.lens,toolPrefix(st.lens))[FIDX[st.form]][1];}
  567. // 通用维度 5 项(移除 production_relevance / recency:前者搬到过滤指标顶层,后者改硬指标 recency_hard)
  568. const commonLabels={relevance:"相关性",result_quality:"成品质量",credibility:"可信度",novelty_coverage:"新增覆盖",concrete_use_case:"具体用例",completeness:"流程完整",step_structure:"步骤结构",step_reproducibility:"可复现性",capability_definition:"能力定义",implementation_depth:"实现深度",boundary_failure_eval:"边界/踩坑",generality:"通用性",capability_coverage:"工具覆盖",effective_comparison:"有效对比",param_specificity:"参数具体",worked_example:"示例完整",version_limits:"限制说明"};
  569. // 过滤指标(独立组,数据从 it 顶层取、不在 it.scores;量程不同,用 filterMax 在 renderScoreGroup 归一化)
  570. const filterLabels={production_relevance:"制作相关性",recency_hard:"发布时效",overall:"综合均分"};
  571. const filterMax={production_relevance:3,recency_hard:3,overall:5};
  572. const scoreGroups=[
  573. {id:"filter",title:"过滤指标",short:"过滤",hint:"独立于通用维度·过滤逻辑待定",topLevelKeys:["production_relevance","recency_hard","overall"]},
  574. {id:"common",title:"通用维度",short:"通用",keys:["relevance","result_quality","credibility","novelty_coverage","concrete_use_case"]},
  575. {id:"procedure",title:"工序维度",short:"工序",keys:["completeness","step_structure","step_reproducibility"]},
  576. {id:"step",title:"步骤维度",short:"步骤",keys:["capability_definition","implementation_depth","boundary_failure_eval","generality"]},
  577. {id:"tool",title:"工具维度",short:"工具",keys:["capability_coverage","effective_comparison","param_specificity","worked_example","version_limits"]}
  578. ];
  579. const PLATC={xhs:"小红书",gzh:"公众号",zhihu:"知乎",x:"X",bili:"B站",douyin:"抖音",sph:"视频号",youtube:"YouTube",github:"GitHub",toutiao:"头条",weibo:"微博"};
  580. const FN={A:"原词",B:"句子",C:"同义"};
  581. const NOIMG="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='400'%3E%3Crect width='600' height='400' fill='%23eee7dc'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%236f6961' font-size='28'%3ENo image%3C/text%3E%3C/svg%3E";
  582. function curForm(){return st.qi===-1?null:(DATA.queries[st.qi]?DATA.queries[st.qi].forms[st.fi]:null);}
  583. // groupAverage 支持 topLevelKeys(filter 组从 it 顶层取数据,不进 scores)
  584. function groupAverage(it,g){const keys=g.topLevelKeys||g.keys;const src=g.topLevelKeys?it:it.scores;const vs=keys.map(k=>src[k]).filter(Number.isFinite);return vs.length?vs.reduce((a,b)=>a+b,0)/vs.length:null;}
  585. function fmt(v){return v===null?"N/A":v.toFixed(1);}
  586. // filter 组的 pill 不取均分(分轴不同),直接展示 pr·rh·overall 三个数;其他组仍显示均分
  587. function groupSnapshot(it){return scoreGroups.map(g=>{
  588. if(g.id==='filter'){
  589. const pr=it.production_relevance,rh=it.recency_hard,ov=it.overall;
  590. const f=(v,max)=>(v==null||!Number.isFinite(v))?'-':(max===5?(typeof v==='number'?v.toFixed(1):v):v);
  591. return `<div class="group-pill" title="${g.hint||''}"><span>${g.short}</span><strong style="font-size:12px;">${f(pr,3)}·${f(rh,3)}·${f(ov,5)}</strong></div>`;
  592. }
  593. const a=groupAverage(it,g);
  594. return `<div class="group-pill"><span>${g.short}</span><strong>${fmt(a)}</strong></div>`;
  595. }).join("");}
  596. // filter 组:数据从 it 顶层、量程按 filterMax 归一化到 5 量程(meter 的 --v*20% 公式不动)
  597. function renderScoreGroup(it,g){
  598. const isFilter=g.id==='filter';
  599. const keys=g.topLevelKeys||g.keys;
  600. const src=isFilter?it:it.scores;
  601. const labels=isFilter?filterLabels:commonLabels;
  602. const rows=keys.map(k=>{
  603. const v=src[k];
  604. const m=!Number.isFinite(v);
  605. const max=isFilter?(filterMax[k]||5):5;
  606. const meterV=m?0:(v*5/max);
  607. const suffix=isFilter&&max!==5?`/${max}`:'';
  608. const valStr=m?'-':(typeof v==='number'?(Number.isInteger(v)?v:v.toFixed(1)):v);
  609. const reason = (!isFilter && it.score_reasons) ? it.score_reasons[k] : '';
  610. const infoIcon = reason ? `<span class="info-icon" onclick="pinScoreReason(this, '${esc(labels[k]||k)}', '${esc(k)}')" title="点击定格查看评判理由" style="margin-left: 5px; cursor: pointer; color: var(--muted); opacity: 0.7; font-size: 11px; font-weight: normal; user-select: none;">ⓘ</span>` : '';
  611. return `<div class="score-row ${m?'missing':''}"><span>${labels[k]||k}</span><div class="meter" style="--v:${meterV}"><span></span></div><strong style="display: inline-flex; align-items: center;">${valStr}${suffix}${infoIcon}</strong></div>`;
  612. }).join("");
  613. const headRight=isFilter?(g.hint?`<small>${g.hint}</small>`:''):`<small>均分 ${fmt(groupAverage(it,g))}</small>`;
  614. return `<section class="score-group"><div class="score-group-head"><span>${g.title}</span>${headRight}</div><div class="score-group-body">${rows}</div></section>`;
  615. }
  616. function curDims(){
  617. if(st.qi===-1) { if (openCell) { const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti; return { type: TYPES[ti].name, action: ACTIONS[ai].name }; } return {}; }
  618. const q=DATA.queries[st.qi]; return (q&&q.dims)?q.dims:{};
  619. }
  620. function spans(arr,key){const o=[];let cur=null,s=0;arr.forEach((x,i)=>{if(x[key]!==cur){if(cur!==null)o.push([cur,s,i-s]);cur=x[key];s=i;}});o.push([cur,s,arr.length-s]);return o;}
  621. const l1sp=spans(ACTIONS,'l1'),l2sp=spans(ACTIONS,'l2');
  622. const l1Start=new Set(l1sp.map(s=>s[1])),l2Start=new Set(l2sp.map(s=>s[1]));
  623. function detectLens(q) {
  624. const text = (q.original_q || (q.forms && q.forms[0] && q.forms[0].query) || "").toLowerCase();
  625. if (/流程|步骤|教程|方法|教学|SOP|pipeline|工序/.test(text)) return '工序';
  626. if (/一键|自动|直出|秒出|同款|复刻|效果|技巧|能力/.test(text)) return '能力';
  627. if (/用什么|软件|工具|推荐|哪个好用|有哪些/.test(text)) return '工具';
  628. return '工序';
  629. }
  630. function findQuery(aName, tName, lens, tool) {
  631. const matches = DATA.queries.filter(q => {
  632. if (!q.dims || q.dims.action !== aName || q.dims.type !== tName) return false;
  633. const qLens = detectLens(q);
  634. if (qLens !== lens) return false;
  635. const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
  636. if (tool) {
  637. if (!hasToolConstraint || q.dims.constraint.value !== tool) return false;
  638. } else {
  639. if (hasToolConstraint) return false;
  640. }
  641. return true;
  642. });
  643. if (matches.length > 0) {
  644. matches.sort((x, y) => (y.hits || 0) - (x.hits || 0));
  645. return matches[0];
  646. }
  647. return null;
  648. }
  649. function selectQueryByActiveCellAndControls(aName, tName) {
  650. const activeTool = st.tools[0] || null;
  651. let match = findQuery(aName, tName, st.lens, activeTool);
  652. if (match) {
  653. st.qi = DATA.queries.indexOf(match);
  654. const fi = match.forms.findIndex(f => f.form === st.form);
  655. st.fi = fi >= 0 ? fi : 0;
  656. st.selectedAction = aName;
  657. st.selectedType = tName;
  658. } else {
  659. // Fallback: search for any query matching active lens & tool constraint with hits
  660. const anyMatch = DATA.queries.find(q => {
  661. if (!q.dims || !q.dims.action || !q.dims.type) return false;
  662. const qLens = detectLens(q);
  663. if (qLens !== st.lens) return false;
  664. const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
  665. if (activeTool) {
  666. if (!hasToolConstraint || q.dims.constraint.value !== activeTool) return false;
  667. } else {
  668. if (hasToolConstraint) return false;
  669. }
  670. return q.hits > 0;
  671. });
  672. if (anyMatch) {
  673. st.qi = DATA.queries.indexOf(anyMatch);
  674. const fi = anyMatch.forms.findIndex(f => f.form === st.form);
  675. st.fi = fi >= 0 ? fi : 0;
  676. st.selectedAction = anyMatch.dims.action;
  677. st.selectedType = anyMatch.dims.type;
  678. } else {
  679. st.qi = -1;
  680. st.fi = 0;
  681. }
  682. }
  683. }
  684. // 角标 hits = 当前 form 下被采纳(report)的条数,与下面卡片所列的 report 子集口径一致 (≤30)
  685. function formReport(q) {
  686. const f = q.forms && q.forms.find(x => x.form === st.form);
  687. return f ? (f.report || 0) : 0;
  688. }
  689. function getFilteredHitsMap() {
  690. const cm = {};
  691. const activeTool = st.tools[0] || null;
  692. DATA.queries.forEach((q, i) => {
  693. const d = q.dims;
  694. if (!d || !d.type || !d.action) return;
  695. // Filter by lens
  696. if (detectLens(q) !== st.lens) return;
  697. // Filter by active tool constraint
  698. const hasToolConstraint = d.constraint && d.constraint.kind === "工具类型";
  699. if (activeTool) {
  700. if (!hasToolConstraint || d.constraint.value !== activeTool) return;
  701. } else {
  702. if (hasToolConstraint) return;
  703. }
  704. const k = d.type + '|' + d.action;
  705. const h = formReport(q);
  706. if (!cm[k] || h > cm[k].hits) {
  707. cm[k] = { i, hits: h };
  708. }
  709. });
  710. return cm;
  711. }
  712. function renderMatrix(){
  713. const showFull = document.getElementById('showFullMx').checked;
  714. const cm = getFilteredHitsMap();
  715. const activeActions = showFull ? ACTIONS : ACTIONS.filter(a => TYPES.some(t => {
  716. const c = cm[t.name+'|'+a.name];
  717. return c && c.hits > 0;
  718. }));
  719. const activeTypes = showFull ? TYPES : TYPES.filter(t => ACTIONS.some(a => {
  720. const c = cm[t.name+'|'+a.name];
  721. return c && c.hits > 0;
  722. }));
  723. const displayActions = activeActions.length ? activeActions : ACTIONS;
  724. const displayTypes = activeTypes.length ? activeTypes : TYPES;
  725. const l1sp = spans(displayActions, 'l1'), l2sp = spans(displayActions, 'l2');
  726. const l1Start = new Set(l1sp.map(s => s[1])), l2Start = new Set(l2sp.map(s => s[1]));
  727. let h = '<thead><tr class="l1"><th class="corner" rowspan="3">类型 \ 动作</th>' + l1sp.map(([v, s, c]) => `<th colspan="${c}" class="l1div">${v}</th>`).join('') + '</tr>';
  728. h += '<tr class="l2">' + l2sp.map(([v, s, c]) => `<th colspan="${c}" class="${l1Start.has(s) ? 'l1div' : 'l2div'}">${v}</th>`).join('') + '</tr>';
  729. h += '<tr class="leaf">' + displayActions.map((a, i) => `<th data-ai="${ACTIONS.indexOf(a)}" class="${l1Start.has(i) ? 'l1div' : (l2Start.has(i) ? 'l2div' : '')}">${a.name}</th>`).join('') + '</tr></thead><tbody>';
  730. const typeCategories = ['程序控制类型', '数据复用类型', '内容类型', '知识类型'];
  731. typeCategories.forEach(l1 => {
  732. const catTypes = displayTypes.filter(t => t.l1 === l1);
  733. if (catTypes.length === 0) return;
  734. h += `<tr class="l1row"><td colspan="${displayActions.length + 1}">${l1}</td></tr>`;
  735. catTypes.forEach((t) => {
  736. h += `<tr data-type="${t.name}"><th class="rh" data-ti="${TYPES.indexOf(t)}">${t.name}</th>` + displayActions.map((a) => {
  737. const ai = ACTIONS.indexOf(a);
  738. const ti = TYPES.indexOf(t);
  739. const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
  740. const s = cell.tier !== undefined ? cell.tier : cell.s;
  741. const cls = (s === null || s === undefined) ? 'tNA' : ('t' + s);
  742. const isSel = (st.selectedAction === a.name && st.selectedType === t.name) ? ' sel' : '';
  743. return `<td class="cell ${cls}${isSel}" data-ai="${ai}" data-ti="${ti}"></td>`;
  744. }).join('') + '</tr>';
  745. });
  746. });
  747. document.getElementById('comboMx').innerHTML = h + '</tbody>';
  748. refresh();
  749. applyCrosshair();
  750. }
  751. // 点中 cell 时浅高亮整行整列(含 row/col 表头),方便定位
  752. function applyCrosshair() {
  753. document.querySelectorAll('.rowsel,.colsel').forEach(el => el.classList.remove('rowsel','colsel'));
  754. const ai = st.selectedAction ? ACTIONS.findIndex(a => a.name === st.selectedAction) : -1;
  755. const ti = st.selectedType ? TYPES.findIndex(t => t.name === st.selectedType) : -1;
  756. if (ai < 0 && ti < 0) return;
  757. document.querySelectorAll(`#comboMx [data-ai="${ai}"]`).forEach(el => el.classList.add('colsel'));
  758. document.querySelectorAll(`#comboMx [data-ti="${ti}"]`).forEach(el => el.classList.add('rowsel'));
  759. }
  760. function refresh(){
  761. const cm = getFilteredHitsMap();
  762. document.querySelectorAll('tr[data-type]').forEach(tr=>{
  763. const ty=tr.dataset.type;
  764. tr.querySelectorAll('td.cell').forEach(td=>{
  765. const ai=+td.dataset.ai, ti=+td.dataset.ti, a=ACTIONS[ai];
  766. const cell=(DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti]||{}) : {};
  767. const s=cell.tier!==undefined?cell.tier:cell.s;
  768. const c=cm[ty+'|'+a.name];
  769. const hits=c?c.hits:0;
  770. td.textContent = genQ(a.name,ty);
  771. if(hits > 0) {
  772. const badge = document.createElement('span');
  773. badge.className = 'hit-badge';
  774. badge.textContent = hits;
  775. td.appendChild(badge);
  776. }
  777. td.classList.toggle('hide',(s==null?0:s)<(+st.tier));
  778. td.title = `${ty} × ${a.name}\nGemini评分: ${(s===null||s===undefined)?'未判':['无效','低','中','高'][s]}\n当前 form(${st.form}) 命中: ${hits} 篇`;
  779. });
  780. });
  781. }
  782. const pop=document.getElementById('pop'); let openCell=null;
  783. function showPop(td,x,y){
  784. openCell=td;
  785. const ai=+td.dataset.ai,ti=+td.dataset.ti,a=ACTIONS[ai],t=TYPES[ti];
  786. const cell=(DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti]||{}) : {};
  787. const s=cell.tier!==undefined?cell.tier:cell.s;
  788. const tn=(s==null)?'未判':['无效','低','中','高'][s], tb=(s==null)?'NA':s;
  789. const gen=genForms(a.name,t.name,st.lens,toolPrefix(st.lens));
  790. // Find matching query in database for this cell
  791. const activeTool = st.tools[0] || null;
  792. const matches = DATA.queries.filter(q => {
  793. if (!q.dims || q.dims.action !== a.name || q.dims.type !== t.name) return false;
  794. const qLens = detectLens(q);
  795. if (qLens !== st.lens) return false;
  796. const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
  797. if (activeTool) {
  798. if (!hasToolConstraint || q.dims.constraint.value !== activeTool) return false;
  799. } else {
  800. if (hasToolConstraint) return false;
  801. }
  802. return true;
  803. });
  804. let dbQuery = null;
  805. if (matches.length > 0) {
  806. matches.sort((x, y) => (y.hits || 0) - (x.hits || 0));
  807. dbQuery = matches[0];
  808. }
  809. const dbFormMap = {};
  810. if (dbQuery) {
  811. dbQuery.forms.forEach(f => {
  812. dbFormMap[f.form] = f.query;
  813. });
  814. }
  815. const formKeys = ['A', 'B', 'C'];
  816. let html=`<span class="close">×</span>
  817. <div class="pt">
  818. <span class="path a">${a.l1}›${a.l2}›${a.name}</span>
  819. <span class="path t">${t.l1}›${t.name}</span>
  820. <span class="tier tb${tb}">gemini·${tn}</span>
  821. </div>
  822. <div class="reason${s===0?' inv':''}">${esc(cell.r||'(模型未给该格评分)')}</div>
  823. <div class="lbl">系统生成 · 知识类型=${st.lens}${st.tools.length?' · 工具类型='+st.tools.join('/'):''}</div>
  824. <ul>` + gen.map(([fName, genQStr], idx) => {
  825. const formKey = formKeys[idx];
  826. const actualQ = dbFormMap[formKey];
  827. const isCurrent = formKey === st.form;
  828. let content = `<span class="fm">${fName}</span>${esc(genQStr)}`;
  829. if (actualQ) {
  830. if (actualQ !== genQStr) {
  831. content += `<div style="font-size: 13px; margin-top: 6px; color: #047857; font-weight: 600; font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; line-height: 1.45; background: #ecfdf5; border: 1px dashed #34d399; padding: 6px 10px; border-radius: 6px; width: 100%; box-sizing: border-box; display: flex; align-items: center; gap: 4px; box-shadow: inset 0 1px 2px rgba(4, 120, 87, 0.04);">
  832. <span style="background: #10b981; padding: 2px 6px; border-radius: 4px; font-size: 11px; color: #fff; font-weight: bold; font-family: -apple-system, sans-serif; white-space: nowrap;">实际搜索</span>
  833. <span style="word-break: break-all; font-family: inherit;">${esc(actualQ)}</span>
  834. </div>`;
  835. } else {
  836. content += `<div style="font-size: 13px; margin-top: 6px; color: #374151; font-weight: 600; font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; line-height: 1.45; background: #f9fafb; border: 1px dashed #d1d5db; padding: 6px 10px; border-radius: 6px; width: 100%; box-sizing: border-box; display: flex; align-items: center; gap: 4px; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);">
  837. <span style="background: #9ca3af; padding: 2px 6px; border-radius: 4px; font-size: 11px; color: #fff; font-weight: bold; font-family: -apple-system, sans-serif; white-space: nowrap;">实际搜索</span>
  838. <span style="font-style: italic; font-family: inherit; color: #6b7280;">(同上)</span>
  839. </div>`;
  840. }
  841. }
  842. return `<li class="gen${isCurrent ? ' cur' : ''}" style="display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: 6px 8px;">${content}</li>`;
  843. }).join('') + '</ul>';
  844. pop.innerHTML=html;
  845. pop.style.display='block';
  846. pop.style.left=Math.max(8,Math.min(x,window.innerWidth-466))+'px';
  847. pop.style.top=Math.max(8,Math.min(y,window.innerHeight-pop.offsetHeight-12))+'px';
  848. pop.querySelector('.close').onclick=()=>{
  849. pop.style.display='none';
  850. openCell=null;
  851. };
  852. }
  853. function computeStats() {
  854. const stat = {"0": 0, "1": 0, "2": 0, "3": 0, "na": 0};
  855. if (DATA.matrix) {
  856. DATA.matrix.forEach(row => row.forEach(cell => {
  857. const s = cell ? (cell.tier !== undefined ? cell.tier : cell.s) : null;
  858. if (s === null || s === undefined) stat["na"]++;
  859. else if (stat[s] !== undefined) stat[s]++;
  860. }));
  861. }
  862. document.getElementById('s3').textContent = stat['3'];
  863. document.getElementById('s2').textContent = stat['2'];
  864. document.getElementById('s1').textContent = stat['1'];
  865. document.getElementById('s0').textContent = stat['0'];
  866. document.getElementById('sna').textContent = stat['na'];
  867. }
  868. function renderFormsChan(){
  869. const q=st.qi===-1?null:DATA.queries[st.qi];
  870. if(!q) {
  871. document.getElementById("navC").innerHTML='';
  872. return;
  873. }
  874. const f=curForm();
  875. if(!f) {
  876. document.getElementById("navC").innerHTML='';
  877. return;
  878. }
  879. const chans=["all",...f.platforms];
  880. document.getElementById("navC").innerHTML=chans.map(c=>{
  881. const n=c==="all"?f.results.length:f.results.filter(r=>r.platformKey===c).length;
  882. return `<span class="tab ${c===st.channel?'on':''}" data-c="${c}">${c==="all"?"全部":(PLATC[c]||c)} <small>${n}</small></span>`;
  883. }).join("");
  884. }
  885. function renderNav(){ renderMatrix(); renderFormsChan(); }
  886. function renderHead(){
  887. const f=curForm();
  888. if(!f) return;
  889. document.getElementById("lede").textContent="";
  890. let it=f.results;
  891. if(st.channel!=="all") it=it.filter(r=>r.platformKey===st.channel);
  892. const valid=it.filter(r=>!r.anomaly);
  893. const rep=valid.filter(r=>r.decision==="report").length;
  894. const dis=valid.filter(r=>r.decision==="discard").length;
  895. const anom=it.filter(r=>r.anomaly).length;
  896. const avg=(valid.reduce((s,r)=>s+r.overall,0)/(valid.length||1)).toFixed(1);
  897. const lab=st.channel==="all"?"该形式结果数":(PLATC[st.channel]||st.channel)+" 结果数";
  898. document.getElementById("stats").innerHTML=[[it.length,lab],[rep,"建议上报"],[avg,"平均综合分 / 5"],[dis,"丢弃"],[anom,"异常"]].map(([n,l])=>`<div class="stat"><strong>${n}</strong><span>${l}</span></div>`).join("");
  899. }
  900. // POST /api/reeval —— 后台只对当前 query 的所有 form 文件复评(不重新搜索);
  901. // server.py 立即返回 {status:'started', pid, log},前端只显示状态、不轮询,刷新页面看新数据。
  902. function reevalCurrentQuery(){
  903. if (st.qi === -1 || !DATA.queries[st.qi]) { alert('请先选一个 query 再重评'); return; }
  904. const q = DATA.queries[st.qi].key;
  905. if (!confirm(`重评 ${q} 的所有帖子(A/B/C 三种 form)?\n约 1-3 分钟(视帖子数),过程中页面可继续浏览。\n完成后刷新页面看新数据。`)) return;
  906. const btn = document.getElementById('reevalBtn');
  907. const oldText = btn.textContent;
  908. btn.disabled = true; btn.textContent = '♻️ 提交中…';
  909. fetch('/api/reeval', {
  910. method: 'POST', headers: {'Content-Type': 'application/json'},
  911. body: JSON.stringify({q}),
  912. }).then(r => r.json().then(d => ({ok: r.ok, d}))).then(({ok, d}) => {
  913. if (ok && d.status === 'started') {
  914. btn.textContent = `♻️ 重评中 ${q} (PID ${d.pid}) · 完成后刷新页面`;
  915. // 不自动恢复 —— 让按钮停留在"重评中"状态直到用户主动刷新;日志见 runs/${q}/_reeval.log
  916. } else {
  917. btn.disabled = false; btn.textContent = oldText;
  918. alert('启动失败:' + (d.error || JSON.stringify(d)));
  919. }
  920. }).catch(e => {
  921. btn.disabled = false; btn.textContent = oldText;
  922. alert('请求失败:' + e);
  923. });
  924. }
  925. function sortedItems(){
  926. const f=curForm();
  927. if(!f) return [];
  928. let it=f.results.slice();
  929. if(st.channel!=="all") it=it.filter(r=>r.platformKey===st.channel);
  930. const s=document.getElementById("sort").value;
  931. const decWeight = r => (r.anomaly ? 2 : (r.decision === "report" ? 0 : 1));
  932. it.sort((a,b)=>{
  933. const wA = decWeight(a), wB = decWeight(b);
  934. if (wA !== wB) return wA - wB;
  935. if(s==="score") return b.overall - a.overall;
  936. if(s==="date") return (b.date||"").localeCompare(b.date||"");
  937. if(s==="platform") return (a.platform||"").localeCompare(b.platform||"","zh-Hans") || b.overall - a.overall;
  938. return 0;
  939. });
  940. return it;
  941. }
  942. function renderGrid(){
  943. VIEW=sortedItems();
  944. document.getElementById("grid").innerHTML=VIEW.map((it,idx)=>{
  945. const imgs=it.images.length?it.images.slice(0,3).map(s=>`<img src="${esc(s)}" referrerpolicy="no-referrer" loading="lazy" onerror="this.src='${NOIMG}'">`).join(""):`<img src="${NOIMG}">`;
  946. const bars=["relevance","result_quality","credibility","novelty_coverage","concrete_use_case"].map(k=>`<span title="${commonLabels[k]} ${it.scores[k]||0}" style="--v:${it.scores[k]||0}"></span>`).join("");
  947. return `<article class="result">
  948. <div class="thumbs">${imgs}</div>
  949. <div class="body">
  950. <div class="meta"><span class="platform p-${esc(it.platformKey)}">${esc(it.platform)}</span><span>${esc(it.date)} · ${esc(it.engagement)}</span></div>
  951. <h2>${esc(it.title)}</h2>
  952. <div class="excerpt">${esc(it.text)}</div>
  953. <div class="tags">${it.tools.slice(0,4).map(t=>`<span class="tag">${esc(t)}</span>`).join("")}</div>
  954. <div class="scorebar">
  955. <div class="overall">
  956. <div>
  957. <div class="score">${it.anomaly?'—':it.overall.toFixed(1)}</div>
  958. <small>综合分</small>
  959. </div>
  960. <div class="decision ${it.anomaly?'':it.decision}">${it.anomaly?'异常':esc(it.decision)}</div>
  961. </div>
  962. <div class="mini-bars">${bars}</div>
  963. <div class="group-snapshot">${groupSnapshot(it)}</div>
  964. <div class="actions">
  965. <button onclick="openDetail(${idx})">查看详情</button>
  966. <a href="${esc(it.url)}" target="_blank" rel="noreferrer">原链接</a>
  967. </div>
  968. </div>
  969. </div>
  970. </article>`;
  971. }).join("")||(st.qi===-1?'<p style="color:var(--muted);padding: 20px 0;text-align:center;grid-column: 1 / -1;">该组合暂无数据库扫描结果。</p>':'<p style="color:var(--muted);grid-column: 1 / -1;">该渠道无结果</p>');
  972. }
  973. function openDetail(i){
  974. const it=VIEW[i];
  975. detailDialog.dataset.activeIdx = i;
  976. currentPinnedScoreEl = null;
  977. document.getElementById("modalMeta").innerHTML=`<span class="platform p-${esc(it.platformKey)}">${esc(it.platform)}</span><span>${esc(it.date)} · ${esc(it.engagement)} · 质量 ${esc(it.grade)} ${esc(it.qscore)}</span>`;
  978. document.getElementById("modalTitle").textContent=it.title;
  979. document.getElementById("modalReason").textContent=it.reason;
  980. document.getElementById("modalText").textContent=it.text||"(无正文)";
  981. document.getElementById("modalImages").innerHTML=it.images.length?it.images.map(s=>`<img src="${esc(s)}" referrerpolicy="no-referrer" loading="lazy" onerror="this.style.opacity=.3">`).join(""):"<p>搜索详情未返回图片。</p>";
  982. document.getElementById("modalTags").innerHTML=[...it.knowledge_type.map(t=>"类型:"+(KTM[t]||t)),...it.found_by.map(q=>"命中:"+q)].map(t=>`<span class="tag">${esc(t)}</span>`).join("");
  983. document.getElementById("modalScores").innerHTML=scoreGroups.map(g=>renderScoreGroup(it,g)).join("");
  984. detailDialog.showModal();
  985. }
  986. let currentPinnedScoreEl = null;
  987. function pinScoreReason(el, label, k) {
  988. const modalReason = document.getElementById("modalReason");
  989. const activeIdx = detailDialog.dataset.activeIdx;
  990. const it = VIEW[activeIdx];
  991. if (!it || !it.score_reasons) return;
  992. const reason = it.score_reasons[k] || '';
  993. if (currentPinnedScoreEl === el) {
  994. modalReason.textContent = it.reason;
  995. modalReason.style.borderLeftColor = 'var(--cyan)';
  996. modalReason.style.background = 'var(--soft-cyan)';
  997. modalReason.style.color = '#254c5d';
  998. el.style.color = '';
  999. currentPinnedScoreEl = null;
  1000. } else {
  1001. if (currentPinnedScoreEl) {
  1002. currentPinnedScoreEl.style.color = '';
  1003. }
  1004. modalReason.textContent = `【指标判定 - ${label}】\n${reason}`;
  1005. modalReason.style.borderLeftColor = 'var(--amber)';
  1006. modalReason.style.background = 'var(--soft-amber)';
  1007. modalReason.style.color = '#78350f';
  1008. el.style.color = 'var(--amber)';
  1009. currentPinnedScoreEl = el;
  1010. }
  1011. }
  1012. const KTM={procedure:"工序",step:"步骤",tool:"工具"};
  1013. function rerender(mxClick){
  1014. if (st.qi === -1) {
  1015. document.getElementById("stats").innerHTML = "";
  1016. document.getElementById("lede").textContent = "未找到对应的数据库 Query,请尝试切换筛选组合。";
  1017. renderFormsChan();
  1018. renderGrid();
  1019. if (!mxClick) {
  1020. document.querySelectorAll('td.cell.sel').forEach(x => x.classList.remove('sel'));
  1021. }
  1022. return;
  1023. }
  1024. if(st.qi>=DATA.queries.length) st.qi=0;
  1025. if(st.fi>=DATA.queries[st.qi].forms.length) st.fi=0;
  1026. if (!mxClick) {
  1027. renderMatrix();
  1028. }
  1029. renderFormsChan();
  1030. renderHead();
  1031. renderGrid();
  1032. }
  1033. function loadData(keep){
  1034. fetch("/api/data").then(r=>r.json()).then(d=>{
  1035. DATA=d;
  1036. if(!keep){
  1037. st={form:'A',lens:'工序',tools:[],tier:0,qi:0,fi:0,channel:"all",selectedAction:null,selectedType:null};
  1038. if(DATA.queries.length > 0) {
  1039. const firstQ = DATA.queries[0];
  1040. if(firstQ.dims) {
  1041. st.selectedAction = firstQ.dims.action;
  1042. st.selectedType = firstQ.dims.type;
  1043. st.lens = detectLens(firstQ);
  1044. }
  1045. }
  1046. }
  1047. // Dynamically populate tool type buttons
  1048. document.getElementById('toolBtns').innerHTML=TOOL_TYPES.map(t=>`<button class="btn" data-k="tool" data-v="${t}">${t}</button>`).join('');
  1049. // Synchronize buttons states to st
  1050. document.querySelectorAll('.ctl .btn').forEach(btn => {
  1051. const k = btn.dataset.k, v = btn.dataset.v;
  1052. if (MULTI[k]) {
  1053. const ak = MULTI[k];
  1054. btn.classList.toggle('on', v === '' ? st[ak].length === 0 : st[ak].includes(v));
  1055. } else {
  1056. btn.classList.toggle('on', st[k] === v);
  1057. }
  1058. });
  1059. computeStats();
  1060. if(st.selectedAction && st.selectedType) {
  1061. selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
  1062. }
  1063. rerender();
  1064. });
  1065. }
  1066. // Matrix click listener (links matrix select to database select + opens pop-up)
  1067. document.getElementById("comboMx").addEventListener("click", e=>{
  1068. const td = e.target.closest("td.cell");
  1069. if(!td) return;
  1070. const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai], t = TYPES[ti];
  1071. st.selectedAction = a.name;
  1072. st.selectedType = t.name;
  1073. showPop(td, e.clientX, e.clientY);
  1074. // Select the query
  1075. selectQueryByActiveCellAndControls(a.name, t.name);
  1076. document.querySelectorAll('td.cell.sel').forEach(x => x.classList.remove('sel'));
  1077. td.classList.add('sel');
  1078. applyCrosshair();
  1079. rerender(true); // mxClick = true, updates results without redrawing full table
  1080. });
  1081. // Header controls event delegation
  1082. const MULTI={tool:'tools'};
  1083. document.querySelector('.ctl').addEventListener('click', e => {
  1084. const b = e.target.closest('.btn');
  1085. if (!b) return;
  1086. const k = b.dataset.k, v = b.dataset.v, grp = document.querySelectorAll(`.ctl .btn[data-k="${k}"]`);
  1087. if (MULTI[k]) {
  1088. const ak = MULTI[k];
  1089. if (v === '') {
  1090. st[ak] = [];
  1091. } else {
  1092. if (st[ak].includes(v)) {
  1093. st[ak] = [];
  1094. } else {
  1095. st[ak] = [v];
  1096. }
  1097. }
  1098. grp.forEach(x => {
  1099. const xv = x.dataset.v;
  1100. x.classList.toggle('on', xv === '' ? st[ak].length === 0 : st[ak].includes(xv));
  1101. });
  1102. } else {
  1103. grp.forEach(x => x.classList.remove('on'));
  1104. b.classList.add('on');
  1105. st[k] = v;
  1106. }
  1107. if(st.selectedAction && st.selectedType) {
  1108. selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
  1109. }
  1110. renderMatrix();
  1111. if (openCell) {
  1112. const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti;
  1113. const newCell = document.querySelector(`td.cell[data-ai="${ai}"][data-ti="${ti}"]`);
  1114. if (newCell) {
  1115. showPop(newCell, parseInt(pop.style.left), parseInt(pop.style.top));
  1116. }
  1117. }
  1118. rerender(true);
  1119. });
  1120. // Close popups on clicking outside
  1121. document.addEventListener('click', e => {
  1122. if (!e.target.closest('td.cell') && !e.target.closest('.pop') && !e.target.closest('.btn')) {
  1123. pop.style.display = 'none';
  1124. openCell = null;
  1125. }
  1126. });
  1127. document.getElementById("navC").addEventListener("click",e=>{const t=e.target.closest(".tab");if(!t)return;st.channel=t.dataset.c;renderNav();renderHead();renderGrid();});
  1128. document.getElementById("sort").addEventListener("change",renderGrid);
  1129. loadData();
  1130. </script>
  1131. </body>
  1132. </html>