feat: 增加计数功能

This commit is contained in:
foxhui
2026-01-11 19:25:34 +08:00
Unverified
parent 3183f7bdc5
commit aa730e7f22
29 changed files with 446 additions and 30 deletions
+3 -3
View File
@@ -41,11 +41,11 @@
| [**Gemini Enterprise Business**](https://business.gemini.google/) | ✅ | ✅ | ✅ |
| [**Nano Banana Free**](https://nanobananafree.ai/) | 🚫 | ✅ | 🚫 |
| [**zAI**](https://zai.is/) | ✅ | ✅ | 🚫 |
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅ | ✅ |
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅💧 | ✅💧 |
| [**ZenMux**](https://zenmux.ai/) | ✅ | ❌ | 🚫 |
| [**ChatGPT**](https://chatgpt.com/) | ✅ | ✅ | 🚫 |
| [**DeepSeek**](https://chat.deepseek.com/) | ✅ | 🚫 | 🚫 |
| [**Sora**](https://sora.chatgpt.com/) | 🚫 | 🚫 | ✅ |
| [**Sora**](https://sora.chatgpt.com/) | 🚫 | 🚫 | ✅💧 |
| [**Google Flow**](https://labs.google/fx/zh/tools/flow) | 🚫 | ✅ | ❌ |
| [**豆包**](https://www.doubao.com/) | ✅ | ✅ | ❌ |
| 待续... | - | - | - |
@@ -53,7 +53,7 @@
> [!NOTE]
> **获取完整模型列表**: 通过 `GET /v1/models` 接口查看当前配置下所有可用模型及其详细信息。
>
> ✅目前支持;❌目前不支持,但未来可能会支持;🚫网站不支持, 未来是否在支持看网站具体情况;
> ✅目前支持;❌目前不支持,但未来可能会支持;🚫网站不支持, 未来是否在支持看网站具体情况;💧结果带水印且无法去除;
---
+3 -3
View File
@@ -44,11 +44,11 @@
| [**Gemini Enterprise Business**](https://business.gemini.google/) | ✅ | ✅ | ✅ |
| [**Nano Banana Free**](https://nanobananafree.ai/) | 🚫 | ✅ | 🚫 |
| [**zAI**](https://zai.is/) | ✅ | ✅ | 🚫 |
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅ | ✅ |
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅💧 | ✅💧 |
| [**ZenMux**](https://zenmux.ai/) | ✅ | ❌ | 🚫 |
| [**ChatGPT**](https://chatgpt.com/) | ✅ | ✅ | 🚫 |
| [**DeepSeek**](https://chat.deepseek.com/) | ✅ | 🚫 | 🚫 |
| [**Sora**](https://sora.chatgpt.com/) | 🚫 | 🚫 | ✅ |
| [**Sora**](https://sora.chatgpt.com/) | 🚫 | 🚫 | ✅💧 |
| [**Google Flow**](https://labs.google/fx/zh/tools/flow) | 🚫 | ✅ | ❌ |
| [**Doubao**](https://www.doubao.com/) | ✅ | ✅ | ❌ |
| To be continued... | - | - | - |
@@ -56,7 +56,7 @@
> [!NOTE]
> **Get full model list**: Use the `GET /v1/models` endpoint to view all available models and their details under the current configuration.
>
> ✅ Supported; ❌ Not currently supported, but may be in the future; 🚫 Website does not support, future support depends on the website's status;
> ✅ Supported; ❌ Not currently supported, but may be in the future; 🚫 Website does not support, future support depends on the website's status; 💧 Results contain watermarks that cannot be removed.
---
+38 -2
View File
@@ -35,6 +35,7 @@ import {
} from '../../../config/validator.js';
import { registry } from '../../../backend/registry.js';
import { sendRestartSignal, sendStopSignal, isUnderSupervisor, getVncInfo } from '../../../utils/ipc.js';
import { getTodayStats, getStatsRange, clearStatsRange } from '../../../utils/stats.js';
/**
* 读取请求体
@@ -395,18 +396,53 @@ export function createAdminRouter(context) {
// ==================== 统计与监控 ====================
// GET /admin/stats - 基本统计
// GET /admin/stats - 基本统计(包含今日成功/失败)
if (method === 'GET' && pathname === '/stats') {
const instances = config.backend?.pool?.instances || [];
const workers = config.backend?.pool?.workers || [];
const todayStats = getTodayStats();
sendJson(res, 200, {
instances: instances.length,
workers: workers.length
workers: workers.length,
success: todayStats.success,
failed: todayStats.failed
});
return;
}
// GET /admin/stats/range - 查询日期范围统计
if (method === 'GET' && pathname === '/stats/range') {
const url = new URL(req.url, `http://${req.headers.host}`);
const start = url.searchParams.get('start');
const end = url.searchParams.get('end');
if (!start || !end) {
sendApiError(res, { code: ERROR_CODES.INVALID_REQUEST_BODY, message: '缺少 start 或 end 参数' });
return;
}
const result = await getStatsRange(start, end);
sendJson(res, 200, result);
return;
}
// DELETE /admin/stats/range - 删除日期范围统计
if (method === 'DELETE' && pathname === '/stats/range') {
const url = new URL(req.url, `http://${req.headers.host}`);
const start = url.searchParams.get('start');
const end = url.searchParams.get('end');
if (!start || !end) {
sendApiError(res, { code: ERROR_CODES.INVALID_REQUEST_BODY, message: '缺少 start 或 end 参数' });
return;
}
const result = await clearStatsRange(start, end);
sendJson(res, 200, { success: true, deleted: result.deleted });
return;
}
// GET /admin/queue - 任务队列状态
if (method === 'GET' && pathname === '/queue') {
const queueStatus = queueManager.getStatus();
+6 -1
View File
@@ -14,6 +14,7 @@ import {
buildChatCompletionChunk
} from './respond.js';
import { ERROR_CODES } from './errors.js';
import { incrementSuccess, incrementFailed } from '../utils/stats.js';
/**
* @typedef {object} TaskContext
@@ -118,7 +119,8 @@ export function createQueueManager(queueConfig, callbacks) {
// 处理结果
if (result.error) {
// 生成失败:使用标准错误格式返回
// 生成失败:记录统计并返回错误
await incrementFailed();
sendApiError(res, {
code: ERROR_CODES.GENERATION_FAILED,
message: result.error,
@@ -141,6 +143,7 @@ export function createQueueManager(queueConfig, callbacks) {
finalContent = result.text || '生成失败';
}
logger.info('服务器', '结果已准备就绪', { id });
await incrementSuccess();
// 发送成功响应
logger.info('服务器', '准备发送响应...', { id, isStreaming, contentLength: finalContent.length });
@@ -159,6 +162,8 @@ export function createQueueManager(queueConfig, callbacks) {
// 清除心跳
if (heartbeatInterval) clearInterval(heartbeatInterval);
// 记录失败统计
await incrementFailed();
logger.error('服务器', '任务处理失败', { id, error: err.message });
sendApiError(res, {
code: ERROR_CODES.INTERNAL_ERROR,
+4
View File
@@ -25,6 +25,7 @@ const { getBackend } = await import('../backend/index.js');
const { logger } = await import('../utils/logger.js');
const { createQueueManager, createGlobalRouter } = await import('./index.js');
const { isUnderSupervisor } = await import('../utils/ipc.js');
const { loadTodayStats } = await import('../utils/stats.js');
// ==================== 初始化配置 ====================
@@ -131,6 +132,9 @@ const handleRequest = createGlobalRouter({
* @returns {Promise<void>}
*/
async function startServer() {
// 加载今日统计
await loadTodayStats();
// 登录模式提示
if (isLoginMode) {
logger.info('服务器', '登录模式已就绪,请在浏览器中完成登录操作');
+2 -2
View File
@@ -4,7 +4,7 @@
*
* - 环境变量:LOG_LEVEL=debug|info|warn|error
* - 输出格式:YYYY-MM-DD HH:mm:ss.SSS [LEVEL] [模块] 消息 | k=v ...
* - 日志文件:data/temp/system.log(超过 5MB 自动轮转)
* - 日志文件:data/logs/system.log(超过 5MB 自动轮转)
*/
import process from 'process';
@@ -23,7 +23,7 @@ const COLORS = {
};
// 日志文件配置
const LOG_DIR = path.join(process.cwd(), 'data', 'temp');
const LOG_DIR = path.join(process.cwd(), 'data', 'logs');
const LOG_FILE = path.join(LOG_DIR, 'system.log');
const LOG_FILE_OLD = path.join(LOG_DIR, 'system.log.old');
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
+183
View File
@@ -0,0 +1,183 @@
/**
* @fileoverview 请求统计管理模块
* @description 按日期存储成功/失败请求计数,支持日期范围查询和删除
*/
import { promises as fs } from 'fs';
import path from 'path';
// 日志目录
const LOG_DIR = path.join(process.cwd(), 'data', 'logs');
/**
* 获取指定日期的统计文件路径
* @param {string} date - YYYY-MM-DD 格式的日期
* @returns {string}
*/
function getStatsFilePath(date) {
return path.join(LOG_DIR, `stats_${date}.json`);
}
/**
* 获取今日日期字符串
* @returns {string} YYYY-MM-DD 格式
*/
function getTodayDateStr() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 内存缓存:今日统计
let todayStats = { success: 0, failed: 0 };
let todayDate = getTodayDateStr();
/**
* 确保日志目录存在
*/
async function ensureLogDir() {
try {
await fs.mkdir(LOG_DIR, { recursive: true });
} catch { /* 忽略已存在错误 */ }
}
/**
* 检查并切换日期(跨天时自动重置缓存)
*/
async function checkDateRollover() {
const currentDate = getTodayDateStr();
if (currentDate !== todayDate) {
// 保存昨日数据
await saveStats(todayDate, todayStats);
// 重置为新的一天
todayDate = currentDate;
todayStats = { success: 0, failed: 0 };
// 尝试加载今日已有数据
await loadTodayStats();
}
}
/**
* 保存统计到文件
* @param {string} date - 日期
* @param {object} stats - 统计数据
*/
async function saveStats(date, stats) {
await ensureLogDir();
const filePath = getStatsFilePath(date);
await fs.writeFile(filePath, JSON.stringify(stats, null, 2));
}
/**
* 加载今日统计(服务启动时调用)
*/
export async function loadTodayStats() {
await ensureLogDir();
todayDate = getTodayDateStr();
const filePath = getStatsFilePath(todayDate);
try {
const data = await fs.readFile(filePath, 'utf-8');
todayStats = JSON.parse(data);
} catch {
todayStats = { success: 0, failed: 0 };
}
return todayStats;
}
/**
* 增加成功计数
*/
export async function incrementSuccess() {
await checkDateRollover();
todayStats.success++;
await saveStats(todayDate, todayStats);
}
/**
* 增加失败计数
*/
export async function incrementFailed() {
await checkDateRollover();
todayStats.failed++;
await saveStats(todayDate, todayStats);
}
/**
* 获取今日统计
* @returns {{success: number, failed: number}}
*/
export function getTodayStats() {
// 检查是否跨天(同步版本,仅检查不保存)
const currentDate = getTodayDateStr();
if (currentDate !== todayDate) {
// 返回空数据,等待下次写入时触发跨天处理
return { success: 0, failed: 0 };
}
return { ...todayStats };
}
/**
* 获取日期范围内的汇总统计
* @param {string} startDate - 开始日期 YYYY-MM-DD
* @param {string} endDate - 结束日期 YYYY-MM-DD
* @returns {Promise<{success: number, failed: number, days: number}>}
*/
export async function getStatsRange(startDate, endDate) {
const result = { success: 0, failed: 0, days: 0 };
const start = new Date(startDate);
const end = new Date(endDate);
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const filePath = getStatsFilePath(dateStr);
try {
const data = await fs.readFile(filePath, 'utf-8');
const stats = JSON.parse(data);
result.success += stats.success || 0;
result.failed += stats.failed || 0;
result.days++;
} catch {
// 文件不存在,跳过
}
}
return result;
}
/**
* 删除日期范围内的统计文件
* @param {string} startDate - 开始日期 YYYY-MM-DD
* @param {string} endDate - 结束日期 YYYY-MM-DD
* @returns {Promise<{deleted: number}>}
*/
export async function clearStatsRange(startDate, endDate) {
let deleted = 0;
const start = new Date(startDate);
const end = new Date(endDate);
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const filePath = getStatsFilePath(dateStr);
try {
await fs.unlink(filePath);
deleted++;
// 如果删除的是今日文件,重置内存缓存
if (dateStr === todayDate) {
todayStats = { success: 0, failed: 0 };
}
} catch {
// 文件不存在,跳过
}
}
return { deleted };
}
@@ -1 +1 @@
import{c as i,I as u}from"./index-CeQVA4cs.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"}}]},name:"desktop",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return i(u,c({},n,{icon:l}),null)};o.displayName="DesktopOutlined";o.inheritAttrs=!1;export{o as D};
import{c as i,I as u}from"./index-BQYktfbJ.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"}}]},name:"desktop",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return i(u,c({},n,{icon:l}),null)};o.displayName="DesktopOutlined";o.inheritAttrs=!1;export{o as D};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.log-container[data-v-6c3b8e99]{max-height:600px;overflow-y:auto;font-family:Consolas,Monaco,monospace;font-size:12px;background:#fafafa;border-radius:4px;padding:12px}.log-line[data-v-6c3b8e99]{padding:4px 0;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.log-line[data-v-6c3b8e99]:hover{background:#e6f7ff;white-space:normal;word-break:break-all}.log-time[data-v-6c3b8e99]{color:#8c8c8c}.log-module[data-v-6c3b8e99]{color:#1890ff;margin-right:8px}.log-message[data-v-6c3b8e99]{color:#333}.level-erro .log-message[data-v-6c3b8e99]{color:#ff4d4f}.level-warn .log-message[data-v-6c3b8e99]{color:#faad14}.level-dbug .log-message[data-v-6c3b8e99]{color:#722ed1}.toolbar[data-v-6c3b8e99]{margin-bottom:16px}.toolbar-row[data-v-6c3b8e99]{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}.toolbar-row[data-v-6c3b8e99]:last-child{margin-bottom:0}@media(min-width:768px){.toolbar[data-v-6c3b8e99]{display:flex;justify-content:space-between;align-items:center;gap:12px}.toolbar-row[data-v-6c3b8e99]{margin-bottom:0}.toolbar-row[data-v-6c3b8e99]:last-child{flex:1;max-width:300px}}
-2
View File
@@ -1,2 +0,0 @@
import{c as l,I as P,_ as q,j as J,r as p,l as Q,o as X,a as Y,b as N,d as b,w as o,g as v,f,h as m,u as C,R as k,y as Z,D as K,i as L,e as S,t as _,m as ee,F as te,s as R,M as ae,z as le,A as ne,B as oe}from"./index-CeQVA4cs.js";var se={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M304 280h56c4.4 0 8-3.6 8-8 0-28.3 5.9-53.2 17.1-73.5 10.6-19.4 26-34.8 45.4-45.4C450.9 142 475.7 136 504 136h16c28.3 0 53.2 5.9 73.5 17.1 19.4 10.6 34.8 26 45.4 45.4C650 218.9 656 243.7 656 272c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1a184.31 184.31 0 00-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9a184.31 184.31 0 00-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z"}},{tag:"path",attrs:{d:"M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8a63 63 0 01-63 63H232a63 63 0 01-63-63c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5.2 13 .7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7a273 273 0 0022.7 49c24.3 41.5 59 76.2 100.5 100.5S460.5 960 512 960s99.8-13.9 141.3-38.2a281.38 281.38 0 00123.2-149.5A120 120 0 01836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7.4-6.4.7-12.8.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM716 680c0 36.8-9.7 72-27.8 102.9-17.7 30.3-43 55.6-73.3 73.3C584 874.3 548.8 884 512 884s-72-9.7-102.9-27.8c-30.3-17.7-55.6-43-73.3-73.3A202.75 202.75 0 01308 680V412h408v268z"}}]},name:"bug",theme:"outlined"};function D(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){re(n,c,a[c])})}return n}function re(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var j=function(t,a){var s=D({},t,a.attrs);return l(P,D({},s,{icon:se}),null)};j.displayName="BugOutlined";j.inheritAttrs=!1;var ce={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"}}]},name:"warning",theme:"outlined"};function E(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){ie(n,c,a[c])})}return n}function ie(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var I=function(t,a){var s=E({},t,a.attrs);return l(P,E({},s,{icon:ce}),null)};I.displayName="WarningOutlined";I.inheritAttrs=!1;const ue={class:"toolbar"},de={class:"toolbar-row"},fe={class:"toolbar-row"},ve={style:{"margin-bottom":"12px",color:"#8c8c8c","font-size":"12px"}},me={key:0,style:{color:"#1890ff","margin-left":"8px"}},pe={class:"log-container"},ge={class:"log-time"},_e={class:"log-module"},he={class:"log-message"},Oe={__name:"logs",setup(n){const t=J(),a=p([]),s=p(!1),c=p(0),h=p(!1),g=p(null),y=p(""),w=p("all"),U={INFO:{color:"#1890ff",icon:oe},WARN:{color:"#faad14",icon:I},ERRO:{color:"#ff4d4f",icon:ne},DBUG:{color:"#722ed1",icon:j}},z=async()=>{s.value=!0;try{const r=await fetch("/admin/logs?lines=500",{headers:t.getHeaders()});if(r.ok){const e=await r.json();a.value=A(e.logs||[]),c.value=e.total||0}}catch{R.error("获取日志失败")}finally{s.value=!1}},A=r=>r.map((e,i)=>{const d=e.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[([^\]]+)\] (.*)$/);return d?{id:i,time:d[1],level:d[2],module:d[3],message:d[4],raw:e}:{id:i,raw:e,level:"INFO",time:"",module:"",message:e}}),x=Q(()=>a.value.filter(e=>{if(w.value!=="all"&&e.level!==w.value)return!1;if(y.value){const i=y.value.toLowerCase();return e.raw.toLowerCase().includes(i)}return!0}).reverse()),F=()=>{ae.confirm({title:"确认清除日志",content:"此操作将删除所有系统日志文件,是否继续?",okText:"确认清除",okType:"danger",cancelText:"取消",async onOk(){try{(await fetch("/admin/logs",{method:"DELETE",headers:t.getHeaders()})).ok?(R.success("日志已清除"),a.value=[],c.value=0):R.error("清除失败")}catch{R.error("请求失败")}}})},M=()=>{const r=a.value.map(O=>O.raw).join(`
`),e=new Blob([r],{type:"text/plain"}),i=URL.createObjectURL(e),d=document.createElement("a");d.href=i,d.download=`system-${new Date().toISOString().split("T")[0]}.log`,d.click(),URL.revokeObjectURL(i)},T=r=>{h.value=r,r?(z(),g.value=setInterval(z,5e3)):g.value&&(clearInterval(g.value),g.value=null)};return X(()=>{z()}),Y(()=>{g.value&&clearInterval(g.value)}),(r,e)=>{const i=f("a-select-option"),d=f("a-select"),O=f("a-button"),B=f("a-tooltip"),V=f("a-space"),W=f("a-input-search"),$=f("a-tag"),H=f("a-empty"),G=f("a-card");return b(),N(G,{title:"系统日志",bordered:!1},{default:o(()=>[v("div",ue,[v("div",de,[l(d,{value:w.value,"onUpdate:value":e[0]||(e[0]=u=>w.value=u),style:{width:"90px"},size:"small"},{default:o(()=>[l(i,{value:"all"},{default:o(()=>[...e[3]||(e[3]=[m("全部",-1)])]),_:1}),l(i,{value:"INFO"},{default:o(()=>[...e[4]||(e[4]=[m("INFO",-1)])]),_:1}),l(i,{value:"WARN"},{default:o(()=>[...e[5]||(e[5]=[m("WARN",-1)])]),_:1}),l(i,{value:"ERRO"},{default:o(()=>[...e[6]||(e[6]=[m("ERROR",-1)])]),_:1}),l(i,{value:"DBUG"},{default:o(()=>[...e[7]||(e[7]=[m("DEBUG",-1)])]),_:1})]),_:1},8,["value"]),l(V,{size:4},{default:o(()=>[l(B,{title:h.value?"关闭自动刷新":"开启自动刷新"},{default:o(()=>[l(O,{size:"small",type:h.value?"primary":"default",onClick:e[1]||(e[1]=u=>T(!h.value))},{icon:o(()=>[l(C(k))]),_:1},8,["type"])]),_:1},8,["title"]),l(B,{title:"导出日志"},{default:o(()=>[l(O,{size:"small",onClick:M},{icon:o(()=>[l(C(Z))]),_:1})]),_:1}),l(B,{title:"清除日志"},{default:o(()=>[l(O,{size:"small",danger:"",onClick:F},{icon:o(()=>[l(C(K))]),_:1})]),_:1})]),_:1})]),v("div",fe,[l(W,{value:y.value,"onUpdate:value":e[2]||(e[2]=u=>y.value=u),placeholder:"搜索日志",size:"small","enter-button":"","allow-clear":"",style:{width:"100%"}},null,8,["value"])])]),v("div",ve,[m(" 共 "+_(c.value)+" 条日志,当前显示 "+_(x.value.length)+" 条 ",1),h.value?(b(),L("span",me,[l(C(k),{spin:!0}),e[8]||(e[8]=m(" 自动刷新中 ",-1))])):S("",!0)]),v("div",pe,[(b(!0),L(te,null,ee(x.value,u=>(b(),L("div",{key:u.id,class:le(["log-line","level-"+u.level.toLowerCase()])},[v("span",ge,_(u.time),1),l($,{color:U[u.level]?.color||"#8c8c8c",size:"small",style:{margin:"0 8px"}},{default:o(()=>[m(_(u.level),1)]),_:2},1032,["color"]),v("span",_e,"["+_(u.module)+"]",1),v("span",he,_(u.message),1)],2))),128)),x.value.length===0?(b(),N(H,{key:0,description:"暂无日志"})):S("",!0)])]),_:1})}}},ye=q(Oe,[["__scopeId","data-v-6c3b8e99"]]);export{ye as default};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.log-container[data-v-f89c259b]{max-height:600px;overflow-y:auto;font-family:Consolas,Monaco,monospace;font-size:12px;background:#fafafa;border-radius:4px;padding:12px}.log-line[data-v-f89c259b]{padding:4px 0;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.log-line[data-v-f89c259b]:hover{background:#e6f7ff;white-space:normal;word-break:break-all}.log-time[data-v-f89c259b]{color:#8c8c8c}.log-module[data-v-f89c259b]{color:#1890ff;margin-right:8px}.log-message[data-v-f89c259b]{color:#333}.level-erro .log-message[data-v-f89c259b]{color:#ff4d4f}.level-warn .log-message[data-v-f89c259b]{color:#faad14}.level-dbug .log-message[data-v-f89c259b]{color:#722ed1}.toolbar[data-v-f89c259b]{margin-bottom:16px}.toolbar-row[data-v-f89c259b]{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}.toolbar-row[data-v-f89c259b]:last-child{margin-bottom:0}@media(min-width:768px){.toolbar[data-v-f89c259b]{display:flex;justify-content:space-between;align-items:center;gap:12px}.toolbar-row[data-v-f89c259b]{margin-bottom:0}.toolbar-row[data-v-f89c259b]:last-child{flex:1;max-width:300px}}.stats-content[data-v-f89c259b]{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.stats-numbers[data-v-f89c259b]{display:flex;align-items:center;gap:20px}.stat-item[data-v-f89c259b]{display:flex;align-items:center;gap:6px;padding:4px 12px;background:#fafafa;border-radius:6px;transition:all .2s}.stat-item[data-v-f89c259b]:hover{background:#f0f0f0}.stat-item.success[data-v-f89c259b]{color:#52c41a}.stat-item.error[data-v-f89c259b]{color:#ff4d4f}.stat-item.neutral[data-v-f89c259b]{color:#8c8c8c}.stat-value[data-v-f89c259b]{font-size:18px;font-weight:600;font-family:SF Mono,Monaco,monospace}.stat-label[data-v-f89c259b]{font-size:12px;color:#8c8c8c}@media(max-width:576px){.stats-content[data-v-f89c259b]{flex-direction:column;align-items:flex-start}.stats-content .ant-divider[data-v-f89c259b]{display:none}.stats-numbers[data-v-f89c259b]{margin-top:8px}}
+1
View File
@@ -0,0 +1 @@
import{_ as b,k as w,l as k,o as c,b as C,d as S,w as n,c as o,g as e,f as a,h as i}from"./index-BQYktfbJ.js";const B={style:{"margin-bottom":"8px"}},T={style:{"margin-bottom":"8px"}},z={style:{"margin-bottom":"8px"}},U={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},M={style:{"margin-bottom":"8px"}},j={style:{"margin-bottom":"8px"}},q={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},L={__name:"server",setup(N){const d=w(),s=k({port:5173,authToken:"",keepaliveMode:"comment",queueBuffer:2,imageLimit:5});c(async()=>{await d.fetchServerConfig(),Object.assign(s,d.serverConfig)});const m=async()=>{await d.saveServerConfig(s)};return(V,t)=>{const p=a("a-input-number"),r=a("a-col"),y=a("a-input-password"),u=a("a-select-option"),g=a("a-select"),f=a("a-row"),v=a("a-button"),x=a("a-card"),_=a("a-layout");return S(),C(_,{style:{background:"transparent"}},{default:n(()=>[o(x,{title:"服务器设置",bordered:!1,style:{width:"100%"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",B,[t[5]||(t[5]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"监听端口",-1)),t[6]||(t[6]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 设置服务器监听的端口号,默认为 5173 ",-1)),o(p,{value:s.port,"onUpdate:value":t[0]||(t[0]=l=>s.port=l),min:1,max:65535,placeholder:"请输入端口号",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",T,[t[7]||(t[7]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"鉴权 Token",-1)),t[8]||(t[8]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 用于 API 请求鉴权的密钥,留空则不启用鉴权 ",-1)),o(y,{value:s.authToken,"onUpdate:value":t[1]||(t[1]=l=>s.authToken=l),placeholder:"请输入 Token",type:"password"},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",z,[t[11]||(t[11]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"心跳包类型",-1)),t[12]||(t[12]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 选择 SSE 流式响应的心跳包格式 ",-1)),o(g,{value:s.keepaliveMode,"onUpdate:value":t[2]||(t[2]=l=>s.keepaliveMode=l),style:{width:"100%"},placeholder:"请选择心跳包类型"},{default:n(()=>[o(u,{value:"comment"},{default:n(()=>[...t[9]||(t[9]=[i("Comment - 注释格式",-1)])]),_:1}),o(u,{value:"content"},{default:n(()=>[...t[10]||(t[10]=[i("Content - 内容格式",-1)])]),_:1})]),_:1},8,["value"])])]),_:1})]),_:1}),e("div",U,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[13]||(t[13]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1}),o(x,{title:"队列设置",bordered:!1,style:{width:"100%","margin-top":"10px"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",M,[t[14]||(t[14]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"队列缓冲区大小",-1)),t[15]||(t[15]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 非流式请求的额外排队数(设为 0 则不限制非流式请求数量)"),e("br"),i(" 实际队列上限 = Workers数量 + 缓冲区大小 ")],-1)),o(p,{value:s.queueBuffer,"onUpdate:value":t[3]||(t[3]=l=>s.queueBuffer=l),min:0,max:100,placeholder:"默认为 2",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",j,[t[16]||(t[16]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"图片数量上限",-1)),t[17]||(t[17]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 单次请求最多支持的图片附件数量"),e("br"),i(" 网页最多支持10个附件,超出会被丢弃 ")],-1)),o(p,{value:s.imageLimit,"onUpdate:value":t[4]||(t[4]=l=>s.imageLimit=l),min:1,max:10,placeholder:"默认为 5",style:{width:"100%"}},null,8,["value"])])]),_:1})]),_:1}),e("div",q,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[18]||(t[18]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1})]),_:1})}}},A=b(L,[["__scopeId","data-v-bd32923f"]]);export{A as default};
-1
View File
@@ -1 +0,0 @@
import{_ as b,j as w,k,o as c,b as C,d as S,w as n,c as o,g as e,f as a,h as i}from"./index-CeQVA4cs.js";const B={style:{"margin-bottom":"8px"}},T={style:{"margin-bottom":"8px"}},z={style:{"margin-bottom":"8px"}},U={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},j={style:{"margin-bottom":"8px"}},M={style:{"margin-bottom":"8px"}},q={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},L={__name:"server",setup(N){const d=w(),s=k({port:5173,authToken:"",keepaliveMode:"comment",queueBuffer:2,imageLimit:5});c(async()=>{await d.fetchServerConfig(),Object.assign(s,d.serverConfig)});const m=async()=>{await d.saveServerConfig(s)};return(V,t)=>{const p=a("a-input-number"),r=a("a-col"),y=a("a-input-password"),u=a("a-select-option"),g=a("a-select"),f=a("a-row"),v=a("a-button"),x=a("a-card"),_=a("a-layout");return S(),C(_,{style:{background:"transparent"}},{default:n(()=>[o(x,{title:"服务器设置",bordered:!1,style:{width:"100%"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",B,[t[5]||(t[5]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"监听端口",-1)),t[6]||(t[6]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 设置服务器监听的端口号,默认为 5173 ",-1)),o(p,{value:s.port,"onUpdate:value":t[0]||(t[0]=l=>s.port=l),min:1,max:65535,placeholder:"请输入端口号",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",T,[t[7]||(t[7]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"鉴权 Token",-1)),t[8]||(t[8]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 用于 API 请求鉴权的密钥,留空则不启用鉴权 ",-1)),o(y,{value:s.authToken,"onUpdate:value":t[1]||(t[1]=l=>s.authToken=l),placeholder:"请输入 Token",type:"password"},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",z,[t[11]||(t[11]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"心跳包类型",-1)),t[12]||(t[12]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 选择 SSE 流式响应的心跳包格式 ",-1)),o(g,{value:s.keepaliveMode,"onUpdate:value":t[2]||(t[2]=l=>s.keepaliveMode=l),style:{width:"100%"},placeholder:"请选择心跳包类型"},{default:n(()=>[o(u,{value:"comment"},{default:n(()=>[...t[9]||(t[9]=[i("Comment - 注释格式",-1)])]),_:1}),o(u,{value:"content"},{default:n(()=>[...t[10]||(t[10]=[i("Content - 内容格式",-1)])]),_:1})]),_:1},8,["value"])])]),_:1})]),_:1}),e("div",U,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[13]||(t[13]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1}),o(x,{title:"队列设置",bordered:!1,style:{width:"100%","margin-top":"10px"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",j,[t[14]||(t[14]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"队列缓冲区大小",-1)),t[15]||(t[15]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 非流式请求的额外排队数(设为 0 则不限制非流式请求数量)"),e("br"),i(" 实际队列上限 = Workers数量 + 缓冲区大小 ")],-1)),o(p,{value:s.queueBuffer,"onUpdate:value":t[3]||(t[3]=l=>s.queueBuffer=l),min:0,max:100,placeholder:"默认为 2",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",M,[t[16]||(t[16]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"图片数量上限",-1)),t[17]||(t[17]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 单次请求最多支持的图片附件数量"),e("br"),i(" 网页最多支持10个附件,超出会被丢弃 ")],-1)),o(p,{value:s.imageLimit,"onUpdate:value":t[4]||(t[4]=l=>s.imageLimit=l),min:1,max:10,placeholder:"默认为 5",style:{width:"100%"}},null,8,["value"])])]),_:1})]),_:1}),e("div",q,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[18]||(t[18]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1})]),_:1})}}},A=b(L,[["__scopeId","data-v-bd32923f"]]);export{A as default};
-1
View File
@@ -1 +0,0 @@
import{x as i,j as a,s as r}from"./index-CeQVA4cs.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},safeMode:{enabled:!1,reason:null},stats:{totalRequests:0,successRate:0,activeWorkers:0,totalWorkers:0,avgResponseTime:0}}),actions:{async fetchStatus(){const t=a();try{const e=await fetch("/admin/status",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.$patch(s)}}catch(e){console.error("Failed to fetch system status:",e)}},async fetchStats(){const t=a();try{const e=await fetch("/admin/stats",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.stats=s}}catch(e){console.error("Failed to fetch stats:",e)}},async restartService(t={}){const e=a(),{loginMode:s,workerName:n}=t;try{const o=await(await fetch("/admin/restart",{method:"POST",headers:{...e.getHeaders(),"Content-Type":"application/json"},body:JSON.stringify({loginMode:s,workerName:n})})).json();return o.success?(r.success(o.message||"服务重启中..."),!0):(r.error("重启失败"),!1)}catch{return r.error("重启请求失败"),!1}},async stopService(){const t=a();try{const s=await(await fetch("/admin/stop",{method:"POST",headers:t.getHeaders()})).json();return s.success?(r.success(s.message||"服务停止中..."),!0):(r.error("停止失败"),!1)}catch{return r.error("停止请求失败"),!1}}}});export{u};
+1
View File
@@ -0,0 +1 @@
import{y as i,k as a,v as r}from"./index-BQYktfbJ.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},safeMode:{enabled:!1,reason:null},stats:{totalRequests:0,successRate:0,activeWorkers:0,totalWorkers:0,avgResponseTime:0,success:0,failed:0}}),actions:{async fetchStatus(){const t=a();try{const e=await fetch("/admin/status",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.$patch(s)}}catch(e){console.error("Failed to fetch system status:",e)}},async fetchStats(){const t=a();try{const e=await fetch("/admin/stats",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.stats=s}}catch(e){console.error("Failed to fetch stats:",e)}},async restartService(t={}){const e=a(),{loginMode:s,workerName:n}=t;try{const o=await(await fetch("/admin/restart",{method:"POST",headers:{...e.getHeaders(),"Content-Type":"application/json"},body:JSON.stringify({loginMode:s,workerName:n})})).json();return o.success?(r.success(o.message||"服务重启中..."),!0):(r.error("重启失败"),!1)}catch{return r.error("重启请求失败"),!1}},async stopService(){const t=a();try{const s=await(await fetch("/admin/stop",{method:"POST",headers:t.getHeaders()})).json();return s.success?(r.success(s.message||"服务停止中..."),!0):(r.error("停止失败"),!1)}catch{return r.error("停止请求失败"),!1}}}});export{u};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebAI2API</title>
<script type="module" crossorigin src="/assets/index-CeQVA4cs.js"></script>
<script type="module" crossorigin src="/assets/index-BQYktfbJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BVr8U7Bl.css">
</head>
+18 -1
View File
@@ -10,7 +10,8 @@ import {
LineChartOutlined,
SyncOutlined,
ExclamationCircleOutlined,
CheckCircleOutlined
CheckCircleOutlined,
CloseCircleOutlined
} from '@ant-design/icons-vue';
const systemStore = useSystemStore();
@@ -205,6 +206,22 @@ onUnmounted(() => {
</a-statistic>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px">
<a-col :span="12">
<a-statistic title="今日成功" :value="systemStore.stats.success || 0">
<template #prefix>
<CheckCircleOutlined style="color: #52c41a" />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic title="今日失败" :value="systemStore.stats.failed || 0">
<template #prefix>
<CloseCircleOutlined style="color: #ff4d4f" />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
+171 -2
View File
@@ -9,7 +9,8 @@ import {
WarningOutlined,
CloseCircleOutlined,
InfoCircleOutlined,
BugOutlined
BugOutlined,
CheckCircleOutlined
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
@@ -23,6 +24,11 @@ const refreshInterval = ref(null);
const searchText = ref('');
const levelFilter = ref('all');
// 统计查询相关
const dateRange = ref([]);
const rangeStats = ref({ success: 0, failed: 0, days: 0 });
const statsLoading = ref(false);
// 日志级别配置
const levelConfig = {
'INFO': { color: '#1890ff', icon: InfoCircleOutlined },
@@ -141,6 +147,61 @@ const toggleAutoRefresh = (newState) => {
}
};
// 查询日期范围统计
const fetchRangeStats = async () => {
if (!dateRange.value || dateRange.value.length !== 2) {
rangeStats.value = { success: 0, failed: 0, days: 0 };
return;
}
statsLoading.value = true;
try {
const [start, end] = dateRange.value;
const res = await fetch(
`/admin/stats/range?start=${start.format('YYYY-MM-DD')}&end=${end.format('YYYY-MM-DD')}`,
{ headers: settingsStore.getHeaders() }
);
if (res.ok) {
rangeStats.value = await res.json();
}
} catch (e) {
message.error('获取统计失败');
} finally {
statsLoading.value = false;
}
};
// 删除选定范围的统计数据
const clearRangeStats = () => {
if (!dateRange.value || dateRange.value.length !== 2) {
message.warning('请先选择日期范围');
return;
}
Modal.confirm({
title: '确认删除',
content: `确定要删除 ${dateRange.value[0].format('YYYY-MM-DD')}${dateRange.value[1].format('YYYY-MM-DD')} 的统计数据吗?`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
const [start, end] = dateRange.value;
const res = await fetch(
`/admin/stats/range?start=${start.format('YYYY-MM-DD')}&end=${end.format('YYYY-MM-DD')}`,
{ method: 'DELETE', headers: settingsStore.getHeaders() }
);
if (res.ok) {
message.success('统计数据已删除');
rangeStats.value = { success: 0, failed: 0, days: 0 };
}
} catch (e) {
message.error('删除失败');
}
}
});
};
onMounted(() => {
fetchLogs();
});
@@ -153,7 +214,47 @@ onUnmounted(() => {
</script>
<template>
<a-card title="系统日志" :bordered="false">
<!-- 统计查询面板 -->
<a-card title="请求统计" :bordered="false">
<template #extra>
<a-button type="link" danger size="small" @click="clearRangeStats"
:disabled="!dateRange || dateRange.length !== 2">
<template #icon>
<DeleteOutlined />
</template>
删除统计
</a-button>
</template>
<div class="stats-content">
<a-range-picker v-model:value="dateRange" :format="'YYYY-MM-DD'" :placeholder="['开始日期', '结束日期']"
size="small" style="width: 240px" @change="fetchRangeStats" />
<a-divider type="vertical" style="height: 32px; margin: 0 16px" />
<a-spin :spinning="statsLoading" size="small">
<div class="stats-numbers">
<div class="stat-item success">
<CheckCircleOutlined />
<span class="stat-value">{{ rangeStats.success }}</span>
<span class="stat-label">成功</span>
</div>
<div class="stat-item error">
<CloseCircleOutlined />
<span class="stat-value">{{ rangeStats.failed }}</span>
<span class="stat-label">失败</span>
</div>
<div class="stat-item neutral">
<span class="stat-value">{{ rangeStats.days }}</span>
<span class="stat-label"></span>
</div>
</div>
</a-spin>
</div>
</a-card>
<!-- 系统日志 -->
<a-card title="系统日志" :bordered="false" style="margin-top: 24px">
<!-- 工具栏 -->
<div class="toolbar">
<!-- 第一行级别筛选和操作按钮 -->
@@ -305,4 +406,72 @@ onUnmounted(() => {
max-width: 300px;
}
}
/* 统计内容样式 */
.stats-content {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.stats-numbers {
display: flex;
align-items: center;
gap: 20px;
}
.stat-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: #fafafa;
border-radius: 6px;
transition: all 0.2s;
}
.stat-item:hover {
background: #f0f0f0;
}
.stat-item.success {
color: #52c41a;
}
.stat-item.error {
color: #ff4d4f;
}
.stat-item.neutral {
color: #8c8c8c;
}
.stat-value {
font-size: 18px;
font-weight: 600;
font-family: 'SF Mono', 'Monaco', monospace;
}
.stat-label {
font-size: 12px;
color: #8c8c8c;
}
/* 响应式:小屏幕统计面板垂直布局 */
@media (max-width: 576px) {
.stats-content {
flex-direction: column;
align-items: flex-start;
}
.stats-content .ant-divider {
display: none;
}
.stats-numbers {
margin-top: 8px;
}
}
</style>
+3 -1
View File
@@ -28,7 +28,9 @@ export const useSystemStore = defineStore('system', {
successRate: 0,
activeWorkers: 0,
totalWorkers: 0,
avgResponseTime: 0
avgResponseTime: 0,
success: 0,
failed: 0
}
}),