Explorar o código

fix: LangGraph管理页全屏布局 + Java后端健康检查修复

- 页面改为 flex 全屏布局,标签页占满剩余高度
- 表格高度自适应,无固定 max-height
- Java后端健康检查由 /health 改为 /api/(返回401=正常需认证)
- 组件详情展示 detail 字段而非 code
iwt hai 1 mes
pai
achega
f7f5576e57
Modificáronse 2 ficheiros con 212 adicións e 278 borrados
  1. 6 4
      cfc-langgraph/app/monitoring.py
  2. 206 274
      cfc-web/src/views/admin/LangGraphAdmin.vue

+ 6 - 4
cfc-langgraph/app/monitoring.py

@@ -124,14 +124,16 @@ async def detailed_health():
         from app.tools.java_client import JavaClient
         client = JavaClient()
         jc = await client._get_client()
-        resp = await jc.get("/health")
-        if resp.status_code == 200:
+        resp = await jc.get("/api/")
+        if resp.status_code == 401:
+            status["components"]["java_backend"] = {"status": "ok", "detail": "需认证(正常)"}
+        elif resp.status_code == 200:
             status["components"]["java_backend"] = {"status": "ok"}
         else:
-            status["components"]["java_backend"] = {"status": "error", "code": resp.status_code}
+            status["components"]["java_backend"] = {"status": "error", "code": resp.status_code, "detail": f"HTTP {resp.status_code}"}
             status["status"] = "degraded"
     except Exception as e:
-        status["components"]["java_backend"] = {"status": "error", "message": str(e)}
+        status["components"]["java_backend"] = {"status": "error", "detail": str(e)}
         status["status"] = "degraded"
 
     return status

+ 206 - 274
cfc-web/src/views/admin/LangGraphAdmin.vue

@@ -1,93 +1,86 @@
 <template>
-  <div class="lg-admin page-container">
-    <!-- 顶部状态栏(固定) -->
-    <el-row :gutter="12" class="top-bar">
-      <el-col :span="4">
-        <el-card shadow="hover" class="mini-card" :class="statusClass">
-          <div class="mini-icon">{{ statusIcon }}</div>
-          <div class="mini-text">{{ statusText }}</div>
-          <div class="mini-sub">{{ statusTime }}</div>
-        </el-card>
-      </el-col>
-      <el-col :span="4">
-        <el-card shadow="hover" class="mini-card">
-          <div class="mini-icon" style="background:linear-gradient(135deg,#667eea,#764ba2)">🔗</div>
-          <div class="mini-text">{{ langgraphUrl }}</div>
-          <div class="mini-sub">端口 9000 · v{{ serviceVersion }}</div>
-        </el-card>
-      </el-col>
-      <el-col :span="4">
-        <el-card shadow="hover" class="mini-card">
-          <div class="mini-icon" style="background:linear-gradient(135deg,#11998e,#38ef7d)">📡</div>
-          <div class="mini-text">{{ apiCount }} 个端点</div>
-          <div class="mini-sub">API 接口总数</div>
-        </el-card>
-      </el-col>
-      <el-col :span="4">
-        <el-card shadow="hover" class="mini-card">
-          <div class="mini-icon" style="background:linear-gradient(135deg,#fc4a1a,#f7b733)">📋</div>
-          <div class="mini-text">{{ totalLogs }} 条日志</div>
-          <div class="mini-sub">请求历史记录</div>
-        </el-card>
-      </el-col>
-      <el-col :span="8" class="top-actions">
-        <el-button size="small" type="primary" @click="refreshAll" :loading="refreshing">🔄 刷新</el-button>
-        <el-button size="small" @click="fetchLogs">📋 拉取日志</el-button>
-        <el-button size="small" type="danger" @click="clearLogsConfirm" :disabled="!logList.length">清空日志</el-button>
-      </el-col>
-    </el-row>
+  <div class="lg-admin">
+    <div class="lg-top-bar">
+      <div class="lg-stat" :class="statusClass">
+        <span class="lg-stat-icon">{{ statusIcon }}</span>
+        <div class="lg-stat-info">
+          <div class="lg-stat-label">服务状态</div>
+          <div class="lg-stat-value">{{ statusText }}</div>
+          <div class="lg-stat-sub">{{ statusTime }}</div>
+        </div>
+      </div>
+      <div class="lg-stat">
+        <span class="lg-stat-icon">🔗</span>
+        <div class="lg-stat-info">
+          <div class="lg-stat-label">服务地址</div>
+          <div class="lg-stat-value" style="font-size:12px">{{ langgraphUrl }}</div>
+          <div class="lg-stat-sub">端口 9000 · v{{ serviceVersion }}</div>
+        </div>
+      </div>
+      <div class="lg-stat">
+        <span class="lg-stat-icon">📡</span>
+        <div class="lg-stat-info">
+          <div class="lg-stat-label">API 端点数</div>
+          <div class="lg-stat-value">{{ apiCount }}</div>
+          <div class="lg-stat-sub">个可用接口</div>
+        </div>
+      </div>
+      <div class="lg-stat">
+        <span class="lg-stat-icon">📋</span>
+        <div class="lg-stat-info">
+          <div class="lg-stat-label">请求日志</div>
+          <div class="lg-stat-value">{{ totalLogs }}</div>
+          <div class="lg-stat-sub">条历史记录</div>
+        </div>
+      </div>
+      <div class="lg-actions">
+        <el-button size="small" type="primary" @click="refreshAll" :loading="refreshing">刷新</el-button>
+        <el-button size="small" @click="fetchLogs">拉取日志</el-button>
+      </div>
+    </div>
+
+    <el-tabs v-model="activeTab" type="border-card" class="lg-tabs">
 
