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:
parent
596c6ac918
commit
9f4fe0b9dd
@ -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
16
pnpm-lock.yaml
generated
@ -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
|
||||
|
52
src/App.vue
52
src/App.vue
@ -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>
|
||||
|
@ -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;
|
||||
|
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
|
||||
import { createApp } from 'vue'
|
||||
import Clarity from '@microsoft/clarity';
|
||||
const projectId = "rhp8uqoc3l"
|
||||
|
||||
Clarity.init(projectId);
|
||||
import messageService from './utils/message';
|
||||
|
||||
const app = createApp(App)
|
||||
|
Loading…
x
Reference in New Issue
Block a user