添加学号追踪

This commit is contained in:
feie9456 2026-01-05 12:02:12 +08:00
parent c6d10b6221
commit d14b2e5ca8
2 changed files with 158 additions and 2 deletions

View File

@ -31,12 +31,61 @@ const trackerSessionId = (() => {
}
})();
// =========================
//
// =========================
const STUDENT_ID_KEY = 'sound-speed:student-id';
const studentId = ref<string | null>(null);
const showStudentIdModal = ref(false);
const studentIdInput = ref('');
const loadStudentId = () => {
try {
const saved = window.localStorage.getItem(STUDENT_ID_KEY);
if (typeof saved === 'string' && saved.trim()) {
studentId.value = saved.trim();
studentIdInput.value = saved.trim();
}
} catch {
// ignore
}
};
const persistStudentId = (v: string | null) => {
try {
if (!v) window.localStorage.removeItem(STUDENT_ID_KEY);
else window.localStorage.setItem(STUDENT_ID_KEY, v);
} catch {
// ignore
}
};
const normalizedStudentIdInput = computed(() => studentIdInput.value.trim());
const canContinueWithStudentId = computed(() => normalizedStudentIdInput.value.length > 0);
const chooseGuestMode = () => {
studentId.value = null;
studentIdInput.value = '';
persistStudentId(null);
showStudentIdModal.value = false;
};
const continueWithStudentId = () => {
if (!canContinueWithStudentId.value) return;
studentId.value = normalizedStudentIdInput.value;
persistStudentId(studentId.value);
showStudentIdModal.value = false;
};
const postTracker = (event: string, data?: unknown) => {
const body = JSON.stringify({
event,
data,
ts: Date.now(),
sessionId: trackerSessionId,
studentId: studentId.value ?? undefined,
step: currentStepTitle.value,
});
@ -229,6 +278,7 @@ const guideImgRef = ref<HTMLImageElement | null>(null);
const closeGuide = () => {
showGuide.value = false;
showStudentIdModal.value = true;
};
const onGuideClick = (e: MouseEvent) => {
@ -741,6 +791,7 @@ const updateCablePaths = (onlyPins?: Set<PinId>) => {
};
onMounted(() => {
loadStudentId();
scheduleUpdateCablePaths();
window.addEventListener('resize', scheduleUpdateCablePaths);
});
@ -764,6 +815,21 @@ onBeforeUnmount(() => {
</div>
</Transition>
<Transition name="guide">
<div v-if="showStudentIdModal" class="studentid-overlay" @pointerdown.stop>
<div class="studentid-modal" role="dialog" aria-modal="true" aria-label="学号确认">
<div class="studentid-title">请输入学号</div>
<input v-model="studentIdInput" class="studentid-input" type="text" inputmode="numeric"
autocomplete="off" placeholder="学号" @keydown.enter.prevent="continueWithStudentId" />
<div class="studentid-actions">
<button class="studentid-btn" type="button" @click="chooseGuestMode">游客模式</button>
<button class="studentid-btn primary" type="button" :disabled="!canContinueWithStudentId"
@click="continueWithStudentId">继续</button>
</div>
</div>
</div>
</Transition>
<div v-if="tooltip.visible" class="tooltip" :class="`tooltip--${tooltip.side}`" :style="tooltipStyle">
<div class="tooltip-inner">{{ tooltip.text }}</div>
</div>
@ -798,7 +864,8 @@ onBeforeUnmount(() => {
<DraggableInstrumentWrapper :style="notebookStyle" :instrument="notebook" @start-drag="startDrag">
<Notebook class="machine" :wiring="wiringConnected"
:osc="{ vdch1: oscStatus.vdch1, vdch2: oscStatus.vdch2, currentModeIndex: oscStatus.currentModeIndex, xyMode: oscStatus.xyMode, power: oscStatus.power }"
:signal-frequency-k-hz="signalFrequencyKHz" :lissajous-line-streak="lissajousLineStreak" />
:signal-frequency-k-hz="signalFrequencyKHz" :lissajous-line-streak="lissajousLineStreak"
:student-id="studentId ?? undefined" :tracker-session-id="trackerSessionId" />
</DraggableInstrumentWrapper>
<DraggableInstrumentWrapper :style="oscilloscopeStyle" :instrument="oscilloscope" @start-drag="startDrag">
@ -838,6 +905,84 @@ onBeforeUnmount(() => {
justify-content: center;
}
.studentid-overlay {
position: fixed;
inset: 0;
z-index: 9998;
background: rgba(0, 0, 0, 0.70);
display: flex;
align-items: center;
justify-content: center;
}
.studentid-modal {
width: 420px;
max-width: calc(100vw - 40px);
background:
linear-gradient(rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.96)),
repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.035) 0px,
rgba(0, 0, 0, 0.035) 1px,
rgba(0, 0, 0, 0.0) 10px,
rgba(0, 0, 0, 0.0) 18px
);
border: 2px solid rgba(20, 20, 20, 0.70);
border-radius: 14px;
box-shadow: 0 16px 28px rgba(0, 0, 0, 0.35);
padding: 14px 14px 12px;
color: rgba(20, 20, 20, 0.95);
}
.studentid-title {
font-size: 14px;
font-weight: 700;
margin-bottom: 10px;
}
.studentid-input {
width: 100%;
box-sizing: border-box;
border-radius: 10px;
border: 2px solid rgba(20, 20, 20, 0.45);
background: rgba(255, 255, 255, 0.78);
padding: 10px 10px;
font-size: 14px;
outline: none;
}
.studentid-input:focus {
border-color: rgba(20, 20, 20, 0.70);
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.12);
}
.studentid-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.studentid-btn {
appearance: none;
border: 2px solid rgba(20, 20, 20, 0.55);
background: rgba(255, 255, 255, 0.78);
border-radius: 999px;
padding: 8px 14px;
font-size: 13px;
cursor: pointer;
}
.studentid-btn.primary {
background: rgba(20, 20, 20, 0.90);
color: rgba(255, 255, 255, 0.95);
}
.studentid-btn:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.guide-img {
width: 100vw;
height: 100vh;

View File

@ -21,6 +21,8 @@ const props = defineProps<{
osc: OscStatus;
signalFrequencyKHz: number;
lissajousLineStreak: number;
studentId?: string;
trackerSessionId?: string;
}>();
const inRange = (v: number, a: number, b: number) => v >= a && v <= b;
@ -144,7 +146,13 @@ const postJson = async (url: string, data: unknown) => {
};
const postTracker = (event: string, data?: unknown) => {
const body = JSON.stringify({ event, data, ts: Date.now() });
const body = JSON.stringify({
event,
data,
ts: Date.now(),
sessionId: props.trackerSessionId,
studentId: props.studentId,
});
try {
if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
const blob = new Blob([body], { type: 'application/json' });
@ -183,6 +191,7 @@ const submitRecord = async () => {
const payload = {
ts: Date.now(),
studentId: props.studentId,
raw: {
naturalFreqKHz: rawData.value.naturalFreqKHz,
rows: rawData.value.rows,
@ -194,6 +203,8 @@ const submitRecord = async () => {
validDistancesMm: parsedDistances.value,
},
context: {
trackerSessionId: props.trackerSessionId,
studentId: props.studentId,
wiring: props.wiring,
osc: props.osc,
signalFrequencyKHz: props.signalFrequencyKHz,