191 lines
5.6 KiB
JavaScript
191 lines
5.6 KiB
JavaScript
/**
|
||
* AKSRT Admin API Client
|
||
*
|
||
* Communicates with the WordPress REST API at /wp-json/aksrt/v1
|
||
* (or index.php?rest_route=/aksrt/v1 when pretty permalinks are off).
|
||
* Methods are organized into domain modules while the client keeps the shared
|
||
* request pipeline and schema-domain normalization contract.
|
||
*/
|
||
|
||
import { apiMethods } from './modules/index.js'
|
||
|
||
const API_ROOT = window.aksrtAdminData?.apiUrl || '/wp-json/aksrt/v1'
|
||
const NONCE = window.aksrtAdminData?.nonce || ''
|
||
|
||
function normalizeDomainKey(domain) {
|
||
return String(domain || 'global').trim().replace(/-/g, '_')
|
||
}
|
||
|
||
/**
|
||
* Join REST base + endpoint correctly for both pretty and plain permalinks.
|
||
*
|
||
* Pretty: https://site/wp-json/aksrt/v1 + /banners?kind=x
|
||
* → https://site/wp-json/aksrt/v1/banners?kind=x
|
||
*
|
||
* Plain: https://site/index.php?rest_route=/aksrt/v1 + /banners?kind=x
|
||
* → https://site/index.php?rest_route=/aksrt/v1/banners&kind=x
|
||
* (must NOT produce rest_route=/aksrt/v1/banners?kind=x)
|
||
*
|
||
* @param {string} baseUrl
|
||
* @param {string} endpoint path starting with /, may include ?query
|
||
* @returns {string}
|
||
*/
|
||
function joinRestUrl(baseUrl, endpoint) {
|
||
let path = endpoint == null || endpoint === '' ? '/' : String(endpoint)
|
||
let extraQuery = ''
|
||
const qIdx = path.indexOf('?')
|
||
if (qIdx !== -1) {
|
||
extraQuery = path.slice(qIdx + 1)
|
||
path = path.slice(0, qIdx)
|
||
}
|
||
if (!path.startsWith('/')) {
|
||
path = '/' + path
|
||
}
|
||
|
||
const base = String(baseUrl || '').trim() || '/wp-json/aksrt/v1'
|
||
|
||
// Plain permalinks: rest_route query mode
|
||
if (base.includes('rest_route=')) {
|
||
let u
|
||
try {
|
||
u = new URL(base, typeof window !== 'undefined' ? window.location.origin : 'https://localhost')
|
||
} catch {
|
||
// Fallback: naive append (should not hit with valid rest_url)
|
||
return base.replace(/\/?$/, '') + path + (extraQuery ? (base.includes('?') ? '&' : '?') + extraQuery : '')
|
||
}
|
||
|
||
const baseRoute = (u.searchParams.get('rest_route') || '/aksrt/v1').replace(/\/$/, '') || '/aksrt/v1'
|
||
let fullRoute
|
||
if (path === '/') {
|
||
fullRoute = baseRoute
|
||
} else if (path.startsWith('/aksrt/')) {
|
||
fullRoute = path
|
||
} else {
|
||
fullRoute = baseRoute + path
|
||
}
|
||
u.searchParams.set('rest_route', fullRoute)
|
||
|
||
if (extraQuery) {
|
||
const extra = new URLSearchParams(extraQuery)
|
||
extra.forEach((v, k) => {
|
||
u.searchParams.set(k, v)
|
||
})
|
||
}
|
||
return u.toString()
|
||
}
|
||
|
||
// Pretty /wp-json mode
|
||
const root = base.replace(/\/$/, '')
|
||
let url = root + (path === '/' ? '' : path)
|
||
if (extraQuery) {
|
||
url += (url.includes('?') ? '&' : '?') + extraQuery
|
||
}
|
||
return url
|
||
}
|
||
|
||
class ApiClient {
|
||
constructor() {
|
||
this.baseUrl = API_ROOT
|
||
}
|
||
|
||
normalizeDomainKey(domain) {
|
||
return normalizeDomainKey(domain)
|
||
}
|
||
|
||
async request(endpointOrConfig, options = {}) {
|
||
// Dual-signature support:
|
||
// request('/forum/overview')
|
||
// request({ url: '/aksrt/v1/forum/topics', method: 'GET', params: { limit: 20 } })
|
||
// request({ url: '/aksrt/v1/users/1/restrictions', method: 'GET' })
|
||
let endpoint, method, headers, params, body, rest
|
||
|
||
if (typeof endpointOrConfig === 'object' && endpointOrConfig !== null) {
|
||
const cfg = endpointOrConfig
|
||
endpoint = cfg.url || cfg.endpoint || ''
|
||
method = cfg.method || 'GET'
|
||
params = cfg.params
|
||
body = cfg.data
|
||
headers = cfg.headers
|
||
rest = { ...cfg }
|
||
delete rest.url; delete rest.endpoint; delete rest.method
|
||
delete rest.params; delete rest.data; delete rest.headers
|
||
} else {
|
||
endpoint = endpointOrConfig
|
||
method = options.method || 'GET'
|
||
params = options.params
|
||
body = options.body !== undefined ? options.body : options.data
|
||
headers = options.headers
|
||
rest = { ...options }
|
||
delete rest.method; delete rest.params; delete rest.body
|
||
delete rest.data; delete rest.headers
|
||
}
|
||
|
||
if (endpoint.startsWith('/wp-json/aksrt/v1')) {
|
||
endpoint = endpoint.replace('/wp-json/aksrt/v1', '') || '/'
|
||
} else if (endpoint.startsWith('/aksrt/v1')) {
|
||
endpoint = endpoint.replace('/aksrt/v1', '') || '/'
|
||
}
|
||
|
||
let url = joinRestUrl(this.baseUrl, endpoint)
|
||
|
||
if (params && typeof params === 'object') {
|
||
const qs = new URLSearchParams(params).toString()
|
||
if (qs) {
|
||
url += (url.includes('?') ? '&' : '?') + qs
|
||
}
|
||
}
|
||
|
||
let finalBody = body
|
||
if (body !== undefined && typeof body === 'object' && !(body instanceof FormData)) {
|
||
finalBody = JSON.stringify(body)
|
||
}
|
||
|
||
const config = {
|
||
method,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-WP-Nonce': NONCE,
|
||
...headers,
|
||
},
|
||
...rest,
|
||
}
|
||
if (finalBody !== undefined && method !== 'GET') {
|
||
config.body = finalBody
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(url, config)
|
||
|
||
const text = await response.text()
|
||
let data = {}
|
||
try {
|
||
data = text ? JSON.parse(text) : {}
|
||
} catch {
|
||
throw new Error(
|
||
`REST 返回非 JSON,endpoint=${endpoint},status=${response.status},body=${text.slice(0, 160)}`
|
||
)
|
||
}
|
||
|
||
if (!response.ok || data.success === false) {
|
||
const msg = data.message || data.data?.message || `HTTP ${response.status}`
|
||
|
||
if (response.status === 403 || response.status === 401) {
|
||
console.warn(`[API Auth] ${endpoint}: ${msg} — 可能需要刷新页面重新登录`)
|
||
}
|
||
|
||
throw new Error(msg)
|
||
}
|
||
|
||
return data
|
||
} catch (error) {
|
||
console.error(`[API Error] ${endpoint}:`, error)
|
||
throw error
|
||
}
|
||
}
|
||
}
|
||
|
||
Object.assign(ApiClient.prototype, apiMethods)
|
||
|
||
export { normalizeDomainKey, joinRestUrl }
|
||
export default new ApiClient()
|