|
@@ -2068,24 +2068,48 @@
|
|
|
transform: translateY(-8px);
|
|
transform: translateY(-8px);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- .qr-row { display:flex; align-items:flex-start; gap:10px; padding:5px 0; border-bottom:1px solid #f4f4f4; }
|
|
|
|
|
- .qr-lab { min-width:64px; color:#999; font-size:12.5px; padding-top:6px; flex-shrink:0; }
|
|
|
|
|
- .qr-chips { display:flex; flex-wrap:wrap; gap:7px; flex:1; }
|
|
|
|
|
- .chip { padding:4px 13px; border-radius:18px; font-size:12.5px; cursor:pointer; border:1.5px solid #e2e2e2; background:#fff; color:#666; }
|
|
|
|
|
- .chip:hover { border-color:#bbb; color:#333; }
|
|
|
|
|
- .chip.on { background:#1a1a1a; border-color:#1a1a1a; color:#fff; }
|
|
|
|
|
|
|
+ /* Query 规则弹层 */
|
|
|
|
|
+ .qr-box { background:#fff; border-radius:14px; box-shadow:0 10px 44px rgba(0,0,0,.18);
|
|
|
|
|
+ width:96vw; max-width:1200px; max-height:92vh; display:flex; flex-direction:column; overflow:hidden; }
|
|
|
|
|
+ .qr-head { display:flex; align-items:center; padding:15px 20px; border-bottom:1px solid #eee; }
|
|
|
|
|
+ .qr-title { font-size:15px; font-weight:600; color:#1a1a1a; }
|
|
|
|
|
+ .qr-x { margin-left:auto; border:none; background:none; font-size:16px; color:#999; cursor:pointer; padding:4px 9px; border-radius:7px; line-height:1; }
|
|
|
|
|
+ .qr-x:hover { background:#f2f2f2; color:#333; }
|
|
|
|
|
+ .qr-bar { display:flex; align-items:center; gap:10px; padding:11px 20px; border-bottom:1px solid #f0f0f0; background:#fcfcfc; }
|
|
|
|
|
+ .qr-hint { color:#888; font-size:12px; margin-left:4px; }
|
|
|
|
|
+ .qr-dims { padding:8px 20px 10px; max-height:32vh; overflow:auto; border-bottom:1px solid #f0f0f0; }
|
|
|
|
|
+ .qr-row { display:flex; align-items:flex-start; gap:10px; padding:4px 0; }
|
|
|
|
|
+ .qr-row + .qr-row { border-top:1px solid #f6f6f6; }
|
|
|
|
|
+ .qr-lab { min-width:64px; color:#999; font-size:12.5px; padding-top:5px; flex-shrink:0; }
|
|
|
|
|
+ .qr-chips { display:flex; flex-wrap:wrap; gap:6px; flex:1; }
|
|
|
|
|
+ .chip { padding:4px 13px; border-radius:16px; font-size:12.5px; cursor:pointer; border:1.5px solid #e2e2e2; background:#fff; color:#555; line-height:1.4; }
|
|
|
|
|
+ .chip:hover { border-color:#bbb; color:#222; }
|
|
|
|
|
+ .chip.on { background:#1a1a1a; border-color:#1a1a1a; color:#fff; font-weight:500; }
|
|
|
|
|
+ .chip.axis.on { background:#2563eb; border-color:#2563eb; }
|
|
|
|
|
+ .qr-table-wrap { overflow:auto; flex:1; }
|
|
|
.qr-tab { border-collapse:collapse; font-size:11px; }
|
|
.qr-tab { border-collapse:collapse; font-size:11px; }
|
|
|
- .qr-tab th, .qr-tab td { border:1px solid #eee; padding:3px 6px; white-space:nowrap; }
|
|
|
|
|
|
|
+ .qr-tab th, .qr-tab td { border:1px solid #eee; padding:3px 7px; white-space:nowrap; }
|
|
|
.qr-tab thead th { position:sticky; top:0; background:#f7f7f7; z-index:2; }
|
|
.qr-tab thead th { position:sticky; top:0; background:#f7f7f7; z-index:2; }
|
|
|
- .qr-corner { position:sticky; left:0; z-index:3; background:#f0f0f0; }
|
|
|
|
|
- .qr-th { position:sticky; left:0; background:#fafafa; z-index:1; text-align:left; }
|
|
|
|
|
- .qr-tl1 { background:#f3f4f6; font-weight:600; color:#444; }
|
|
|
|
|
|
|
+ .qr-corner { position:sticky; left:0; z-index:3; background:#eef0f3; font-weight:600; color:#555; }
|
|
|
|
|
+ .qr-th { position:sticky; left:0; background:#fafafa; z-index:1; text-align:left; color:#444; }
|
|
|
|
|
+ .qr-tl1 { background:#eef0f3; font-weight:600; color:#444; }
|
|
|
|
|
+ .qr-gh { background:#e8ebef; font-weight:600; color:#333; text-align:center; }
|
|
|
|
|
+ .qr-ah { background:#f3f4f6; color:#555; text-align:center; }
|
|
|
.qr-c.t0 { background:#fafafa; color:#bbb; } .qr-c.t1 { background:#f1f8f1; }
|
|
.qr-c.t0 { background:#fafafa; color:#bbb; } .qr-c.t1 { background:#f1f8f1; }
|
|
|
- .qr-c.t2 { background:#dcecdc; } .qr-c.t3 { background:#bfe0bf; }
|
|
|
|
|
- .qr-c.dead { color:#ccc; } .qr-c.keep { outline:2px solid #2563eb; outline-offset:-2px; cursor:pointer; font-weight:600; }
|
|
|
|
|
- .qr-bd { display:inline-block; margin-left:4px; font-style:normal; font-size:9px; color:#2563eb; }
|
|
|
|
|
- .qr-gh { background:#eef0f3; font-weight:600; color:#333; text-align:center; }
|
|
|
|
|
- .qr-ah { background:#f7f7f7; color:#555; text-align:center; }
|
|
|
|
|
|
|
+ .qr-c.t2 { background:#d8ecd8; } .qr-c.t3 { background:#b9ddb9; }
|
|
|
|
|
+ .qr-c.dead { color:#ccc; }
|
|
|
|
|
+ .qr-c.keep { outline:2px solid #2563eb; outline-offset:-2px; cursor:pointer; font-weight:600; }
|
|
|
|
|
+ .qr-c.keep:hover { background:#dbe7ff; }
|
|
|
|
|
+ .qr-bd { display:inline-block; margin-left:4px; font-style:normal; font-size:9px; font-weight:700; color:#2563eb; }
|
|
|
|
|
+ .qr-pop { position:fixed; z-index:60; width:300px; background:#fff; border:1px solid #e2e2e2;
|
|
|
|
|
+ border-radius:11px; box-shadow:0 8px 30px rgba(0,0,0,.18); padding:13px 15px; font-size:12.5px; color:#333; }
|
|
|
|
|
+ .qr-pop .q { font-size:13.5px; font-weight:600; color:#1a1a1a; margin-bottom:2px; word-break:break-all; }
|
|
|
|
|
+ .qr-pop .orig { font-size:11px; color:#aaa; margin-bottom:8px; }
|
|
|
|
|
+ .qr-pop .sc { display:flex; align-items:baseline; gap:8px; margin:6px 0; }
|
|
|
|
|
+ .qr-pop .sc b { font-size:22px; color:#2563eb; }
|
|
|
|
|
+ .qr-pop .dim { color:#888; font-size:11.5px; }
|
|
|
|
|
+ .qr-pop .rsn { color:#555; margin:7px 0 10px; line-height:1.5; }
|
|
|
|
|
+ .qr-pop .acts { display:flex; gap:8px; }
|
|
|
</style>
|
|
</style>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
@@ -2436,18 +2460,20 @@
|
|
|
|
|
|
|
|
<!-- Query 规则组织器 弹层 -->
|
|
<!-- Query 规则组织器 弹层 -->
|
|
|
<div class="modal-bg" id="qr-modal" hidden>
|
|
<div class="modal-bg" id="qr-modal" hidden>
|
|
|
- <div class="modal" style="max-width:1180px;width:96vw;max-height:92vh;display:flex;flex-direction:column">
|
|
|
|
|
- <h2>Query 词组织器</h2>
|
|
|
|
|
- <div id="qr-dims" style="margin:6px 0 10px"></div>
|
|
|
|
|
- <div style="display:flex;gap:8px;align-items:center;margin:6px 0 10px">
|
|
|
|
|
|
|
+ <div class="qr-box">
|
|
|
|
|
+ <div class="qr-head">
|
|
|
|
|
+ <span class="qr-title">Query 词组织器</span>
|
|
|
|
|
+ <button class="qr-x" id="qr-close" title="关闭">✕</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="qr-bar">
|
|
|
<button class="btn seal" id="qr-score">生成正交表 & 评估高亮</button>
|
|
<button class="btn seal" id="qr-score">生成正交表 & 评估高亮</button>
|
|
|
<button class="btn" id="qr-search-all" hidden>搜全部达标</button>
|
|
<button class="btn" id="qr-search-all" hidden>搜全部达标</button>
|
|
|
- <span id="qr-hint" style="color:#888;font-size:12px"></span>
|
|
|
|
|
- <div style="flex:1"></div>
|
|
|
|
|
- <button class="btn" id="qr-close">关闭</button>
|
|
|
|
|
|
|
+ <span id="qr-hint" class="qr-hint"></span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div id="qr-table-wrap" style="overflow:auto;flex:1;border:1px solid #eee;border-radius:8px"></div>
|
|
|
|
|
|
|
+ <div id="qr-dims" class="qr-dims"></div>
|
|
|
|
|
+ <div id="qr-table-wrap" class="qr-table-wrap"></div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div id="qr-pop" class="qr-pop" hidden></div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
@@ -3665,63 +3691,87 @@
|
|
|
];
|
|
];
|
|
|
const qrState = {
|
|
const qrState = {
|
|
|
flat: { tool_type: null, modality: null, suffix: null },
|
|
flat: { tool_type: null, modality: null, suffix: null },
|
|
|
- treePath: { substance: [], form: [] }, // 选中节点的祖先+自身 name 路径
|
|
|
|
|
- treeData: { substance: null, form: null }, // 接口返回的 {tree:[...]}
|
|
|
|
|
|
|
+ treePath: { substance: [], form: [] }, // 实质/形式 选中 name 路径(仅作 Sonnet 上下文)
|
|
|
|
|
+ treeData: { substance: null, form: null },
|
|
|
|
|
+ axisPath: { action: [], type: [] }, // 动作/类型 筛选路径(l1/l2/叶子,来自 matrix,筛表显示)
|
|
|
matrix: null, // /api/query_matrix
|
|
matrix: null, // /api/query_matrix
|
|
|
scores: null, // 最近一次评分结果 cells
|
|
scores: null, // 最近一次评分结果 cells
|
|
|
};
|
|
};
|
|
|
- function qrChip(label, active, on) {
|
|
|
|
|
|
|
+ function qrChip(label, active, on, cls) {
|
|
|
const b = document.createElement("button");
|
|
const b = document.createElement("button");
|
|
|
- b.className = "chip" + (active ? " on" : "");
|
|
|
|
|
|
|
+ b.className = "chip" + (cls ? " " + cls : "") + (active ? " on" : "");
|
|
|
b.textContent = label;
|
|
b.textContent = label;
|
|
|
b.onclick = on;
|
|
b.onclick = on;
|
|
|
return b;
|
|
return b;
|
|
|
}
|
|
}
|
|
|
|
|
+ function qrRow(root, labtxt) {
|
|
|
|
|
+ const row = document.createElement("div");
|
|
|
|
|
+ row.className = "qr-row";
|
|
|
|
|
+ row.innerHTML = `<span class="qr-lab">${labtxt}</span>`;
|
|
|
|
|
+ const wrap = document.createElement("span");
|
|
|
|
|
+ wrap.className = "qr-chips";
|
|
|
|
|
+ row.appendChild(wrap);
|
|
|
|
|
+ root.appendChild(row);
|
|
|
|
|
+ return wrap;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 动作/类型 三级(l1/l2/叶子)取自 matrix items 的 l1/l2/name
|
|
|
|
|
+ function qrAxisLevelOptions(items, prefix) {
|
|
|
|
|
+ if (prefix.length === 0) return [...new Set(items.map((x) => x.l1))];
|
|
|
|
|
+ if (prefix.length === 1) return [...new Set(items.filter((x) => x.l1 === prefix[0]).map((x) => x.l2))];
|
|
|
|
|
+ return items.filter((x) => x.l1 === prefix[0] && x.l2 === prefix[1]).map((x) => x.name);
|
|
|
|
|
+ }
|
|
|
|
|
+ function qrAxisMatch(item, path) {
|
|
|
|
|
+ if (path[0] && item.l1 !== path[0]) return false;
|
|
|
|
|
+ if (path[1] && item.l2 !== path[1]) return false;
|
|
|
|
|
+ if (path[2] && item.name !== path[2]) return false;
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
function renderQrDims() {
|
|
function renderQrDims() {
|
|
|
const root = $("#qr-dims");
|
|
const root = $("#qr-dims");
|
|
|
root.innerHTML = "";
|
|
root.innerHTML = "";
|
|
|
- // 扁平维度
|
|
|
|
|
|
|
+ // 扁平维度(工具类型/模态/后缀):拼词上下文
|
|
|
QR_FLAT.forEach((d) => {
|
|
QR_FLAT.forEach((d) => {
|
|
|
- const row = document.createElement("div");
|
|
|
|
|
- row.className = "qr-row";
|
|
|
|
|
- row.innerHTML = `<span class="qr-lab">· ${d.label}</span>`;
|
|
|
|
|
- const wrap = document.createElement("span");
|
|
|
|
|
- wrap.className = "qr-chips";
|
|
|
|
|
- wrap.appendChild(qrChip("无", qrState.flat[d.id] == null, () => { qrState.flat[d.id] = null; renderQrDims(); }));
|
|
|
|
|
|
|
+ const wrap = qrRow(root, `· ${d.label}`);
|
|
|
|
|
+ wrap.appendChild(qrChip("无", qrState.flat[d.id] == null, () => { qrState.flat[d.id] = null; renderQrDims(); renderQrTable(); }));
|
|
|
d.items.forEach((it) =>
|
|
d.items.forEach((it) =>
|
|
|
- wrap.appendChild(qrChip(it, qrState.flat[d.id] === it, () => { qrState.flat[d.id] = it; renderQrDims(); })));
|
|
|
|
|
- row.appendChild(wrap);
|
|
|
|
|
- root.appendChild(row);
|
|
|
|
|
|
|
+ wrap.appendChild(qrChip(it, qrState.flat[d.id] === it, () => { qrState.flat[d.id] = it; renderQrDims(); renderQrTable(); })));
|
|
|
});
|
|
});
|
|
|
- // 实质/形式 下钻:每一层一行,展示当前层可选项;选中后再展开下一层
|
|
|
|
|
|
|
+ // 实质/形式 接口下钻:仅作 Sonnet 上下文,不拼词、不筛表
|
|
|
QR_TREE.forEach((d) => {
|
|
QR_TREE.forEach((d) => {
|
|
|
- const data = qrState.treeData[d.id];
|
|
|
|
|
const path = qrState.treePath[d.id];
|
|
const path = qrState.treePath[d.id];
|
|
|
- // 沿 path 定位到当前层的 children 列表(逐层)
|
|
|
|
|
- let level = data ? data.tree : [];
|
|
|
|
|
- const rows = [];
|
|
|
|
|
|
|
+ let level = qrState.treeData[d.id] ? qrState.treeData[d.id].tree : [];
|
|
|
for (let depth = 0; ; depth++) {
|
|
for (let depth = 0; ; depth++) {
|
|
|
- const labtxt = depth === 0 ? `· ${d.label}` : "";
|
|
|
|
|
- const row = document.createElement("div");
|
|
|
|
|
- row.className = "qr-row";
|
|
|
|
|
- row.innerHTML = `<span class="qr-lab">${labtxt}</span>`;
|
|
|
|
|
- const wrap = document.createElement("span");
|
|
|
|
|
- wrap.className = "qr-chips";
|
|
|
|
|
|
|
+ const wrap = qrRow(root, depth === 0 ? `· ${d.label}` : "");
|
|
|
wrap.appendChild(qrChip("无", path.length === depth, () => { qrState.treePath[d.id] = path.slice(0, depth); renderQrDims(); }));
|
|
wrap.appendChild(qrChip("无", path.length === depth, () => { qrState.treePath[d.id] = path.slice(0, depth); renderQrDims(); }));
|
|
|
(level || []).forEach((node) =>
|
|
(level || []).forEach((node) =>
|
|
|
wrap.appendChild(qrChip(node.name, path[depth] === node.name,
|
|
wrap.appendChild(qrChip(node.name, path[depth] === node.name,
|
|
|
() => { qrState.treePath[d.id] = [...path.slice(0, depth), node.name]; renderQrDims(); })));
|
|
() => { qrState.treePath[d.id] = [...path.slice(0, depth), node.name]; renderQrDims(); })));
|
|
|
- row.appendChild(wrap);
|
|
|
|
|
- rows.push(row);
|
|
|
|
|
- // 进入已选中节点的下一层
|
|
|
|
|
const sel = (level || []).find((n) => n.name === path[depth]);
|
|
const sel = (level || []).find((n) => n.name === path[depth]);
|
|
|
if (!sel || !(sel.children && sel.children.length)) break;
|
|
if (!sel || !(sel.children && sel.children.length)) break;
|
|
|
level = sel.children;
|
|
level = sel.children;
|
|
|
}
|
|
}
|
|
|
- rows.forEach((r) => root.appendChild(r));
|
|
|
|
|
});
|
|
});
|
|
|
|
|
+ // 动作/类型 下钻(来自 matrix l1/l2):筛选表格显示
|
|
|
|
|
+ if (qrState.matrix) {
|
|
|
|
|
+ [{ id: "action", label: "动作", items: qrState.matrix.actions },
|
|
|
|
|
+ { id: "type", label: "类型", items: qrState.matrix.types }].forEach((d) => {
|
|
|
|
|
+ const path = qrState.axisPath[d.id];
|
|
|
|
|
+ for (let depth = 0; depth <= 2; depth++) {
|
|
|
|
|
+ const opts = qrAxisLevelOptions(d.items, path.slice(0, depth));
|
|
|
|
|
+ if (!opts.length) break;
|
|
|
|
|
+ const wrap = qrRow(root, depth === 0 ? `· ${d.label}` : "");
|
|
|
|
|
+ wrap.appendChild(qrChip("无", path.length === depth,
|
|
|
|
|
+ () => { qrState.axisPath[d.id] = path.slice(0, depth); renderQrDims(); renderQrTable(); }, "axis"));
|
|
|
|
|
+ opts.forEach((name) =>
|
|
|
|
|
+ wrap.appendChild(qrChip(name, path[depth] === name,
|
|
|
|
|
+ () => { qrState.axisPath[d.id] = [...path.slice(0, depth), name]; renderQrDims(); renderQrTable(); }, "axis")));
|
|
|
|
|
+ if (path[depth] == null) break; // 未选中本层 → 不展开下一层
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
async function qrOpen() {
|
|
async function qrOpen() {
|
|
|
|
|
+ qrClosePop();
|
|
|
renderQrDims();
|
|
renderQrDims();
|
|
|
// 并行拉矩阵 + 两棵分类树(接口挂则该维度降级:提示不可选)
|
|
// 并行拉矩阵 + 两棵分类树(接口挂则该维度降级:提示不可选)
|
|
|
try { qrState.matrix = qrState.matrix || await api("/api/query_matrix"); }
|
|
try { qrState.matrix = qrState.matrix || await api("/api/query_matrix"); }
|
|
@@ -3745,39 +3795,38 @@
|
|
|
if (!m) return;
|
|
if (!m) return;
|
|
|
const { actions, types, matrix } = m;
|
|
const { actions, types, matrix } = m;
|
|
|
const sc = qrState.scores; // {"ai_ti":{keep,score,rewrite,...}}
|
|
const sc = qrState.scores; // {"ai_ti":{keep,score,rewrite,...}}
|
|
|
- // 表头:动作按 l1 分组(参考 Image #2,列头两行:l1 跨列 + 叶子)
|
|
|
|
|
|
|
+ // 按 动作/类型 筛选路径过滤可见行列(保留原始索引 ai/ti 供评分键 ai_ti)
|
|
|
|
|
+ const aIdx = actions.map((_, i) => i).filter((i) => qrAxisMatch(actions[i], qrState.axisPath.action));
|
|
|
|
|
+ const tIdx = types.map((_, i) => i).filter((i) => qrAxisMatch(types[i], qrState.axisPath.type));
|
|
|
let html = '<table class="qr-tab"><thead>';
|
|
let html = '<table class="qr-tab"><thead>';
|
|
|
- // l1 行
|
|
|
|
|
html += '<tr><th class="qr-corner" rowspan="2">类型 \\ 动作</th>';
|
|
html += '<tr><th class="qr-corner" rowspan="2">类型 \\ 动作</th>';
|
|
|
const groups = [];
|
|
const groups = [];
|
|
|
- actions.forEach((a, ai) => {
|
|
|
|
|
|
|
+ aIdx.forEach((ai) => {
|
|
|
|
|
+ const l1 = actions[ai].l1;
|
|
|
const last = groups[groups.length - 1];
|
|
const last = groups[groups.length - 1];
|
|
|
- if (last && last.l1 === a.l1) last.span++;
|
|
|
|
|
- else groups.push({ l1: a.l1, span: 1, start: ai });
|
|
|
|
|
|
|
+ if (last && last.l1 === l1) last.span++;
|
|
|
|
|
+ else groups.push({ l1, span: 1 });
|
|
|
});
|
|
});
|
|
|
groups.forEach((g) => { html += `<th colspan="${g.span}" class="qr-gh">${g.l1}</th>`; });
|
|
groups.forEach((g) => { html += `<th colspan="${g.span}" class="qr-gh">${g.l1}</th>`; });
|
|
|
html += "</tr><tr>";
|
|
html += "</tr><tr>";
|
|
|
- actions.forEach((a) => { html += `<th class="qr-ah">${a.name}</th>`; });
|
|
|
|
|
|
|
+ aIdx.forEach((ai) => { html += `<th class="qr-ah">${actions[ai].name}</th>`; });
|
|
|
html += "</tr></thead><tbody>";
|
|
html += "</tr></thead><tbody>";
|
|
|
- // 行:类型按 l1 分组,组首插一行组标题
|
|
|
|
|
let curL1 = null;
|
|
let curL1 = null;
|
|
|
- types.forEach((t, ti) => {
|
|
|
|
|
|
|
+ tIdx.forEach((ti) => {
|
|
|
|
|
+ const t = types[ti];
|
|
|
if (t.l1 !== curL1) {
|
|
if (t.l1 !== curL1) {
|
|
|
curL1 = t.l1;
|
|
curL1 = t.l1;
|
|
|
- html += `<tr><td class="qr-tl1" colspan="${actions.length + 1}">${t.l1}</td></tr>`;
|
|
|
|
|
|
|
+ html += `<tr><td class="qr-tl1" colspan="${aIdx.length + 1}">${t.l1}</td></tr>`;
|
|
|
}
|
|
}
|
|
|
html += `<tr><th class="qr-th">${t.name}</th>`;
|
|
html += `<tr><th class="qr-th">${t.name}</th>`;
|
|
|
- actions.forEach((a, ai) => {
|
|
|
|
|
- const cell = matrix[ai][ti] || {};
|
|
|
|
|
- const tier = cell.tier || 0;
|
|
|
|
|
- const q = qrCellQuery(a.name, t.name);
|
|
|
|
|
|
|
+ aIdx.forEach((ai) => {
|
|
|
|
|
+ const tier = (matrix[ai][ti] || {}).tier || 0;
|
|
|
|
|
+ const q = qrCellQuery(actions[ai].name, t.name);
|
|
|
const v = sc ? sc[`${ai}_${ti}`] : null;
|
|
const v = sc ? sc[`${ai}_${ti}`] : null;
|
|
|
const keep = v && v.keep;
|
|
const keep = v && v.keep;
|
|
|
const cls = `qr-c t${tier}` + (tier === 0 ? " dead" : "") + (keep ? " keep" : "");
|
|
const cls = `qr-c t${tier}` + (tier === 0 ? " dead" : "") + (keep ? " keep" : "");
|
|
|
const badge = keep && v.score != null ? `<i class="qr-bd">${v.score}</i>` : "";
|
|
const badge = keep && v.score != null ? `<i class="qr-bd">${v.score}</i>` : "";
|
|
|
- const title = v ? `${v.reason || ""} (n${v.natural}/f${v.findable}/u${v.useful})` : `tier ${tier}`;
|
|
|
|
|
- html += `<td class="${cls}" data-ai="${ai}" data-ti="${ti}" title="${title.replace(/"/g, """)}">`
|
|
|
|
|
- + `${q}${badge}</td>`;
|
|
|
|
|
|
|
+ html += `<td class="${cls}" data-ai="${ai}" data-ti="${ti}">${q}${badge}</td>`;
|
|
|
});
|
|
});
|
|
|
html += "</tr>";
|
|
html += "</tr>";
|
|
|
});
|
|
});
|
|
@@ -3817,19 +3866,42 @@
|
|
|
const r = await api("/api/run_search", { method: "POST", body: JSON.stringify(body) });
|
|
const r = await api("/api/run_search", { method: "POST", body: JSON.stringify(body) });
|
|
|
return r; // {task_id, query_id}
|
|
return r; // {task_id, query_id}
|
|
|
}
|
|
}
|
|
|
- // 点高亮格 → 确认 → 搜
|
|
|
|
|
- $("#qr-table-wrap").onclick = async (e) => {
|
|
|
|
|
|
|
+ // 点高亮格 → 气泡显示评分/理由 + 发起搜索/复制(不再用浏览器原生 confirm)
|
|
|
|
|
+ function qrClosePop() { $("#qr-pop").hidden = true; }
|
|
|
|
|
+ function qrShowPop(td, v) {
|
|
|
|
|
+ const pop = $("#qr-pop");
|
|
|
|
|
+ const query = v.rewrite || v.query;
|
|
|
|
|
+ const orig = v.rewrite && v.rewrite !== v.query ? `<div class="orig">原: ${v.query}</div>` : "";
|
|
|
|
|
+ const tier = td.classList.contains("t3") ? 3 : td.classList.contains("t2") ? 2 : td.classList.contains("t1") ? 1 : 0;
|
|
|
|
|
+ pop.innerHTML =
|
|
|
|
|
+ `<div class="q">${query}</div>${orig}` +
|
|
|
|
|
+ `<div class="sc"><b>${v.score ?? "?"}</b><span class="dim">人话 ${v.natural ?? "?"} · 可搜 ${v.findable ?? "?"} · 价值 ${v.useful ?? "?"} · tier ${tier}</span></div>` +
|
|
|
|
|
+ `<div class="rsn">${v.reason || "(无理由)"}</div>` +
|
|
|
|
|
+ `<div class="acts"><button class="btn seal" id="qr-pop-go">发起搜索</button><button class="btn" id="qr-pop-copy">复制</button></div>`;
|
|
|
|
|
+ pop.hidden = false;
|
|
|
|
|
+ const r = td.getBoundingClientRect(), pw = 300, ph = pop.offsetHeight || 150;
|
|
|
|
|
+ let left = r.left, top = r.bottom + 6;
|
|
|
|
|
+ if (left + pw > window.innerWidth - 10) left = window.innerWidth - pw - 10;
|
|
|
|
|
+ if (top + ph > window.innerHeight - 10) top = r.top - ph - 6;
|
|
|
|
|
+ pop.style.left = Math.max(10, left) + "px";
|
|
|
|
|
+ pop.style.top = Math.max(10, top) + "px";
|
|
|
|
|
+ $("#qr-pop-go").onclick = async () => {
|
|
|
|
|
+ qrClosePop();
|
|
|
|
|
+ try { const rr = await qrRunSearch(query); showTask(`搜索 · ${rr.query_id} ${query}`, rr.task_id, null); }
|
|
|
|
|
+ catch (err) { toast("搜索启动失败:" + (err.body?.error || err.status), "error"); }
|
|
|
|
|
+ };
|
|
|
|
|
+ $("#qr-pop-copy").onclick = () => { navigator.clipboard && navigator.clipboard.writeText(query); toast("已复制", "info", 1200); };
|
|
|
|
|
+ }
|
|
|
|
|
+ $("#qr-table-wrap").onclick = (e) => {
|
|
|
const td = e.target.closest("td.qr-c.keep");
|
|
const td = e.target.closest("td.qr-c.keep");
|
|
|
if (!td) return;
|
|
if (!td) return;
|
|
|
- const v = qrState.scores[`${td.dataset.ai}_${td.dataset.ti}`];
|
|
|
|
|
- if (!v) return;
|
|
|
|
|
- const query = v.rewrite || v.query;
|
|
|
|
|
- if (!confirm(`发起搜索(小红书+公众号 各20):\n${query}`)) return;
|
|
|
|
|
- try {
|
|
|
|
|
- const r = await qrRunSearch(query);
|
|
|
|
|
- showTask(`搜索 · ${r.query_id} ${query}`, r.task_id, null);
|
|
|
|
|
- } catch (err) { toast("搜索启动失败:" + (err.body?.error || err.status), "error"); }
|
|
|
|
|
|
|
+ const v = qrState.scores && qrState.scores[`${td.dataset.ai}_${td.dataset.ti}`];
|
|
|
|
|
+ if (v) qrShowPop(td, v);
|
|
|
};
|
|
};
|
|
|
|
|
+ // 点气泡外/表格空白处 关闭气泡
|
|
|
|
|
+ $("#qr-modal").addEventListener("click", (e) => {
|
|
|
|
|
+ if (!e.target.closest("#qr-pop") && !e.target.closest("td.qr-c.keep")) qrClosePop();
|
|
|
|
|
+ });
|
|
|
// 搜全部达标:逐格起任务(朴素循环,失败不阻断)
|
|
// 搜全部达标:逐格起任务(朴素循环,失败不阻断)
|
|
|
$("#qr-search-all").onclick = async () => {
|
|
$("#qr-search-all").onclick = async () => {
|
|
|
const keeps = Object.entries(qrState.scores || {}).filter(([, v]) => v.keep);
|
|
const keeps = Object.entries(qrState.scores || {}).filter(([, v]) => v.keep);
|