|
@@ -156,7 +156,8 @@
|
|
|
<button v-for="(opt, idx) in timerPresets" :key="idx" class="shTimerOpt"
|
|
<button v-for="(opt, idx) in timerPresets" :key="idx" class="shTimerOpt"
|
|
|
:class="{ shTimerOptActive: work.timerDuration === opt.value }"
|
|
:class="{ shTimerOptActive: work.timerDuration === opt.value }"
|
|
|
@click="work.timerDuration = opt.value">{{ opt.label }}</button>
|
|
@click="work.timerDuration = opt.value">{{ opt.label }}</button>
|
|
|
- <button class="shTimerOpt" :class="{ shTimerOptActive: timerCustomFocused }" @click="focusTimerCustom">{{ lang.ssSHTimerCustom }}</button>
|
|
|
|
|
|
|
+ <button class="shTimerOpt" :class="{ shTimerOptActive: timerCustomFocused }" @click="focusTimerCustom">{{
|
|
|
|
|
+ lang.ssSHTimerCustom }}</button>
|
|
|
<input v-if="timerCustomFocused" type="number" min="1" class="shTimerCustomInput"
|
|
<input v-if="timerCustomFocused" type="number" min="1" class="shTimerCustomInput"
|
|
|
v-model.number="work.timerCustomValue" @blur="onTimerCustomBlur" @keyup.enter="onTimerCustomBlur" />
|
|
v-model.number="work.timerCustomValue" @blur="onTimerCustomBlur" @keyup.enter="onTimerCustomBlur" />
|
|
|
</div>
|
|
</div>
|
|
@@ -206,7 +207,13 @@
|
|
|
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z">
|
|
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z">
|
|
|
</path>
|
|
</path>
|
|
|
</svg>
|
|
</svg>
|
|
|
- <svg v-else-if="hat.key === 'black'" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert w-4 h-4 text-white" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>
|
|
|
|
|
|
|
+ <svg v-else-if="hat.key === 'black'" xmlns="http://www.w3.org/2000/svg" width="28" height="28"
|
|
|
|
|
+ viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"
|
|
|
|
|
+ stroke-linejoin="round" class="lucide lucide-triangle-alert w-4 h-4 text-white" aria-hidden="true">
|
|
|
|
|
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path>
|
|
|
|
|
+ <path d="M12 9v4"></path>
|
|
|
|
|
+ <path d="M12 17h.01"></path>
|
|
|
|
|
+ </svg>
|
|
|
<svg v-else-if="hat.key === 'yellow'" viewBox="0 0 24 24" width="28" height="28" fill="none"
|
|
<svg v-else-if="hat.key === 'yellow'" viewBox="0 0 24 24" width="28" height="28" fill="none"
|
|
|
stroke="#ca8a04" stroke-width="2">
|
|
stroke="#ca8a04" stroke-width="2">
|
|
|
<polygon
|
|
<polygon
|
|
@@ -282,8 +289,11 @@
|
|
|
<!-- 任务说明:有讨论主题时显示 -->
|
|
<!-- 任务说明:有讨论主题时显示 -->
|
|
|
<div class="shTaskDescCard" v-if="work.stage === 'activity' && work.scenario && work.scenario.trim()">
|
|
<div class="shTaskDescCard" v-if="work.stage === 'activity' && work.scenario && work.scenario.trim()">
|
|
|
<div class="shTaskDescIcon">
|
|
<div class="shTaskDescIcon">
|
|
|
- <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#F5A623" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
- <path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"></path>
|
|
|
|
|
|
|
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#F5A623" stroke-width="2"
|
|
|
|
|
+ stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z">
|
|
|
|
|
+ </path>
|
|
|
</svg>
|
|
</svg>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="shTaskDescBody">
|
|
<div class="shTaskDescBody">
|
|
@@ -320,7 +330,13 @@
|
|
|
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z">
|
|
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z">
|
|
|
</path>
|
|
</path>
|
|
|
</svg>
|
|
</svg>
|
|
|
- <svg v-else-if="currentHat.key === 'black'" xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert w-4 h-4 text-white" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>
|
|
|
|
|
|
|
+ <svg v-else-if="currentHat.key === 'black'" xmlns="http://www.w3.org/2000/svg" width="44" height="44"
|
|
|
|
|
+ viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"
|
|
|
|
|
+ stroke-linejoin="round" class="lucide lucide-triangle-alert w-4 h-4 text-white" aria-hidden="true">
|
|
|
|
|
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path>
|
|
|
|
|
+ <path d="M12 9v4"></path>
|
|
|
|
|
+ <path d="M12 17h.01"></path>
|
|
|
|
|
+ </svg>
|
|
|
<svg v-else-if="currentHat.key === 'yellow'" viewBox="0 0 24 24" width="44" height="44" fill="none"
|
|
<svg v-else-if="currentHat.key === 'yellow'" viewBox="0 0 24 24" width="44" height="44" fill="none"
|
|
|
stroke="#ca8a04" stroke-width="2">
|
|
stroke="#ca8a04" stroke-width="2">
|
|
|
<polygon
|
|
<polygon
|
|
@@ -345,7 +361,7 @@
|
|
|
<div class="shActivityHatDesc">{{ currentHat.desc }}</div>
|
|
<div class="shActivityHatDesc">{{ currentHat.desc }}</div>
|
|
|
<div class="shActivityHatProgress">{{ lang.ssSHHatProgress.replace('{n}', currentHatIdx +
|
|
<div class="shActivityHatProgress">{{ lang.ssSHHatProgress.replace('{n}', currentHatIdx +
|
|
|
1).replace('{total}',
|
|
1).replace('{total}',
|
|
|
- selectedHats.length) }}</div>
|
|
|
|
|
|
|
+ selectedHats.length) }}</div>
|
|
|
<!-- 引导问题 -->
|
|
<!-- 引导问题 -->
|
|
|
<div class="shActivityHatPrompt">{{ currentHatPrompt }}</div>
|
|
<div class="shActivityHatPrompt">{{ currentHatPrompt }}</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -419,7 +435,7 @@
|
|
|
<button class="shSecondaryBtn" :class="{ shSecondaryBtnDisabled: currentHatIdx === 0 }"
|
|
<button class="shSecondaryBtn" :class="{ shSecondaryBtnDisabled: currentHatIdx === 0 }"
|
|
|
:disabled="currentHatIdx === 0" @click="prevHat">{{ lang.ssSHPrevHat }}</button>
|
|
:disabled="currentHatIdx === 0" @click="prevHat">{{ lang.ssSHPrevHat }}</button>
|
|
|
<button class="shPrimaryBtn" @click="nextOrFinish">{{ isLastHat ? lang.ssSHFinish : lang.ssSHNextHat
|
|
<button class="shPrimaryBtn" @click="nextOrFinish">{{ isLastHat ? lang.ssSHFinish : lang.ssSHNextHat
|
|
|
- }}</button>
|
|
|
|
|
|
|
+ }}</button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -446,7 +462,13 @@
|
|
|
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z">
|
|
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z">
|
|
|
</path>
|
|
</path>
|
|
|
</svg>
|
|
</svg>
|
|
|
- <svg v-else-if="hat.key === 'black'" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert w-4 h-4 text-white" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>
|
|
|
|
|
|
|
+ <svg v-else-if="hat.key === 'black'" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
|
|
|
|
+ viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"
|
|
|
|
|
+ stroke-linejoin="round" class="lucide lucide-triangle-alert w-4 h-4 text-white" aria-hidden="true">
|
|
|
|
|
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path>
|
|
|
|
|
+ <path d="M12 9v4"></path>
|
|
|
|
|
+ <path d="M12 17h.01"></path>
|
|
|
|
|
+ </svg>
|
|
|
<svg v-else-if="hat.key === 'yellow'" viewBox="0 0 24 24" width="22" height="22" fill="none"
|
|
<svg v-else-if="hat.key === 'yellow'" viewBox="0 0 24 24" width="22" height="22" fill="none"
|
|
|
stroke="#ca8a04" stroke-width="2">
|
|
stroke="#ca8a04" stroke-width="2">
|
|
|
<polygon
|
|
<polygon
|
|
@@ -776,6 +798,11 @@ export default {
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
|
|
+ // 清理 iframe 录音会话(统一清理入口)
|
|
|
|
|
+ _cleanupTranscriber(iframe) {
|
|
|
|
|
+ iframe.contentWindow.onSessionStopped = null;
|
|
|
|
|
+ iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
|
|
+ },
|
|
|
// —— 加载班级学生(接口无数据时保持 mock)——
|
|
// —— 加载班级学生(接口无数据时保持 mock)——
|
|
|
async loadClassStudents() {
|
|
async loadClassStudents() {
|
|
|
try {
|
|
try {
|
|
@@ -960,7 +987,8 @@ export default {
|
|
|
this.$message.error(this.lang.ssSHRecServiceNotReady);
|
|
this.$message.error(this.lang.ssSHRecServiceNotReady);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- try {
|
|
|
|
|
|
|
+ // try {
|
|
|
|
|
+ const startTranscriber = () => {
|
|
|
if (this.lang.lang === 'com') iframe.contentWindow.window.document.getElementById('languageOptions').selectedIndex = 8;
|
|
if (this.lang.lang === 'com') iframe.contentWindow.window.document.getElementById('languageOptions').selectedIndex = 8;
|
|
|
else if (this.lang.lang === 'hk') iframe.contentWindow.window.document.getElementById('languageOptions').selectedIndex = 4;
|
|
else if (this.lang.lang === 'hk') iframe.contentWindow.window.document.getElementById('languageOptions').selectedIndex = 4;
|
|
|
else iframe.contentWindow.window.document.getElementById('languageOptions').selectedIndex = 2;
|
|
else iframe.contentWindow.window.document.getElementById('languageOptions').selectedIndex = 2;
|
|
@@ -977,23 +1005,45 @@ export default {
|
|
|
iframe.contentWindow.ConversationTranscriber();
|
|
iframe.contentWindow.ConversationTranscriber();
|
|
|
this.recordObj.status = '1';
|
|
this.recordObj.status = '1';
|
|
|
this.recordObj.startTime = new Date().getTime();
|
|
this.recordObj.startTime = new Date().getTime();
|
|
|
- setTimeout(() => {
|
|
|
|
|
- if (!navigator.permissions) return;
|
|
|
|
|
- navigator.permissions.query({ name: 'microphone' }).then(ps => {
|
|
|
|
|
- if (ps.state !== 'granted') {
|
|
|
|
|
- this.recordObj.status = '0';
|
|
|
|
|
- this.recordObj.loading = false;
|
|
|
|
|
- iframe.contentWindow.onSessionStopped = null;
|
|
|
|
|
- iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
|
|
- this.$message.warning(this.lang.ssSHNoPermStop);
|
|
|
|
|
- }
|
|
|
|
|
- }).catch(() => { });
|
|
|
|
|
- }, 2000);
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.error('startRecord error:', err);
|
|
|
|
|
- this.$message.error(this.lang.ssSHRecStartFail);
|
|
|
|
|
- this.recordObj.status = '0';
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ // 先通过 getUserMedia 主动请求麦克风权限,避免 ConversationTranscriber
|
|
|
|
|
+ // 在浏览器权限弹窗期间被中断导致内部状态异常
|
|
|
|
|
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
|
|
|
+ navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
|
|
+ .then(stream => {
|
|
|
|
|
+ // 权限已获取,立即释放 stream,让 ConversationTranscriber 自己接管麦克风
|
|
|
|
|
+ stream.getTracks().forEach(t => t.stop());
|
|
|
|
|
+ startTranscriber();
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => {
|
|
|
|
|
+ // 用户拒绝权限
|
|
|
|
|
+ this._cleanupTranscriber(iframe);
|
|
|
|
|
+ this.$message.success(this.lang.ssNoPermStop);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 降级:不支持 getUserMedia 的浏览器直接启动
|
|
|
|
|
+ startTranscriber();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (!navigator.permissions) return;
|
|
|
|
|
+ navigator.permissions.query({ name: 'microphone' }).then(ps => {
|
|
|
|
|
+ if (ps.state !== 'granted') {
|
|
|
|
|
+ this.recordObj.status = '0';
|
|
|
|
|
+ this.recordObj.loading = false;
|
|
|
|
|
+ this._cleanupTranscriber(iframe);
|
|
|
|
|
+ // iframe.contentWindow.onSessionStopped = null;
|
|
|
|
|
+ // iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
|
|
+ this.$message.warning(this.lang.ssSHNoPermStop);
|
|
|
|
|
+ }
|
|
|
|
|
+ }).catch(() => { });
|
|
|
|
|
+ }, 2000);
|
|
|
|
|
+ // } catch (err) {
|
|
|
|
|
+ // console.error('startRecord error:', err);
|
|
|
|
|
+ // this.$message.error(this.lang.ssSHRecStartFail);
|
|
|
|
|
+ // this.recordObj.status = '0';
|
|
|
|
|
+ // }
|
|
|
},
|
|
},
|
|
|
stopRecord() {
|
|
stopRecord() {
|
|
|
if (new Date().getTime() - this.recordObj.startTime < 2000) {
|
|
if (new Date().getTime() - this.recordObj.startTime < 2000) {
|
|
@@ -1019,8 +1069,9 @@ export default {
|
|
|
this.recordObj.status = '0';
|
|
this.recordObj.status = '0';
|
|
|
this.recordObj.loading = false;
|
|
this.recordObj.loading = false;
|
|
|
this.$message.success(this.lang.ssSHRecStopped);
|
|
this.$message.success(this.lang.ssSHRecStopped);
|
|
|
- iframe.contentWindow.onSessionStopped = null;
|
|
|
|
|
- iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
|
|
|
|
+ this._cleanupTranscriber(iframe);
|
|
|
|
|
+ // iframe.contentWindow.onSessionStopped = null;
|
|
|
|
|
+ // iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|