mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat(webui): 请求历史记录与请求模型页面
后端: - 新增 SQLite 存储请求/响应历史 (src/utils/history.js) - Admin API 历史查询、删除、媒体重试等端点 - PoolManager.downloadMedia 用于媒体下载 WebUI: - 请求历史页面:筛选、分页、详情、媒体预览 - 请求历史升级为请求模型页面,支持直接发送请求 - 移动端适配与体验优化 - 批量代理设置与删除实例 - 实例配置编辑删除改用 name 匹配
This commit is contained in:
@@ -121,7 +121,7 @@ const openBatchProxy = () => {
|
||||
|
||||
const handleBatchProxySave = async () => {
|
||||
const newList = (instanceData.value || []).map(inst => {
|
||||
if (!selectedRowKeys.value.includes(inst.id)) return inst;
|
||||
if (!selectedRowKeys.value.includes(inst.name)) return inst;
|
||||
return {
|
||||
...inst,
|
||||
proxy: batchProxyForm.value.proxy ? {
|
||||
@@ -152,7 +152,7 @@ const handleBatchDelete = () => {
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
const newList = (instanceData.value || []).filter(
|
||||
inst => !selectedRowKeys.value.includes(inst.id)
|
||||
inst => !selectedRowKeys.value.includes(inst.name)
|
||||
);
|
||||
const success = await settingsStore.saveWorkerConfig(newList);
|
||||
if (success) {
|
||||
@@ -225,7 +225,7 @@ const handleEdit = (record) => {
|
||||
|
||||
// 删除实例
|
||||
const handleDelete = async (record) => {
|
||||
const newList = instanceData.value.filter(item => item.id !== record.id);
|
||||
const newList = instanceData.value.filter(item => item.name !== record.name);
|
||||
await settingsStore.saveWorkerConfig(newList);
|
||||
};
|
||||
|
||||
@@ -233,7 +233,6 @@ const handleDelete = async (record) => {
|
||||
const handleSaveEdit = async () => {
|
||||
// 构建要保存的对象结构
|
||||
const instanceToSave = {
|
||||
id: editingInstance.value ? editingInstance.value.id : `inst_${Date.now()}`,
|
||||
name: editForm.value.name,
|
||||
userDataMark: editForm.value.userDataMark,
|
||||
workers: editForm.value.workers,
|
||||
@@ -254,8 +253,8 @@ const handleSaveEdit = async () => {
|
||||
// 创建
|
||||
newList.push(instanceToSave);
|
||||
} else {
|
||||
// 更新
|
||||
const index = newList.findIndex(item => item.id === editingInstance.value.id);
|
||||
// 更新 - 用原始 name 查找
|
||||
const index = newList.findIndex(item => item.name === editingInstance.value.name);
|
||||
if (index > -1) {
|
||||
newList[index] = instanceToSave;
|
||||
}
|
||||
@@ -371,34 +370,7 @@ const handleRemoveWorker = (index) => {
|
||||
故障转移时最大重试次数,范围 1-10
|
||||
</div>
|
||||
<a-input-number v-model:value="poolConfig.failover.maxRetries" :min="1" :max="10"
|
||||
:disabled="!poolConfig.failover.enabled" style="width: 100%"
|
||||
placeholder="请输入重试次数" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider style="margin: 12px 0;" />
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">图片下载重试</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
|
||||
启用后,图片/视频下载失败时会自动重试下载(不重新生成)
|
||||
</div>
|
||||
<a-switch v-model:checked="poolConfig.failover.imgDlRetry" />
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :md="12">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">下载重试次数</div>
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
|
||||
图片下载失败时的最大重试次数,范围 1-10
|
||||
</div>
|
||||
<a-input-number v-model:value="poolConfig.failover.imgDlRetryMaxRetries" :min="1"
|
||||
:max="10" :disabled="!poolConfig.failover.imgDlRetry" style="width: 100%"
|
||||
placeholder="请输入下载重试次数" />
|
||||
:disabled="!poolConfig.failover.enabled" style="width: 100%" placeholder="请输入重试次数" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -435,8 +407,8 @@ const handleRemoveWorker = (index) => {
|
||||
</template>
|
||||
|
||||
<!-- 实例表格 -->
|
||||
<a-table :columns="columns" :data-source="instanceData" :pagination="false" :row-selection="rowSelection"
|
||||
row-key="id">
|
||||
<a-table :columns="columns" :data-source="instanceData" :pagination="false"
|
||||
:row-selection="rowSelection" row-key="name">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 实例名称 -->
|
||||
<template v-if="column.key === 'name'">
|
||||
@@ -634,23 +606,18 @@ const handleRemoveWorker = (index) => {
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<!-- 批量代理设置弹窗 -->
|
||||
<a-modal v-model:open="batchProxyVisible" title="批量设置代理" okText="应用" cancelText="取消" @ok="handleBatchProxySave">
|
||||
<!-- 批量代理设置模态框 -->
|
||||
<a-modal v-model:open="batchProxyVisible" title="批量设置代理" okText="确定" cancelText="取消"
|
||||
@ok="handleBatchProxySave">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 16px;">
|
||||
将对选中的 {{ selectedRowKeys.length }} 个实例统一设置代理
|
||||
</div>
|
||||
<a-switch v-model:checked="batchProxyForm.proxy" />
|
||||
<span style="margin-left: 8px;">
|
||||
{{ batchProxyForm.proxy ? '启用代理' : '禁用代理' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 是否启用代理 -->
|
||||
<template v-if="batchProxyForm.proxy">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-switch v-model:checked="batchProxyForm.proxy" />
|
||||
<span style="margin-left: 8px;">
|
||||
{{ batchProxyForm.proxy ? '启用代理' : '关闭代理(将清除选中实例的代理配置)' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 代理类型 -->
|
||||
<div style="margin-bottom: 16px;" v-if="batchProxyForm.proxy">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">代理类型</div>
|
||||
<a-segmented v-model:value="batchProxyForm.proxyType" block :options="[
|
||||
{ label: 'SOCKS5', value: 'socks5' },
|
||||
@@ -658,21 +625,18 @@ const handleRemoveWorker = (index) => {
|
||||
]" style="width: 100%" />
|
||||
</div>
|
||||
|
||||
<!-- 服务器地址 -->
|
||||
<div style="margin-bottom: 16px;" v-if="batchProxyForm.proxy">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">服务器地址</div>
|
||||
<a-input v-model:value="batchProxyForm.proxyHost" placeholder="例如: 127.0.0.1" />
|
||||
</div>
|
||||
|
||||
<!-- 端口 -->
|
||||
<div style="margin-bottom: 16px;" v-if="batchProxyForm.proxy">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">端口</div>
|
||||
<a-input-number v-model:value="batchProxyForm.proxyPort" :min="1" :max="65535" style="width: 100%"
|
||||
placeholder="例如: 1080" />
|
||||
<a-input-number v-model:value="batchProxyForm.proxyPort" :min="1" :max="65535"
|
||||
style="width: 100%" placeholder="例如: 1080" />
|
||||
</div>
|
||||
|
||||
<!-- 身份验证 -->
|
||||
<div style="margin-bottom: 16px;" v-if="batchProxyForm.proxy">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">身份验证</div>
|
||||
<a-switch v-model:checked="batchProxyForm.proxyAuth" />
|
||||
<span style="margin-left: 8px;">
|
||||
@@ -680,18 +644,16 @@ const handleRemoveWorker = (index) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 用户名 -->
|
||||
<div style="margin-bottom: 16px;" v-if="batchProxyForm.proxy && batchProxyForm.proxyAuth">
|
||||
<div style="margin-bottom: 16px;" v-if="batchProxyForm.proxyAuth">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">用户名</div>
|
||||
<a-input v-model:value="batchProxyForm.proxyUsername" placeholder="请输入用户名" />
|
||||
</div>
|
||||
|
||||
<!-- 密码 -->
|
||||
<div style="margin-bottom: 16px;" v-if="batchProxyForm.proxy && batchProxyForm.proxyAuth">
|
||||
<div style="margin-bottom: 16px;" v-if="batchProxyForm.proxyAuth">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">密码</div>
|
||||
<a-input-password v-model:value="batchProxyForm.proxyPassword" placeholder="请输入密码" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
@@ -228,7 +228,7 @@ onUnmounted(() => {
|
||||
|
||||
<div class="stats-content">
|
||||
<a-range-picker v-model:value="dateRange" :format="'YYYY-MM-DD'" :placeholder="['开始日期', '结束日期']"
|
||||
size="small" style="width: 240px" @change="fetchRangeStats" />
|
||||
size="small" class="stats-date-picker" @change="fetchRangeStats" />
|
||||
|
||||
<a-divider type="vertical" style="height: 32px; margin: 0 16px" />
|
||||
|
||||
@@ -309,11 +309,13 @@ onUnmounted(() => {
|
||||
<!-- 日志列表 -->
|
||||
<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>
|
||||
<div class="log-meta">
|
||||
<a-tag :color="levelConfig[log.level]?.color || '#8c8c8c'" size="small" style="margin: 0;">
|
||||
{{ log.level }}
|
||||
</a-tag>
|
||||
<span class="log-module">[{{ log.module }}]</span>
|
||||
<span class="log-time">{{ log.time }}</span>
|
||||
</div>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
<a-empty v-if="filteredLogs.length === 0" description="暂无日志" />
|
||||
@@ -325,6 +327,7 @@ onUnmounted(() => {
|
||||
.log-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
background: #fafafa;
|
||||
@@ -335,6 +338,9 @@ onUnmounted(() => {
|
||||
.log-line {
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -346,17 +352,26 @@ onUnmounted(() => {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.log-module {
|
||||
color: #1890ff;
|
||||
margin-right: 8px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.level-erro .log-message {
|
||||
@@ -459,6 +474,11 @@ onUnmounted(() => {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
/* 日期选择器 */
|
||||
.stats-date-picker {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
/* 响应式:小屏幕统计面板垂直布局 */
|
||||
@media (max-width: 576px) {
|
||||
.stats-content {
|
||||
@@ -474,4 +494,43 @@ onUnmounted(() => {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式:移动端日志堆叠布局 */
|
||||
@media (max-width: 768px) {
|
||||
.stats-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.log-line:hover {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.log-meta {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user