2025-08-24 22:32:37 +08:00
|
|
|
|
import { useUserStore, checkAdminPermission, checkSuperAdminPermission } from '@/stores/user'
|
2025-05-02 23:56:59 +08:00
|
|
|
|
import { message } from 'ant-design-vue'
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 基础API请求封装
|
|
|
|
|
|
* 提供统一的请求方法,自动处理认证头和错误
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-06-22 21:21:25 +08:00
|
|
|
|
// 默认请求超时时间(毫秒)
|
|
|
|
|
|
const DEFAULT_TIMEOUT = 30000
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 提取并拼接 422 验证错误详情为用户可读的中文提示
|
|
|
|
|
|
* 后端返回格式可能为 { detail: [{ msg, loc }] } 或 { detail: { field: [errors] } } 或字符串
|
|
|
|
|
|
* @param {*} detail - 后端返回的验证错误详情
|
|
|
|
|
|
* @returns {string} 拼接后的错误信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
function formatValidationError(detail) {
|
|
|
|
|
|
if (!detail) return '请求参数验证失败'
|
|
|
|
|
|
if (typeof detail === 'string') return detail
|
|
|
|
|
|
if (Array.isArray(detail)) {
|
|
|
|
|
|
return detail
|
|
|
|
|
|
.map((err) => {
|
|
|
|
|
|
if (typeof err === 'string') return err
|
|
|
|
|
|
const field = err.loc?.slice(-1)?.[0] ?? err.field ?? ''
|
|
|
|
|
|
const msg = err.msg ?? err.message ?? ''
|
|
|
|
|
|
return field ? `${field}: ${msg}` : msg
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join('; ')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof detail === 'object') {
|
|
|
|
|
|
return Object.entries(detail)
|
|
|
|
|
|
.map(([field, errors]) => {
|
|
|
|
|
|
const msg = Array.isArray(errors) ? errors.join(', ') : String(errors)
|
|
|
|
|
|
return `${field}: ${msg}`
|
|
|
|
|
|
})
|
|
|
|
|
|
.join('; ')
|
|
|
|
|
|
}
|
|
|
|
|
|
return '请求参数验证失败'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 发送API请求的基础函数
|
|
|
|
|
|
* @param {string} url - API端点
|
|
|
|
|
|
* @param {Object} options - 请求选项
|
|
|
|
|
|
* @param {boolean} requiresAuth - 是否需要认证头
|
2025-09-21 23:48:56 +08:00
|
|
|
|
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
2025-05-02 23:56:59 +08:00
|
|
|
|
* @returns {Promise} - 请求结果
|
|
|
|
|
|
*/
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export async function apiRequest(url, options = {}, requiresAuth = true, responseType = 'json') {
|
2026-06-22 21:21:25 +08:00
|
|
|
|
// 超时控制:默认 30s,可通过 options.timeout 自定义;options.signal 支持外部主动取消
|
|
|
|
|
|
const { timeout = DEFAULT_TIMEOUT, signal: userSignal, ...restOptions } = options
|
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
|
let isTimeout = false
|
|
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
|
|
|
isTimeout = true
|
|
|
|
|
|
controller.abort()
|
|
|
|
|
|
}, timeout)
|
|
|
|
|
|
|
|
|
|
|
|
// 联动用户传入的 signal,支持外部主动取消
|
|
|
|
|
|
if (userSignal) {
|
|
|
|
|
|
if (userSignal.aborted) controller.abort()
|
|
|
|
|
|
else {
|
|
|
|
|
|
const onAbort = () => controller.abort()
|
|
|
|
|
|
userSignal.addEventListener('abort', onAbort, { once: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
try {
|
2025-11-08 10:51:30 +08:00
|
|
|
|
const isFormData = options?.body instanceof FormData
|
2025-05-02 23:56:59 +08:00
|
|
|
|
// 默认请求配置
|
|
|
|
|
|
const requestOptions = {
|
2026-06-22 21:21:25 +08:00
|
|
|
|
...restOptions,
|
|
|
|
|
|
signal: controller.signal,
|
2025-05-02 23:56:59 +08:00
|
|
|
|
headers: {
|
2025-11-08 10:51:30 +08:00
|
|
|
|
...(!isFormData ? { 'Content-Type': 'application/json' } : {}),
|
2026-01-15 06:01:34 +08:00
|
|
|
|
...options.headers
|
|
|
|
|
|
}
|
2025-05-02 23:56:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果需要认证,添加认证头
|
|
|
|
|
|
if (requiresAuth) {
|
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
|
if (!userStore.isLoggedIn) {
|
|
|
|
|
|
throw new Error('用户未登录')
|
|
|
|
|
|
}
|
2025-05-04 21:04:01 +08:00
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
Object.assign(requestOptions.headers, userStore.getAuthHeaders())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送请求
|
2026-06-22 21:21:25 +08:00
|
|
|
|
let response
|
|
|
|
|
|
try {
|
|
|
|
|
|
response = await fetch(url, requestOptions)
|
|
|
|
|
|
} catch (fetchError) {
|
|
|
|
|
|
// 超时:AbortError 且 isTimeout 标志为真
|
|
|
|
|
|
if (fetchError.name === 'AbortError') {
|
|
|
|
|
|
if (isTimeout) {
|
|
|
|
|
|
message.error('请求超时,请稍后重试')
|
|
|
|
|
|
throw new Error('请求超时,请稍后重试', { cause: fetchError })
|
|
|
|
|
|
}
|
|
|
|
|
|
// 用户主动取消,静默抛出
|
|
|
|
|
|
throw fetchError
|
|
|
|
|
|
}
|
|
|
|
|
|
// TypeError 通常是网络中断 / DNS 失败 / CORS
|
|
|
|
|
|
if (fetchError instanceof TypeError) {
|
|
|
|
|
|
message.error('网络连接失败,请检查网络')
|
|
|
|
|
|
throw new Error('网络连接失败,请检查网络', { cause: fetchError })
|
|
|
|
|
|
}
|
|
|
|
|
|
throw fetchError
|
|
|
|
|
|
}
|
2025-05-04 21:04:01 +08:00
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
// 处理API返回的错误
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
// 尝试解析错误信息
|
2025-05-07 00:38:33 +08:00
|
|
|
|
let errorMessage = `请求失败: ${response.status}, ${response.statusText}`
|
2025-05-04 21:04:01 +08:00
|
|
|
|
let errorData = null
|
2025-05-05 16:31:35 +08:00
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
try {
|
2025-05-04 21:04:01 +08:00
|
|
|
|
errorData = await response.json()
|
2025-05-02 23:56:59 +08:00
|
|
|
|
errorMessage = errorData.detail || errorData.message || errorMessage
|
2025-11-16 21:24:26 +08:00
|
|
|
|
|
2026-06-22 21:21:25 +08:00
|
|
|
|
// 422 验证错误:提取字段错误并提示用户
|
2025-11-16 21:24:26 +08:00
|
|
|
|
if (response.status === 422) {
|
2026-06-22 21:21:25 +08:00
|
|
|
|
console.error('422验证错误:', {
|
2025-11-16 21:24:26 +08:00
|
|
|
|
url,
|
2026-06-22 21:21:25 +08:00
|
|
|
|
status: response.status,
|
2025-11-16 21:24:26 +08:00
|
|
|
|
responseData: errorData
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2026-06-22 21:21:25 +08:00
|
|
|
|
message.error(formatValidationError(errorData.detail))
|
2025-11-16 21:24:26 +08:00
|
|
|
|
}
|
2026-06-22 21:21:25 +08:00
|
|
|
|
} catch {
|
2025-05-02 23:56:59 +08:00
|
|
|
|
// 如果无法解析JSON,使用默认错误信息
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 特殊处理401和403错误
|
2026-03-28 17:53:53 +08:00
|
|
|
|
const error = new Error(errorMessage)
|
|
|
|
|
|
error.response = {
|
|
|
|
|
|
status: response.status,
|
|
|
|
|
|
statusText: response.statusText,
|
|
|
|
|
|
data: errorData
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
if (response.status === 401) {
|
|
|
|
|
|
// 如果是认证失败,可能需要重新登录
|
|
|
|
|
|
const userStore = useUserStore()
|
2025-09-21 23:48:56 +08:00
|
|
|
|
|
2026-06-22 21:21:25 +08:00
|
|
|
|
// 检查是否是token过期(detail 可能为字符串或对象)
|
|
|
|
|
|
const detailStr = typeof errorData?.detail === 'string' ? errorData.detail : ''
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const isTokenExpired =
|
2026-06-22 21:21:25 +08:00
|
|
|
|
detailStr.includes('令牌已过期') ||
|
|
|
|
|
|
detailStr.includes('token expired') ||
|
|
|
|
|
|
errorMessage?.includes('令牌已过期') ||
|
|
|
|
|
|
errorMessage?.includes('token expired')
|
2025-09-21 23:48:56 +08:00
|
|
|
|
|
|
|
|
|
|
message.error(isTokenExpired ? '登录已过期,请重新登录' : '认证失败,请重新登录')
|
|
|
|
|
|
|
|
|
|
|
|
// 如果用户当前认为自己已登录,则登出
|
2025-05-02 23:56:59 +08:00
|
|
|
|
if (userStore.isLoggedIn) {
|
|
|
|
|
|
userStore.logout()
|
|
|
|
|
|
}
|
2025-09-21 23:48:56 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用setTimeout确保消息显示后再跳转
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
window.location.href = '/login'
|
|
|
|
|
|
}, 1500)
|
|
|
|
|
|
|
2026-03-28 17:53:53 +08:00
|
|
|
|
throw error
|
2025-05-02 23:56:59 +08:00
|
|
|
|
} else if (response.status === 403) {
|
2026-03-28 17:53:53 +08:00
|
|
|
|
error.message = '没有权限执行此操作'
|
|
|
|
|
|
throw error
|
2025-05-23 22:16:34 +08:00
|
|
|
|
} else if (response.status === 500) {
|
2026-07-07 16:24:35 +08:00
|
|
|
|
// 提取后端统一错误响应中的 message 和 trace_id(格式:{success, error: {code, message, trace_id, details}})
|
|
|
|
|
|
const serverError = errorData?.error
|
|
|
|
|
|
const traceId = serverError?.trace_id
|
|
|
|
|
|
error.message = serverError?.message || '服务器内部错误,请稍后重试'
|
|
|
|
|
|
if (traceId) {
|
|
|
|
|
|
error.traceId = traceId
|
|
|
|
|
|
// 控制台记录 trace_id 便于开发者排查,不暴露给用户
|
|
|
|
|
|
console.error(`[500] trace_id=${traceId}`, { url, status: response.status })
|
|
|
|
|
|
}
|
2026-03-28 17:53:53 +08:00
|
|
|
|
throw error
|
2025-05-02 23:56:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 17:53:53 +08:00
|
|
|
|
throw error
|
2025-05-02 23:56:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
// 根据responseType处理响应
|
|
|
|
|
|
if (responseType === 'blob') {
|
|
|
|
|
|
return response
|
|
|
|
|
|
} else if (responseType === 'json') {
|
|
|
|
|
|
// 检查Content-Type以确定如何处理响应
|
|
|
|
|
|
const contentType = response.headers.get('Content-Type')
|
|
|
|
|
|
if (contentType && contentType.includes('application/json')) {
|
|
|
|
|
|
return await response.json()
|
|
|
|
|
|
}
|
|
|
|
|
|
return await response.text()
|
|
|
|
|
|
} else if (responseType === 'text') {
|
|
|
|
|
|
return await response.text()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return response
|
2025-05-02 23:56:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-05-21 23:06:33 +08:00
|
|
|
|
if (error.name !== 'AbortError') {
|
|
|
|
|
|
console.error('API请求错误:', error)
|
|
|
|
|
|
}
|
2025-05-02 23:56:59 +08:00
|
|
|
|
throw error
|
2026-06-22 21:21:25 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
clearTimeout(timeoutId)
|
2025-05-02 23:56:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送GET请求
|
2025-05-04 21:04:01 +08:00
|
|
|
|
* @param {string} url - API端点
|
2025-05-02 23:56:59 +08:00
|
|
|
|
* @param {Object} options - 请求选项
|
|
|
|
|
|
* @param {boolean} requiresAuth - 是否需要认证
|
2025-09-21 23:48:56 +08:00
|
|
|
|
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
2025-05-02 23:56:59 +08:00
|
|
|
|
* @returns {Promise} - 请求结果
|
|
|
|
|
|
*/
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiGet(url, options = {}, requiresAuth = true, responseType = 'json') {
|
|
|
|
|
|
return apiRequest(url, { method: 'GET', ...options }, requiresAuth, responseType)
|
2025-05-02 23:56:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiAdminGet(url, options = {}, responseType = 'json') {
|
2025-08-24 22:32:37 +08:00
|
|
|
|
checkAdminPermission()
|
2025-09-21 23:48:56 +08:00
|
|
|
|
return apiGet(url, options, true, responseType)
|
2025-08-24 22:32:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiSuperAdminGet(url, options = {}, responseType = 'json') {
|
2025-08-24 22:32:37 +08:00
|
|
|
|
checkSuperAdminPermission()
|
2025-09-21 23:48:56 +08:00
|
|
|
|
return apiGet(url, options, true, responseType)
|
2025-08-24 22:32:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 发送POST请求
|
|
|
|
|
|
* @param {string} url - API端点
|
|
|
|
|
|
* @param {Object} data - 请求体数据
|
|
|
|
|
|
* @param {Object} options - 其他请求选项
|
|
|
|
|
|
* @param {boolean} requiresAuth - 是否需要认证
|
2025-09-21 23:48:56 +08:00
|
|
|
|
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
2025-05-02 23:56:59 +08:00
|
|
|
|
* @returns {Promise} - 请求结果
|
|
|
|
|
|
*/
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiPost(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') {
|
2025-05-02 23:56:59 +08:00
|
|
|
|
return apiRequest(
|
2025-05-04 21:04:01 +08:00
|
|
|
|
url,
|
|
|
|
|
|
{
|
|
|
|
|
|
method: 'POST',
|
2025-12-09 16:21:18 +08:00
|
|
|
|
body: data instanceof FormData ? data : JSON.stringify(data),
|
2025-05-04 21:04:01 +08:00
|
|
|
|
...options
|
|
|
|
|
|
},
|
2025-09-21 23:48:56 +08:00
|
|
|
|
requiresAuth,
|
|
|
|
|
|
responseType
|
2025-05-02 23:56:59 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiAdminPost(url, data = {}, options = {}, responseType = 'json') {
|
2025-08-24 22:32:37 +08:00
|
|
|
|
checkAdminPermission()
|
2025-09-21 23:48:56 +08:00
|
|
|
|
return apiPost(url, data, options, true, responseType)
|
2025-08-24 22:32:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiSuperAdminPost(url, data = {}, options = {}, responseType = 'json') {
|
2025-08-24 22:32:37 +08:00
|
|
|
|
checkSuperAdminPermission()
|
2025-09-21 23:48:56 +08:00
|
|
|
|
return apiPost(url, data, options, true, responseType)
|
2025-08-24 22:32:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 发送PUT请求
|
|
|
|
|
|
* @param {string} url - API端点
|
|
|
|
|
|
* @param {Object} data - 请求体数据
|
|
|
|
|
|
* @param {Object} options - 其他请求选项
|
|
|
|
|
|
* @param {boolean} requiresAuth - 是否需要认证
|
2025-09-21 23:48:56 +08:00
|
|
|
|
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
2025-05-02 23:56:59 +08:00
|
|
|
|
* @returns {Promise} - 请求结果
|
|
|
|
|
|
*/
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiPut(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') {
|
2025-05-02 23:56:59 +08:00
|
|
|
|
return apiRequest(
|
2025-05-04 21:04:01 +08:00
|
|
|
|
url,
|
|
|
|
|
|
{
|
|
|
|
|
|
method: 'PUT',
|
2025-12-09 16:21:18 +08:00
|
|
|
|
body: data instanceof FormData ? data : JSON.stringify(data),
|
2025-05-04 21:04:01 +08:00
|
|
|
|
...options
|
|
|
|
|
|
},
|
2025-09-21 23:48:56 +08:00
|
|
|
|
requiresAuth,
|
|
|
|
|
|
responseType
|
2025-05-02 23:56:59 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiAdminPut(url, data = {}, options = {}, responseType = 'json') {
|
2025-08-24 22:32:37 +08:00
|
|
|
|
checkAdminPermission()
|
2025-09-21 23:48:56 +08:00
|
|
|
|
return apiPut(url, data, options, true, responseType)
|
2025-08-24 22:32:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiSuperAdminPut(url, data = {}, options = {}, responseType = 'json') {
|
2025-08-24 22:32:37 +08:00
|
|
|
|
checkSuperAdminPermission()
|
2025-09-21 23:48:56 +08:00
|
|
|
|
return apiPut(url, data, options, true, responseType)
|
2025-08-24 22:32:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 22:17:45 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 发送PATCH请求
|
|
|
|
|
|
* @param {string} url - API端点
|
|
|
|
|
|
* @param {Object} data - 请求体数据
|
|
|
|
|
|
* @param {Object} options - 其他请求选项
|
|
|
|
|
|
* @param {boolean} requiresAuth - 是否需要认证
|
|
|
|
|
|
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
|
|
|
|
|
* @returns {Promise} - 请求结果
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function apiPatch(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') {
|
|
|
|
|
|
return apiRequest(
|
|
|
|
|
|
url,
|
|
|
|
|
|
{
|
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
|
body: data instanceof FormData ? data : JSON.stringify(data),
|
|
|
|
|
|
...options
|
|
|
|
|
|
},
|
|
|
|
|
|
requiresAuth,
|
|
|
|
|
|
responseType
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function apiAdminPatch(url, data = {}, options = {}, responseType = 'json') {
|
|
|
|
|
|
checkAdminPermission()
|
|
|
|
|
|
return apiPatch(url, data, options, true, responseType)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function apiSuperAdminPatch(url, data = {}, options = {}, responseType = 'json') {
|
|
|
|
|
|
checkSuperAdminPermission()
|
|
|
|
|
|
return apiPatch(url, data, options, true, responseType)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 发送DELETE请求
|
|
|
|
|
|
* @param {string} url - API端点
|
|
|
|
|
|
* @param {Object} options - 请求选项
|
|
|
|
|
|
* @param {boolean} requiresAuth - 是否需要认证
|
2025-09-21 23:48:56 +08:00
|
|
|
|
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
|
2025-05-02 23:56:59 +08:00
|
|
|
|
* @returns {Promise} - 请求结果
|
|
|
|
|
|
*/
|
2025-09-21 23:48:56 +08:00
|
|
|
|
export function apiDelete(url, options = {}, requiresAuth = true, responseType = 'json') {
|
|
|
|
|
|
return apiRequest(url, { method: 'DELETE', ...options }, requiresAuth, responseType)
|
2025-05-28 15:16:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-24 22:32:37 +08:00
|
|
|
|
export function apiAdminDelete(url, options = {}) {
|
|
|
|
|
|
checkAdminPermission()
|
|
|
|
|
|
return apiDelete(url, options, true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function apiSuperAdminDelete(url, options = {}) {
|
|
|
|
|
|
checkSuperAdminPermission()
|
|
|
|
|
|
return apiDelete(url, options, true)
|
|
|
|
|
|
}
|