Files
claude-relay-service/web/admin-spa/src/config/api.js
iRubbish 1ee71ffbc9 feat: 完善 API Keys 批量删除功能并修复搜索跨选择问题
## 主要改进

### 🔧 核心修复
- 修复搜索时勾选状态无法保存的问题
- 优化全选/取消全选逻辑,支持跨搜索结果保持选择状态
- 改进批量删除的用户体验
- 添加 Unicode 字符处理中间件,提升请求体解析稳定性

### 🎯 具体变更
- **路由修复**: 解决批量删除路由匹配问题,调整路由顺序
- **API客户端**: 修复 DELETE 方法支持请求体数据传输
- **前端逻辑**: 分离筛选和搜索的监听器,搜索时保持已选中状态
- **全选优化**: 取消全选时只移除当前页选中项,保留其他页面选择
- **Unicode处理**: 添加无效 UTF-16 代理对清理和错误处理机制
- **配置管理**: 将 .mcp.json 添加到 .gitignore,避免本地配置被提交

### 🚀 用户体验提升
- 支持跨搜索结果批量选择和删除
- 批量删除按钮显示选中数量
- 智能的全选状态管理
- 更好的 Unicode 字符处理容错性

### 🧪 测试验证
- 验证搜索切换时选择状态保持
- 确认批量删除功能正常工作
- 检查 Redis 数据清理完整性
- 测试 Unicode 字符处理稳定性

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-19 09:59:54 +08:00

175 lines
4.0 KiB
JavaScript

// API 配置
import { APP_CONFIG, getLoginUrl } from './app'
// 开发环境使用 /webapi 前缀,生产环境不使用前缀
export const API_PREFIX = APP_CONFIG.apiPrefix
// 创建完整的 API URL
export function createApiUrl(path) {
// 确保路径以 / 开头
if (!path.startsWith('/')) {
path = '/' + path
}
return API_PREFIX + path
}
// API 请求的基础配置
export function getRequestConfig(token) {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
}
// 统一的 API 请求类
class ApiClient {
constructor() {
this.baseURL = API_PREFIX
}
// 获取认证 token
getAuthToken() {
const authToken = localStorage.getItem('authToken')
return authToken || null
}
// 构建请求配置
buildConfig(options = {}) {
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
}
// 添加认证 token
const token = this.getAuthToken()
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
}
// 处理响应
async handleResponse(response) {
// 401 未授权,需要重新登录
if (response.status === 401) {
// 如果当前已经在登录页面,不要再次跳转
const currentPath = window.location.pathname + window.location.hash
const isLoginPage = currentPath.includes('/login') || currentPath.endsWith('/')
if (!isLoginPage) {
localStorage.removeItem('authToken')
// 使用统一的登录URL
window.location.href = getLoginUrl()
}
throw new Error('Unauthorized')
}
// 尝试解析 JSON
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
const data = await response.json()
// 如果响应不成功,抛出错误
if (!response.ok) {
throw new Error(data.message || `HTTP ${response.status}`)
}
return data
}
// 非 JSON 响应
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response
}
// GET 请求
async get(url, options = {}) {
const fullUrl = createApiUrl(url)
const config = this.buildConfig({
...options,
method: 'GET'
})
try {
const response = await fetch(fullUrl, config)
return await this.handleResponse(response)
} catch (error) {
console.error('API GET Error:', error)
throw error
}
}
// POST 请求
async post(url, data = null, options = {}) {
const fullUrl = createApiUrl(url)
const config = this.buildConfig({
...options,
method: 'POST',
body: data ? JSON.stringify(data) : undefined
})
try {
const response = await fetch(fullUrl, config)
return await this.handleResponse(response)
} catch (error) {
console.error('API POST Error:', error)
throw error
}
}
// PUT 请求
async put(url, data = null, options = {}) {
const fullUrl = createApiUrl(url)
const config = this.buildConfig({
...options,
method: 'PUT',
body: data ? JSON.stringify(data) : undefined
})
try {
const response = await fetch(fullUrl, config)
return await this.handleResponse(response)
} catch (error) {
console.error('API PUT Error:', error)
throw error
}
}
// DELETE 请求
async delete(url, options = {}) {
const fullUrl = createApiUrl(url)
const { data, ...restOptions } = options
const config = this.buildConfig({
...restOptions,
method: 'DELETE',
body: data ? JSON.stringify(data) : undefined
})
try {
const response = await fetch(fullUrl, config)
return await this.handleResponse(response)
} catch (error) {
console.error('API DELETE Error:', error)
throw error
}
}
}
// 导出单例实例
export const apiClient = new ApiClient()