添加学号追踪
This commit is contained in:
parent
c6d10b6221
commit
d14b2e5ca8
147
src/App.vue
147
src/App.vue
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user