fix: 改进错误提示、代理测试和落地IP查询
- 前端所有页面显示后端真实错误信息,不再显示通用"失败" - 新增代理测试功能和落地IP查询 - 修复凭证未配置时返回500改为400+中文提示 - 修复Settings页面字段名与后端一致(proxy_ip) - 修复favicon 404、bcrypt版本兼容、tsconfig配置
This commit is contained in:
@@ -83,3 +83,12 @@ def test_connection(
|
||||
):
|
||||
"""Test Airwallex API connection with current settings."""
|
||||
return airwallex_service.test_connection(db)
|
||||
|
||||
|
||||
@router.post("/test-proxy")
|
||||
def test_proxy(
|
||||
db: Session = Depends(get_db),
|
||||
user: AdminUser = Depends(get_current_user),
|
||||
):
|
||||
"""Test proxy connectivity and query outbound IP."""
|
||||
return airwallex_service.test_proxy(db)
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from fastapi import HTTPException
|
||||
from app.models.db_models import SystemSetting
|
||||
from app.proxy_client import ProxiedAirwallexClient
|
||||
|
||||
@@ -44,7 +45,7 @@ def get_client(db: Session) -> ProxiedAirwallexClient:
|
||||
proxy_url = _build_proxy_url(db)
|
||||
|
||||
if not client_id or not api_key:
|
||||
raise ValueError("Airwallex credentials not configured. Please set client_id and api_key in Settings.")
|
||||
raise HTTPException(status_code=400, detail="Airwallex 凭证未配置,请在系统设置中填写 Client ID 和 API Key")
|
||||
|
||||
config_hash = f"{client_id}:{api_key}:{base_url}:{proxy_url}"
|
||||
if _client_instance and _client_config_hash == config_hash:
|
||||
@@ -249,10 +250,58 @@ def get_issuing_config(db: Session) -> Optional[Dict[str, Any]]:
|
||||
|
||||
|
||||
def test_connection(db: Session) -> Dict[str, Any]:
|
||||
"""Test Airwallex API connection."""
|
||||
"""Test Airwallex API connection with detailed error info."""
|
||||
try:
|
||||
client = get_client(db)
|
||||
client.authenticate()
|
||||
return {"success": True, "message": "Connection successful"}
|
||||
return {"success": True, "message": "连接成功,认证通过"}
|
||||
except HTTPException as e:
|
||||
return {"success": False, "message": e.detail}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
error_msg = str(e)
|
||||
# Try to extract more detail from httpx response errors
|
||||
if hasattr(e, "response"):
|
||||
try:
|
||||
detail = e.response.json()
|
||||
error_msg = f"HTTP {e.response.status_code}: {detail.get('message', detail)}"
|
||||
except Exception:
|
||||
error_msg = f"HTTP {e.response.status_code}: {e.response.text[:200]}"
|
||||
return {"success": False, "message": error_msg}
|
||||
|
||||
|
||||
def test_proxy(db: Session) -> Dict[str, Any]:
|
||||
"""Test proxy connectivity and return the outbound IP address."""
|
||||
import httpx
|
||||
|
||||
proxy_url = _build_proxy_url(db)
|
||||
result: Dict[str, Any] = {"proxy_configured": bool(proxy_url), "proxy_url": None}
|
||||
|
||||
if proxy_url:
|
||||
# Mask password in display
|
||||
masked = proxy_url
|
||||
if "@" in proxy_url:
|
||||
prefix, rest = proxy_url.rsplit("@", 1)
|
||||
if ":" in prefix:
|
||||
scheme_user = prefix.rsplit(":", 1)[0]
|
||||
masked = f"{scheme_user}:****@{rest}"
|
||||
result["proxy_url"] = masked
|
||||
|
||||
# Query outbound IP — with proxy if configured, otherwise direct
|
||||
for label, use_proxy in [("proxy", True), ("direct", False)]:
|
||||
if label == "proxy" and not proxy_url:
|
||||
continue
|
||||
try:
|
||||
client_kwargs: Dict[str, Any] = {"timeout": 10}
|
||||
if use_proxy and proxy_url:
|
||||
client_kwargs["proxy"] = proxy_url
|
||||
with httpx.Client(**client_kwargs) as client:
|
||||
resp = client.get("https://api.ipify.org?format=json")
|
||||
ip_data = resp.json()
|
||||
result[f"{label}_ip"] = ip_data.get("ip", "unknown")
|
||||
result[f"{label}_status"] = "ok"
|
||||
except Exception as e:
|
||||
result[f"{label}_ip"] = None
|
||||
result[f"{label}_status"] = f"failed: {str(e)[:150]}"
|
||||
|
||||
result["success"] = result.get("proxy_status", result.get("direct_status")) == "ok"
|
||||
return result
|
||||
|
||||
3111
frontend/package-lock.json
generated
Normal file
3111
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Modal, Form, Input, Select, InputNumber, Divider, message } from 'antd'
|
||||
import { cardsApi, cardholdersApi } from '@/services/api'
|
||||
import { cardsApi, cardholdersApi, getErrorMsg } from '@/services/api'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
@@ -29,8 +29,8 @@ export default function CreateCardModal({ open, onClose, onSuccess }: Props) {
|
||||
.then((res) => {
|
||||
setCardholders(res.data.items || res.data || [])
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('获取持卡人列表失败')
|
||||
.catch((err) => {
|
||||
message.error(getErrorMsg(err, '获取持卡人列表失败'))
|
||||
})
|
||||
.finally(() => setChLoading(false))
|
||||
}
|
||||
@@ -43,9 +43,8 @@ export default function CreateCardModal({ open, onClose, onSuccess }: Props) {
|
||||
message.success('卡片创建成功')
|
||||
form.resetFields()
|
||||
onSuccess()
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { detail?: string } } }
|
||||
message.error(error.response?.data?.detail || '创建卡片失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '创建卡片失败'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Modal, Form, Input, DatePicker, Select, Row, Col, message } from 'antd'
|
||||
import { cardholdersApi } from '@/services/api'
|
||||
import { cardholdersApi, getErrorMsg } from '@/services/api'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
@@ -36,9 +36,8 @@ export default function CreateCardholderModal({ open, onClose, onSuccess }: Prop
|
||||
message.success('持卡人创建成功')
|
||||
form.resetFields()
|
||||
onSuccess()
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { detail?: string } } }
|
||||
message.error(error.response?.data?.detail || '创建持卡人失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '创建持卡人失败'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { PlusOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { tokensApi } from '@/services/api'
|
||||
import { tokensApi, getErrorMsg } from '@/services/api'
|
||||
|
||||
const { Paragraph } = Typography
|
||||
|
||||
@@ -49,8 +49,8 @@ export default function ApiTokens() {
|
||||
try {
|
||||
const res = await tokensApi.getTokens()
|
||||
setData(res.data.items || res.data || [])
|
||||
} catch {
|
||||
message.error('获取令牌列表失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '获取令牌列表失败'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -71,8 +71,8 @@ export default function ApiTokens() {
|
||||
setModalOpen(false)
|
||||
form.resetFields()
|
||||
fetchData()
|
||||
} catch {
|
||||
message.error('创建令牌失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '创建令牌失败'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ export default function ApiTokens() {
|
||||
await tokensApi.deleteToken(id)
|
||||
message.success('令牌已删除')
|
||||
fetchData()
|
||||
} catch {
|
||||
message.error('删除令牌失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '删除令牌失败'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Table, DatePicker, message } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { auditLogsApi } from '@/services/api'
|
||||
import { auditLogsApi, getErrorMsg } from '@/services/api'
|
||||
|
||||
const { RangePicker } = DatePicker
|
||||
|
||||
@@ -37,8 +37,8 @@ export default function AuditLog() {
|
||||
const res = await auditLogsApi.getAuditLogs(params)
|
||||
setData(res.data.items || res.data || [])
|
||||
setTotal(res.data.total || 0)
|
||||
} catch {
|
||||
message.error('获取操作日志失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '获取操作日志失败'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { cardsApi, transactionsApi } from '@/services/api'
|
||||
import { cardsApi, transactionsApi, getErrorMsg } from '@/services/api'
|
||||
|
||||
interface CardInfo {
|
||||
card_id: string
|
||||
@@ -75,8 +75,8 @@ export default function CardDetail() {
|
||||
try {
|
||||
const res = await cardsApi.getCard(id)
|
||||
setCard(res.data)
|
||||
} catch {
|
||||
message.error('获取卡片信息失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '获取卡片信息失败'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -111,8 +111,8 @@ export default function CardDetail() {
|
||||
const res = await cardsApi.getCardDetails(id)
|
||||
setCard((prev) => (prev ? { ...prev, ...res.data } : prev))
|
||||
setShowSensitive(true)
|
||||
} catch {
|
||||
message.error('获取敏感信息失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '获取敏感信息失败'))
|
||||
} finally {
|
||||
setSensitiveLoading(false)
|
||||
}
|
||||
@@ -126,8 +126,8 @@ export default function CardDetail() {
|
||||
})
|
||||
message.success(action === 'activate' ? '卡片已激活' : '卡片已冻结')
|
||||
fetchCard()
|
||||
} catch {
|
||||
message.error('操作失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '操作失败'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +138,8 @@ export default function CardDetail() {
|
||||
message.success('限额更新成功')
|
||||
setLimitModalOpen(false)
|
||||
fetchCard()
|
||||
} catch {
|
||||
message.error('更新限额失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '更新限额失败'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Table, Button, Tag, message } from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { cardholdersApi } from '@/services/api'
|
||||
import { cardholdersApi, getErrorMsg } from '@/services/api'
|
||||
import CreateCardholderModal from '@/components/CreateCardholderModal'
|
||||
|
||||
interface Cardholder {
|
||||
@@ -29,8 +29,8 @@ export default function Cardholders() {
|
||||
const res = await cardholdersApi.getCardholders({ page, page_size: pageSize })
|
||||
setData(res.data.items || res.data)
|
||||
setTotal(res.data.total || 0)
|
||||
} catch {
|
||||
message.error('获取持卡人列表失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '获取持卡人列表失败'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Table, Button, Select, Input, Tag, Space, message } from 'antd'
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { cardsApi } from '@/services/api'
|
||||
import { cardsApi, getErrorMsg } from '@/services/api'
|
||||
import CreateCardModal from '@/components/CreateCardModal'
|
||||
|
||||
interface CardRecord {
|
||||
@@ -43,8 +43,8 @@ export default function Cards() {
|
||||
const res = await cardsApi.getCards(params)
|
||||
setData(res.data.items || res.data)
|
||||
setTotal(res.data.total || 0)
|
||||
} catch {
|
||||
message.error('获取卡片列表失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '获取卡片列表失败'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
PlusCircleOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { dashboardApi } from '@/services/api'
|
||||
import { dashboardApi, getErrorMsg } from '@/services/api'
|
||||
|
||||
interface DashboardData {
|
||||
account_balance: number
|
||||
@@ -26,8 +26,8 @@ export default function Dashboard() {
|
||||
try {
|
||||
const res = await dashboardApi.getDashboard()
|
||||
setData(res.data)
|
||||
} catch {
|
||||
message.error('获取仪表板数据失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '获取仪表板数据失败'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, Form, Input, InputNumber, Button, Divider, message, Space } from 'antd'
|
||||
import { SaveOutlined, ApiOutlined } from '@ant-design/icons'
|
||||
import { Card, Form, Input, InputNumber, Button, Divider, message, Space, Descriptions, Tag } from 'antd'
|
||||
import { SaveOutlined, ApiOutlined, GlobalOutlined } from '@ant-design/icons'
|
||||
import { settingsApi } from '@/services/api'
|
||||
import api from '@/services/api'
|
||||
|
||||
interface SettingsData {
|
||||
airwallex_client_id: string
|
||||
airwallex_api_key: string
|
||||
airwallex_base_url: string
|
||||
proxy_host: string
|
||||
proxy_port: number | null
|
||||
proxy_username: string
|
||||
proxy_password: string
|
||||
daily_card_limit: number
|
||||
interface ProxyTestResult {
|
||||
success: boolean
|
||||
proxy_configured: boolean
|
||||
proxy_url: string | null
|
||||
proxy_ip: string | null
|
||||
proxy_status: string | null
|
||||
direct_ip: string | null
|
||||
direct_status: string | null
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
@@ -20,15 +18,18 @@ export default function Settings() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [testingProxy, setTestingProxy] = useState(false)
|
||||
const [proxyResult, setProxyResult] = useState<ProxyTestResult | null>(null)
|
||||
|
||||
const fetchSettings = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await settingsApi.getSettings()
|
||||
// Backend returns a list of {key, value} — convert to object for form
|
||||
const data: Record<string, string> = {}
|
||||
if (Array.isArray(res.data)) {
|
||||
for (const item of res.data) {
|
||||
for (const item of res.data as { key: string; value: string }[]) {
|
||||
// Don't fill masked values into password fields
|
||||
if (item.value === '********') continue
|
||||
data[item.key] = item.value
|
||||
}
|
||||
}
|
||||
@@ -45,12 +46,12 @@ export default function Settings() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleSave = async (values: SettingsData) => {
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
// Convert form object to list of {key, value} for backend
|
||||
const values = await form.validateFields()
|
||||
const updates = Object.entries(values)
|
||||
.filter(([, v]) => v !== undefined && v !== null)
|
||||
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
||||
.map(([key, value]) => ({ key, value: String(value) }))
|
||||
await settingsApi.updateSettings(updates)
|
||||
message.success('设置已保存')
|
||||
@@ -64,9 +65,9 @@ export default function Settings() {
|
||||
const handleTestConnection = async () => {
|
||||
setTesting(true)
|
||||
try {
|
||||
const res = await api.post('/settings/test-connection')
|
||||
const res = await settingsApi.testConnection()
|
||||
if (res.data.success) {
|
||||
message.success('连接测试成功')
|
||||
message.success(res.data.message || '连接测试成功')
|
||||
} else {
|
||||
message.error(res.data.message || '连接测试失败')
|
||||
}
|
||||
@@ -77,12 +78,30 @@ export default function Settings() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTestProxy = async () => {
|
||||
setTestingProxy(true)
|
||||
setProxyResult(null)
|
||||
try {
|
||||
const res = await settingsApi.testProxy()
|
||||
setProxyResult(res.data)
|
||||
if (res.data.success) {
|
||||
message.success('代理测试成功')
|
||||
} else {
|
||||
message.warning('代理测试失败,请检查配置')
|
||||
}
|
||||
} catch {
|
||||
message.error('代理测试请求失败')
|
||||
} finally {
|
||||
setTestingProxy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-header">
|
||||
<h2>系统设置</h2>
|
||||
</div>
|
||||
<Form form={form} layout="vertical" onFinish={handleSave} disabled={loading}>
|
||||
<Form form={form} layout="vertical" disabled={loading}>
|
||||
<Card title="Airwallex 凭证" style={{ marginBottom: 16 }}>
|
||||
<Form.Item
|
||||
label="Client ID"
|
||||
@@ -96,10 +115,10 @@ export default function Settings() {
|
||||
name="airwallex_api_key"
|
||||
rules={[{ required: true, message: '请输入 API Key' }]}
|
||||
>
|
||||
<Input.Password placeholder="请输入 API Key" />
|
||||
<Input.Password placeholder="请输入 API Key(留空则保持不变)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Base URL" name="airwallex_base_url">
|
||||
<Input placeholder="https://api-demo.airwallex.com" />
|
||||
<Input placeholder="https://api-demo.airwallex.com/" />
|
||||
</Form.Item>
|
||||
<Button
|
||||
icon={<ApiOutlined />}
|
||||
@@ -111,7 +130,7 @@ export default function Settings() {
|
||||
</Card>
|
||||
|
||||
<Card title="代理设置" style={{ marginBottom: 16 }}>
|
||||
<Form.Item label="代理 IP" name="proxy_host">
|
||||
<Form.Item label="代理 IP" name="proxy_ip">
|
||||
<Input placeholder="例如: 127.0.0.1" />
|
||||
</Form.Item>
|
||||
<Form.Item label="代理端口" name="proxy_port">
|
||||
@@ -121,8 +140,38 @@ export default function Settings() {
|
||||
<Input placeholder="可选" />
|
||||
</Form.Item>
|
||||
<Form.Item label="代理密码" name="proxy_password">
|
||||
<Input.Password placeholder="可选" />
|
||||
<Input.Password placeholder="可选(留空则保持不变)" />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<GlobalOutlined />}
|
||||
onClick={handleTestProxy}
|
||||
loading={testingProxy}
|
||||
>
|
||||
测试代理 / 查询落地 IP
|
||||
</Button>
|
||||
</Space>
|
||||
{proxyResult && (
|
||||
<Descriptions bordered size="small" column={1} style={{ marginTop: 16 }}>
|
||||
<Descriptions.Item label="代理配置">
|
||||
{proxyResult.proxy_configured
|
||||
? <Tag color="blue">{proxyResult.proxy_url}</Tag>
|
||||
: <Tag color="default">未配置</Tag>}
|
||||
</Descriptions.Item>
|
||||
{proxyResult.proxy_ip !== undefined && (
|
||||
<Descriptions.Item label="代理落地 IP">
|
||||
{proxyResult.proxy_status === 'ok'
|
||||
? <Tag color="green">{proxyResult.proxy_ip}</Tag>
|
||||
: <Tag color="red">{proxyResult.proxy_status}</Tag>}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="服务器直连 IP">
|
||||
{proxyResult.direct_status === 'ok'
|
||||
? <Tag color="green">{proxyResult.direct_ip}</Tag>
|
||||
: <Tag color="red">{proxyResult.direct_status}</Tag>}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="卡片限制" style={{ marginBottom: 16 }}>
|
||||
@@ -133,7 +182,7 @@ export default function Settings() {
|
||||
|
||||
<Divider />
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={saving}>
|
||||
<Button type="primary" onClick={handleSave} icon={<SaveOutlined />} loading={saving}>
|
||||
保存所有设置
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Table, Tag, DatePicker, Input, Select, Tabs, message } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { transactionsApi, authorizationsApi } from '@/services/api'
|
||||
import { transactionsApi, authorizationsApi, getErrorMsg } from '@/services/api'
|
||||
|
||||
const { RangePicker } = DatePicker
|
||||
|
||||
@@ -68,8 +68,8 @@ export default function Transactions() {
|
||||
setAuthData(res.data.items || res.data)
|
||||
setTotal(res.data.total || 0)
|
||||
}
|
||||
} catch {
|
||||
message.error('获取数据失败')
|
||||
} catch (err) {
|
||||
message.error(getErrorMsg(err, '获取数据失败'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,18 @@ api.interceptors.response.use(
|
||||
}
|
||||
)
|
||||
|
||||
/** Extract readable error message from axios error */
|
||||
export function getErrorMsg(err: unknown, fallback = '操作失败'): string {
|
||||
if (axios.isAxiosError(err)) {
|
||||
const detail = err.response?.data?.detail
|
||||
if (typeof detail === 'string') return detail
|
||||
if (typeof detail === 'object' && detail?.msg) return detail.msg
|
||||
if (err.response?.data?.message) return err.response.data.message
|
||||
if (err.response?.status) return `请求失败 (HTTP ${err.response.status})`
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
login: (username: string, password: string) =>
|
||||
api.post('/auth/login', { username, password }),
|
||||
@@ -71,6 +83,7 @@ export const settingsApi = {
|
||||
getSettings: () => api.get('/settings'),
|
||||
updateSettings: (data: { key: string; value: string }[]) => api.put('/settings', data),
|
||||
testConnection: () => api.post('/settings/test-connection'),
|
||||
testProxy: () => api.post('/settings/test-proxy'),
|
||||
}
|
||||
|
||||
export const cardLogsApi = {
|
||||
|
||||
Reference in New Issue
Block a user