1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-12-08 22:03:09 +00:00

Compare commits

..

No commits in common. "0ac7f6e6b19a14eb7850cc0e5bdcf02732ee335d" and "752f6b6bd159a5363c7d87634eb02092710fff69" have entirely different histories.

9 changed files with 44 additions and 273 deletions

View File

@ -22,10 +22,8 @@ jobs:
with:
submodules: true
lfs: false
- name: Install OIDC Client from Core Package
run: npm install @actions/core@1.6.0 @actions/http-client
- name: Get Id Token
uses: actions/github-script@v6
id: idtoken
@ -34,19 +32,17 @@ jobs:
const coredemo = require('@actions/core')
return await coredemo.getIDToken()
result-encoding: string
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
env:
NODE_VERSION: 20 # 👈 Force Node.js 20.x instead of Oryx default (18.x)
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_RIVER_041D8AB00 }}
action: "upload"
###### Repository/Build Configurations ######
app_location: "/"
api_location: ""
output_location: "dist"
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: "/" # App source code path
api_location: "" # Api source code path - optional
output_location: "dist" # Built app content directory - optional
github_id_token: ${{ steps.idtoken.outputs.result }}
###### End of Repository/Build Configurations ######
@ -59,4 +55,4 @@ jobs:
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
action: "close"
action: "close"

View File

