function normalizeApiBase(apiBase) { const base = String(apiBase ?? '/api').trim() || '/api'; return base.replace(/\/$/, ''); } export function buildPageDataAuthPath(apiBase, pageId) { const base = normalizeApiBase(apiBase); return `${base}/public/pages/${encodeURIComponent(String(pageId ?? '').trim())}/data-auth`; } export function buildPageDataPublicPath(apiBase, pageId, dataset, suffix = '') { const base = normalizeApiBase(apiBase); const safePageId = encodeURIComponent(String(pageId ?? '').trim()); const safeDataset = encodeURIComponent(String(dataset ?? '').trim()); const tail = suffix ? (suffix.startsWith('/') ? suffix : `/${suffix}`) : ''; return `${base}/public/pages/${safePageId}/data/${safeDataset}${tail}`; } function buildAuthHeaders(token) { const headers = { accept: 'application/json' }; if (token) headers['x-page-data-token'] = token; return headers; } async function parseJsonResponse(response) { const payload = await response.json().catch(() => ({})); if (!response.ok) { const error = payload?.error ?? {}; throw Object.assign(new Error(error.message ?? '页面数据请求失败'), { code: error.code ?? 'page_data_failed', status: response.status, }); } return payload?.data ?? payload; } export function createPageDataBrowserClient({ pageId, apiBase = '/api', token = null, fetchImpl = globalThis.fetch, } = {}) { if (!pageId) throw new Error('pageId 不能为空'); if (typeof fetchImpl !== 'function') throw new Error('fetch 不可用'); let sessionToken = token; async function request(method, path, { body, query, auth = true, extraHeaders = {} } = {}) { const url = new URL(path, typeof window !== 'undefined' ? window.location.origin : 'http://localhost'); if (query && typeof query === 'object') { for (const [key, value] of Object.entries(query)) { if (value == null || value === '') continue; url.searchParams.set(key, String(value)); } } const headers = { ...(body ? { 'content-type': 'application/json' } : {}), ...(auth ? buildAuthHeaders(sessionToken) : { accept: 'application/json' }), ...(extraHeaders && typeof extraHeaders === 'object' ? extraHeaders : {}), }; const response = await fetchImpl(url.toString(), { method, headers, credentials: 'include', body: body ? JSON.stringify(body) : undefined, }); return parseJsonResponse(response); } return { get pageId() { return pageId; }, get token() { return sessionToken; }, setToken(nextToken) { sessionToken = nextToken ? String(nextToken) : null; }, async authenticate(password) { const result = await request('POST', buildPageDataAuthPath(apiBase, pageId), { body: { password }, auth: false, }); if (result?.token) sessionToken = result.token; return result; }, async listRows(dataset, query = {}) { return request('GET', buildPageDataPublicPath(apiBase, pageId, dataset), { query }); }, async getSchema(dataset) { return request('GET', buildPageDataPublicPath(apiBase, pageId, dataset, 'schema')); }, async getStats(dataset) { return request('GET', buildPageDataPublicPath(apiBase, pageId, dataset, 'stats')); }, async insertRow(dataset, row, options = {}) { const captchaToken = options.captchaToken ?? options.turnstileToken ?? options.captcha_token ?? null; const extraHeaders = captchaToken ? { 'x-page-data-captcha': String(captchaToken) } : {}; return request('POST', buildPageDataPublicPath(apiBase, pageId, dataset, 'rows'), { body: row, extraHeaders, }); }, async updateRow(dataset, rowId, row) { return request('PATCH', buildPageDataPublicPath(apiBase, pageId, dataset, `rows/${rowId}`), { body: row }); }, async deleteRow(dataset, rowId) { return request('DELETE', buildPageDataPublicPath(apiBase, pageId, dataset, `rows/${rowId}`)); }, }; }