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:
SunWuyuan 2025-10-06 20:40:39 +08:00
parent e3a9901c34
commit 934cdb7040
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
10 changed files with 153 additions and 25 deletions

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Classworks KV</title> <title>Classworks KV</title>
</head> </head>

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

23
src/assets/logo.svg Normal file
View 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

View File

@ -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

View File

@ -1,3 +1,5 @@
import axiosInstance from './axios'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3030' const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3030'
const SITE_KEY = import.meta.env.VITE_SITE_KEY || '' const SITE_KEY = import.meta.env.VITE_SITE_KEY || ''
@ -8,27 +10,23 @@ class ApiClient {
} }
async fetch(endpoint, options = {}) { async fetch(endpoint, options = {}) {
const headers = { const method = options.method || 'GET'
'Content-Type': 'application/json', const headers = { ...options.headers }
'x-site-key': this.siteKey, const data = options.body
...options.headers, const params = options.params
}
const response = await fetch(`${this.baseUrl}${endpoint}`, { // 通过 axios 实例发起请求(已内置 baseURL 与 x-site-key
...options, const result = await axiosInstance.request({
url: endpoint,
method,
headers, headers,
data,
params,
}) })
if (!response.ok) { // axios 响应拦截器已返回 response.data这里做空值统一
const error = await response.json().catch(() => ({ message: 'Unknown error' })) if (result === '' || result === undefined || result === null) return {}
throw new Error(error.message || `HTTP ${response.status}`) return result
}
if (response.status === 204) {
return {}
}
return response.json()
} }
// 带认证的fetch // 带认证的fetch

View File

@ -1,4 +1,5 @@
import axios from 'axios' import axios from 'axios'
import { deviceStore } from './deviceStore'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3030' const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3030'
const SITE_KEY = import.meta.env.VITE_SITE_KEY || '' 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( axiosInstance.interceptors.response.use(
(response) => { (response) => {
return response.data return response.data
}, },
(error) => { async (error) => {
const message = error.response?.data?.message || error.message || 'Unknown 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)) return Promise.reject(new Error(message))
} }
) )

View File

@ -389,9 +389,9 @@ onMounted(async () => {
<div class="container mx-auto px-6 py-4"> <div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-3"> <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" /> <Shield class="h-6 w-6 text-primary-foreground" />
</div> </div>-->
<div> <div>
<h1 class="text-2xl font-bold bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text"> <h1 class="text-2xl font-bold bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
Classworks KV Classworks KV