@ -12,7 +12,6 @@
"dependencies": {
"@examaware-cs/core": "^1.0.0",
"@examaware-cs/player": "^1.0.2",
"@fingerprintjs/fingerprintjs": "^5.0.1",
"@mdi/font": "7.4.47",
"@microsoft/clarity": "^1.0.2",
"@vueuse/core": "^14.1.0",

8
pnpm-lock.yaml generated
View File

@ -14,9 +14,6 @@ importers:
'@examaware-cs/player':
specifier: ^1.0.2
version: 1.0.2(tdesign-vue-next@1.17.5(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
'@fingerprintjs/fingerprintjs':
specifier: ^5.0.1
version: 5.0.1
'@mdi/font':
specifier: 7.4.47
version: 7.4.47
@ -874,9 +871,6 @@ packages:
tdesign-vue-next: ^1.15.5
vue: ^3.0.0
'@fingerprintjs/fingerprintjs@5.0.1':
resolution: {integrity: sha512-KbaeE/rk2WL8MfpRP6jTI4lSr42SJPjvkyrjP3QU6uUDkOMWWYC2Ts1sNSYcegHC8avzOoYTHBj+2fTqvZWQBA==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -4565,8 +4559,6 @@ snapshots:
tdesign-vue-next: 1.17.5(vue@3.5.25(typescript@5.9.3))
vue: 3.5.25(typescript@5.9.3)
'@fingerprintjs/fingerprintjs@5.0.1': {}
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':

View File

@ -2,20 +2,27 @@
<v-app>
<!-- 正常路由 -->
<router-view v-slot="{ Component, route }">
<transition mode="out-in" name="md3">
<component :is="Component" :key="route.path" />
<transition
mode="out-in"
name="md3"
>
<component
:is="Component"
:key="route.path"
/>
</transition>
</router-view>
<global-message />
<rate-limit-modal />
<global-message/>
<rate-limit-modal/>
</v-app>
</template>
<script setup>
import { onMounted } from "vue";
import { useTheme } from "vuetify";
import { getSetting } from "@/utils/settings";
import {onMounted} from "vue";
import {useTheme} from "vuetify";
import {getSetting} from "@/utils/settings";
import RateLimitModal from "@/components/RateLimitModal.vue";
import Clarity from "@microsoft/clarity";
const theme = useTheme();
@ -23,13 +30,22 @@ onMounted(() => {
//
const savedTheme = getSetting("theme.mode");
theme.global.name.value = savedTheme;
// Clarity App
Clarity.identify(
getSetting("device.uuid"),
getSetting("server.domain"),
getSetting("server.provider"),
getSetting("server.classNumber")
);
});
</script>
<style>
.md3-enter-active,
.md3-leave-active {
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.md3-enter-from {

View File

@ -687,6 +687,11 @@ export default {
console.error('删除失败', e)
this.$message?.error('删除失败')
}
},
deletePersistentNotification(id) {
this.itemToDelete = id
this.deleteConfirmDialog = true
}
}
}

View File

@ -24,9 +24,10 @@
<div class="d-flex gap-2 flex-wrap mb-6">
<v-btn
color="red"
href="https://github.com/ClassworksDev/Classworks/issues"
prepend-icon="mdi-bug"
target="_blank"
variant="tonal"
@click="openReportDialog"
>
报告问题
</v-btn>
@ -169,50 +170,6 @@
</v-card>
</v-dialog>
<!-- 报告问题对话框 -->
<v-dialog v-model="showReportDialog" max-width="640">
<v-card>
<v-toolbar density="compact">
<v-btn icon="mdi-close" @click="showReportDialog = false"></v-btn>
<v-toolbar-title>报告问题</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<v-card-text>
<p class="mb-4">
调试ID与下方的浏览器环境信息将帮助我们快速定位问题请在反馈中一并附上
</p>
<v-sheet class="mb-3 pa-3 bg-grey-lighten-4 rounded" style="max-height: 260px; overflow: auto;">
<pre class="text-body-2" style="white-space: pre-wrap; margin: 0;">{{ envBoxText }}</pre>
</v-sheet>
<div class="d-flex gap-2 flex-wrap mb-4">
<v-btn size="small" variant="text" prepend-icon="mdi-refresh" @click="reloadVisitorId" :loading="visitorLoading">刷新</v-btn>
<v-btn size="small" variant="text" prepend-icon="mdi-content-copy" @click="copyEnvInfo">复制信息</v-btn>
<v-btn size="small" variant="text" prepend-icon="mdi-open-in-new" @click="goToDebug">查看 /debug 页面</v-btn>
</div>
<v-alert v-if="copyOk" type="success" density="compact" class="mb-4">已复制到剪贴板</v-alert>
<h4 class="text-subtitle-1 mb-2">反馈渠道</h4>
<v-list lines="one" class="bg-transparent">
<v-list-item :href="qqGroupLink" target="_blank" prepend-icon="mdi-qqchat">
<v-list-item-title>QQ群 ({{ qqGroupNumber }})</v-list-item-title>
<v-list-item-subtitle>964979747</v-list-item-subtitle>
</v-list-item>
<v-list-item :href="githubIssueUrl" target="_blank" prepend-icon="mdi-github">
<v-list-item-title>GitHub Issue</v-list-item-title>
<v-list-item-subtitle>ZeroCatDev/Classworks</v-list-item-subtitle>
</v-list-item>
<v-list-item :href="mailtoLink" target="_blank" prepend-icon="mdi-email">
<v-list-item-title>邮件</v-list-item-title>
<v-list-item-subtitle>sun@wuyuan.dev</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="text" @click="showReportDialog = false">关闭</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<p class="text-caption text-medium-emphasis">
Copyright © {{ new Date().getFullYear() }} Sunwuyuan
</p>
@ -224,23 +181,14 @@
</template>
<script>
import {ref, onMounted, computed} from "vue";
import { useRouter } from 'vue-router'
import { getVisitorId } from '@/utils/fingerprint'
import {ref, onMounted} from "vue";
import packageJson from "../../../package.json";
export default {
name: "AboutCard",
setup() {
const Dependencies = ref([]);
const showDeps = ref(false);
const showReportDialog = ref(false);
const debugIdInput = ref('');
const visitorLoading = ref(false);
const copyOk = ref(false);
const qqGroupNumber = '964979747';
const qqGroupLink = 'https://qm.qq.com/q/T6qImKJjGi';
const router = useRouter();
const showDeps = ref(false); //
const loadDependencies = () => {
try {
@ -276,87 +224,6 @@ export default {
return descriptions[name] || "";
};
const goToDebug = () => {
router.push('/debug');
};
const loadVisitorId = async () => {
visitorLoading.value = true;
try {
const id = await getVisitorId();
debugIdInput.value = id || '';
} catch (e) {
console.error('获取访客ID失败', e);
} finally {
visitorLoading.value = false;
}
};
const reloadVisitorId = () => loadVisitorId();
const openReportDialog = async () => {
showReportDialog.value = true;
if (!debugIdInput.value) await loadVisitorId();
};
const copyEnvInfo = async () => {
try {
await navigator.clipboard.writeText(envBoxText.value);
copyOk.value = true;
setTimeout(() => (copyOk.value = false), 1800);
} catch (e) {
console.error('复制失败', e);
}
};
const envInfo = computed(() => {
const nav = navigator || {};
const intl = (typeof Intl !== 'undefined' && Intl.DateTimeFormat) ? Intl.DateTimeFormat().resolvedOptions() : {};
const tz = intl && intl.timeZone ? intl.timeZone : '';
const routePath = router.currentRoute?.value?.fullPath || location.pathname;
const lines = [
`App 版本: v${packageJson?.version || 'unknown'}`,
`URL: ${location.href}`,
`路由: ${routePath}`,
`UserAgent: ${nav.userAgent || ''}`,
`语言: ${nav.language || ''}`,
`时区: ${tz}`,
`平台: ${nav.platform || ''}`,
`在线: ${String(nav.onLine)}`,
`屏幕: ${screen?.width || '-'}x${screen?.height || '-'}`,
`视口: ${window.innerWidth || '-'}x${window.innerHeight || '-'}`,
];
return lines.join('\n');
});
const envBoxText = computed(() => {
return `调试ID: ${debugIdInput.value || '获取失败'}\n\n浏览器/环境信息:\n${envInfo.value}`;
});
const reportBody = computed(() => {
return [
`问题描述:`,
`1. 期望行为:`,
`2. 实际行为:`,
`3. 复现步骤:`,
'',
envBoxText.value,
].join('\n');
});
const githubIssueUrl = computed(() => {
const base = 'https://github.com/ZeroCatDev/Classworks/issues/new';
const title = encodeURIComponent('问题报告');
const body = encodeURIComponent(reportBody.value);
return `${base}?title=${title}&body=${body}`;
});
const mailtoLink = computed(() => {
const subject = encodeURIComponent('Classworks 问题报告');
const body = encodeURIComponent(reportBody.value);
return `mailto:sun@wuyuan.dev?subject=${subject}&body=${body}`;
});
onMounted(() => {
loadDependencies();
});
@ -364,21 +231,6 @@ export default {
return {
Dependencies,
showDeps,
showReportDialog,
debugIdInput,
visitorLoading,
copyOk,
qqGroupNumber,
qqGroupLink,
goToDebug,
reloadVisitorId,
openReportDialog,
copyEnvInfo,
envBoxText,
envInfo,
reportBody,
githubIssueUrl,
mailtoLink,
};
},
};

View File

@ -16,12 +16,15 @@ import GlobalMessage from '@/components/GlobalMessage.vue'
// Composables
import {createApp} from 'vue'
import Clarity from '@microsoft/clarity';
const projectId = "rhp8uqoc3l"
//import TDesign from 'tdesign-vue-next'
//import 'tdesign-vue-next/es/style/index.css'
//import '@examaware-cs/player/dist/player.css'
Clarity.init(projectId);
import messageService from './utils/message';
import { getVisitorId } from './utils/fingerprint';
const app = createApp(App)
@ -34,30 +37,7 @@ app.component('GlobalMessage', GlobalMessage)
app.mount('#app')
// 异步加载 Clarity 以提升初始加载速度
if (document.readyState === 'complete') {
loadClarity();
} else {
window.addEventListener('load', loadClarity, { once: true });
}
async function loadClarity() {
try {
const Clarity = (await import('@microsoft/clarity')).default;
const projectId = "rhp8uqoc3l";
Clarity.init(projectId);
// 获取并设置访客标识
const visitorId = await getVisitorId();
console.log('Visitor ID:', visitorId);
Clarity.identify(visitorId);
Clarity.setTag('fingerprintjs', visitorId);
} catch (error) {
console.warn('Clarity 加载或标识设置失败:', error);
}
}
// 移除首屏 CSS 加载覆盖层(在 Vue 挂载完成后)
// 移除首屏 CSS 加载覆盖层(在 Vue 挂载完成后)
try {
const removeLoader = () => {
document.body.classList.add('app-loaded');

View File

@ -1,47 +0,0 @@
<template>
<v-container>
<v-card class="mb-4">
<v-card-title>调试信息</v-card-title>
<v-card-subtitle>
请将这个ID复制并私聊给开发者以便进行问题排查
</v-card-subtitle>
<v-card-text>
<div class="text-h6 mb-2">访客 ID</div>
<v-code class="d-block pa-2 bg-grey-lighten-4 rounded mb-4">
{{ visitorId || '加载中...' }}
</v-code>
</v-card-text>
<v-card-actions>
<v-btn color="primary" @click="loadData" :loading="loading">
Refresh
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getVisitorId, getFingerprintData } from '@/utils/fingerprint'
const visitorId = ref('')
const fingerprintData = ref({})
const loading = ref(false)
const loadData = async () => {
loading.value = true
try {
visitorId.value = await getVisitorId()
fingerprintData.value = await getFingerprintData()
} catch (e) {
console.error(e)
visitorId.value = 'Error loading visitor ID'
} finally {
loading.value = false
}
}
onMounted(() => {
loadData()
})
</script>

View File

@ -1,22 +0,0 @@
import FingerprintJS from '@fingerprintjs/fingerprintjs'
let fpPromise
export const loadFingerprint = () => {
if (!fpPromise) {
fpPromise = FingerprintJS.load()
}
return fpPromise
}
export const getVisitorId = async () => {
const fp = await loadFingerprint()
const result = await fp.get()
return result.visitorId
}
export const getFingerprintData = async () => {
const fp = await loadFingerprint()
const result = await fp.get()
return result
}