1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-12-07 13:03:59 +00:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Sunwuyuan
0ac7f6e6b1
Merge pull request #19 from ZeroCatDev/copilot/fix-persistent-notification-bug
Fix: Remove duplicate deletePersistentNotification method definition
2025-11-30 18:17:41 +08:00
copilot-swe-agent[bot]
7049c35f2a revert: remove unrelated formatting changes, keep only the fix for duplicate deletePersistentNotification method
Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2025-11-30 10:16:43 +00:00
copilot-swe-agent[bot]
8d8e338d34 chore: remove accidentally committed generated files and update .gitignore
Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2025-11-30 10:07:41 +00:00
copilot-swe-agent[bot]
5d3721d069 fix: remove duplicate deletePersistentNotification method definition
Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2025-11-30 10:07:04 +00:00
copilot-swe-agent[bot]
c5ad23f9d1 Initial plan 2025-11-30 10:02:05 +00:00
Sunwuyuan
4988199c35
feat: 异步加载 Clarity,优化初始加载速度并集成访客标识功能 2025-11-30 17:10:12 +08:00
Sunwuyuan
5c111e198d
feat: 添加报告问题对话框,集成调试信息和反馈渠道 2025-11-30 17:01:22 +08:00
Sunwuyuan
9fde71fea5
优化调试页面提示 2025-11-30 16:33:00 +08:00
Sunwuyuan
5f363aba38
feat: 添加 FingerprintJS 依赖,集成访客 ID 和指纹数据功能 2025-11-30 16:23:43 +08:00
Sunwuyuan
ba96069a9b
chore: 优化 Azure 工作流配置,调整环境变量和格式化注释 2025-11-30 13:48:11 +08:00
9 changed files with 273 additions and 44 deletions

View File

@ -22,8 +22,10 @@ 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
@ -32,17 +34,19 @@ 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 - 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
###### Repository/Build Configurations ######
app_location: "/"
api_location: ""
output_location: "dist"
github_id_token: ${{ steps.idtoken.outputs.result }}
###### End of Repository/Build Configurations ######
@ -55,4 +59,4 @@ jobs:
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
action: "close"
action: "close"

View File

@ -12,6 +12,7 @@
"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,6 +14,9 @@ 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
@ -871,6 +874,9 @@ 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'}
@ -4559,6 +4565,8 @@ 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,27 +2,20 @@
<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();
@ -30,22 +23,13 @@ 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,11 +687,6 @@ export default {
console.error('删除失败', e)
this.$message?.error('删除失败')
}
},
deletePersistentNotification(id) {
this.itemToDelete = id
this.deleteConfirmDialog = true
}
}
}

View File

@ -24,10 +24,9 @@
<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>
@ -170,6 +169,50 @@
</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>
@ -181,14 +224,23 @@
</template>
<script>
import {ref, onMounted} from "vue";
import {ref, onMounted, computed} from "vue";
import { useRouter } from 'vue-router'
import { getVisitorId } from '@/utils/fingerprint'
import packageJson from "../../../package.json";
export default {
name: "AboutCard",
setup() {
const Dependencies = ref([]);
const showDeps = ref(false); //
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 loadDependencies = () => {
try {
@ -224,6 +276,87 @@ 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();
});
@ -231,6 +364,21 @@ export default {
return {
Dependencies,
showDeps,
showReportDialog,
debugIdInput,
visitorLoading,
copyOk,
qqGroupNumber,
qqGroupLink,
goToDebug,
reloadVisitorId,
openReportDialog,
copyEnvInfo,
envBoxText,
envInfo,
reportBody,
githubIssueUrl,
mailtoLink,
};
},
};

View File

@ -16,15 +16,12 @@ 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)
@ -37,7 +34,30 @@ app.component('GlobalMessage', GlobalMessage)
app.mount('#app')
// 移除首屏 CSS 加载覆盖层(在 Vue 挂载完成后)
// 异步加载 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 挂载完成后)
try {
const removeLoader = () => {
document.body.classList.add('app-loaded');

47
src/pages/debug.vue Normal file
View File

@ -0,0 +1,47 @@
<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>

22
src/utils/fingerprint.js Normal file
View File

@ -0,0 +1,22 @@
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
}