mirror of
https://github.com/ZeroCatDev/ClassworksKVAdmin.git
synced 2025-10-22 03:23:10 +00:00
feat: update favicon and logo assets; modify index.vue layout
- Added new favicon.ico and logo.png files to the assets directory. - Introduced a new logo.svg file for scalable vector graphics support. - Commented out the shield icon in index.vue for a cleaner header layout.
This commit is contained in:
parent
e3a9901c34
commit
934cdb7040
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Classworks KV</title>
|
||||
</head>
|
||||
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 367 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/favicon.ico
Normal file
BIN
src/assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 367 B |
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
23
src/assets/logo.svg
Normal file
23
src/assets/logo.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" viewBox="0 0 256 256" fill="none">
|
||||
<g clip-path="url(#clip-path-74_1)">
|
||||
<path fill="#FFFFFF" d="M0 256L256 256L256 0L0 0L0 256Z">
|
||||
</path>
|
||||
<rect x="0" y="0" width="256" height="128" fill="#D8C4A0" >
|
||||
</rect>
|
||||
<rect x="0" y="128" width="256" height="128" fill="#F5E0BB" >
|
||||
</rect>
|
||||
<path d="M28 228L128 128L228 128L128 228L28 228Z" fill-rule="evenodd" fill="#241A04" >
|
||||
</path>
|
||||
<path d="M28 128L128 28L228 28L128 128L28 128Z" fill-rule="evenodd" fill="#52452A" >
|
||||
</path>
|
||||
<g >
|
||||
<path fill="#000000" d="M-3049.01 2467.94L-3043.48 2467.94L-3043.48 2466.99L-3045.92 2466.99C-3046.36 2466.99 -3046.9 2467.04 -3047.36 2467.08C-3045.29 2465.12 -3043.9 2463.33 -3043.9 2461.57C-3043.9 2460.01 -3044.9 2458.99 -3046.47 2458.99C-3047.58 2458.99 -3048.35 2459.49 -3049.06 2460.27L-3048.43 2460.9C-3047.93 2460.31 -3047.32 2459.88 -3046.6 2459.88C-3045.51 2459.88 -3044.98 2460.61 -3044.98 2461.62C-3044.98 2463.13 -3046.25 2464.88 -3049.01 2467.29L-3049.01 2467.94ZM-3039.27 2468.1C-3037.9 2468.1 -3036.74 2466.95 -3036.74 2465.24C-3036.74 2463.39 -3037.7 2462.48 -3039.19 2462.48C-3039.87 2462.48 -3040.64 2462.88 -3041.18 2463.54C-3041.13 2460.81 -3040.13 2459.89 -3038.91 2459.89C-3038.38 2459.89 -3037.85 2460.15 -3037.52 2460.56L-3036.89 2459.89C-3037.39 2459.36 -3038.04 2458.99 -3038.96 2458.99C-3040.66 2458.99 -3042.21 2460.3 -3042.21 2463.74C-3042.21 2466.65 -3040.95 2468.1 -3039.27 2468.1ZM-3041.15 2464.41C-3040.58 2463.6 -3039.91 2463.3 -3039.36 2463.3C-3038.3 2463.3 -3037.78 2464.05 -3037.78 2465.24C-3037.78 2466.44 -3038.43 2467.23 -3039.27 2467.23C-3040.37 2467.23 -3041.03 2466.24 -3041.15 2464.41ZM-3035.17 2467.94L-3030.34 2467.94L-3030.34 2467.03L-3032.1 2467.03L-3032.1 2459.15L-3032.95 2459.15C-3033.43 2459.42 -3033.99 2459.62 -3034.77 2459.77L-3034.77 2460.47L-3033.2 2460.47L-3033.2 2467.03L-3035.17 2467.03L-3035.17 2467.94ZM-3029.51 2467.94L-3028.4 2467.94L-3027.54 2465.25L-3024.33 2465.25L-3023.49 2467.94L-3022.31 2467.94L-3025.3 2459.15L-3026.54 2459.15L-3029.51 2467.94ZM-3027.27 2464.38L-3026.84 2463.02C-3026.52 2462.02 -3026.24 2461.08 -3025.96 2460.04L-3025.91 2460.04C-3025.62 2461.06 -3025.35 2462.02 -3025.02 2463.02L-3024.6 2464.38L-3027.27 2464.38ZM-3018.93 2468.1C-3017.26 2468.1 -3016.19 2466.58 -3016.19 2463.51C-3016.19 2460.47 -3017.26 2458.99 -3018.93 2458.99C-3020.61 2458.99 -3021.67 2460.47 -3021.67 2463.51C-3021.67 2466.58 -3020.61 2468.1 -3018.93 2468.1ZM-3018.93 2467.21C-3019.93 2467.21 -3020.61 2466.09 -3020.61 2463.51C-3020.61 2460.95 -3019.93 2459.85 -3018.93 2459.85C-3017.93 2459.85 -3017.25 2460.95 -3017.25 2463.51C-3017.25 2466.09 -3017.93 2467.21 -3018.93 2467.21ZM-3012.27 2468.1C-3010.6 2468.1 -3009.53 2466.58 -3009.53 2463.51C-3009.53 2460.47 -3010.6 2458.99 -3012.27 2458.99C-3013.95 2458.99 -3015.01 2460.47 -3015.01 2463.51C-3015.01 2466.58 -3013.95 2468.1 -3012.27 2468.1ZM-3012.27 2467.21C-3013.27 2467.21 -3013.95 2466.09 -3013.95 2463.51C-3013.95 2460.95 -3013.27 2459.85 -3012.27 2459.85C-3011.27 2459.85 -3010.59 2460.95 -3010.59 2463.51C-3010.59 2466.09 -3011.27 2467.21 -3012.27 2467.21Z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip-path-74_1">
|
||||
<path d="M0 256L256 256L256 0L0 0L0 256Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
Before Width: | Height: | Size: 496 B |
@ -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
|
||||
|
115
src/lib/axios.js
115
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))
|
||||
}
|
||||
)
|
||||
|
@ -389,9 +389,9 @@ onMounted(async () => {
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="rounded-lg bg-gradient-to-br from-primary to-primary/80 p-2.5 shadow-lg">
|
||||
<!--<div class="rounded-lg bg-gradient-to-br from-primary to-primary/80 p-2.5 shadow-lg">
|
||||
<Shield class="h-6 w-6 text-primary-foreground" />
|
||||
</div>
|
||||
</div>-->
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
|
||||
Classworks KV
|
||||
|
Loading…
x
Reference in New Issue
Block a user