1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-07-01 16:49:22 +00:00

Add @microsoft/clarity and ratelimit-header-parser dependencies; integrate Clarity for user tracking and implement rate limit handling in axios instance. Update App.vue to include RateLimitModal for user notifications.

This commit is contained in:
SunWuyuan 2025-05-11 11:43:47 +08:00
parent 596c6ac918
commit 9f4fe0b9dd
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
6 changed files with 226 additions and 26 deletions

View File

@ -11,10 +11,12 @@
},
"dependencies": {
"@mdi/font": "7.4.47",
"@microsoft/clarity": "^1.0.0",
"axios": "^1.8.4",
"idb": "^8.0.2",
"js-yaml": "^4.1.0",
"pinyin-pro": "^3.26.0",
"ratelimit-header-parser": "^0.1.0",
"roboto-fontface": "*",
"typewriter-effect": "^2.21.0",
"uuid": "^9.0.1",

16
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ importers:
'@mdi/font':
specifier: 7.4.47
version: 7.4.47
'@microsoft/clarity':
specifier: ^1.0.0
version: 1.0.0
axios:
specifier: ^1.8.4
version: 1.8.4
@ -23,6 +26,9 @@ importers:
pinyin-pro:
specifier: ^3.26.0
version: 3.26.0
ratelimit-header-parser:
specifier: ^0.1.0
version: 0.1.0
roboto-fontface:
specifier: '*'
version: 0.10.0
@ -939,6 +945,9 @@ packages:
'@mdi/font@7.4.47':
resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==}
'@microsoft/clarity@1.0.0':
resolution: {integrity: sha512-2QY6SmXnqRj6dWhNY8NYCN3e53j4zCFebH4wGnNhdGV1mqAsQwql2fT0w8TISxCvwwfVp8idsWLIdrRHOms1PQ==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -2624,6 +2633,9 @@ packages:
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
ratelimit-header-parser@0.1.0:
resolution: {integrity: sha512-+gg0VX4h0nBT5JWZfaPNwAV8pWRZa3MAFHLZNUYO5yqw+4IvU64HmPtA3aRapQ2uSP1x3Ta4TZO0k516dtNLZA==}
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
@ -4332,6 +4344,8 @@ snapshots:
'@mdi/font@7.4.47': {}
'@microsoft/clarity@1.0.0': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -6175,6 +6189,8 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
ratelimit-header-parser@0.1.0: {}
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0

View File

@ -1,47 +1,52 @@
<template>
<v-app>
<router-view v-slot="{ Component, route }">
<transition name="md3" mode="out-in">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
<global-message />
<transition name="md3" mode="out-in">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
<global-message />
<rate-limit-modal />
</v-app>
</template>
<script setup>
import { onMounted, watch } from 'vue';
import { useTheme } from 'vuetify';
import { getSetting } from '@/utils/settings';
import { useRouter, useRoute } from 'vue-router';
import { onMounted, watch } from "vue";
import { useTheme } from "vuetify";
import { getSetting } from "@/utils/settings";
import { useRouter, useRoute } from "vue-router";
import RateLimitModal from "@/components/RateLimitModal.vue";
import Clarity from "@microsoft/clarity";
const theme = useTheme();
const router = useRouter();
const route = useRoute();
onMounted(() => {
//
const savedTheme = getSetting('theme.mode');
const savedTheme = getSetting("theme.mode");
theme.global.name.value = savedTheme;
//
checkProviderType();
Clarity.identify(getSetting("device.uuid"), getSetting("server.domain"), getSetting("server.provider"), getSetting("server.classNumber")); // only custom-id is required
});
//
function checkProviderType() {
const currentProvider = getSetting('server.provider');
const currentProvider = getSetting("server.provider");
//
if ((currentProvider === 'server' || currentProvider === 'indexedDB') &&
route.path !== '/datamigration') {
console.log('检测到旧的数据提供者类型,正在重定向到数据迁移页面...');
if (
(currentProvider === "server" || currentProvider === "indexedDB") &&
route.path !== "/datamigration"
) {
console.log("检测到旧的数据提供者类型,正在重定向到数据迁移页面...");
router.push({
path: '/datamigration',
path: "/datamigration",
query: {
reason: 'legacy_provider',
provider: currentProvider
}
reason: "legacy_provider",
provider: currentProvider,
},
});
}
}
@ -50,17 +55,17 @@ function checkProviderType() {
watch(
() => route.path,
(newPath) => {
if (newPath !== '/datamigration') {
if (newPath !== "/datamigration") {
checkProviderType();
}
}
);
</script>
<style>
.md3-enter-active,
.md3-leave-active {
transition: opacity 0.3s cubic-bezier(0.4, 0.0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.md3-enter-from {
@ -71,4 +76,5 @@ watch(
.md3-leave-to {
opacity: 0;
transform: translateX(-0.5vw);
}</style>
}
</style>

View File

@ -1,6 +1,7 @@
import axios from "axios";
import { getSetting } from '@/utils/settings';
import { getSetting } from "@/utils/settings";
import { parseRateLimit } from "ratelimit-header-parser";
import RateLimitModal from "@/components/RateLimitModal.vue";
// 基本配置
const axiosInstance = axios.create({
// 可以在这里添加基础配置,例如超时时间等
@ -11,7 +12,7 @@ const axiosInstance = axios.create({
axiosInstance.interceptors.request.use(
(requestConfig) => {
// 确保每次请求时都获取最新的 siteKey
const siteKey = getSetting('server.siteKey');
const siteKey = getSetting("server.siteKey");
if (siteKey) {
requestConfig.headers["x-site-key"] = siteKey;
}
@ -23,4 +24,33 @@ axiosInstance.interceptors.request.use(
}
);
// 响应拦截器
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
// 处理限速响应 (HTTP 429)
if (error.response && error.response.status === 429) {
try {
// 解析限速头信息
const rateLimitInfo = parseRateLimit(error.response);
if (rateLimitInfo) {
// 显示限速弹窗,直接传递重置时间
RateLimitModal.show(
rateLimitInfo.reset,
error.config.url,
error.config.method.toUpperCase()
);
}
} catch (parseError) {
console.error("解析限速头信息失败:", parseError);
}
}
return Promise.reject(error);
}
);
export default axiosInstance;

View File

@ -0,0 +1,143 @@
<template>
<v-dialog v-model="isVisible" max-width="500" persistent>
<v-card class="rate-limit-modal">
<v-card-title class="text-center pa-4 bg-error text-white">
<v-icon icon="mdi-clock-alert-outline" size="large" class="mr-2" />
请求频率超限
</v-card-title>
<v-card-text class="pa-6">
<div class="text-body-1 mb-4">您的请求过于频繁请稍后再试</div>
<v-card flat class="mb-4" v-if="activeRequests.length > 0">
<v-card-text>
<v-list
v-for="(request, index) in activeRequests"
:key="index"
class="mb-4"
><v-list-item prepend-icon="mdi-web" color="primary">
<v-list-item-title>
等待时间:
<span class="text-primary font-weight-bold">{{
request.remainingSeconds
}}</span>
</v-list-item-title>
<v-list-item-subtitle>
{{ request.method }} {{ request.path }}
</v-list-item-subtitle>
</v-list-item></v-list
>
<v-divider
v-if="index < activeRequests.length - 1"
class="my-3"
></v-divider>
</v-card-text>
</v-card>
<div class="text-body-2 text-grey">
请在等待时间后再次尝试或减少请求频率以避免限制
</div>
</v-card-text>
<v-card-actions class="pa-4 pt-0">
<v-spacer></v-spacer>
<v-btn color="primary" variant="tonal" @click="close"> 我知道了 </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
//
let instance = null;
const RateLimitModalComponent = {
name: "RateLimitModal",
data() {
return {
isVisible: false,
activeRequests: [],
};
},
computed: {
hasActiveRequests() {
return this.activeRequests.length > 0;
},
},
watch: {
hasActiveRequests(newValue) {
this.isVisible = newValue;
},
},
methods: {
close() {
this.isVisible = false;
},
show(resetTime, path, method) {
const id = Date.now() + Math.random().toString(36).substring(2, 9);
const remainingSeconds = Math.max(
0,
Math.floor((new Date(resetTime) - new Date()) / 1000)
);
const request = {
id,
resetTime,
path,
method,
remainingSeconds,
};
this.activeRequests.push(request);
this.startCountdown(id);
this.isVisible = true;
},
startCountdown(id) {
const request = this.activeRequests.find((req) => req.id === id);
if (!request) return;
const intervalId = setInterval(() => {
const index = this.activeRequests.findIndex((req) => req.id === id);
if (index === -1) {
clearInterval(intervalId);
return;
}
this.activeRequests[index].remainingSeconds--;
if (this.activeRequests[index].remainingSeconds <= 0) {
clearInterval(intervalId);
this.activeRequests.splice(index, 1);
}
}, 1000);
// intervalId便
request.intervalId = intervalId;
},
clearAllCountdowns() {
this.activeRequests.forEach((request) => {
if (request.intervalId) {
clearInterval(request.intervalId);
}
});
this.activeRequests = [];
},
},
beforeUnmount() {
this.clearAllCountdowns();
},
created() {
// 便访
instance = this;
},
};
//
RateLimitModalComponent.show = function (resetTime, path, method) {
if (instance) {
instance.show(resetTime, path, method);
}
};
export default RateLimitModalComponent;
</script>

View File

@ -13,7 +13,10 @@ import GlobalMessage from '@/components/GlobalMessage.vue'
// Composables
import { createApp } from 'vue'
import Clarity from '@microsoft/clarity';
const projectId = "rhp8uqoc3l"
Clarity.init(projectId);
import messageService from './utils/message';
const app = createApp(App)