-    <!-- 标签页 -->
-    <el-tabs v-model="activeTab" type="border-card" class="main-tabs">
-      
-      <!-- ===== 概览 ===== -->
-      <el-tab-pane label="📊 概览" name="overview">
-        <el-row :gutter="16">
-          <el-col :span="16">
-            <el-card shadow="never" class="panel">
-              <div slot="header"><span>🧩 组件健康状态</span></div>
-              <el-table :data="componentList" border stripe size="small" max-height="300">
-                <el-table-column prop="name" label="组件" width="140" />
-                <el-table-column prop="status" label="状态" width="100">
-                  <template slot-scope="{ row }">
-                    <el-tag :type="row.status === 'ok' ? 'success' : row.status === 'error' ? 'danger' : 'info'" size="small">
-                      {{ row.status === 'ok' ? '正常' : row.status === 'error' ? '异常' : '未知' }}
-                    </el-tag>
-                  </template>
-                </el-table-column>
-                <el-table-column prop="detail" label="详情" show-overflow-tooltip />
-              </el-table>
-            </el-card>
-          </el-col>
-          <el-col :span="8">
-            <el-card shadow="never" class="panel">
-              <div slot="header"><span>⚙️ 服务配置</span></div>
-              <el-descriptions :column="1" border size="small">
-                <el-descriptions-item label="LLM 模型">{{ config.LLM_MODEL || '-' }}</el-descriptions-item>
-                <el-descriptions-item label="LLM Base URL">{{ config.LLM_BASE_URL || '-' }}</el-descriptions-item>
-                <el-descriptions-item label="Embedding 模型">{{ config.EMBEDDING_MODEL || '-' }}</el-descriptions-item>
-                <el-descriptions-item label="Embedding URL">{{ config.EMBEDDING_BASE_URL || '-' }}</el-descriptions-item>
-                <el-descriptions-item label="Java 后端">{{ config.JAVA_BASE_URL || '-' }}</el-descriptions-item>
-                <el-descriptions-item label="Chroma DB">{{ config.CHROMA_DB_PATH || '-' }}</el-descriptions-item>
-                <el-descriptions-item label="LangSmith 追踪">
-                  <el-tag :type="config.LANGCHAIN_TRACING_V2 === 'true' ? 'success' : 'info'" size="mini">
-                    {{ config.LANGCHAIN_TRACING_V2 === 'true' ? '已启用' : '未启用' }}
-                  </el-tag>
-                </el-descriptions-item>
-                <el-descriptions-item label="日志级别">{{ config.LOG_LEVEL || 'info' }}</el-descriptions-item>
-              </el-descriptions>
-            </el-card>
-          </el-col>
-        </el-row>
+      <el-tab-pane label="概览" name="overview">
+        <div class="lg-panel">
+          <div class="lg-panel-title">组件健康状态</div>
+          <el-table :data="componentList" border stripe size="small" class="lg-table">
+            <el-table-column prop="name" label="组件" width="140" />
+            <el-table-column prop="status" label="状态" width="100">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.status === 'ok' ? 'success' : row.status === 'error' ? 'danger' : 'info'" size="small">
+                  {{ row.status === 'ok' ? '正常' : row.status === 'error' ? '异常' : '未知' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="detail" label="详情" show-overflow-tooltip />
+          </el-table>
+        </div>
+        <div class="lg-panel">
+          <div class="lg-panel-title">服务配置</div>
+          <el-descriptions :column="2" border size="small" class="lg-desc">
+            <el-descriptions-item label="LLM 模型"><el-tag size="mini">{{ config.LLM_MODEL || '-' }}</el-tag></el-descriptions-item>
+            <el-descriptions-item label="LLM Base URL">{{ config.LLM_BASE_URL || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="Embedding 模型"><el-tag size="mini">{{ config.EMBEDDING_MODEL || '-' }}</el-tag></el-descriptions-item>
+            <el-descriptions-item label="Embedding URL">{{ config.EMBEDDING_BASE_URL || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="Java 后端">{{ config.JAVA_BASE_URL || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="Chroma DB">{{ config.CHROMA_DB_PATH || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="LangSmith 追踪">
+              <el-tag :type="config.LANGCHAIN_TRACING_V2 === 'true' ? 'success' : 'info'" size="mini">
+                {{ config.LANGCHAIN_TRACING_V2 === 'true' ? '已启用' : '未启用' }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="日志级别">{{ config.LOG_LEVEL || 'info' }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
       </el-tab-pane>
 
-      <!-- ===== API 端点 ===== -->
-      <el-tab-pane label="📡 API 端点" name="apis">
-        <div class="tab-toolbar">
-          <el-input v-model="apiFilter" placeholder="筛选路径或说明..." size="small" prefix-icon="el-icon-search" style="width:280px" clearable />
-          <el-button size="small" @click="fetchSpec">刷新列表</el-button>
+      <el-tab-pane label="API 端点" name="apis">
+        <div class="lg-toolbar">
+          <el-input v-model="apiFilter" placeholder="筛选路径或说明..." prefix-icon="el-icon-search" size="small" clearable style="width:280px" />
+          <el-button size="small" @click="fetchSpec">刷新</el-button>
         </div>
-        <el-table :data="filteredApiEndpoints" border stripe size="small" max-height="500" style="width:100%">
+        <el-table :data="filteredApiEndpoints" border stripe size="small" class="lg-table lg-table-full">
           <el-table-column prop="method" label="方法" width="80" fixed>
             <template slot-scope="{ row }">
               <el-tag :type="row.method === 'GET' ? 'info' : 'primary'" size="mini">{{ row.method }}</el-tag>
@@ -103,19 +96,17 @@
         </el-table>
       </el-tab-pane>
 
-      <!-- ===== 请求历史 ===== -->
-      <el-tab-pane label="📋 请求历史" name="logs">
-        <div class="tab-toolbar">
-          <el-select v-model="logFilterMethod" placeholder="方法筛选" size="small" clearable style="width:100px;margin-right:8px">
+      <el-tab-pane label="请求历史" name="logs">
+        <div class="lg-toolbar">
+          <el-select v-model="logFilterMethod" placeholder="方法" size="small" clearable style="width:90px;margin-right:8px">
             <el-option label="GET" value="GET" />
             <el-option label="POST" value="POST" />
           </el-select>
-          <el-input v-model="logFilterPath" placeholder="路径筛选..." size="small" prefix-icon="el-icon-search" style="width:200px;margin-right:8px" clearable />
+          <el-input v-model="logFilterPath" placeholder="路径筛选..." prefix-icon="el-icon-search" size="small" clearable style="width:200px;margin-right:8px" />
           <el-tag size="small" type="info">共 {{ totalLogs }} 条</el-tag>
           <el-button size="small" @click="fetchLogs" :loading="logLoading">刷新</el-button>
-          <el-button size="small" type="danger" @click="clearLogsConfirm" :disabled="!logList.length">清空</el-button>
         </div>
-        <el-table :data="filteredLogs" border stripe size="small" max-height="520" style="width:100%">
+        <el-table :data="filteredLogs" border stripe size="small" class="lg-table lg-table-full">
           <el-table-column prop="ts" label="时间" width="70" fixed />
           <el-table-column prop="method" label="方法" width="60">
             <template slot-scope="{ row }">
@@ -140,40 +131,24 @@
             </template>
           </el-table-column>
         </el-table>
-        <!-- 详情展开行 -->
-        <el-table :data="logDetailRows" border stripe size="small" max-height="300" class="log-detail-table" v-if="expandedLog">
-          <el-table-column label="请求时间" width="100"><template slot-scope="{ row }">{{ row.ts }}</template></el-table-column>
-          <el-table-column label="方法"><template slot-scope="{ row }">{{ row.method }}</template></el-table-column>
+        <el-table :data="logDetailRows" border stripe size="small" class="lg-table lg-table-full lg-detail-table" v-if="expandedLog">
+          <el-table-column label="时间" width="90"><template slot-scope="{ row }">{{ row.ts }}</template></el-table-column>
+          <el-table-column label="方法" width="60"><template slot-scope="{ row }">{{ row.method }}</template></el-table-column>
           <el-table-column label="路径"><template slot-scope="{ row }">{{ row.path }}</template></el-table-column>
-          <el-table-column label="状态"><template slot-scope="{ row }"><el-tag :type="row.status < 400 ? 'success' : 'danger'" size="mini">{{ row.status }}</el-tag></template></el-table-column>
-          <el-table-column label="耗时(ms)"><template slot-scope="{ row }">{{ row.duration_ms }}</template></el-table-column>
-          <el-table-column label="客户端IP"><template slot-scope="{ row }">{{ row.client_ip }}</template></el-table-column>
-          <el-table-column label="请求体" min-width="300">
-            <template slot-scope="{ row }">
-              <pre class="detail-pre">{{ row.body || '(空)' }}</pre>
-            </template>
-          </el-table-column>
-          <el-table-column label="响应体" min-width="300">
-            <template slot-scope="{ row }">
-              <pre class="detail-pre">{{ (row.response || '').substring(0, 2000) }}</pre>
-            </template>
-          </el-table-column>
-          <el-table-column label="错误" min-width="200" v-if="row.error">
-            <template slot-scope="{ row }">
-              <pre class="detail-pre error-pre">{{ row.error }}</pre>
-            </template>
-          </el-table-column>
+          <el-table-column label="状态" width="70"><template slot-scope="{ row }"><el-tag :type="row.status < 400 ? 'success' : 'danger'" size="mini">{{ row.status }}</el-tag></template></el-table-column>
+          <el-table-column label="耗时" width="80"><template slot-scope="{ row }">{{ row.duration_ms }}ms</template></el-table-column>
+          <el-table-column label="IP" width="90"><template slot-scope="{ row }">{{ row.client_ip }}</template></el-table-column>
+          <el-table-column label="请求体" min-width="200"><template slot-scope="{ row }"><pre class="detail-pre">{{ row.body || '(空)' }}</pre></template></el-table-column>
+          <el-table-column label="响应体" min-width="200"><template slot-scope="{ row }"><pre class="detail-pre">{{ (row.response || '').substring(0, 2000) }}</pre></template></el-table-column>
         </el-table>
       </el-tab-pane>
 
     </el-tabs>
 
-    <!-- API 测试对话框 -->
     <el-dialog :visible.sync="testDialogVisible" :title="'测试: ' + testingEndpoint.path" width="720px" :close-on-click-modal="false">
       <el-form size="small">
         <el-form-item label="请求参数">
-          <el-input v-model="testParams" type="textarea" :rows="6"
-            placeholder='请输入 JSON 参数,例如: {"user_id": 1, "query": "你好"}' />
+          <el-input v-model="testParams" type="textarea" :rows="6" placeholder='{"user_id": 1, "query": "你好"}' />
         </el-form-item>
         <el-form-item>
           <el-button type="primary" @click="runTest" :loading="testing">发送请求</el-button>
@@ -182,7 +157,7 @@
       </el-form>
       <el-divider>响应结果</el-divider>
       <pre class="response-pre" v-if="testResponse">{{ testResponse }}</pre>
-      <el-alert v-else type="info" :closable="false" description="点击"发送请求"查看响应结果" />
+      <el-alert v-else type="info" :closable="false" description="点击发送请求查看响应" />
     </el-dialog>
   </div>
 </template>
@@ -191,13 +166,8 @@
 const BASE = 'https://ai.etotem.com.cn'
 
 function fetchJSON(url, options = {}) {
-  return fetch(url, {
-    ...options,
-    headers: { 'Content-Type': 'application/json', ...options.headers }
-  }).then(function(res) {
-    if (!res.ok) throw new Error('HTTP ' + res.status)
-    return res.json()
-  })
+  return fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers } })
+    .then(function(res) { if (!res.ok) throw new Error('HTTP ' + res.status) return res.json() })
 }
 
 export default {
@@ -207,18 +177,14 @@ export default {
       activeTab: 'overview',
       langgraphUrl: 'https://ai.etotem.com.cn',
       serviceVersion: '0.3.0',
-      // 状态
       statusText: '检测中...',
       statusClass: 'status-checking',
       statusIcon: '🔄',
       statusTime: '',
       apiCount: 0,
-      // API 列表
       apiEndpoints: [],
       apiFilter: '',
-      // 组件健康
       componentList: [],
-      // 配置
       config: {
         LLM_MODEL: 'deepseek',
         LLM_BASE_URL: 'http://axonhub.iwintrue.com/v1',
@@ -229,14 +195,12 @@ export default {
         LANGCHAIN_TRACING_V2: 'true',
         LOG_LEVEL: 'info'
       },
-      // 请求日志
       logList: [],
       totalLogs: 0,
       logFilterMethod: '',
       logFilterPath: '',
       logLoading: false,
       expandedLog: null,
-      // 测试
       testDialogVisible: false,
       testingEndpoint: {},
       testParams: '{}',
@@ -248,10 +212,9 @@ export default {
   },
   computed: {
     filteredApiEndpoints: function() {
-      var self = this
-      if (!self.apiFilter) return self.apiEndpoints
-      var kw = self.apiFilter.toLowerCase()
-      return self.apiEndpoints.filter(function(e) {
+      if (!this.apiFilter) return this.apiEndpoints
+      var kw = this.apiFilter.toLowerCase()
+      return this.apiEndpoints.filter(function(e) {
         return e.path.toLowerCase().indexOf(kw) >= 0 || (e.summary && e.summary.toLowerCase().indexOf(kw) >= 0)
       })
     },
@@ -265,6 +228,9 @@ export default {
         list = list.filter(function(l) { return l.path.toLowerCase().indexOf(kw) >= 0 })
       }
       return list
+    },
+    logDetailRows: function() {
+      return this.expandedLog ? [this.expandedLog] : []
     }
   },
   created: function() {
@@ -277,34 +243,20 @@ export default {
   methods: {
     async refreshAll() {
       this.refreshing = true
-      try {
-        await Promise.all([this.fetchHealth(), this.fetchSpec()])
-      } finally {
-        this.refreshing = false
-      }
+      try { await Promise.all([this.fetchHealth(), this.fetchSpec()]) }
+      finally { this.refreshing = false }
     },
     async fetchHealth() {
       try {
-        var img = new Image()
         var healthy = await new Promise(function(resolve) {
+          var img = new Image()
           img.onload = function() { resolve(true) }
           img.onerror = function() { resolve(false) }
           img.src = BASE + '/health?t=' + Date.now()
         })
-        if (healthy) {
-          this.statusText = '运行中'
-          this.statusClass = 'status-healthy'
-          this.statusIcon = '✅'
-        } else {
-          this.statusText = '未响应'
-          this.statusClass = 'status-unhealthy'
-          this.statusIcon = '❌'
-        }
-      } catch (e) {
-        this.statusText = '异常'
-        this.statusClass = 'status-unhealthy'
-        this.statusIcon = '❌'
-      }
+        if (healthy) { this.statusText = '运行中'; this.statusClass = 'status-healthy'; this.statusIcon = '✅' }
+        else { this.statusText = '未响应'; this.statusClass = 'status-unhealthy'; this.statusIcon = '❌' }
+      } catch (e) { this.statusText = '异常'; this.statusClass = 'status-unhealthy'; this.statusIcon = '❌' }
       this.statusTime = new Date().toLocaleTimeString()
     },
     async fetchSpec() {
@@ -316,11 +268,7 @@ export default {
           var methods = paths[path]
           Object.keys(methods).forEach(function(method) {
             var info = methods[method]
-            endpoints.push({
-              method: method.toUpperCase(),
-              path: path,
-              summary: info.summary || info.description || ''
-            })
+            endpoints.push({ method: method.toUpperCase(), path: path, summary: info.summary || info.description || '' })
           })
         })
         endpoints.sort(function(a, b) {
@@ -330,9 +278,7 @@ export default {
         this.apiEndpoints = endpoints
         this.apiCount = endpoints.length
         this.fetchDetailedHealth()
-      } catch (e) {
-        console.error('获取 API 规范失败', e)
-      }
+      } catch (e) { console.error('获取 API 规范失败', e) }
     },
     async fetchDetailedHealth() {
       try {
@@ -340,15 +286,9 @@ export default {
         var data = res || {}
         var comps = []
         var c = data.components || {}
-        if (c.chromadb) {
-          comps.push({ name: 'ChromaDB', status: c.chromadb.status || 'unknown', detail: c.chromadb.path || '' })
-        }
-        if (c.llm) {
-          comps.push({ name: 'LLM', status: c.llm.status || 'unknown', detail: c.llm.message || '' })
-        }
-        if (c.java_backend) {
-          comps.push({ name: 'Java 后端', status: c.java_backend.status || 'unknown', detail: c.java_backend.code ? 'HTTP ' + c.java_backend.code : (c.java_backend.message || '') })
-        }
+        if (c.chromadb) comps.push({ name: 'ChromaDB', status: c.chromadb.status || 'unknown', detail: (c.chromadb.detail || c.chromadb.path || '') })
+        if (c.llm) comps.push({ name: 'LLM', status: c.llm.status || 'unknown', detail: c.llm.message || '' })
+        if (c.java_backend) comps.push({ name: 'Java 后端', status: c.java_backend.status || 'unknown', detail: c.java_backend.detail || (c.java_backend.code ? 'HTTP ' + c.java_backend.code : '') })
         this.componentList = comps
         var llmOk = !c.llm || c.llm.status === 'ok'
         var chromaOk = !c.chromadb || c.chromadb.status === 'ok'
@@ -361,7 +301,7 @@ export default {
           this.statusClass = 'status-unhealthy'
           this.statusIcon = '❌'
         } else {
-          this.statusText = '运行中(部分检测跳过)'
+          this.statusText = '运行中'
           this.statusClass = 'status-degraded'
           this.statusIcon = '⚠️'
         }
@@ -370,7 +310,6 @@ export default {
         this.componentList = [{ name: '健康检查', status: 'error', detail: e.message }]
       }
     },
-    // ===== 请求日志 =====
     async fetchLogs() {
       this.logLoading = true
       try {
@@ -380,37 +319,19 @@ export default {
       } catch (e) {
         this.logList = []
         this.totalLogs = 0
-        this.$message.error('获取日志失败: ' + e.message)
       } finally {
         this.logLoading = false
       }
     },
-    async clearLogsConfirm() {
-      var self = this
-      this.$confirm('确认清空所有请求日志?', '提示', { type: 'warning' }).then(async function() {
-        try {
-          var res = await fetch(BASE + '/api/v1/logs/clear', { method: 'POST' })
-          var data = await res.json()
-          self.logList = []
-          self.totalLogs = 0
-          self.$message.success('已清空 ' + (data.cleared || 0) + ' 条日志')
-        } catch (e) {
-          self.$message.error('清空失败: ' + e.message)
-        }
-      }).catch(function() {})
-    },
     toggleLogDetail: function(row) {
       this.expandedLog = this.expandedLog === row ? null : row
     },
-    // ===== API 测试 =====
     openTestDialog: function(endpoint) {
       this.testingEndpoint = endpoint
       var defaults = {
         '/api/v1/food/recognize': JSON.stringify({ image_url: 'https://example.com/food.jpg' }, null, 2),
         '/api/v1/menu/generate': JSON.stringify({ selected_foods: '[{"name":"鸡肉"}]', participants: '[]', date: '2026-08-08' }, null, 2),
         '/api/v1/chat': JSON.stringify({ query: '你好', user_id: 1 }, null, 2),
-        '/api/v1/recommend': JSON.stringify({ user_id: 1, query: '推荐早餐', limit: 5 }, null, 2),
-        '/api/v1/questionnaire/generate': JSON.stringify({ member_name: '小明', relationship_type: 'parent' }, null, 2),
         '/api/v1/health/coach': JSON.stringify({ query: '孩子最近睡眠不好', user_id: '1' }, null, 2),
         '/api/v1/health/coach/generate': JSON.stringify({ familyId: 1, memberIds: '1', dimensions: 'body', goal: '改善睡眠' }, null, 2)
       }
@@ -424,132 +345,143 @@ export default {
       self.testResponse = ''
       try {
         var payload = {}
-        if (self.testParams && self.testParams !== '请上传舌苔图片 (MIME 类型表单)') {
-          payload = JSON.parse(self.testParams)
-        }
-        var path = self.testingEndpoint.path
-        var res = await fetch(BASE + path, {
-          method: 'POST',
-          headers: { 'Content-Type': 'application/json' },
-          body: JSON.stringify(payload),
-          signal: AbortSignal.timeout(60000)
+        if (self.testParams && self.testParams !== '请上传舌苔图片 (MIME 类型表单)') payload = JSON.parse(self.testParams)
+        var res = await fetch(BASE + self.testingEndpoint.path, {
+          method: 'POST', headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify(payload), signal: AbortSignal.timeout(60000)
         })
-        var data = await res.json()
-        self.testResponse = JSON.stringify(data, null, 2)
-      } catch (e) {
-        self.testResponse = '错误: ' + e.message
-      } finally {
-        self.testing = false
-      }
+        self.testResponse = JSON.stringify(await res.json(), null, 2)
+      } catch (e) { self.testResponse = '错误: ' + e.message }
+      finally { self.testing = false }
     }
   }
 }
 </script>
 
 <style scoped>
-.page-container {
-  padding: 16px;
-  background: #f0f2f5;
-  min-height: 100vh;
+.lg-admin {
+  display: flex;
+  flex-direction: column;
+  height: calc(100vh - 60px);
+  overflow: hidden;
+  padding: 12px;
+  box-sizing: border-box;
 }
 
-/* 顶部状态栏 */
-.top-bar {
+.lg-top-bar {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-shrink: 0;
   margin-bottom: 12px;
 }
-.mini-card {
+.lg-stat {
   display: flex;
-  flex-direction: column;
   align-items: center;
-  justify-content: center;
-  padding: 12px 8px;
-  text-align: center;
-  cursor: default;
-  transition: all 0.3s;
-  border-left: 4px solid transparent;
-}
-.mini-card:hover {
-  transform: translateY(-2px);
-}
-.mini-card.status-healthy { border-left-color: #67c23a; }
-.mini-card.status-unhealthy { border-left-color: #f56c6c; }
-.mini-card.status-degraded { border-left-color: #e6a23c; }
-.mini-card.status-checking { border-left-color: #409eff; }
-.mini-icon {
-  font-size: 24px;
-  margin-bottom: 4px;
+  gap: 10px;
+  padding: 10px 16px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.08);
+  border-left: 3px solid transparent;
+  flex: 1;
+  min-width: 0;
 }
-.mini-text {
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-  white-space: nowrap;
+.lg-stat.status-healthy  { border-left-color: #67c23a; }
+.lg-stat.status-unhealthy { border-left-color: #f56c6c; }
+.lg-stat.status-degraded  { border-left-color: #e6a23c; }
+.lg-stat-icon { font-size: 22px; flex-shrink: 0; }
+.lg-stat-label { font-size: 11px; color: #909399; }
+.lg-stat-value { font-size: 16px; font-weight: bold; color: #303133; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.lg-stat-sub { font-size: 11px; color: #bbb; }
+.lg-actions { display: flex; gap: 8px; flex-shrink: 0; }
+
+.lg-tabs {
+  flex: 1;
   overflow: hidden;
-  text-overflow: ellipsis;
-  max-width: 100%;
+  display: flex;
+  flex-direction: column;
 }
-.mini-sub {
-  font-size: 11px;
-  color: #909399;
-  margin-top: 2px;
+.lg-tabs >>> .el-tabs__content {
+  flex: 1;
+  overflow: hidden;
+  padding: 12px;
 }
-.top-actions {
+.lg-tabs >>> .el-tab-pane {
+  height: 100%;
   display: flex;
-  align-items: center;
-  gap: 8px;
-  justify-content: flex-start;
-  padding-left: 16px;
+  flex-direction: column;
+  overflow: hidden;
 }
 
-/* 标签页 */
-.main-tabs {
+.lg-panel {
+  background: #fff;
   border-radius: 8px;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
+  margin-bottom: 12px;
+  display: flex;
+  flex-direction: column;
   overflow: hidden;
 }
-.tab-toolbar {
+.lg-panel-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  padding: 10px 16px;
+  border-bottom: 1px solid #ebeef5;
+  flex-shrink: 0;
+}
+.lg-desc {
+  flex: 1;
+  overflow: auto;
+  padding: 8px 12px;
+}
+
+.lg-toolbar {
   display: flex;
   align-items: center;
   gap: 8px;
-  margin-bottom: 12px;
+  margin-bottom: 8px;
+  flex-shrink: 0;
   flex-wrap: wrap;
 }
 
-/* 面板 */
-.panel {
-  margin-bottom: 16px;
+.lg-table {
+  flex: 1;
+  overflow: hidden;
 }
-
-/* 响应体 */
-.response-pre {
-  background: #f5f7fa;
-  padding: 12px;
-  border-radius: 4px;
-  font-size: 12px;
-  max-height: 400px;
+.lg-table-full {
+  flex: 1;
+  min-height: 0;
+}
+.lg-table >>> .el-table__body-wrapper {
   overflow-y: auto;
-  white-space: pre-wrap;
-  word-break: break-all;
 }
 
-/* 日志详情 */
-.log-detail-table {
+.lg-detail-table {
   margin-top: 8px;
-  border-radius: 4px;
-  overflow: hidden;
+  flex-shrink: 0;
 }
 .detail-pre {
   background: #1e1e1e;
   color: #d4d4d4;
-  padding: 8px;
+  padding: 6px 8px;
   border-radius: 4px;
   font-size: 11px;
-  max-height: 200px;
+  max-height: 160px;
   overflow-y: auto;
   white-space: pre-wrap;
   word-break: break-all;
   margin: 0;
 }
-.error-pre {
-  color: #f56c6c;
+.response-pre {
+  background: #f5f7fa;
+  padding: 12px;
+  border-radius: 4px;
+  font-size: 12px;
+  max-height: 300px;
+  overflow-y: auto;
+  white-space: pre-wrap;
+  word-break: break-all;
 }
 </style>