mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-02 09:19:23 +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:
parent
596c6ac918
commit
9f4fe0b9dd
@ -11,10 +11,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "7.4.47",
|
"@mdi/font": "7.4.47",
|
||||||
|
"@microsoft/clarity": "^1.0.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"idb": "^8.0.2",
|
"idb": "^8.0.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"pinyin-pro": "^3.26.0",
|
"pinyin-pro": "^3.26.0",
|
||||||
|
"ratelimit-header-parser": "^0.1.0",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"typewriter-effect": "^2.21.0",
|
"typewriter-effect": "^2.21.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
|
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
|||||||
'@mdi/font':
|
'@mdi/font':
|
||||||
specifier: 7.4.47
|
specifier: 7.4.47
|
||||||
version: 7.4.47
|
version: 7.4.47
|
||||||
|
'@microsoft/clarity':
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.8.4
|
specifier: ^1.8.4
|
||||||
version: 1.8.4
|
version: 1.8.4
|
||||||
@ -23,6 +26,9 @@ importers:
|
|||||||
pinyin-pro:
|
pinyin-pro:
|
||||||
specifier: ^3.26.0
|
specifier: ^3.26.0
|
||||||
version: 3.26.0
|
version: 3.26.0
|
||||||
|
ratelimit-header-parser:
|
||||||
|
specifier: ^0.1.0
|
||||||
|
version: 0.1.0
|
||||||
roboto-fontface:
|
roboto-fontface:
|
||||||
specifier: '*'
|
specifier: '*'
|
||||||
version: 0.10.0
|
version: 0.10.0
|
||||||
@ -939,6 +945,9 @@ packages:
|
|||||||
'@mdi/font@7.4.47':
|
'@mdi/font@7.4.47':
|
||||||
resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==}
|
resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==}
|
||||||
|
|
||||||
|
'@microsoft/clarity@1.0.0':
|
||||||
|
resolution: {integrity: sha512-2QY6SmXnqRj6dWhNY8NYCN3e53j4zCFebH4wGnNhdGV1mqAsQwql2fT0w8TISxCvwwfVp8idsWLIdrRHOms1PQ==}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -2624,6 +2633,9 @@ packages:
|
|||||||
randombytes@2.1.0:
|
randombytes@2.1.0:
|
||||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||||
|
|
||||||
|
ratelimit-header-parser@0.1.0:
|
||||||
|
resolution: {integrity: sha512-+gg0VX4h0nBT5JWZfaPNwAV8pWRZa3MAFHLZNUYO5yqw+4IvU64HmPtA3aRapQ2uSP1x3Ta4TZO0k516dtNLZA==}
|
||||||
|
|
||||||
react-dom@18.3.1:
|
react-dom@18.3.1:
|
||||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4332,6 +4344,8 @@ snapshots:
|
|||||||
|
|
||||||
'@mdi/font@7.4.47': {}
|
'@mdi/font@7.4.47': {}
|
||||||
|
|
||||||
|
'@microsoft/clarity@1.0.0': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@ -6175,6 +6189,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
ratelimit-header-parser@0.1.0: {}
|
||||||
|
|
||||||
react-dom@18.3.1(react@18.3.1):
|
react-dom@18.3.1(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
52
src/App.vue
52
src/App.vue
@ -1,47 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<router-view v-slot="{ Component, route }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<transition name="md3" mode="out-in">
|
<transition name="md3" mode="out-in">
|
||||||
<component :is="Component" :key="route.path" />
|
<component :is="Component" :key="route.path" />
|
||||||
</transition>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
<global-message />
|
<global-message />
|
||||||
|
<rate-limit-modal />
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, watch } from 'vue';
|
import { onMounted, watch } from "vue";
|
||||||
import { useTheme } from 'vuetify';
|
import { useTheme } from "vuetify";
|
||||||
import { getSetting } from '@/utils/settings';
|
import { getSetting } from "@/utils/settings";
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import RateLimitModal from "@/components/RateLimitModal.vue";
|
||||||
|
import Clarity from "@microsoft/clarity";
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 应用保存的主题设置
|
// 应用保存的主题设置
|
||||||
const savedTheme = getSetting('theme.mode');
|
const savedTheme = getSetting("theme.mode");
|
||||||
theme.global.name.value = savedTheme;
|
theme.global.name.value = savedTheme;
|
||||||
|
|
||||||
// 检查存储提供者类型
|
// 检查存储提供者类型
|
||||||
checkProviderType();
|
checkProviderType();
|
||||||
|
Clarity.identify(getSetting("device.uuid"), getSetting("server.domain"), getSetting("server.provider"), getSetting("server.classNumber")); // only custom-id is required
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查存储提供者类型,如果是已废弃的类型则重定向
|
// 检查存储提供者类型,如果是已废弃的类型则重定向
|
||||||
function checkProviderType() {
|
function checkProviderType() {
|
||||||
const currentProvider = getSetting('server.provider');
|
const currentProvider = getSetting("server.provider");
|
||||||
|
|
||||||
// 如果是旧的提供者类型且当前不在迁移页面,则重定向到数据迁移页面
|
// 如果是旧的提供者类型且当前不在迁移页面,则重定向到数据迁移页面
|
||||||
if ((currentProvider === 'server' || currentProvider === 'indexedDB') &&
|
if (
|
||||||
route.path !== '/datamigration') {
|
(currentProvider === "server" || currentProvider === "indexedDB") &&
|
||||||
console.log('检测到旧的数据提供者类型,正在重定向到数据迁移页面...');
|
route.path !== "/datamigration"
|
||||||
|
) {
|
||||||
|
console.log("检测到旧的数据提供者类型,正在重定向到数据迁移页面...");
|
||||||
router.push({
|
router.push({
|
||||||
path: '/datamigration',
|
path: "/datamigration",
|
||||||
query: {
|
query: {
|
||||||
reason: 'legacy_provider',
|
reason: "legacy_provider",
|
||||||
provider: currentProvider
|
provider: currentProvider,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,17 +55,17 @@ function checkProviderType() {
|
|||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
(newPath) => {
|
(newPath) => {
|
||||||
if (newPath !== '/datamigration') {
|
if (newPath !== "/datamigration") {
|
||||||
checkProviderType();
|
checkProviderType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.md3-enter-active,
|
.md3-enter-active,
|
||||||
.md3-leave-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 {
|
.md3-enter-from {
|
||||||
@ -71,4 +76,5 @@ watch(
|
|||||||
.md3-leave-to {
|
.md3-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-0.5vw);
|
transform: translateX(-0.5vw);
|
||||||
}</style>
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import axios from "axios";
|
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({
|
const axiosInstance = axios.create({
|
||||||
// 可以在这里添加基础配置,例如超时时间等
|
// 可以在这里添加基础配置,例如超时时间等
|
||||||
@ -11,7 +12,7 @@ const axiosInstance = axios.create({
|
|||||||
axiosInstance.interceptors.request.use(
|
axiosInstance.interceptors.request.use(
|
||||||
(requestConfig) => {
|
(requestConfig) => {
|
||||||
// 确保每次请求时都获取最新的 siteKey
|
// 确保每次请求时都获取最新的 siteKey
|
||||||
const siteKey = getSetting('server.siteKey');
|
const siteKey = getSetting("server.siteKey");
|
||||||
if (siteKey) {
|
if (siteKey) {
|
||||||
requestConfig.headers["x-site-key"] = 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;
|
export default axiosInstance;
|
||||||
|
143
src/components/RateLimitModal.vue
Normal file
143
src/components/RateLimitModal.vue
Normal 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>
|
@ -13,7 +13,10 @@ import GlobalMessage from '@/components/GlobalMessage.vue'
|
|||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import Clarity from '@microsoft/clarity';
|
||||||
|
const projectId = "rhp8uqoc3l"
|
||||||
|
|
||||||
|
Clarity.init(projectId);
|
||||||
import messageService from './utils/message';
|
import messageService from './utils/message';
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user