diff --git a/index.html b/index.html index 98d8148..257f3c4 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Classworks KV diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..8fe5a93 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/favicon.ico b/src/assets/favicon.ico new file mode 100644 index 0000000..8fe5a93 Binary files /dev/null and b/src/assets/favicon.ico differ diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..a5f23ae Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..a33474c --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/vue.svg b/src/assets/vue.svg deleted file mode 100644 index 770e9d3..0000000 --- a/src/assets/vue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/api.js b/src/lib/api.js index cc03606..ada7c6e 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -1,3 +1,5 @@ +import axiosInstance from './axios' + const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3030' const SITE_KEY = import.meta.env.VITE_SITE_KEY || '' @@ -8,27 +10,23 @@ class ApiClient { } async fetch(endpoint, options = {}) { - const headers = { - 'Content-Type': 'application/json', - 'x-site-key': this.siteKey, - ...options.headers, - } + const method = options.method || 'GET' + const headers = { ...options.headers } + const data = options.body + const params = options.params - const response = await fetch(`${this.baseUrl}${endpoint}`, { - ...options, + // 通过 axios 实例发起请求(已内置 baseURL 与 x-site-key) + const result = await axiosInstance.request({ + url: endpoint, + method, headers, + data, + params, }) - if (!response.ok) { - const error = await response.json().catch(() => ({ message: 'Unknown error' })) - throw new Error(error.message || `HTTP ${response.status}`) - } - - if (response.status === 204) { - return {} - } - - return response.json() + // axios 响应拦截器已返回 response.data,这里做空值统一 + if (result === '' || result === undefined || result === null) return {} + return result } // 带认证的fetch diff --git a/src/lib/axios.js b/src/lib/axios.js index 67a6b02..893761a 100644 --- a/src/lib/axios.js +++ b/src/lib/axios.js @@ -1,4 +1,5 @@ import axios from 'axios' +import { deviceStore } from './deviceStore' const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3030' const SITE_KEY = import.meta.env.VITE_SITE_KEY || '' @@ -23,13 +24,121 @@ axiosInstance.interceptors.request.use( } ) -// 响应拦截器 +// 设备注册去重(避免并发重复注册) +const registrationLocks = new Map() + +function getHeaderIgnoreCase(headers = {}, key) { + if (!headers) return undefined + const lowerKey = key.toLowerCase() + for (const k of Object.keys(headers)) { + if (k.toLowerCase() === lowerKey) return headers[k] + } + return undefined +} + +function extractUuidFromUrl(url = '') { + try { + const path = url || '' + // /apps/devices/{uuid}/... + let m = path.match(/\/apps\/devices\/([0-9a-fA-F-]{8,})/) + if (m) return m[1] + // /devices/{uuid}/... + m = path.match(/\/devices\/([0-9a-fA-F-]{8,})/) + if (m) return m[1] + // /accounts/device/{uuid} + m = path.match(/\/accounts\/device\/([0-9a-fA-F-]{8,})/) + if (m) return m[1] + + // query ?uuid=... + const qIndex = path.indexOf('?') + if (qIndex >= 0) { + const usp = new URLSearchParams(path.slice(qIndex + 1)) + const q = usp.get('uuid') + if (q) return q + } + } catch (e) { + // ignore + } + return undefined +} + +async function ensureDeviceRegistered(uuid, authHeader) { + if (!uuid) return false + if (registrationLocks.has(uuid)) { + try { + await registrationLocks.get(uuid) + return true + } catch { + return false + } + } + const deviceName = '未命名设备' + const headers = {} + if (authHeader) headers['Authorization'] = authHeader + + const p = axiosInstance.post( + '/devices', + { uuid, deviceName }, + { headers, // 避免递归触发注册重试 + skipDeviceRegistrationRetry: true, + __isRegistrationRequest: true } + ) + registrationLocks.set(uuid, p) + try { + await p + // 保存UUID到本地存储,确保后续可用 + try { deviceStore.setDeviceUuid(uuid) } catch {} + return true + } catch (e) { + return false + } finally { + registrationLocks.delete(uuid) + } +} + +// 响应拦截器(含自动注册并重试) axiosInstance.interceptors.response.use( (response) => { return response.data }, - (error) => { - const message = error.response?.data?.message || error.message || 'Unknown error' + async (error) => { + const config = error.config || {} + const skip = config.skipDeviceRegistrationRetry || config.__isRegistrationRequest + const backendMessage = error.response?.data?.message + const message = backendMessage || error.message || 'Unknown error' + + // 仅在后端提示设备不存在时尝试注册并重试,且保证只重试一次 + if (!skip && !config.__retriedAfterRegistration && typeof backendMessage === 'string' && backendMessage.startsWith('设备不存在')) { + // 从 headers / url / body 提取 uuid + const uuidFromHeader = getHeaderIgnoreCase(config.headers, 'x-device-uuid') + const uuidFromUrl = extractUuidFromUrl(config.url) + let uuid = uuidFromHeader || uuidFromUrl || deviceStore.getDeviceUuid() + if (!uuid && config.data) { + try { + const body = typeof config.data === 'string' ? JSON.parse(config.data) : config.data + if (body && typeof body === 'object' && body.uuid) uuid = body.uuid + } catch {} + } + + // 可能需要账户授权头 + const authHeader = getHeaderIgnoreCase(config.headers, 'Authorization') + + if (uuid) { + const ok = await ensureDeviceRegistered(uuid, authHeader) + if (ok) { + try { + config.__retriedAfterRegistration = true + // 原样重试 + const retryResp = await axiosInstance.request(config) + return retryResp + } catch (retryErr) { + const retryMsg = retryErr?.response?.data?.message || retryErr.message || message + return Promise.reject(new Error(retryMsg)) + } + } + } + } + return Promise.reject(new Error(message)) } ) diff --git a/src/pages/index.vue b/src/pages/index.vue index 1ae2d8d..6af1694 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -389,9 +389,9 @@ onMounted(async () => {
-
+

Classworks KV