query-builder.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  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.0">
  6. <title>Query 词组织器</title>
  7. <style>
  8. *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  9. body {
  10. font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Noto Sans SC',
  11. 'Helvetica Neue', sans-serif;
  12. background: #f2f2f2;
  13. min-height: 100vh;
  14. padding: 32px 24px;
  15. color: #1a1a1a;
  16. }
  17. .container {
  18. max-width: 980px;
  19. margin: 0 auto;
  20. background: #fff;
  21. border-radius: 18px;
  22. padding: 36px 40px 40px;
  23. box-shadow: 0 2px 16px rgba(0,0,0,0.07);
  24. }
  25. h1 {
  26. font-size: 17px;
  27. font-weight: 600;
  28. color: #888;
  29. margin-bottom: 28px;
  30. letter-spacing: 0.02em;
  31. }
  32. .dimensions { display: flex; flex-direction: column; }
  33. .dim-group {
  34. padding: 4px 0;
  35. border-bottom: 1px solid #f2f2f2;
  36. }
  37. .dim-group:last-child { border-bottom: none; }
  38. .level-row {
  39. display: flex;
  40. align-items: flex-start;
  41. padding: 7px 0;
  42. gap: 10px;
  43. }
  44. .level-row.indent { margin-left: 78px; }
  45. .dim-label {
  46. font-size: 12.5px;
  47. color: #999;
  48. min-width: 68px;
  49. padding-top: 6px;
  50. flex-shrink: 0;
  51. display: flex;
  52. align-items: center;
  53. gap: 3px;
  54. }
  55. .dim-label::before { content: '·'; color: #bbb; }
  56. .sub-label {
  57. font-size: 11.5px;
  58. color: #bbb;
  59. min-width: 56px;
  60. padding-top: 6px;
  61. flex-shrink: 0;
  62. }
  63. .chips-wrap {
  64. display: flex;
  65. flex-wrap: wrap;
  66. gap: 7px;
  67. align-items: center;
  68. flex: 1;
  69. }
  70. .chip {
  71. padding: 5px 15px;
  72. border-radius: 20px;
  73. font-size: 13px;
  74. cursor: pointer;
  75. border: 1.5px solid #e2e2e2;
  76. background: #fff;
  77. color: #666;
  78. transition: border-color .12s, background .12s, color .12s;
  79. user-select: none;
  80. white-space: nowrap;
  81. line-height: 1.4;
  82. }
  83. .chip:hover { border-color: #c0c0c0; color: #333; }
  84. .chip.none-active {
  85. background: #1a1a1a;
  86. border-color: #1a1a1a;
  87. color: #fff;
  88. font-weight: 500;
  89. }
  90. .chip.none-active:hover { background: #333; border-color: #333; }
  91. .chip.selected {
  92. background: #1a1a1a;
  93. border-color: #1a1a1a;
  94. color: #fff;
  95. font-weight: 500;
  96. }
  97. .chip.selected:hover { background: #333; border-color: #333; }
  98. .placeholder-text {
  99. font-size: 13px;
  100. color: #ccc;
  101. font-style: italic;
  102. padding: 6px 0;
  103. }
  104. .preview-section {
  105. margin-top: 28px;
  106. padding: 18px 20px;
  107. background: #fafafa;
  108. border-radius: 12px;
  109. border: 1.5px solid #ebebeb;
  110. }
  111. .preview-header {
  112. font-size: 11px;
  113. color: #aaa;
  114. font-weight: 600;
  115. text-transform: uppercase;
  116. letter-spacing: 0.08em;
  117. margin-bottom: 12px;
  118. }
  119. .preview-tags {
  120. display: flex;
  121. flex-wrap: wrap;
  122. gap: 6px;
  123. min-height: 34px;
  124. align-items: center;
  125. margin-bottom: 14px;
  126. }
  127. .preview-tag {
  128. background: #1a1a1a;
  129. color: #fff;
  130. padding: 4px 11px;
  131. border-radius: 7px;
  132. font-size: 13px;
  133. }
  134. .preview-sep { color: #d0d0d0; font-size: 14px; font-weight: 300; }
  135. .preview-empty { color: #ccc; font-size: 13px; }
  136. .preview-path {
  137. font-size: 12px;
  138. color: #bbb;
  139. margin-bottom: 14px;
  140. min-height: 18px;
  141. font-family: monospace;
  142. letter-spacing: 0.01em;
  143. }
  144. .btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
  145. .btn {
  146. padding: 6px 18px;
  147. border-radius: 9px;
  148. font-size: 13px;
  149. cursor: pointer;
  150. border: 1.5px solid #e2e2e2;
  151. background: #fff;
  152. color: #555;
  153. transition: all .12s;
  154. }
  155. .btn:hover { background: #f5f5f5; }
  156. .btn-dark { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
  157. .btn-dark:hover { background: #333; border-color: #333; }
  158. .history-section { margin-top: 20px; }
  159. .history-list { margin-top: 8px; }
  160. .history-item {
  161. font-size: 12.5px;
  162. color: #888;
  163. padding: 5px 0;
  164. border-bottom: 1px solid #f5f5f5;
  165. cursor: pointer;
  166. display: flex;
  167. align-items: center;
  168. gap: 8px;
  169. transition: color .1s;
  170. }
  171. .history-item:hover { color: #333; }
  172. .hist-num { font-size: 10px; color: #ccc; min-width: 20px; }
  173. </style>
  174. </head>
  175. <body>
  176. <div class="container">
  177. <h1>Query 词组织器</h1>
  178. <div class="dimensions" id="dimensions"></div>
  179. <div class="preview-section">
  180. <div class="preview-header">Query 预览</div>
  181. <div class="preview-tags" id="previewTags">
  182. <span class="preview-empty">(请选择维度标签)</span>
  183. </div>
  184. <div class="preview-path" id="previewPath"></div>
  185. <div class="btn-row">
  186. <button class="btn btn-dark" id="copyBtn" onclick="copyQuery()">复制 Query</button>
  187. <button class="btn" onclick="clearAll()">清空</button>
  188. <button class="btn" onclick="saveQuery()">保存记录</button>
  189. </div>
  190. </div>
  191. <div class="history-section" id="histSection" style="display:none">
  192. <div class="preview-header" style="margin-top:0">历史记录</div>
  193. <div class="history-list" id="histList"></div>
  194. </div>
  195. </div>
  196. <script>
  197. const DIMS = [
  198. {
  199. "id": "substance",
  200. "label": "实质",
  201. "type": "flat",
  202. "items": [
  203. "人像",
  204. "信息"
  205. ]
  206. },
  207. {
  208. "id": "form",
  209. "label": "形式",
  210. "type": "flat",
  211. "items": [
  212. "真实感",
  213. "写实风格",
  214. "实景拍摄",
  215. "版面设计",
  216. "版面解构"
  217. ]
  218. },
  219. {
  220. "id": "modality",
  221. "label": "模态",
  222. "type": "flat",
  223. "items": [
  224. "图片",
  225. "视频"
  226. ]
  227. },
  228. {
  229. "id": "action",
  230. "label": "动作",
  231. "type": "hierarchical",
  232. "data": {
  233. "获取": {
  234. "搜索": [
  235. "检索",
  236. "下载"
  237. ],
  238. "查询": [
  239. "调取"
  240. ],
  241. "录入": [
  242. "上传",
  243. "拍摄",
  244. "录音",
  245. "键入"
  246. ],
  247. "引用": [
  248. "选取"
  249. ]
  250. },
  251. "提取": {
  252. "物理提取": [
  253. "裁切",
  254. "抠取",
  255. "抽帧"
  256. ],
  257. "化学提取": [
  258. "识别",
  259. "反推",
  260. "解构"
  261. ]
  262. },
  263. "生成": {
  264. "元素生成": [
  265. "元素生成"
  266. ],
  267. "关系生成": [
  268. "数组生成",
  269. "结构生成"
  270. ]
  271. },
  272. "修改": {
  273. "增": [
  274. "添加",
  275. "叠加"
  276. ],
  277. "删": [
  278. "抹除",
  279. "剪除"
  280. ],
  281. "变": [
  282. "重述",
  283. "风格化",
  284. "转换",
  285. "替换",
  286. "调整",
  287. "增强"
  288. ]
  289. }
  290. }
  291. },
  292. {
  293. "id": "type",
  294. "label": "类型",
  295. "type": "hierarchical",
  296. "data": {
  297. "程序控制类型": {
  298. "指令": [
  299. "提示词",
  300. "负向提示词",
  301. "描述"
  302. ],
  303. "参数": [
  304. "生成参数",
  305. "规格参数",
  306. "模型权重"
  307. ],
  308. "评估": [
  309. "评分",
  310. "评语"
  311. ],
  312. "流程": [
  313. "工作流",
  314. "批处理"
  315. ]
  316. },
  317. "数据复用类型": {
  318. "原子": [
  319. "数字人",
  320. "版式"
  321. ],
  322. "序列": [
  323. "模板"
  324. ]
  325. },
  326. "内容类型": {
  327. "素材/化学变化": [
  328. "参考图",
  329. "参考视频",
  330. "参考音频",
  331. "对标内容",
  332. "分镜图",
  333. "转场",
  334. "蒙版",
  335. "控制图",
  336. "运动轨迹",
  337. "滤镜",
  338. "构图布局"
  339. ],
  340. "素材/物理变化": [
  341. "截图",
  342. "视频片段",
  343. "转场片段",
  344. "关键帧",
  345. "音效",
  346. "特效"
  347. ],
  348. "半成品/序列": [
  349. "大纲",
  350. "脚本",
  351. "分镜脚本",
  352. "剪辑脚本",
  353. "配音文案"
  354. ],
  355. "半成品/原子": [
  356. "底图",
  357. "样图",
  358. "分镜视频"
  359. ],
  360. "半成品/组合": [
  361. "图层组合",
  362. "拼图"
  363. ],
  364. "准成品": [
  365. "歌词",
  366. "配音",
  367. "BGM",
  368. "字幕",
  369. "标题",
  370. "正文"
  371. ],
  372. "成品": [
  373. "成品图",
  374. "视频成品",
  375. "合成图"
  376. ]
  377. },
  378. "知识类型": {
  379. "知识库": [
  380. "知识库"
  381. ]
  382. }
  383. }
  384. },
  385. {
  386. "id": "tool_type",
  387. "label": "工具类型",
  388. "type": "flat",
  389. "items": [
  390. "AI 模型",
  391. "桌面 APP",
  392. "云端 Web",
  393. "API·CLI",
  394. "插件扩展"
  395. ]
  396. },
  397. {
  398. "id": "suffix",
  399. "label": "后缀",
  400. "type": "flat",
  401. "default": null,
  402. "items": [
  403. "怎么做"
  404. ]
  405. }
  406. ];
  407. const state = {};
  408. DIMS.forEach(d => { state[d.id] = { l0: d.default !== undefined ? d.default : null, l1: null, l2: null }; });
  409. function makeRow(indent) {
  410. const el = document.createElement('div');
  411. el.className = 'level-row' + (indent ? ' indent' : '');
  412. return el;
  413. }
  414. function makeLabel(text, small) {
  415. const el = document.createElement('span');
  416. el.className = small ? 'sub-label' : 'dim-label';
  417. el.textContent = text;
  418. return el;
  419. }
  420. function makeChipsWrap() {
  421. const el = document.createElement('div');
  422. el.className = 'chips-wrap';
  423. return el;
  424. }
  425. function makeChip(label, cls, onClick) {
  426. const btn = document.createElement('button');
  427. btn.className = 'chip ' + cls;
  428. btn.textContent = label;
  429. btn.addEventListener('click', onClick);
  430. return btn;
  431. }
  432. function renderDim(dimId) {
  433. const dim = DIMS.find(d => d.id === dimId);
  434. const grp = document.querySelector('.dim-group[data-id="' + dimId + '"]');
  435. grp.innerHTML = '';
  436. const sel = state[dimId];
  437. if (dim.type === 'empty') {
  438. const row = makeRow(false);
  439. row.appendChild(makeLabel(dim.label, false));
  440. const ph = document.createElement('span');
  441. ph.className = 'placeholder-text';
  442. ph.textContent = '(暂无,留位)';
  443. row.appendChild(ph);
  444. grp.appendChild(row);
  445. return;
  446. }
  447. if (dim.type === 'flat') {
  448. const row = makeRow(false);
  449. row.appendChild(makeLabel(dim.label, false));
  450. if (!dim.items || !dim.items.length) {
  451. const ph = document.createElement('span');
  452. ph.className = 'placeholder-text';
  453. ph.textContent = '(待填写)';
  454. row.appendChild(ph);
  455. } else {
  456. const chips = makeChipsWrap();
  457. chips.appendChild(makeChip('无', sel.l0 == null ? 'none-active' : '', () => {
  458. state[dimId].l0 = null; renderDim(dimId); updateQuery();
  459. }));
  460. dim.items.forEach(item => {
  461. chips.appendChild(makeChip(item, sel.l0 === item ? 'selected' : '', () => {
  462. state[dimId].l0 = item; renderDim(dimId); updateQuery();
  463. }));
  464. });
  465. row.appendChild(chips);
  466. }
  467. grp.appendChild(row);
  468. return;
  469. }
  470. if (dim.type === 'hierarchical') {
  471. const row0 = makeRow(false);
  472. row0.appendChild(makeLabel(dim.label, false));
  473. const chips0 = makeChipsWrap();
  474. chips0.appendChild(makeChip('无', sel.l0 == null ? 'none-active' : '', () => {
  475. state[dimId] = { l0: null, l1: null, l2: null }; renderDim(dimId); updateQuery();
  476. }));
  477. Object.keys(dim.data).forEach(key => {
  478. chips0.appendChild(makeChip(key, sel.l0 === key ? 'selected' : '', () => {
  479. state[dimId] = { l0: key, l1: null, l2: null }; renderDim(dimId); updateQuery();
  480. }));
  481. });
  482. row0.appendChild(chips0);
  483. grp.appendChild(row0);
  484. if (sel.l0 && dim.data[sel.l0]) {
  485. const L1keys = Object.keys(dim.data[sel.l0]);
  486. const row1 = makeRow(true);
  487. row1.appendChild(makeLabel(sel.l0, true));
  488. const chips1 = makeChipsWrap();
  489. chips1.appendChild(makeChip('无', sel.l1 == null ? 'none-active' : '', () => {
  490. state[dimId].l1 = null; state[dimId].l2 = null; renderDim(dimId); updateQuery();
  491. }));
  492. L1keys.forEach(key => {
  493. chips1.appendChild(makeChip(key, sel.l1 === key ? 'selected' : '', () => {
  494. state[dimId].l1 = key; state[dimId].l2 = null; renderDim(dimId); updateQuery();
  495. }));
  496. });
  497. row1.appendChild(chips1);
  498. grp.appendChild(row1);
  499. }
  500. if (sel.l0 && sel.l1 && dim.data[sel.l0] && dim.data[sel.l0][sel.l1]) {
  501. const L2items = dim.data[sel.l0][sel.l1];
  502. const row2 = makeRow(true);
  503. row2.appendChild(makeLabel(sel.l1, true));
  504. const chips2 = makeChipsWrap();
  505. chips2.appendChild(makeChip('无', sel.l2 == null ? 'none-active' : '', () => {
  506. state[dimId].l2 = null; renderDim(dimId); updateQuery();
  507. }));
  508. L2items.forEach(item => {
  509. chips2.appendChild(makeChip(item, sel.l2 === item ? 'selected' : '', () => {
  510. state[dimId].l2 = item; renderDim(dimId); updateQuery();
  511. }));
  512. });
  513. row2.appendChild(chips2);
  514. grp.appendChild(row2);
  515. }
  516. }
  517. }
  518. function getSelections() {
  519. return DIMS.map(dim => {
  520. if (dim.type === 'empty') return null;
  521. const sel = state[dim.id];
  522. if (dim.type === 'flat') {
  523. return sel.l0 ? { dim: dim.label, path: [sel.l0], val: sel.l0 } : null;
  524. }
  525. if (dim.type === 'hierarchical') {
  526. const path = [sel.l0, sel.l1, sel.l2].filter(Boolean);
  527. if (!path.length) return null;
  528. return { dim: dim.label, path, val: path[path.length - 1] };
  529. }
  530. return null;
  531. }).filter(Boolean);
  532. }
  533. function updateQuery() {
  534. const sels = getSelections();
  535. const tagsEl = document.getElementById('previewTags');
  536. const pathEl = document.getElementById('previewPath');
  537. tagsEl.innerHTML = '';
  538. pathEl.textContent = '';
  539. if (!sels.length) {
  540. tagsEl.innerHTML = '<span class="preview-empty">(请选择维度标签)</span>';
  541. return;
  542. }
  543. sels.forEach((s, i) => {
  544. if (i > 0) {
  545. const sep = document.createElement('span');
  546. sep.className = 'preview-sep';
  547. sep.textContent = '·';
  548. tagsEl.appendChild(sep);
  549. }
  550. const tag = document.createElement('span');
  551. tag.className = 'preview-tag';
  552. tag.textContent = s.val;
  553. tagsEl.appendChild(tag);
  554. });
  555. pathEl.textContent = sels.map(s => '[' + s.dim + '] ' + s.path.join(' › ')).join(' ');
  556. }
  557. function getQueryText() {
  558. return getSelections().map(s => s.val).join(' ');
  559. }
  560. function copyQuery() {
  561. const text = getQueryText();
  562. if (!text) return;
  563. navigator.clipboard.writeText(text).then(() => {
  564. const btn = document.getElementById('copyBtn');
  565. btn.textContent = '已复制 ✓';
  566. setTimeout(() => { btn.textContent = '复制 Query'; }, 1600);
  567. });
  568. }
  569. function clearAll() {
  570. DIMS.forEach(d => { state[d.id] = { l0: null, l1: null, l2: null }; });
  571. DIMS.forEach(d => renderDim(d.id));
  572. updateQuery();
  573. }
  574. let histCount = 0;
  575. function saveQuery() {
  576. const text = getQueryText();
  577. const path = getSelections().map(s => '[' + s.dim + '] ' + s.path.join('›')).join(' ');
  578. if (!text) return;
  579. histCount++;
  580. const section = document.getElementById('histSection');
  581. const list = document.getElementById('histList');
  582. section.style.display = 'block';
  583. const item = document.createElement('div');
  584. item.className = 'history-item';
  585. item.innerHTML =
  586. '<span class="hist-num">#' + histCount + '</span>' +
  587. '<span>' + text + '</span>' +
  588. '<span style="color:#ddd;font-size:11px;margin-left:auto">' + path + '</span>';
  589. item.addEventListener('click', () => navigator.clipboard.writeText(text));
  590. list.prepend(item);
  591. }
  592. (function init() {
  593. const root = document.getElementById('dimensions');
  594. DIMS.forEach(dim => {
  595. const grp = document.createElement('div');
  596. grp.className = 'dim-group';
  597. grp.dataset.id = dim.id;
  598. root.appendChild(grp);
  599. renderDim(dim.id);
  600. });
  601. updateQuery();
  602. })();
  603. </script>
  604. </body>
  605. </html>