feat(webui): 请求历史记录与请求模型页面

后端:
- 新增 SQLite 存储请求/响应历史 (src/utils/history.js)
- Admin API 历史查询、删除、媒体重试等端点
- PoolManager.downloadMedia 用于媒体下载

WebUI:
- 请求历史页面:筛选、分页、详情、媒体预览
- 请求历史升级为请求模型页面,支持直接发送请求
- 移动端适配与体验优化
- 批量代理设置与删除实例
- 实例配置编辑删除改用 name 匹配
This commit is contained in:
daidai
2026-03-27 19:46:16 +08:00
Unverified
parent c3163729f4
commit 50b09ed740
10 changed files with 2430 additions and 94 deletions
+25 -63
View File
@@ -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>
+66 -7
View File
@@ -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