Преглед изворни кода

feat(web):正交轴拉开间距 + 「查看人工定义的轴」弹窗

正交轴标签改 flex 横向加大间距(18px)。每个面板上方加「查看人工定义的轴」按钮,
点击弹窗列出该方法人工定义轴的全部取值(创作阶段/需求点/载体位置/动作/知识类型/模态),
并标注实质/形式/作用来自分类树。取值来源 = 新接口 /api/manual-axes(单一来源 query.py)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SamLee пре 2 недеља
родитељ
комит
245fdfc474

Разлика између датотеке није приказан због своје велике величине
+ 8 - 0
acquisition/web/app/dist/assets/index-C7iYtVJR.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
acquisition/web/app/dist/assets/index-CIKg5rSi.css


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
acquisition/web/app/dist/assets/index-Cz-31bz9.css


Разлика између датотеке није приказан због своје велике величине
+ 0 - 8
acquisition/web/app/dist/assets/index-JJ8OhAh1.js


+ 2 - 2
acquisition/web/app/dist/index.html

@@ -4,8 +4,8 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>创作知识 · 找帖子</title>
-    <script type="module" crossorigin src="/app/assets/index-JJ8OhAh1.js"></script>
-    <link rel="stylesheet" crossorigin href="/app/assets/index-Cz-31bz9.css">
+    <script type="module" crossorigin src="/app/assets/index-C7iYtVJR.js"></script>
+    <link rel="stylesheet" crossorigin href="/app/assets/index-CIKg5rSi.css">
   </head>
   <body>
     <div id="root"></div>

+ 5 - 0
acquisition/web/app/src/api.js

@@ -24,6 +24,11 @@ export async function fetchMethods(table) {
   try { return (await getJSON(`/api/methods${qs({ table })}`)).methods || [] } catch { return [] }
 }
 
