mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-12-07 13:03:59 +00:00
Compare commits
10 Commits
752f6b6bd1
...
0ac7f6e6b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ac7f6e6b1 | ||
|
|
7049c35f2a | ||
|
|
8d8e338d34 | ||
|
|
5d3721d069 | ||
|
|
c5ad23f9d1 | ||
|
|
4988199c35 | ||
|
|
5c111e198d | ||
|
|
9fde71fea5 | ||
|
|
5f363aba38 | ||
|
|
ba96069a9b |
@ -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"
|
||||
@ -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
8
pnpm-lock.yaml
generated
@ -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':
|
||||
|
||||
32
src/App.vue
32
src/App.vue
@ -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 {
|
||||
|
||||
@ -687,11 +687,6 @@ export default {
|
||||
console.error('删除失败', e)
|
||||
this.$message?.error('删除失败')
|
||||
}
|
||||
},
|
||||
|
||||
deletePersistentNotification(id) {
|
||||
this.itemToDelete = id
|
||||
this.deleteConfirmDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
30
src/main.js
30
src/main.js
@ -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
47
src/pages/debug.vue
Normal 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
22
src/utils/fingerprint.js
Normal 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user