mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 完善 WebUI 功能
This commit is contained in:
@@ -5,12 +5,30 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.2.1] - 2025-12-20
|
||||
|
||||
### ✨ Added
|
||||
- **WebUI**
|
||||
- 完善 WebUI 功能,添加接口测试和日志查看器,优化部分布局
|
||||
- **日志记录**
|
||||
- 会在 data/temp 文件夹下记录日志(最大5MB轮转)
|
||||
|
||||
### 🔄 Changed
|
||||
- **初始化失败逻辑**
|
||||
- 程序初始化失败后不会直接推出,以便利用 WebUI 修改错误的配置
|
||||
- **LMArena 图片适配器**
|
||||
- 支持通过配置直接返回图片URL (但其他不支持的适配器仍然会返回 Base64)
|
||||
|
||||
## [3.2.0] - 2025-12-19
|
||||
|
||||
### ✨ Added
|
||||
- **WebUI**
|
||||
- 为项目添加了网页版管理工具,便于修改配置文件(可能会有问题,可随时反馈)
|
||||
|
||||
- **增加看门狗**
|
||||
- 增加看门狗机制(Supervisor),保证程序失败重载和利于利用 WebUI 完整重启程序
|
||||
- 同时将 Linux 上的虚拟显示器和 VNC 服务器启动程序也迁移至看门狗机制
|
||||
|
||||
## [3.1.0] - 2025-12-17
|
||||
|
||||
### ✨ Added
|
||||
|
||||
@@ -114,6 +114,10 @@ backend:
|
||||
# 入口URL
|
||||
# 示例: "https://business.gemini.google/home/cid/8888a888-b6e0-88be-86e1-888cf3ee8cf4"
|
||||
entryUrl: ""
|
||||
# Lmarena 配置
|
||||
lmarena:
|
||||
# 开启后直接返回图片 URL (但其他不支持的适配器仍然会返回 Base64)
|
||||
returnUrl: false
|
||||
|
||||
|
||||
queue:
|
||||
|
||||
@@ -136,6 +136,13 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
// 9. 提取图片 URL
|
||||
const img = extractImage(content);
|
||||
if (img) {
|
||||
// 检查是否配置了返回 URL
|
||||
const returnUrl = config?.backend?.adapter?.lmarena?.returnUrl || false;
|
||||
if (returnUrl) {
|
||||
logger.info('适配器', '已获取结果,返回 URL', meta);
|
||||
return { image: img };
|
||||
}
|
||||
|
||||
logger.info('适配器', '已获取结果,正在下载图片...', meta);
|
||||
const result = await downloadImage(img, context);
|
||||
if (result.image) {
|
||||
@@ -170,6 +177,17 @@ export const manifest = {
|
||||
id: 'lmarena',
|
||||
displayName: 'LMArena',
|
||||
|
||||
// 配置项模式
|
||||
configSchema: [
|
||||
{
|
||||
key: 'returnUrl',
|
||||
label: '返回图片 URL',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
note: '开启后直接返回图片 URL (但其他不支持的适配器仍然会返回 Base64)'
|
||||
}
|
||||
],
|
||||
|
||||
// 入口 URL
|
||||
getTargetUrl(config, workerConfig) {
|
||||
return TARGET_URL;
|
||||
|
||||
@@ -59,7 +59,7 @@ async function readBody(req) {
|
||||
* @returns {Function} Admin 路由处理函数
|
||||
*/
|
||||
export function createAdminRouter(context) {
|
||||
const { config, queueManager, tempDir } = context;
|
||||
const { config, queueManager, tempDir, getSafeMode } = context;
|
||||
|
||||
/**
|
||||
* Admin 路由处理函数
|
||||
@@ -76,7 +76,8 @@ export function createAdminRouter(context) {
|
||||
// GET /admin/status - 系统状态
|
||||
if (method === 'GET' && pathname === '/status') {
|
||||
const status = getSystemStatus();
|
||||
sendJson(res, 200, status);
|
||||
const safeMode = getSafeMode?.() || { enabled: false, reason: null };
|
||||
sendJson(res, 200, { ...status, safeMode });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,6 +176,27 @@ export function createAdminRouter(context) {
|
||||
return;
|
||||
}
|
||||
|
||||
// GET /admin/logs - 读取系统日志
|
||||
if (method === 'GET' && pathname === '/logs') {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const lines = parseInt(url.searchParams.get('lines') || '200', 10);
|
||||
const result = logger.readLogs(lines);
|
||||
sendJson(res, 200, result);
|
||||
return;
|
||||
}
|
||||
|
||||
// DELETE /admin/logs - 清除系统日志
|
||||
if (method === 'DELETE' && pathname === '/logs') {
|
||||
const success = logger.clearLogs();
|
||||
if (success) {
|
||||
logger.info('管理器', '系统日志已清除');
|
||||
sendJson(res, 200, { success: true, message: '日志已清除' });
|
||||
} else {
|
||||
sendJson(res, 500, { success: false, message: '日志清除失败' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// GET /admin/data-folders - 列出数据文件夹
|
||||
if (method === 'GET' && pathname === '/data-folders') {
|
||||
const workers = config.backend?.pool?.workers || [];
|
||||
|
||||
+14
-2
@@ -36,14 +36,14 @@ const WEBUI_DIR = path.join(process.cwd(), 'webui', 'dist');
|
||||
* @returns {Function} 请求处理函数
|
||||
*/
|
||||
export function createGlobalRouter(context) {
|
||||
const { authToken, config, queueManager, tempDir, loginMode } = context;
|
||||
const { authToken, config, queueManager, tempDir, loginMode, getSafeMode } = context;
|
||||
|
||||
// 创建鉴权中间件
|
||||
const checkAuth = createAuthMiddleware(authToken);
|
||||
|
||||
// 创建子路由处理器
|
||||
const handleOpenAIRequest = loginMode ? null : createOpenAIRouter(context);
|
||||
const handleAdminRequest = createAdminRouter({ config, queueManager, tempDir });
|
||||
const handleAdminRequest = createAdminRouter({ config, queueManager, tempDir, getSafeMode });
|
||||
|
||||
/**
|
||||
* 主路由处理函数
|
||||
@@ -100,6 +100,18 @@ export function createGlobalRouter(context) {
|
||||
|
||||
// OpenAI API (/v1)
|
||||
if (pathname.startsWith('/v1')) {
|
||||
// 安全模式下禁用 OpenAI API
|
||||
const safeMode = getSafeMode?.();
|
||||
if (safeMode?.enabled) {
|
||||
res.writeHead(503, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: {
|
||||
message: `服务运行在安全模式,OpenAI API 不可用。原因: ${safeMode.reason}`,
|
||||
type: 'service_unavailable'
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
// 登录模式下禁用 OpenAI API
|
||||
if (!handleOpenAIRequest) {
|
||||
res.writeHead(503, { 'Content-Type': 'application/json' });
|
||||
|
||||
+18
-4
@@ -101,6 +101,16 @@ const queueManager = createQueueManager(
|
||||
*/
|
||||
const isLoginMode = process.argv.some(arg => arg.startsWith('-login'));
|
||||
|
||||
/**
|
||||
* 安全模式状态
|
||||
* 当 Pool 初始化失败时进入安全模式,此时:
|
||||
* - HTTP 服务器正常启动
|
||||
* - Admin API 和 WebUI 可用
|
||||
* - OpenAI API 返回 503
|
||||
*/
|
||||
let safeMode = false;
|
||||
let safeModeReason = null;
|
||||
|
||||
const handleRequest = createGlobalRouter({
|
||||
authToken: AUTH_TOKEN,
|
||||
backendName,
|
||||
@@ -112,7 +122,8 @@ const handleRequest = createGlobalRouter({
|
||||
imageLimit: IMAGE_LIMIT,
|
||||
queueManager,
|
||||
config,
|
||||
loginMode: isLoginMode
|
||||
loginMode: isLoginMode,
|
||||
getSafeMode: () => ({ enabled: safeMode, reason: safeModeReason })
|
||||
});
|
||||
|
||||
// ==================== 启动服务器 ====================
|
||||
@@ -128,12 +139,15 @@ async function startServer() {
|
||||
logger.info('服务器', '完成后可直接关闭浏览器窗口或按 Ctrl+C 退出');
|
||||
}
|
||||
|
||||
// 预先启动 Pool
|
||||
// 预先启动工作池(失败时进入安全模式)
|
||||
try {
|
||||
await queueManager.initializePool();
|
||||
} catch (err) {
|
||||
logger.error('服务器', 'Pool 初始化失败', { error: err.message });
|
||||
process.exit(1);
|
||||
logger.error('服务器', '工作池初始化失败', { error: err.message });
|
||||
logger.warn('服务器', '进入安全模式:WebUI 和 Admin API 可用,OpenAI API 不可用');
|
||||
logger.warn('服务器', '请通过 配置文件或者 WebUI 修改正确的配置后重启服务');
|
||||
safeMode = true;
|
||||
safeModeReason = err.message;
|
||||
}
|
||||
|
||||
// 创建并启动 HTTP 服务器
|
||||
|
||||
+113
-1
@@ -4,9 +4,12 @@
|
||||
*
|
||||
* - 环境变量:LOG_LEVEL=debug|info|warn|error
|
||||
* - 输出格式:YYYY-MM-DD HH:mm:ss.SSS [LEVEL] [模块] 消息 | k=v ...
|
||||
* - 日志文件:data/temp/system.log(超过 5MB 自动轮转)
|
||||
*/
|
||||
|
||||
import process from 'process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const LEVELS = ['debug', 'info', 'warn', 'error'];
|
||||
|
||||
@@ -19,6 +22,49 @@ const COLORS = {
|
||||
white: '\x1b[37m'
|
||||
};
|
||||
|
||||
// 日志文件配置
|
||||
const LOG_DIR = path.join(process.cwd(), 'data', 'temp');
|
||||
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
|
||||
|
||||
// 确保日志目录存在
|
||||
function ensureLogDir() {
|
||||
if (!fs.existsSync(LOG_DIR)) {
|
||||
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 日志轮转:超过 5MB 时重命名为 .old
|
||||
function rotateLogIfNeeded() {
|
||||
try {
|
||||
if (fs.existsSync(LOG_FILE)) {
|
||||
const stats = fs.statSync(LOG_FILE);
|
||||
if (stats.size >= MAX_LOG_SIZE) {
|
||||
// 删除旧的 .old 文件
|
||||
if (fs.existsSync(LOG_FILE_OLD)) {
|
||||
fs.unlinkSync(LOG_FILE_OLD);
|
||||
}
|
||||
// 重命名当前日志
|
||||
fs.renameSync(LOG_FILE, LOG_FILE_OLD);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略轮转错误
|
||||
}
|
||||
}
|
||||
|
||||
// 写入日志文件
|
||||
function writeToFile(line) {
|
||||
try {
|
||||
ensureLogDir();
|
||||
rotateLogIfNeeded();
|
||||
fs.appendFileSync(LOG_FILE, line + '\n', 'utf8');
|
||||
} catch (e) {
|
||||
// 忽略写入错误
|
||||
}
|
||||
}
|
||||
|
||||
// 根据日志级别获取颜色
|
||||
function getColor(level) {
|
||||
switch (level.toLowerCase()) {
|
||||
@@ -94,6 +140,7 @@ export function log(level, mod, msg, meta = {}) {
|
||||
const color = getColor(level);
|
||||
const coloredLine = `${color}${line}${COLORS.reset}`;
|
||||
|
||||
// 输出到控制台
|
||||
if (level === 'error') {
|
||||
console.error(coloredLine);
|
||||
} else if (level === 'warn') {
|
||||
@@ -101,6 +148,66 @@ export function log(level, mod, msg, meta = {}) {
|
||||
} else {
|
||||
console.log(coloredLine);
|
||||
}
|
||||
|
||||
// 写入日志文件(不带颜色)
|
||||
writeToFile(line);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志文件路径
|
||||
*/
|
||||
export function getLogPath() {
|
||||
return LOG_FILE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取旧日志文件路径
|
||||
*/
|
||||
export function getOldLogPath() {
|
||||
return LOG_FILE_OLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除日志文件
|
||||
*/
|
||||
export function clearLogs() {
|
||||
try {
|
||||
if (fs.existsSync(LOG_FILE)) {
|
||||
fs.unlinkSync(LOG_FILE);
|
||||
}
|
||||
if (fs.existsSync(LOG_FILE_OLD)) {
|
||||
fs.unlinkSync(LOG_FILE_OLD);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取日志文件(返回最后 N 行)
|
||||
* @param {number} lines - 读取行数
|
||||
* @returns {{logs: string[], total: number, file: string}}
|
||||
*/
|
||||
export function readLogs(lines = 200) {
|
||||
const result = { logs: [], total: 0, file: LOG_FILE };
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(LOG_FILE)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(LOG_FILE, 'utf8');
|
||||
const allLines = content.split('\n').filter(line => line.trim());
|
||||
result.total = allLines.length;
|
||||
|
||||
// 返回最后 N 行
|
||||
result.logs = allLines.slice(-lines);
|
||||
} catch (e) {
|
||||
// 忽略读取错误
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
@@ -108,5 +215,10 @@ export const logger = {
|
||||
info: (mod, msg, meta) => log('info', mod, msg, meta),
|
||||
warn: (mod, msg, meta) => log('warn', mod, msg, meta),
|
||||
error: (mod, msg, meta) => log('error', mod, msg, meta),
|
||||
setLevel: setLogLevel
|
||||
setLevel: setLogLevel,
|
||||
getLogPath,
|
||||
getOldLogPath,
|
||||
clearLogs,
|
||||
readLogs
|
||||
};
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{c as i,I as u}from"./index-N039yHLa.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-BGGdzQd9.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};
|
||||
-1
@@ -1 +0,0 @@
|
||||
import{c as l,I as q,j as I,r as x,k as D,o as F,l as T,b as _,d as i,w as s,i as y,e as c,f as v,u as g,t as S,S as L,g as h,h as f,m as G,F as J}from"./index-N039yHLa.js";var Q={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"}}]},name:"appstore",theme:"outlined"};function w(p){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?Object(arguments[n]):{},o=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(o=o.concat(Object.getOwnPropertySymbols(a).filter(function(t){return Object.getOwnPropertyDescriptor(a,t).enumerable}))),o.forEach(function(t){R(p,t,a[t])})}return p}function R(p,n,a){return n in p?Object.defineProperty(p,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):p[n]=a,p}var k=function(n,a){var o=w({},n,a.attrs);return l(q,w({},o,{icon:Q}),null)};k.displayName="AppstoreOutlined";k.inheritAttrs=!1;const W={style:{display:"flex","align-items":"center","justify-content":"space-between"}},X={style:{display:"flex","align-items":"center"}},Y={style:{"font-weight":"600","font-size":"15px"}},Z={key:0},K={key:2},ee={key:4,style:{"font-size":"12px",color:"#8c8c8c","margin-top":"4px"}},te={style:{"text-align":"right"}},ae={__name:"adapters",setup(p){const n=I(),a=x(!1),o=x(null),t=D({});F(async()=>{await Promise.all([n.fetchAdaptersMeta(),n.fetchAdapterConfig()])});const O=T(()=>n.adaptersMeta),C=m=>{o.value=m;const r=n.adapterConfig[m.id]||{};Object.keys(t).forEach(u=>delete t[u]),m.configSchema&&m.configSchema.forEach(u=>{r[u.key]!==void 0?t[u.key]=r[u.key]:t[u.key]=u.default}),a.value=!0},V=async()=>{if(!o.value)return;const m={[o.value.id]:{...t}};await n.saveAdapterConfig(m)&&(a.value=!1)};return(m,r)=>{const u=c("a-button"),b=c("a-card"),z=c("a-list-item"),A=c("a-list"),U=c("a-empty"),H=c("a-input"),j=c("a-input-number"),M=c("a-switch"),P=c("a-select"),B=c("a-form-item"),E=c("a-form"),N=c("a-drawer"),$=c("a-layout");return i(),_($,{style:{background:"transparent"}},{default:s(()=>[l(b,{title:"适配器管理",bordered:!1},{extra:s(()=>[l(u,{type:"link",onClick:g(n).fetchAdaptersMeta},{default:s(()=>[...r[2]||(r[2]=[h("刷新列表",-1)])]),_:1},8,["onClick"])]),default:s(()=>[l(A,{grid:{gutter:16,xs:1,sm:2,md:3,lg:3,xl:4,xxl:4},"data-source":O.value},{renderItem:s(({item:e})=>[l(z,null,{default:s(()=>[l(b,{hoverable:"",onClick:d=>C(e),bodyStyle:{padding:"16px"}},{default:s(()=>[v("div",W,[v("div",X,[l(g(k),{style:{"font-size":"20px",color:"#1890ff","margin-right":"12px"}}),v("span",Y,S(e.id),1)]),l(g(L),{style:{"font-size":"16px",color:"#8c8c8c"}})])]),_:2},1032,["onClick"])]),_:2},1024)]),_:1},8,["data-source"])]),_:1}),o.value?(i(),_(N,{key:0,open:a.value,"onUpdate:open":r[1]||(r[1]=e=>a.value=e),title:`配置适配器 - ${o.value.name}`,width:"500",placement:"right"},{footer:s(()=>[v("div",te,[l(u,{style:{"margin-right":"8px"},onClick:r[0]||(r[0]=e=>a.value=!1)},{default:s(()=>[...r[3]||(r[3]=[h("取消",-1)])]),_:1}),l(u,{type:"primary",onClick:V},{default:s(()=>[...r[4]||(r[4]=[h("保存配置",-1)])]),_:1})])]),default:s(()=>[!o.value.configSchema||o.value.configSchema.length===0?(i(),f("div",Z,[l(U,{description:"该适配器没有可配置项"})])):(i(),_(E,{key:1,layout:"vertical"},{default:s(()=>[(i(!0),f(J,null,G(o.value.configSchema,e=>(i(),_(B,{key:e.key,label:e.label,required:e.required},{default:s(()=>[e.type==="string"?(i(),_(H,{key:0,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,placeholder:e.placeholder},null,8,["value","onUpdate:value","placeholder"])):y("",!0),e.type==="number"?(i(),_(j,{key:1,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,min:e.min,max:e.max,style:{width:"100%"}},null,8,["value","onUpdate:value","min","max"])):y("",!0),e.type==="boolean"?(i(),f("div",K,[l(M,{checked:t[e.key],"onUpdate:checked":d=>t[e.key]=d},null,8,["checked","onUpdate:checked"])])):y("",!0),e.type==="select"?(i(),_(P,{key:3,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,options:e.options},null,8,["value","onUpdate:value","options"])):y("",!0),e.note?(i(),f("div",ee,S(e.note),1)):y("",!0)]),_:2},1032,["label","required"]))),128))]),_:1}))]),_:1},8,["open","title"])):y("",!0)]),_:1})}}};export{ae as default};
|
||||
+1
@@ -0,0 +1 @@
|
||||
import{c as l,I as q,j as I,r as b,k as D,o as F,l as T,b as _,d as u,w as s,e as v,f as c,g as y,u as h,t as w,S as L,h as g,i as f,m as G,F as J}from"./index-BGGdzQd9.js";var Q={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"}}]},name:"appstore",theme:"outlined"};function S(p){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?Object(arguments[n]):{},o=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(o=o.concat(Object.getOwnPropertySymbols(a).filter(function(t){return Object.getOwnPropertyDescriptor(a,t).enumerable}))),o.forEach(function(t){R(p,t,a[t])})}return p}function R(p,n,a){return n in p?Object.defineProperty(p,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):p[n]=a,p}var k=function(n,a){var o=S({},n,a.attrs);return l(q,S({},o,{icon:Q}),null)};k.displayName="AppstoreOutlined";k.inheritAttrs=!1;const W={style:{display:"flex","align-items":"center","justify-content":"space-between",gap:"8px"}},X={style:{display:"flex","align-items":"center","min-width":"0",flex:"1"}},Y={style:{"font-weight":"600","font-size":"14px",overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap"}},Z={key:0},K={key:2},ee={key:4,style:{"font-size":"12px",color:"#8c8c8c","margin-top":"4px"}},te={style:{"text-align":"right"}},ae={__name:"adapters",setup(p){const n=I(),a=b(!1),o=b(null),t=D({});F(async()=>{await Promise.all([n.fetchAdaptersMeta(),n.fetchAdapterConfig()])});const O=T(()=>n.adaptersMeta),C=m=>{o.value=m;const r=n.adapterConfig[m.id]||{};Object.keys(t).forEach(i=>delete t[i]),m.configSchema&&m.configSchema.forEach(i=>{r[i.key]!==void 0?t[i.key]=r[i.key]:t[i.key]=i.default}),a.value=!0},V=async()=>{if(!o.value)return;const m={[o.value.id]:{...t}};await n.saveAdapterConfig(m)&&(a.value=!1)};return(m,r)=>{const i=c("a-button"),x=c("a-card"),z=c("a-list-item"),A=c("a-list"),U=c("a-empty"),H=c("a-input"),j=c("a-input-number"),M=c("a-switch"),P=c("a-select"),B=c("a-form-item"),E=c("a-form"),N=c("a-drawer"),$=c("a-layout");return u(),_($,{style:{background:"transparent"}},{default:s(()=>[l(x,{title:"适配器管理",bordered:!1},{extra:s(()=>[l(i,{type:"link",onClick:h(n).fetchAdaptersMeta},{default:s(()=>[...r[2]||(r[2]=[g("刷新列表",-1)])]),_:1},8,["onClick"])]),default:s(()=>[l(A,{grid:{gutter:16,xs:1,sm:2,md:3,lg:3,xl:4,xxl:4},"data-source":O.value},{renderItem:s(({item:e})=>[l(z,null,{default:s(()=>[l(x,{hoverable:"",onClick:d=>C(e),bodyStyle:{padding:"12px 16px"}},{default:s(()=>[y("div",W,[y("div",X,[l(h(k),{style:{"font-size":"18px",color:"#1890ff","margin-right":"8px","flex-shrink":"0"}}),y("span",Y,w(e.id),1)]),l(h(L),{style:{"font-size":"16px",color:"#8c8c8c","flex-shrink":"0"}})])]),_:2},1032,["onClick"])]),_:2},1024)]),_:1},8,["data-source"])]),_:1}),o.value?(u(),_(N,{key:0,open:a.value,"onUpdate:open":r[1]||(r[1]=e=>a.value=e),title:`配置适配器 - ${o.value.id}`,width:"500",placement:"right"},{footer:s(()=>[y("div",te,[l(i,{style:{"margin-right":"8px"},onClick:r[0]||(r[0]=e=>a.value=!1)},{default:s(()=>[...r[3]||(r[3]=[g("取消",-1)])]),_:1}),l(i,{type:"primary",onClick:V},{default:s(()=>[...r[4]||(r[4]=[g("保存配置",-1)])]),_:1})])]),default:s(()=>[!o.value.configSchema||o.value.configSchema.length===0?(u(),f("div",Z,[l(U,{description:"该适配器没有可配置项"})])):(u(),_(E,{key:1,layout:"vertical"},{default:s(()=>[(u(!0),f(J,null,G(o.value.configSchema,e=>(u(),_(B,{key:e.key,label:e.label,required:e.required},{default:s(()=>[e.type==="string"?(u(),_(H,{key:0,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,placeholder:e.placeholder},null,8,["value","onUpdate:value","placeholder"])):v("",!0),e.type==="number"?(u(),_(j,{key:1,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,min:e.min,max:e.max,style:{width:"100%"}},null,8,["value","onUpdate:value","min","max"])):v("",!0),e.type==="boolean"?(u(),f("div",K,[l(M,{checked:t[e.key],"onUpdate:checked":d=>t[e.key]=d},null,8,["checked","onUpdate:checked"])])):v("",!0),e.type==="select"?(u(),_(P,{key:3,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,options:e.options},null,8,["value","onUpdate:value","options"])):v("",!0),e.note?(u(),f("div",ee,w(e.note),1)):v("",!0)]),_:2},1032,["label","required"]))),128))]),_:1}))]),_:1},8,["open","title"])):v("",!0)]),_:1})}}};export{ae as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
[data-v-9531bced]::-webkit-scrollbar{width:6px;height:6px}[data-v-9531bced]::-webkit-scrollbar-thumb{background:#ccc;border-radius:3px}[data-v-9531bced]::-webkit-scrollbar-track{background:#f1f1f1}html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}
|
||||
[data-v-b38da084]::-webkit-scrollbar{width:6px;height:6px}[data-v-b38da084]::-webkit-scrollbar-thumb{background:#ccc;border-radius:3px}[data-v-b38da084]::-webkit-scrollbar-track{background:#f1f1f1}html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}
|
||||
Vendored
+475
File diff suppressed because one or more lines are too long
Vendored
-474
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
||||
.log-container[data-v-a8b1f18a]{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-a8b1f18a]{padding:4px 0;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.log-line[data-v-a8b1f18a]:hover{background:#e6f7ff;white-space:normal;word-break:break-all}.log-time[data-v-a8b1f18a]{color:#8c8c8c}.log-module[data-v-a8b1f18a]{color:#1890ff;margin-right:8px}.log-message[data-v-a8b1f18a]{color:#333}.level-erro .log-message[data-v-a8b1f18a]{color:#ff4d4f}.level-warn .log-message[data-v-a8b1f18a]{color:#faad14}.level-dbug .log-message[data-v-a8b1f18a]{color:#722ed1}.toolbar[data-v-a8b1f18a]{margin-bottom:16px}.toolbar-row[data-v-a8b1f18a]{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}.toolbar-row[data-v-a8b1f18a]:last-child{margin-bottom:0}@media(min-width:768px){.toolbar[data-v-a8b1f18a]{display:flex;justify-content:space-between;align-items:center;gap:12px}.toolbar-row[data-v-a8b1f18a]{margin-bottom:0}.toolbar-row[data-v-a8b1f18a]:last-child{flex:1;max-width:300px}}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
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 s,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-BGGdzQd9.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]):{},r=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(r=r.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),r.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 r=D({},t,a.attrs);return l(P,D({},r,{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]):{},r=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(r=r.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),r.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 r=E({},t,a.attrs);return l(P,E({},r,{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([]),r=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()=>{r.value=!0;try{const o=await fetch("/admin/logs?lines=500",{headers:t.getHeaders()});if(o.ok){const e=await o.json();a.value=A(e.logs||[]),c.value=e.total||0}}catch{R.error("获取日志失败")}finally{r.value=!1}},A=o=>o.map((e,u)=>{const d=e.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[([^\]]+)\] (.*)$/);return d?{id:u,time:d[1],level:d[2],module:d[3],message:d[4],raw:e}:{id:u,raw:e,level:"INFO",time:"",module:"",message:e}}),x=Q(()=>a.value.filter(o=>{if(w.value!=="all"&&o.level!==w.value)return!1;if(y.value){const e=y.value.toLowerCase();return o.raw.toLowerCase().includes(e)}return!0})),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 o=a.value.map(O=>O.raw).join(`
|
||||
`),e=new Blob([o],{type:"text/plain"}),u=URL.createObjectURL(e),d=document.createElement("a");d.href=u,d.download=`system-${new Date().toISOString().split("T")[0]}.log`,d.click(),URL.revokeObjectURL(u)},T=o=>{h.value=o,o?(z(),g.value=setInterval(z,5e3)):g.value&&(clearInterval(g.value),g.value=null)};return X(()=>{z()}),Y(()=>{g.value&&clearInterval(g.value)}),(o,e)=>{const u=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:s(()=>[v("div",ue,[v("div",de,[l(d,{value:w.value,"onUpdate:value":e[0]||(e[0]=i=>w.value=i),style:{width:"90px"},size:"small"},{default:s(()=>[l(u,{value:"all"},{default:s(()=>[...e[3]||(e[3]=[m("全部",-1)])]),_:1}),l(u,{value:"INFO"},{default:s(()=>[...e[4]||(e[4]=[m("INFO",-1)])]),_:1}),l(u,{value:"WARN"},{default:s(()=>[...e[5]||(e[5]=[m("WARN",-1)])]),_:1}),l(u,{value:"ERRO"},{default:s(()=>[...e[6]||(e[6]=[m("ERROR",-1)])]),_:1}),l(u,{value:"DBUG"},{default:s(()=>[...e[7]||(e[7]=[m("DEBUG",-1)])]),_:1})]),_:1},8,["value"]),l(V,{size:4},{default:s(()=>[l(B,{title:h.value?"关闭自动刷新":"开启自动刷新"},{default:s(()=>[l(O,{size:"small",type:h.value?"primary":"default",onClick:e[1]||(e[1]=i=>T(!h.value))},{icon:s(()=>[l(C(k))]),_:1},8,["type"])]),_:1},8,["title"]),l(B,{title:"导出日志"},{default:s(()=>[l(O,{size:"small",onClick:M},{icon:s(()=>[l(C(Z))]),_:1})]),_:1}),l(B,{title:"清除日志"},{default:s(()=>[l(O,{size:"small",danger:"",onClick:F},{icon:s(()=>[l(C(K))]),_:1})]),_:1})]),_:1})]),v("div",fe,[l(W,{value:y.value,"onUpdate:value":e[2]||(e[2]=i=>y.value=i),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,i=>(b(),L("div",{key:i.id,class:le(["log-line","level-"+i.level.toLowerCase()])},[v("span",ge,_(i.time),1),l($,{color:U[i.level]?.color||"#8c8c8c",size:"small",style:{margin:"0 8px"}},{default:s(()=>[m(_(i.level),1)]),_:2},1032,["color"]),v("span",_e,"["+_(i.module)+"]",1),v("span",he,_(i.message),1)],2))),128)),x.value.length===0?(b(),N(H,{key:0,description:"暂无日志"})):S("",!0)])]),_:1})}}},ye=q(Oe,[["__scopeId","data-v-a8b1f18a"]]);export{ye as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{_ as b,j as w,k,o as c,b as C,d as S,w as n,c as o,f as e,e as a,g as i}from"./index-N039yHLa.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};
|
||||
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-BGGdzQd9.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};
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{x as i,j as a,s as r}from"./index-N039yHLa.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},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};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import{x as i,j as a,s as r}from"./index-BGGdzQd9.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
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -6,8 +6,8 @@
|
||||
<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-N039yHLa.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CAMDRWk5.css">
|
||||
<script type="module" crossorigin src="/assets/index-BGGdzQd9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B8cer5ye.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
+348
-4
@@ -1,13 +1,19 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
SettingOutlined,
|
||||
ToolOutlined,
|
||||
PoweroffOutlined,
|
||||
GithubOutlined
|
||||
GithubOutlined,
|
||||
ApiOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
LoadingOutlined,
|
||||
InboxOutlined,
|
||||
PictureOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import LoginModal from '@/components/auth/LoginModal.vue';
|
||||
@@ -29,6 +35,189 @@ const enterIconLoading = () => {
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 接口测试抽屉
|
||||
const apiTestDrawer = ref(false);
|
||||
const apiTestResults = ref({
|
||||
models: { status: 'pending', data: null, error: null },
|
||||
cookies: { status: 'pending', data: null, error: null },
|
||||
chat: { status: 'pending', data: null, error: null }
|
||||
});
|
||||
const chatTestPrompt = ref('Say hello in one word');
|
||||
const chatTestModel = ref('');
|
||||
const chatModelList = ref([]);
|
||||
const chatImageList = ref([]);
|
||||
const chatStreamMode = ref(false);
|
||||
const chatStreamContent = ref('');
|
||||
|
||||
// 获取模型列表
|
||||
const fetchModelList = async () => {
|
||||
try {
|
||||
const res = await fetch('/v1/models', { headers: settingsStore.getHeaders() });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
chatModelList.value = data.data || [];
|
||||
if (chatModelList.value.length > 0 && !chatTestModel.value) {
|
||||
chatTestModel.value = chatModelList.value[0].id;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取模型列表失败', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 图片转 base64
|
||||
const fileToBase64 = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
});
|
||||
};
|
||||
|
||||
// 图片上传前检查
|
||||
const beforeUpload = (file) => {
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
message.error('仅支持 PNG, JPEG, GIF, WebP 格式');
|
||||
return false;
|
||||
}
|
||||
if (chatImageList.value.length >= 10) {
|
||||
message.error('最多上传 10 张图片');
|
||||
return false;
|
||||
}
|
||||
return false; // 阻止自动上传,手动处理
|
||||
};
|
||||
|
||||
// 处理图片选择
|
||||
const handleImageChange = async (info) => {
|
||||
const file = info.file;
|
||||
if (file.status === 'removed') {
|
||||
chatImageList.value = chatImageList.value.filter(f => f.uid !== file.uid);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const base64 = await fileToBase64(file.originFileObj || file);
|
||||
chatImageList.value.push({
|
||||
uid: file.uid,
|
||||
name: file.name,
|
||||
base64
|
||||
});
|
||||
} catch (e) {
|
||||
message.error('图片读取失败');
|
||||
}
|
||||
};
|
||||
|
||||
const testApi = async (type) => {
|
||||
apiTestResults.value[type].status = 'loading';
|
||||
apiTestResults.value[type].error = null;
|
||||
apiTestResults.value[type].data = null;
|
||||
chatStreamContent.value = '';
|
||||
|
||||
try {
|
||||
let url, options;
|
||||
if (type === 'models') {
|
||||
url = '/v1/models';
|
||||
options = { headers: settingsStore.getHeaders() };
|
||||
} else if (type === 'cookies') {
|
||||
url = '/v1/cookies';
|
||||
options = { headers: settingsStore.getHeaders() };
|
||||
} else if (type === 'chat') {
|
||||
url = '/v1/chat/completions';
|
||||
|
||||
// 构建消息内容
|
||||
let content;
|
||||
if (chatImageList.value.length > 0) {
|
||||
// 多模态请求
|
||||
content = [
|
||||
{ type: 'text', text: chatTestPrompt.value }
|
||||
];
|
||||
for (const img of chatImageList.value) {
|
||||
content.push({
|
||||
type: 'image_url',
|
||||
image_url: { url: img.base64 }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
content = chatTestPrompt.value;
|
||||
}
|
||||
|
||||
options = {
|
||||
method: 'POST',
|
||||
headers: { ...settingsStore.getHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: chatTestModel.value,
|
||||
messages: [{ role: 'user', content }],
|
||||
stream: chatStreamMode.value
|
||||
})
|
||||
};
|
||||
|
||||
// 流式请求处理
|
||||
if (chatStreamMode.value) {
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) {
|
||||
const errData = await res.json();
|
||||
throw new Error(errData.error?.message || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6).trim();
|
||||
if (data === '[DONE]') continue;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const delta = json.choices?.[0]?.delta?.content || '';
|
||||
chatStreamContent.value += delta;
|
||||
} catch { /* 忽略解析错误 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apiTestResults.value[type].status = 'success';
|
||||
apiTestResults.value[type].data = { content: chatStreamContent.value };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(url, options);
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
apiTestResults.value[type].status = 'success';
|
||||
apiTestResults.value[type].data = data;
|
||||
} else {
|
||||
apiTestResults.value[type].status = 'error';
|
||||
apiTestResults.value[type].error = data.error?.message || `HTTP ${res.status}`;
|
||||
}
|
||||
} catch (e) {
|
||||
apiTestResults.value[type].status = 'error';
|
||||
apiTestResults.value[type].error = e.message;
|
||||
}
|
||||
};
|
||||
|
||||
const openApiTestDrawer = () => {
|
||||
apiTestDrawer.value = true;
|
||||
// 重置状态
|
||||
Object.keys(apiTestResults.value).forEach(key => {
|
||||
apiTestResults.value[key] = { status: 'pending', data: null, error: null };
|
||||
});
|
||||
chatImageList.value = [];
|
||||
// 获取模型列表
|
||||
fetchModelList();
|
||||
};
|
||||
|
||||
// 菜单 key 到路由路径的映射
|
||||
const menuRoutes = {
|
||||
'dash': '/',
|
||||
@@ -37,7 +226,8 @@ const menuRoutes = {
|
||||
'settings-browser': '/settings/browser',
|
||||
'settings-adapters': '/settings/adapters',
|
||||
'tools-display': '/tools/display',
|
||||
'tools-cache': '/tools/cache'
|
||||
'tools-cache': '/tools/cache',
|
||||
'tools-logs': '/tools/logs'
|
||||
};
|
||||
|
||||
// 处理菜单点击
|
||||
@@ -134,7 +324,13 @@ onMounted(async () => {
|
||||
<div class="logo" style="font-size: 1.25rem; font-weight: bold; color: #1890ff; margin-right: 24px;">
|
||||
WebAI2API
|
||||
</div>
|
||||
<a-flex justify="end" align="center" style="flex: 1;">
|
||||
<a-flex justify="end" align="center" style="flex: 1;" :gap="12">
|
||||
<a-button @click="openApiTestDrawer">
|
||||
<template #icon>
|
||||
<ApiOutlined />
|
||||
</template>
|
||||
接口测试
|
||||
</a-button>
|
||||
<a-button danger :loading="iconLoading" @click="enterIconLoading">
|
||||
<template #icon>
|
||||
<PoweroffOutlined />
|
||||
@@ -172,6 +368,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
<a-menu-item key="tools-display">虚拟显示器</a-menu-item>
|
||||
<a-menu-item key="tools-cache">缓存与重启</a-menu-item>
|
||||
<a-menu-item key="tools-logs">日志查看器</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
@@ -193,6 +390,153 @@ onMounted(async () => {
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
|
||||
<!-- 接口测试抽屉 -->
|
||||
<a-drawer v-model:open="apiTestDrawer" title="接口测试" placement="right" :width="500">
|
||||
<a-space direction="vertical" style="width: 100%" size="large">
|
||||
<!-- Models 接口 -->
|
||||
<a-card title="GET /v1/models" size="small">
|
||||
<template #extra>
|
||||
<a-button size="small" type="primary" @click="testApi('models')"
|
||||
:loading="apiTestResults.models.status === 'loading'">
|
||||
测试
|
||||
</a-button>
|
||||
</template>
|
||||
<div v-if="apiTestResults.models.status === 'success'">
|
||||
<a-tag color="success">
|
||||
<CheckCircleOutlined /> 成功
|
||||
</a-tag>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #8c8c8c;">
|
||||
返回 {{ apiTestResults.models.data?.data?.length || 0 }} 个模型
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="apiTestResults.models.status === 'error'">
|
||||
<a-tag color="error">
|
||||
<CloseCircleOutlined /> 失败
|
||||
</a-tag>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #ff4d4f;">
|
||||
{{ apiTestResults.models.error }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="color: #8c8c8c; font-size: 12px;">点击测试按钮开始</div>
|
||||
</a-card>
|
||||
|
||||
<!-- Cookies 接口 -->
|
||||
<a-card title="GET /v1/cookies" size="small">
|
||||
<template #extra>
|
||||
<a-button size="small" type="primary" @click="testApi('cookies')"
|
||||
:loading="apiTestResults.cookies.status === 'loading'">
|
||||
测试
|
||||
</a-button>
|
||||
</template>
|
||||
<div v-if="apiTestResults.cookies.status === 'success'">
|
||||
<a-tag color="success">
|
||||
<CheckCircleOutlined /> 成功
|
||||
</a-tag>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #8c8c8c;">
|
||||
返回 {{ apiTestResults.cookies.data?.cookies?.length || 0 }} 个 Cookie
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="apiTestResults.cookies.status === 'error'">
|
||||
<a-tag color="error">
|
||||
<CloseCircleOutlined /> 失败
|
||||
</a-tag>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #ff4d4f;">
|
||||
{{ apiTestResults.cookies.error }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="color: #8c8c8c; font-size: 12px;">点击测试按钮开始</div>
|
||||
</a-card>
|
||||
|
||||
<!-- Chat 接口 -->
|
||||
<a-card title="POST /v1/chat/completions" size="small">
|
||||
<template #extra>
|
||||
<a-button size="small" type="primary" @click="testApi('chat')"
|
||||
:loading="apiTestResults.chat.status === 'loading'" :disabled="!chatTestModel">
|
||||
测试
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 模型选择 -->
|
||||
<div style="margin-bottom: 12px;">
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px;">模型</div>
|
||||
<a-select v-model:value="chatTestModel" style="width: 100%" size="small" placeholder="选择模型" show-search>
|
||||
<a-select-option v-for="model in chatModelList" :key="model.id" :value="model.id">
|
||||
{{ model.id }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- 提示词 -->
|
||||
<div style="margin-bottom: 12px;">
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px;">提示词</div>
|
||||
<a-textarea v-model:value="chatTestPrompt" placeholder="输入提示词" :rows="2" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<div style="margin-bottom: 12px;">
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px;">
|
||||
附加图片 ({{ chatImageList.length }}/10)
|
||||
</div>
|
||||
<a-upload-dragger :file-list="[]" :multiple="true" :before-upload="beforeUpload" @change="handleImageChange"
|
||||
accept=".png,.jpg,.jpeg,.gif,.webp" :show-upload-list="false" style="padding: 8px;">
|
||||
<p style="margin: 0;">
|
||||
<InboxOutlined style="font-size: 24px; color: #1890ff;" />
|
||||
</p>
|
||||
<p style="font-size: 12px; margin: 4px 0 0 0; color: #8c8c8c;">
|
||||
点击或拖拽上传图片 (PNG/JPEG/GIF/WebP)
|
||||
</p>
|
||||
</a-upload-dragger>
|
||||
<div v-if="chatImageList.length > 0" style="margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px;">
|
||||
<a-tag v-for="img in chatImageList" :key="img.uid" closable
|
||||
@close="chatImageList = chatImageList.filter(i => i.uid !== img.uid)">
|
||||
<PictureOutlined /> {{ img.name.slice(0, 15) }}{{ img.name.length > 15 ? '...' : '' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 流式选项 -->
|
||||
<div style="margin-bottom: 12px;">
|
||||
<a-checkbox v-model:checked="chatStreamMode">流式响应</a-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- 测试结果 -->
|
||||
<!-- 流式模式:实时显示内容 -->
|
||||
<div v-if="chatStreamMode && apiTestResults.chat.status === 'loading'"
|
||||
style="background: #fafafa; padding: 12px; border-radius: 4px; font-size: 12px;">
|
||||
<div style="color: #1890ff; margin-bottom: 8px;">
|
||||
<LoadingOutlined /> 正在接收流式响应...
|
||||
</div>
|
||||
<pre style="white-space: pre-wrap; word-break: break-all; margin: 0; min-height: 50px;">{{ chatStreamContent ||
|
||||
'等待内容...' }}</pre>
|
||||
</div>
|
||||
<div v-else-if="apiTestResults.chat.status === 'success'">
|
||||
<a-tag color="success">
|
||||
<CheckCircleOutlined /> 成功
|
||||
</a-tag>
|
||||
<div
|
||||
style="margin-top: 8px; font-size: 12px; max-height: 200px; overflow-y: auto; background: #fafafa; padding: 8px; border-radius: 4px;">
|
||||
<pre v-if="chatStreamMode" style="white-space: pre-wrap; word-break: break-all; margin: 0;">{{
|
||||
apiTestResults.chat.data?.content || '' }}</pre>
|
||||
<pre v-else style="white-space: pre-wrap; word-break: break-all; margin: 0;">{{
|
||||
JSON.stringify(apiTestResults.chat.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="apiTestResults.chat.status === 'error'">
|
||||
<a-tag color="error">
|
||||
<CloseCircleOutlined /> 失败
|
||||
</a-tag>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #ff4d4f;">
|
||||
{{ apiTestResults.chat.error }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="apiTestResults.chat.status === 'loading' && !chatStreamMode"
|
||||
style="color: #1890ff; font-size: 12px;">
|
||||
<LoadingOutlined /> 请求中,可能需要较长时间...
|
||||
</div>
|
||||
</a-card>
|
||||
</a-space>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -95,6 +95,26 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<a-layout style="width: 100%; background: transparent;">
|
||||
<!-- 安全模式告警横幅 -->
|
||||
<a-alert v-if="systemStore.safeMode?.enabled" type="error" show-icon style="margin-bottom: 16px;" closable>
|
||||
<template #message>
|
||||
<span style="font-weight: 600;">⚠️ 安全模式</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>
|
||||
<p style="margin-bottom: 8px;">
|
||||
服务因初始化失败进入安全模式,OpenAI API 不可用。
|
||||
</p>
|
||||
<p style="margin-bottom: 8px; color: #cf1322;">
|
||||
<b>原因:</b>{{ systemStore.safeMode.reason }}
|
||||
</p>
|
||||
<p style="margin: 0;">
|
||||
请前往「系统设置」修改正确的配置后重启服务。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
||||
<!-- 响应式布局:手机竖向,电脑横向 -->
|
||||
<a-row :gutter="[16, 16]" style="margin-bottom: 24px">
|
||||
<!-- 系统信息卡片 -->
|
||||
|
||||
@@ -69,13 +69,16 @@ const handleSave = async () => {
|
||||
<a-list :grid="{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: 4 }" :data-source="adapters">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-card hoverable @click="handleEdit(item)" :bodyStyle="{ padding: '16px' }">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<AppstoreOutlined style="font-size: 20px; color: #1890ff; margin-right: 12px;" />
|
||||
<span style="font-weight: 600; font-size: 15px;">{{ item.id }}</span>
|
||||
<a-card hoverable @click="handleEdit(item)" :bodyStyle="{ padding: '12px 16px' }">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 8px;">
|
||||
<div style="display: flex; align-items: center; min-width: 0; flex: 1;">
|
||||
<AppstoreOutlined
|
||||
style="font-size: 18px; color: #1890ff; margin-right: 8px; flex-shrink: 0;" />
|
||||
<span
|
||||
style="font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{
|
||||
item.id }}</span>
|
||||
</div>
|
||||
<SettingOutlined style="font-size: 16px; color: #8c8c8c;" />
|
||||
<SettingOutlined style="font-size: 16px; color: #8c8c8c; flex-shrink: 0;" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-list-item>
|
||||
@@ -84,8 +87,8 @@ const handleSave = async () => {
|
||||
</a-card>
|
||||
|
||||
<!-- 配置抽屉 -->
|
||||
<a-drawer v-if="currentAdapter" v-model:open="drawerVisible" :title="`配置适配器 - ${currentAdapter.name}`"
|
||||
width="500" placement="right">
|
||||
<a-drawer v-if="currentAdapter" v-model:open="drawerVisible" :title="`配置适配器 - ${currentAdapter.id}`" width="500"
|
||||
placement="right">
|
||||
<div v-if="!currentAdapter.configSchema || currentAdapter.configSchema.length === 0">
|
||||
<a-empty description="该适配器没有可配置项" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import {
|
||||
ReloadOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
DownloadOutlined,
|
||||
WarningOutlined,
|
||||
CloseCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
BugOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const logs = ref([]);
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const autoRefresh = ref(false);
|
||||
const refreshInterval = ref(null);
|
||||
const searchText = ref('');
|
||||
const levelFilter = ref('all');
|
||||
|
||||
// 日志级别配置
|
||||
const levelConfig = {
|
||||
'INFO': { color: '#1890ff', icon: InfoCircleOutlined },
|
||||
'WARN': { color: '#faad14', icon: WarningOutlined },
|
||||
'ERRO': { color: '#ff4d4f', icon: CloseCircleOutlined },
|
||||
'DBUG': { color: '#722ed1', icon: BugOutlined }
|
||||
};
|
||||
|
||||
// 获取日志
|
||||
const fetchLogs = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await fetch('/admin/logs?lines=500', {
|
||||
headers: settingsStore.getHeaders()
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
logs.value = parseLogs(data.logs || []);
|
||||
total.value = data.total || 0;
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('获取日志失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 解析日志行
|
||||
const parseLogs = (lines) => {
|
||||
return lines.map((line, index) => {
|
||||
// 格式: 2025-12-20 17:00:00.000 [INFO] [模块] 消息
|
||||
const match = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[([^\]]+)\] (.*)$/);
|
||||
if (match) {
|
||||
return {
|
||||
id: index,
|
||||
time: match[1],
|
||||
level: match[2],
|
||||
module: match[3],
|
||||
message: match[4],
|
||||
raw: line
|
||||
};
|
||||
}
|
||||
return { id: index, raw: line, level: 'INFO', time: '', module: '', message: line };
|
||||
});
|
||||
};
|
||||
|
||||
// 过滤后的日志
|
||||
const filteredLogs = computed(() => {
|
||||
return logs.value.filter(log => {
|
||||
// 级别过滤
|
||||
if (levelFilter.value !== 'all' && log.level !== levelFilter.value) {
|
||||
return false;
|
||||
}
|
||||
// 搜索过滤
|
||||
if (searchText.value) {
|
||||
const search = searchText.value.toLowerCase();
|
||||
return log.raw.toLowerCase().includes(search);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// 清除日志
|
||||
const clearLogs = () => {
|
||||
Modal.confirm({
|
||||
title: '确认清除日志',
|
||||
content: '此操作将删除所有系统日志文件,是否继续?',
|
||||
okText: '确认清除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
const res = await fetch('/admin/logs', {
|
||||
method: 'DELETE',
|
||||
headers: settingsStore.getHeaders()
|
||||
});
|
||||
if (res.ok) {
|
||||
message.success('日志已清除');
|
||||
logs.value = [];
|
||||
total.value = 0;
|
||||
} else {
|
||||
message.error('清除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('请求失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 导出日志
|
||||
const exportLogs = () => {
|
||||
const content = logs.value.map(l => l.raw).join('\n');
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `system-${new Date().toISOString().split('T')[0]}.log`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// 切换自动刷新
|
||||
const toggleAutoRefresh = (newState) => {
|
||||
autoRefresh.value = newState;
|
||||
if (newState) {
|
||||
fetchLogs(); // 立即刷新一次
|
||||
refreshInterval.value = setInterval(fetchLogs, 5000);
|
||||
} else {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value);
|
||||
refreshInterval.value = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchLogs();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card title="系统日志" :bordered="false">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<!-- 第一行:级别筛选和操作按钮 -->
|
||||
<div class="toolbar-row">
|
||||
<a-select v-model:value="levelFilter" style="width: 90px" size="small">
|
||||
<a-select-option value="all">全部</a-select-option>
|
||||
<a-select-option value="INFO">INFO</a-select-option>
|
||||
<a-select-option value="WARN">WARN</a-select-option>
|
||||
<a-select-option value="ERRO">ERROR</a-select-option>
|
||||
<a-select-option value="DBUG">DEBUG</a-select-option>
|
||||
</a-select>
|
||||
<a-space :size="4">
|
||||
<a-tooltip :title="autoRefresh ? '关闭自动刷新' : '开启自动刷新'">
|
||||
<a-button size="small" :type="autoRefresh ? 'primary' : 'default'"
|
||||
@click="toggleAutoRefresh(!autoRefresh)">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="导出日志">
|
||||
<a-button size="small" @click="exportLogs">
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="清除日志">
|
||||
<a-button size="small" danger @click="clearLogs">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- 第二行:搜索框 -->
|
||||
<div class="toolbar-row">
|
||||
<a-input-search v-model:value="searchText" placeholder="搜索日志" size="small" enter-button allow-clear
|
||||
style="width: 100%;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div style="margin-bottom: 12px; color: #8c8c8c; font-size: 12px;">
|
||||
共 {{ total }} 条日志,当前显示 {{ filteredLogs.length }} 条
|
||||
<span v-if="autoRefresh" style="color: #1890ff; margin-left: 8px;">
|
||||
<ReloadOutlined :spin="true" /> 自动刷新中
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<div class="log-container">
|
||||
<div v-for="log in filteredLogs" :key="log.id" class="log-line" :class="'level-' + log.level.toLowerCase()">
|
||||
<span class="log-time">{{ log.time }}</span>
|
||||
<a-tag :color="levelConfig[log.level]?.color || '#8c8c8c'" size="small" style="margin: 0 8px;">
|
||||
{{ log.level }}
|
||||
</a-tag>
|
||||
<span class="log-module">[{{ log.module }}]</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
<a-empty v-if="filteredLogs.length === 0" description="暂无日志" />
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.log-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.log-line:hover {
|
||||
background: #e6f7ff;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.log-module {
|
||||
color: #1890ff;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.level-erro .log-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.level-warn .log-message {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.level-dbug .log-message {
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
/* 工具栏样式 */
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.toolbar-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 大屏幕:工具栏一行显示 */
|
||||
@media (min-width: 768px) {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.toolbar-row:last-child {
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,7 @@ const routes = [
|
||||
{ path: '/settings/adapters', component: () => import('@/components/settings/adapters.vue') },
|
||||
{ path: '/tools/display', component: () => import('@/components/tools/display.vue') },
|
||||
{ path: '/tools/cache', component: () => import('@/components/tools/cache.vue') },
|
||||
{ path: '/tools/logs', component: () => import('@/components/tools/logs.vue') },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -16,6 +16,12 @@ export const useSystemStore = defineStore('system', {
|
||||
free: 0
|
||||
},
|
||||
|
||||
// 安全模式状态
|
||||
safeMode: {
|
||||
enabled: false,
|
||||
reason: null
|
||||
},
|
||||
|
||||
// 仪表盘统计信息
|
||||
stats: {
|
||||
totalRequests: 0,
|
||||
|
||||
@@ -16,6 +16,10 @@ export default defineConfig({
|
||||
'/admin': {
|
||||
target: 'http://127.0.0.1:3000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/v1': {
|
||||
target: 'http://127.0.0.1:3000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user