添加学号追踪
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 postTracker = (event: string, data?: unknown) => {
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
event,
|
event,
|
||||||
data,
|
data,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
sessionId: trackerSessionId,
|
sessionId: trackerSessionId,
|
||||||
|
studentId: studentId.value ?? undefined,
|
||||||
step: currentStepTitle.value,
|
step: currentStepTitle.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -229,6 +278,7 @@ const guideImgRef = ref<HTMLImageElement | null>(null);
|
|||||||
|
|
||||||
const closeGuide = () => {
|
const closeGuide = () => {
|
||||||
showGuide.value = false;
|
showGuide.value = false;
|
||||||
|
showStudentIdModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onGuideClick = (e: MouseEvent) => {
|
const onGuideClick = (e: MouseEvent) => {
|
||||||
@ -741,6 +791,7 @@ const updateCablePaths = (onlyPins?: Set<PinId>) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadStudentId();
|
||||||
scheduleUpdateCablePaths();
|
scheduleUpdateCablePaths();
|
||||||
window.addEventListener('resize', scheduleUpdateCablePaths);
|
window.addEventListener('resize', scheduleUpdateCablePaths);
|
||||||
});
|
});
|
||||||
@ -764,6 +815,21 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</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 v-if="tooltip.visible" class="tooltip" :class="`tooltip--${tooltip.side}`" :style="tooltipStyle">
|
||||||
<div class="tooltip-inner">{{ tooltip.text }}</div>
|
<div class="tooltip-inner">{{ tooltip.text }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -798,7 +864,8 @@ onBeforeUnmount(() => {
|
|||||||
<DraggableInstrumentWrapper :style="notebookStyle" :instrument="notebook" @start-drag="startDrag">
|
<DraggableInstrumentWrapper :style="notebookStyle" :instrument="notebook" @start-drag="startDrag">
|
||||||
<Notebook class="machine" :wiring="wiringConnected"
|
<Notebook class="machine" :wiring="wiringConnected"
|
||||||
:osc="{ vdch1: oscStatus.vdch1, vdch2: oscStatus.vdch2, currentModeIndex: oscStatus.currentModeIndex, xyMode: oscStatus.xyMode, power: oscStatus.power }"
|
: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>
|
||||||
|
|
||||||
<DraggableInstrumentWrapper :style="oscilloscopeStyle" :instrument="oscilloscope" @start-drag="startDrag">
|
<DraggableInstrumentWrapper :style="oscilloscopeStyle" :instrument="oscilloscope" @start-drag="startDrag">
|
||||||
@ -838,6 +905,84 @@ onBeforeUnmount(() => {
|
|||||||
justify-content: center;
|
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 {
|
.guide-img {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|||||||
@ -21,6 +21,8 @@ const props = defineProps<{
|
|||||||
osc: OscStatus;
|
osc: OscStatus;
|
||||||
signalFrequencyKHz: number;
|
signalFrequencyKHz: number;
|
||||||
lissajousLineStreak: number;
|
lissajousLineStreak: number;
|
||||||
|
studentId?: string;
|
||||||
|
trackerSessionId?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inRange = (v: number, a: number, b: number) => v >= a && v <= b;
|
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 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 {
|
try {
|
||||||
if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
|
if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
|
||||||
const blob = new Blob([body], { type: 'application/json' });
|
const blob = new Blob([body], { type: 'application/json' });
|
||||||
@ -183,6 +191,7 @@ const submitRecord = async () => {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
|
studentId: props.studentId,
|
||||||
raw: {
|
raw: {
|
||||||
naturalFreqKHz: rawData.value.naturalFreqKHz,
|
naturalFreqKHz: rawData.value.naturalFreqKHz,
|
||||||
rows: rawData.value.rows,
|
rows: rawData.value.rows,
|
||||||
@ -194,6 +203,8 @@ const submitRecord = async () => {
|
|||||||
validDistancesMm: parsedDistances.value,
|
validDistancesMm: parsedDistances.value,
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
|
trackerSessionId: props.trackerSessionId,
|
||||||
|
studentId: props.studentId,
|
||||||
wiring: props.wiring,
|
wiring: props.wiring,
|
||||||
osc: props.osc,
|
osc: props.osc,
|
||||||
signalFrequencyKHz: props.signalFrequencyKHz,
|
signalFrequencyKHz: props.signalFrequencyKHz,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user