+// {方法名: {轴名: 取值}} —— 人工定义轴的全部取值,给「查看人工定义的轴」弹窗用
+export async function fetchManualAxes() {
+  try { return (await getJSON('/api/manual-axes')).axes || {} } catch { return {} }
+}
+
 const EMPTY = { total: 0, page: 1, size: 30, items: [] }
 
 export async function fetchQueries({ method, run_id, page, size } = {}) {

+ 53 - 3
acquisition/web/app/src/pages/QueriesPage.jsx

@@ -1,5 +1,5 @@
 import React, { useEffect, useState } from 'react'
-import { fetchMethods, fetchQueries, fetchSearchSummary } from '../api.js'
+import { fetchManualAxes, fetchMethods, fetchQueries, fetchSearchSummary } from '../api.js'
 import Pager from '../components/Pager.jsx'
 
 const SIZE = 30
@@ -14,6 +14,8 @@ export default function QueriesPage() {
   const [page, setPage] = useState(1)
   const [data, setData] = useState({ total: 0, page: 1, size: SIZE, items: [] })
   const [summary, setSummary] = useState({})
+  const [manualAxes, setManualAxes] = useState({})
+  const [showAxes, setShowAxes] = useState(false)
 
   useEffect(() => {
     fetchMethods('queries').then((ms) => {
@@ -23,9 +25,11 @@ export default function QueriesPage() {
     })
   }, [])
   useEffect(() => { fetchSearchSummary().then(setSummary) }, [])
+  useEffect(() => { fetchManualAxes().then(setManualAxes) }, [])
   useEffect(() => { if (method) fetchQueries({ method, page, size: SIZE }).then(setData) }, [method, page])
 
   const pick = (m) => { setMethod(m); setPage(1) }
+  const axes = manualAxes[method]   // 当前面板的人工定义轴
 
   return (
     <div>
@@ -35,9 +39,12 @@ export default function QueriesPage() {
         ))}
       </div>
       <div className="filters">
+        {axes && <button className="btn ghost" onClick={() => setShowAxes(true)}>📋 查看人工定义的轴</button>}
         <span className="count">共 {data.total} 条 query</span>
       </div>
 
+      {showAxes && axes && <AxesModal method={method} axes={axes} onClose={() => setShowAxes(false)} />}
+
       {data.items.length === 0 ? (
         <div className="empty">暂无数据。先跑 <code>scripts/build_query_demo.py</code> 再 <code>scripts/import_to_db.py</code></div>
       ) : (
@@ -48,9 +55,9 @@ export default function QueriesPage() {
           <tbody>
             {data.items.map((it) => (
               <tr key={it.id}>
-                <td>{Object.entries(it.axes || {}).map(([k, v]) => (
+                <td><div className="axes">{Object.entries(it.axes || {}).map(([k, v]) => (
                   <span className="tag k" key={k}>{k}:{v}</span>
-                ))}</td>
+                ))}</div></td>
                 <td className="q">{it.query}</td>
                 <td><ResultBtn s={summary[it.query]} query={it.query} /></td>
               </tr>
@@ -64,6 +71,49 @@ export default function QueriesPage() {
   )
 }
 
+// 人工定义轴弹窗:列出该方法每个人工轴的全部取值。
+// 取值形状有三种:list(动作/阶段/模态)、{key:[..]}(需求点/载体位置)、{key:str}(知识类型后缀)。
+function AxesModal({ method, axes, onClose }) {
+  return (
+    <div className="modal-mask" onClick={onClose}>
+      <div className="modal" onClick={(e) => e.stopPropagation()}>
+        <div className="modal-hd">
+          <b>人工定义的轴 · {method}</b>
+          <span className="x" onClick={onClose}>✕</span>
+        </div>
+        <div className="modal-bd">
+          <p className="note">实质 / 形式 / 作用来自分类树;下面这些是人工定义的轴及全部取值。</p>
+          {Object.entries(axes).map(([name, vals]) => (
+            <div className="axrow" key={name}>
+              <div className="axname">{name}</div>
+              <div className="axvals"><AxValues vals={vals} /></div>
+            </div>
+          ))}
+        </div>
+      </div>
+    </div>
+  )
+}
+
+function AxValues({ vals }) {
+  if (Array.isArray(vals)) {
+    return <>{vals.map((v) => <span className="tag k" key={v}>{v}</span>)}</>
+  }
+  // 字典:值是数组 → 「键:a b c」;值是字符串 → 「键 → 值」
+  return (
+    <div className="axsub">
+      {Object.entries(vals).map(([k, v]) => (
+        <div className="axsubrow" key={k}>
+          <span className="axsubkey">{k}</span>
+          {Array.isArray(v)
+            ? v.map((x) => <span className="tag k" key={x}>{x}</span>)
+            : <span className="tag k">{v}</span>}
+        </div>
+      ))}
+    </div>
+  )
+}
+
 function ResultBtn({ s, query }) {
   if (s && s.ok > 0) {
     return <a className="btn" href={`#/q/${encodeURIComponent(query)}`}>🔎 看 {s.ok} 个结果 ›</a>

+ 26 - 0
acquisition/web/app/src/styles.css

@@ -56,6 +56,32 @@ td.q { font-weight: 600; }
 .tag { display: inline-block; background: var(--accent-soft); color: var(--accent);
   border-radius: 6px; padding: 1px 7px; font-size: 12px; margin: 1px 3px 1px 0; white-space: nowrap; }
 .tag.k { background: #f0f0f3; color: #555; }
+/* 正交轴标签:横向拉开间距 */
+.axes { display: flex; flex-wrap: wrap; gap: 6px 18px; }
+.axes .tag { margin: 0; }
+
+/* 「查看人工定义的轴」幽灵按钮 */
+.btn.ghost { background: #fff; color: var(--accent); border: 1px solid var(--accent-soft); }
+.btn.ghost:hover { background: var(--accent-soft); text-decoration: none; }
+
+/* 人工定义轴弹窗 */
+.modal-mask { position: fixed; inset: 0; background: rgba(20, 20, 30, .38);
+  display: flex; align-items: center; justify-content: center; z-index: 50; padding: 24px; }
+.modal { background: #fff; border-radius: 14px; width: min(560px, 100%); max-height: 80vh;
+  overflow: auto; box-shadow: 0 18px 50px rgba(0, 0, 0, .25); }
+.modal-hd { display: flex; align-items: center; justify-content: space-between;
+  padding: 14px 18px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: #fff; }
+.modal-hd b { font-size: 14.5px; }
+.modal-hd .x { cursor: pointer; color: var(--muted); font-size: 15px; padding: 2px 6px; }
+.modal-bd { padding: 8px 18px 18px; }
+.modal-bd .note { color: var(--muted); font-size: 12px; margin: 8px 0 14px; }
+.axrow { padding: 10px 0; border-bottom: 1px solid var(--line); }
+.axrow:last-child { border-bottom: 0; }
+.axname { font-weight: 700; font-size: 13px; margin-bottom: 7px; }
+.axvals { display: flex; flex-wrap: wrap; gap: 6px; }
+.axsub { display: flex; flex-direction: column; gap: 7px; width: 100%; }
+.axsubrow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
+.axsubkey { font-size: 12px; color: var(--accent); font-weight: 600; min-width: 56px; }
 
 /* 搜索结果卡 */
 .cards { display: grid; grid-template-columns: 1fr; gap: 12px; }

+ 16 - 0
acquisition/web_api.py

@@ -14,9 +14,19 @@ from typing import Optional
 from fastapi import APIRouter, Query
 
 from acquisition import store
+from acquisition.query import (
+    ACTIONS, CARRIER_POS_GROUPED, DEMAND, KTYPE_SUFFIX, MODALITIES, STAGES,
+)
 
 router = APIRouter(prefix="/api")
 
+# 各方法里【人工定义】的轴及其全部取值(实质/形式/作用来自分类树,不在此)。单一来源 = query.py。
+MANUAL_AXES = {
+    "实质 × 创作阶段 × 需求": {"创作阶段": STAGES, "需求点": DEMAND},
+    "形式 + 载体位置": {"载体位置": CARRIER_POS_GROUPED},
+    "多轴正交组合": {"阶段": STAGES, "动作": ACTIONS, "知识类型": KTYPE_SUFFIX, "模态": MODALITIES},
+}
+
 
 @router.get("/runs")
 def runs() -> dict:
@@ -27,6 +37,12 @@ def runs() -> dict:
         conn.close()
 
 
+@router.get("/manual-axes")
+def manual_axes() -> dict:
+    """各方法人工定义轴的全部取值(前端「查看人工定义的轴」弹窗用)。"""
+    return {"axes": MANUAL_AXES}
+
+
 @router.get("/methods")
 def methods(table: str = Query("queries")) -> dict:
     conn = store.connect()

Неке датотеке нису приказане због велике количине промена