|
|
@@ -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>
|