|
|
@@ -0,0 +1,926 @@
|
|
|
+<template>
|
|
|
+ <div class="kwlPage">
|
|
|
+ <div class="kwlCard">
|
|
|
+ <!-- 头部卡片 -->
|
|
|
+ <div class="kwlHeader">
|
|
|
+ <div class="kwlHeaderLeft">
|
|
|
+ <div class="kwlTitle">{{ lang.ssKWLTitle }}</div>
|
|
|
+ <div class="kwlSubtitle">{{ lang.ssKWLSubtitle }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="kwlHeaderRight" @click="showHowTo = !showHowTo">
|
|
|
+ <span>{{ lang.ssKWLHowTo }}</span>
|
|
|
+ <i :class="['kwlArrow', { 'kwlArrowUp': showHowTo }]">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4 transition-transform "><polyline points="6 9 12 15 18 9"></polyline></svg>
|
|
|
+ </i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 怎么做指引 -->
|
|
|
+ <div class="kwlHowTo" v-show="showHowTo">
|
|
|
+ <div class="kwlHowToItem" v-for="(step, index) in howToSteps" :key="index">
|
|
|
+ <div class="kwlStepNum">{{ index + 1 }}</div>
|
|
|
+ <div class="kwlStepText">{{ step }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 任务说明 -->
|
|
|
+ <div class="kwlCard kwlTaskDesc" v-show="work.taskDescription || (work.taskUrl && work.taskUrl.length > 0)">
|
|
|
+ <div class="kwlTaskHeader">
|
|
|
+ <span class="kwlStar">✦</span>
|
|
|
+ <span class="kwlTaskTitle">{{ lang.ssKWLTaskDesc }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="kwlTaskContent" v-show="showTaskDesc" v-if="work.taskDescription">{{ work.taskDescription }}</div>
|
|
|
+ <!-- 任务附件:图片/视频预览 -->
|
|
|
+ <div class="kwlTaskMedia" v-show="showTaskDesc" v-if="work.taskUrl && work.taskUrl.length > 0">
|
|
|
+ <div class="kwlTaskMediaItem" v-for="(url, idx) in work.taskUrl" :key="'taskUrl-' + idx">
|
|
|
+ <img v-if="isImage(url)" :src="url" @click="$hevueImgPreview(url)" />
|
|
|
+ <!-- 视频:缩略图 + 播放按钮,点击打开全屏弹窗 -->
|
|
|
+ <div class="kwlVideoThumb" v-else-if="isVideo(url)" @click="previewVideo(url)">
|
|
|
+ <video :src="url" muted></video>
|
|
|
+ <div class="kwlVideoPlayBtn">
|
|
|
+ <svg viewBox="0 0 24 24" width="32" height="32" fill="#fff">
|
|
|
+ <path d="M8 5v14l11-7z"></path>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <a v-else :href="url" target="_blank" class="kwlTaskMediaLink">{{ url }}</a>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 视频全屏播放弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ :visible.sync="videoDialogVisible"
|
|
|
+ :append-to-body="true"
|
|
|
+ :show-close="true"
|
|
|
+ custom-class="kwlVideoDialog"
|
|
|
+ width="80%"
|
|
|
+ @closed="onVideoDialogClosed"
|
|
|
+ >
|
|
|
+ <div class="kwlVideoDialogBody" v-if="currentVideoUrl">
|
|
|
+ <video :src="currentVideoUrl" controls autoplay class="kwlVideoFull"></video>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- KWL 三列 -->
|
|
|
+ <div class="kwlColumns">
|
|
|
+ <!-- K - 已知 -->
|
|
|
+ <div class="kwlColumn kwlColKnow">
|
|
|
+ <div class="kwlColTitle kwlColTitleKnow">{{ lang.ssKWLColKnow }}</div>
|
|
|
+ <div class="kwlColBody">
|
|
|
+ <div class="kwlEmptyTip" v-if="!work.know || work.know.length === 0">
|
|
|
+ {{ lang.ssKWLAddContent }}
|
|
|
+ </div>
|
|
|
+ <div class="kwlItem" v-for="(item, idx) in work.know" :key="'k-' + idx">
|
|
|
+ <span class="kwlItemText">{{ item }}</span>
|
|
|
+ <span class="kwlItemDel" @click="removeItem('know', idx)">×</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="kwlAddRow">
|
|
|
+ <input
|
|
|
+ v-model="newKnow"
|
|
|
+ class="kwlInput"
|
|
|
+ :placeholder="work.kplaceholder ? work.kplaceholder : lang.ssKWLAddPlaceholder"
|
|
|
+ @keyup.enter="addItem('know')"
|
|
|
+ />
|
|
|
+ <button class="kwlAddBtn kwlAddBtnKnow" @click="addItem('know')">+</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- W - 想知 -->
|
|
|
+ <div class="kwlColumn kwlColWant">
|
|
|
+ <div class="kwlColTitle kwlColTitleWant">{{ lang.ssKWLColWant }}</div>
|
|
|
+ <div class="kwlColBody">
|
|
|
+ <div class="kwlEmptyTip" v-if="!work.want || work.want.length === 0">
|
|
|
+ {{ lang.ssKWLAddContent }}
|
|
|
+ </div>
|
|
|
+ <div class="kwlItem" v-for="(item, idx) in work.want" :key="'w-' + idx">
|
|
|
+ <span class="kwlItemText">{{ item }}</span>
|
|
|
+ <span class="kwlItemDel" @click="removeItem('want', idx)">×</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="kwlAddRow">
|
|
|
+ <input
|
|
|
+ v-model="newWant"
|
|
|
+ class="kwlInput"
|
|
|
+ :placeholder="work.wplaceholder ? work.wplaceholder : lang.ssKWLAddPlaceholder"
|
|
|
+ @keyup.enter="addItem('want')"
|
|
|
+ />
|
|
|
+ <button class="kwlAddBtn kwlAddBtnWant" @click="addItem('want')">+</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- L - 已学 -->
|
|
|
+ <div class="kwlColumn kwlColLearn">
|
|
|
+ <div class="kwlColTitle kwlColTitleLearn">{{ lang.ssKWLColLearn }}</div>
|
|
|
+ <div class="kwlColBody">
|
|
|
+ <div class="kwlEmptyTip" v-if="!work.learned || work.learned.length === 0">
|
|
|
+ {{ lang.ssKWLAddContent }}
|
|
|
+ </div>
|
|
|
+ <div class="kwlItem" v-for="(item, idx) in work.learned" :key="'l-' + idx">
|
|
|
+ <span class="kwlItemText">{{ item }}</span>
|
|
|
+ <span class="kwlItemDel" @click="removeItem('learned', idx)">×</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="kwlAddRow">
|
|
|
+ <input
|
|
|
+ v-model="newLearned"
|
|
|
+ class="kwlInput"
|
|
|
+ :placeholder="work.lplaceholder ? work.lplaceholder : lang.ssKWLAddPlaceholder"
|
|
|
+ @keyup.enter="addItem('learned')"
|
|
|
+ />
|
|
|
+ <button class="kwlAddBtn kwlAddBtnLearn" @click="addItem('learned')">+</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 学习反思 -->
|
|
|
+ <div class="kwlCard kwlReflection" v-if="isShowReflection">
|
|
|
+ <div class="kwlRefTitle">{{ lang.ssKWLReflection }}</div>
|
|
|
+ <div class="kwlRefAreaWrap">
|
|
|
+ <textarea
|
|
|
+ v-model="work.reflection"
|
|
|
+ class="kwlRefTextarea"
|
|
|
+ :placeholder="lang.ssKWLReflectionPH"
|
|
|
+ ></textarea>
|
|
|
+ <!-- 右下角麦克风按钮:点击开始/停止录音 -->
|
|
|
+ <div
|
|
|
+ class="kwlMicBtn"
|
|
|
+ :class="{ kwlMicBtnActive: recordObj.status == '1' }"
|
|
|
+ :title="recordObj.status == '1' ? lang.ssKWLStopRec : lang.ssKWLVoiceInput"
|
|
|
+ @click="recordObj.status == '1' ? stopRecord() : startRecord()"
|
|
|
+ >
|
|
|
+ <!-- 录音中:声波动画 -->
|
|
|
+ <svg v-if="recordObj.status == '1'" width="18" height="18" viewBox="0 0 120 20">
|
|
|
+ <rect x="5" y="5" width="8" :height="10" rx="3" fill="currentColor">
|
|
|
+ <animate attributeName="height" values="10;20;10" dur="1s" repeatCount="indefinite" begin="0s" />
|
|
|
+ <animate attributeName="y" values="10;0;10" dur="1s" repeatCount="indefinite" begin="0s" />
|
|
|
+ </rect>
|
|
|
+ <rect x="29" y="5" width="8" :height="10" rx="3" fill="currentColor">
|
|
|
+ <animate attributeName="height" values="10;20;10" dur="1s" repeatCount="indefinite" begin="0.2s" />
|
|
|
+ <animate attributeName="y" values="10;0;10" dur="1s" repeatCount="indefinite" begin="0.2s" />
|
|
|
+ </rect>
|
|
|
+ <rect x="53" y="5" width="8" :height="10" rx="3" fill="currentColor">
|
|
|
+ <animate attributeName="height" values="10;20;10" dur="1s" repeatCount="indefinite" begin="0.4s" />
|
|
|
+ <animate attributeName="y" values="10;0;10" dur="1s" repeatCount="indefinite" begin="0.4s" />
|
|
|
+ </rect>
|
|
|
+ <rect x="77" y="5" width="8" :height="10" rx="3" fill="currentColor">
|
|
|
+ <animate attributeName="height" values="10;20;10" dur="1s" repeatCount="indefinite" begin="0.6s" />
|
|
|
+ <animate attributeName="y" values="10;0;10" dur="1s" repeatCount="indefinite" begin="0.6s" />
|
|
|
+ </rect>
|
|
|
+ <rect x="101" y="5" width="8" :height="10" rx="3" fill="currentColor">
|
|
|
+ <animate attributeName="height" values="10;20;10" dur="1s" repeatCount="indefinite" begin="0.8s" />
|
|
|
+ <animate attributeName="y" values="10;0;10" dur="1s" repeatCount="indefinite" begin="0.8s" />
|
|
|
+ </rect>
|
|
|
+ </svg>
|
|
|
+ <!-- 空闲:麦克风图标 -->
|
|
|
+ <svg v-else viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
|
|
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
|
|
+ <line x1="12" y1="19" x2="12" y2="23"></line>
|
|
|
+ <line x1="8" y1="23" x2="16" y2="23"></line>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 录音转文字 iframe -->
|
|
|
+ <iframe allow="camera *; microphone *;display-capture;midi;encrypted-media;" :src="iframeUrl" ref="iframeRef"
|
|
|
+ v-show="false"></iframe>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ props: {
|
|
|
+ workData: {
|
|
|
+ type: Object,
|
|
|
+ default: () => {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ work: {
|
|
|
+ know: [],
|
|
|
+ want: [],
|
|
|
+ learned: [],
|
|
|
+ reflection: "",
|
|
|
+ taskDescription: "",
|
|
|
+ taskUrl: [],
|
|
|
+ showHowTo: false,
|
|
|
+ kplaceholder:"",
|
|
|
+ wplaceholder:"",
|
|
|
+ lplaceholder:"",
|
|
|
+ isShowReflection: true
|
|
|
+ },
|
|
|
+ newKnow: "",
|
|
|
+ newWant: "",
|
|
|
+ newLearned: "",
|
|
|
+ showHowTo: false,
|
|
|
+ showTaskDesc: true,
|
|
|
+ isShowReflection: true,
|
|
|
+ recordObj: {
|
|
|
+ status: "0", // 0:默认 1:录音中
|
|
|
+ loading: false,
|
|
|
+ startTime: 0,
|
|
|
+ },
|
|
|
+ videoDialogVisible: false,
|
|
|
+ currentVideoUrl: "",
|
|
|
+ howToSteps: [
|
|
|
+ this.lang.ssKWLStep1,
|
|
|
+ this.lang.ssKWLStep2,
|
|
|
+ this.lang.ssKWLStep3,
|
|
|
+ this.lang.ssKWLStep4
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ iframeUrl() {
|
|
|
+ if (this.lang && this.lang.lang == 'com') {
|
|
|
+ return `https://cloud.cocorobo.com/browser/public/index.html`;
|
|
|
+ } else if (this.lang && this.lang.lang == 'hk') {
|
|
|
+ return `https://cloud.cocorobo.hk/browser/public/index.html`;
|
|
|
+ } else {
|
|
|
+ return `https://beta.cloud.cocorobo.cn/browser/public/index.html`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ work: {
|
|
|
+ deep: true,
|
|
|
+ handler(newValue) {
|
|
|
+ if (JSON.stringify(newValue) != JSON.stringify(this.workData.json)) {
|
|
|
+ this.changeWorkData(newValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ workData: {
|
|
|
+ handler: function (newValue, oldValue) {
|
|
|
+ let _data = JSON.parse(JSON.stringify(this.workData.json || {}));
|
|
|
+ if (JSON.stringify(newValue) != JSON.stringify(oldValue) || JSON.stringify(this.work) != JSON.stringify(_data)) {
|
|
|
+ this.work = Object.assign({
|
|
|
+ know: [],
|
|
|
+ want: [],
|
|
|
+ learned: [],
|
|
|
+ reflection: "",
|
|
|
+ taskDescription: "",
|
|
|
+ taskUrl: [],
|
|
|
+ showHowTo: false,
|
|
|
+ kplaceholder:"",
|
|
|
+ wplaceholder:"",
|
|
|
+ lplaceholder:"",
|
|
|
+ isShowReflection: true
|
|
|
+ }, _data);
|
|
|
+
|
|
|
+ this.showHowTo = this.work.showHowTo;
|
|
|
+ this.isShowReflection = this.work.isShowReflection;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ deep: true,
|
|
|
+ immediate: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ changeWorkData(newValue) {
|
|
|
+ const str = JSON.stringify(newValue);
|
|
|
+ this.$emit("changeWorkData", str);
|
|
|
+ },
|
|
|
+ addItem(type) {
|
|
|
+ const inputMap = { know: "newKnow", want: "newWant", learned: "newLearned" };
|
|
|
+ const val = this[inputMap[type]];
|
|
|
+ if (!val || !val.trim()) return;
|
|
|
+ if (!this.work[type]) this.$set(this.work, type, []);
|
|
|
+ this.work[type].push(val.trim());
|
|
|
+ this[inputMap[type]] = "";
|
|
|
+ },
|
|
|
+ removeItem(type, idx) {
|
|
|
+ this.work[type].splice(idx, 1);
|
|
|
+ },
|
|
|
+ // 判断是否为图片
|
|
|
+ isImage(url) {
|
|
|
+ if (!url) return false;
|
|
|
+ return /\.(jpe?g|png|gif|bmp|webp|svg)(\?.*)?$/i.test(url);
|
|
|
+ },
|
|
|
+ // 判断是否为视频
|
|
|
+ isVideo(url) {
|
|
|
+ if (!url) return false;
|
|
|
+ return /\.(mp4|webm|ogg|mov|avi|wmv|flv|m4v)(\?.*)?$/i.test(url);
|
|
|
+ },
|
|
|
+ // 打开视频弹窗
|
|
|
+ previewVideo(url) {
|
|
|
+ this.currentVideoUrl = url;
|
|
|
+ this.videoDialogVisible = true;
|
|
|
+ },
|
|
|
+ // 视频弹窗关闭后清理
|
|
|
+ onVideoDialogClosed() {
|
|
|
+ this.currentVideoUrl = "";
|
|
|
+ },
|
|
|
+ // 开始录音
|
|
|
+ startRecord() {
|
|
|
+ let iframe = this.$refs["iframeRef"];
|
|
|
+ if (!iframe || !iframe.contentWindow) {
|
|
|
+ this.$message.error(this.lang.ssKWLRecServiceNotReady);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ iframe.contentWindow.window.document.getElementById(
|
|
|
+ "languageOptions"
|
|
|
+ ).selectedIndex = 2;
|
|
|
+
|
|
|
+ iframe.contentWindow.window.onRecognizedResult = (e) => {
|
|
|
+ let privText = e.privText;
|
|
|
+ if (!privText) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 将识别文本追加到反思区域
|
|
|
+ if (this.work.reflection) {
|
|
|
+ this.work.reflection += " " + privText;
|
|
|
+ } else {
|
|
|
+ this.work.reflection = privText;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ iframe.contentWindow.ConversationTranscriber();
|
|
|
+ this.recordObj.status = "1";
|
|
|
+ this.recordObj.startTime = new Date().getTime();
|
|
|
+ setTimeout(() => {
|
|
|
+ navigator.permissions && navigator.permissions.query({ name: 'microphone' }).then(permissionStatus => {
|
|
|
+ if (permissionStatus.state !== "granted") {
|
|
|
+ this.recordObj.status = "0";
|
|
|
+ this.recordObj.loading = false;
|
|
|
+ iframe.contentWindow.onSessionStopped = null;
|
|
|
+ iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
+ this.$message.warning(this.lang.ssKWLNoPermStop);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }).catch(() => {});
|
|
|
+ }, 2000);
|
|
|
+ } catch (err) {
|
|
|
+ console.error("startRecord error:", err);
|
|
|
+ this.$message.error(this.lang.ssKWLRecStartFail);
|
|
|
+ this.recordObj.status = "0";
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 停止录音
|
|
|
+ stopRecord() {
|
|
|
+ if (new Date().getTime() - this.recordObj.startTime < 2000) {
|
|
|
+ this.$message.info(this.lang.ssKWLRecordShort);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ navigator.permissions && navigator.permissions.query({ name: 'microphone' }).then(permissionStatus => {
|
|
|
+ if (permissionStatus.state !== "granted") {
|
|
|
+ this.recordObj.status = "0";
|
|
|
+ this.recordObj.loading = false;
|
|
|
+ let iframe = this.$refs["iframeRef"];
|
|
|
+ if (iframe && iframe.contentWindow) {
|
|
|
+ iframe.contentWindow.onSessionStopped = null;
|
|
|
+ iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
+ }
|
|
|
+ this.$message.warning(this.lang.ssKWLNoMicPerm);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.recordObj.status == '1' && !this.recordObj.loading) {
|
|
|
+ let iframe = this.$refs["iframeRef"];
|
|
|
+ if (!iframe || !iframe.contentWindow) {
|
|
|
+ this.recordObj.status = "0";
|
|
|
+ this.recordObj.loading = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.recordObj.loading = true;
|
|
|
+ try {
|
|
|
+ iframe.contentWindow.window.document
|
|
|
+ .getElementById("scenarioStopButton")
|
|
|
+ .click();
|
|
|
+ } catch (err) {
|
|
|
+ console.error("stop click error:", err);
|
|
|
+ this.recordObj.status = "0";
|
|
|
+ this.recordObj.loading = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ iframe.contentWindow.onSessionStopped = () => {
|
|
|
+ this.recordObj.status = "0";
|
|
|
+ this.recordObj.loading = false;
|
|
|
+ this.$message.success(this.lang.ssKWLRecStopped);
|
|
|
+ iframe.contentWindow.onSessionStopped = null;
|
|
|
+ iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ this.recordObj.status = "0";
|
|
|
+ this.recordObj.loading = false;
|
|
|
+ let iframe = this.$refs["iframeRef"];
|
|
|
+ if (iframe && iframe.contentWindow) {
|
|
|
+ iframe.contentWindow.onSessionStopped = null;
|
|
|
+ iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }).catch(() => {
|
|
|
+ this.recordObj.status = "0";
|
|
|
+ this.recordObj.loading = false;
|
|
|
+ let iframe = this.$refs["iframeRef"];
|
|
|
+ if (iframe && iframe.contentWindow) {
|
|
|
+ iframe.contentWindow.onSessionStopped = null;
|
|
|
+ iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
+ }
|
|
|
+ this.$message.success(this.lang.ssKWLRecStopped);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {},
|
|
|
+ beforeUnmount() {}
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.kwlPage {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding: 1rem;
|
|
|
+ background: #f5f7fa;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 1rem;
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlCard {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: .75rem;
|
|
|
+ padding: 1.25rem 1.5rem;
|
|
|
+ box-shadow: 0 .0625rem .1875rem rgba(0, 0, 0, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
+/* 头部 */
|
|
|
+.kwlHeader {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlTitle {
|
|
|
+ font-size: 1.25rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1a1a1a;
|
|
|
+ margin-bottom: .375rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlSubtitle {
|
|
|
+ font-size: .8125rem;
|
|
|
+ color: #888;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlHeaderRight {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: .375rem;
|
|
|
+ color: #3681FC;
|
|
|
+ font-size: .875rem;
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlArrow {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ transition: transform 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlArrow svg {
|
|
|
+ width: 1rem;
|
|
|
+ height: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlArrowUp {
|
|
|
+ transform: rotate(180deg);
|
|
|
+}
|
|
|
+
|
|
|
+/* 怎么做 */
|
|
|
+.kwlHowTo {
|
|
|
+ border-top: .0625rem solid #e0e0e0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: .75rem;
|
|
|
+ padding-top: .9375rem;
|
|
|
+ margin-top: .9375rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlHowToItem {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: .75rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlStepNum {
|
|
|
+ width: 1.5rem;
|
|
|
+ height: 1.5rem;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #3681FC;
|
|
|
+ color: #fff;
|
|
|
+ font-size: .75rem;
|
|
|
+ font-weight: 600;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlStepText {
|
|
|
+ font-size: .875rem;
|
|
|
+ color: #333;
|
|
|
+ line-height: 1.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+/* 任务说明 */
|
|
|
+.kwlTaskDesc {
|
|
|
+ padding: 1rem .9375rem;
|
|
|
+ border-left: .25rem solid #F5A623;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlTaskHeader {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: .5rem;
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlStar {
|
|
|
+ color: #F5A623;
|
|
|
+ font-size: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlTaskTitle {
|
|
|
+ font-size: .9375rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1a1a1a;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlTaskContent {
|
|
|
+ margin-top: .625rem;
|
|
|
+ font-size: .875rem;
|
|
|
+ color: #444;
|
|
|
+ line-height: 1.375rem;
|
|
|
+ padding-left: 1.625rem;
|
|
|
+}
|
|
|
+
|
|
|
+/* 任务附件:图片/视频 */
|
|
|
+.kwlTaskMedia {
|
|
|
+ margin-top: .75rem;
|
|
|
+ padding-left: 1.625rem;
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: .75rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlTaskMediaItem {
|
|
|
+ width: 9.375rem;
|
|
|
+ max-width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlTaskMediaItem img {
|
|
|
+ width: 100%;
|
|
|
+ height: 6.25rem;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: .5rem;
|
|
|
+ cursor: pointer;
|
|
|
+ border: .0625rem solid #e0e0e0;
|
|
|
+ transition: transform 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlTaskMediaItem img:hover {
|
|
|
+ transform: scale(1.02);
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频缩略图 */
|
|
|
+.kwlVideoThumb {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 6.25rem;
|
|
|
+ border-radius: .5rem;
|
|
|
+ overflow: hidden;
|
|
|
+ cursor: pointer;
|
|
|
+ background: #000;
|
|
|
+ border: .0625rem solid #e0e0e0;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoThumb video {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoPlayBtn {
|
|
|
+ position: absolute;
|
|
|
+ left: 50%;
|
|
|
+ top: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ width: 3rem;
|
|
|
+ height: 3rem;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(0, 0, 0, 0.55);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: background 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoThumb:hover .kwlVideoPlayBtn {
|
|
|
+ background: rgba(54, 129, 252, 0.85);
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoPlayBtn svg {
|
|
|
+ margin-left: .25rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlTaskMediaLink {
|
|
|
+ display: inline-block;
|
|
|
+ font-size: .8125rem;
|
|
|
+ color: #3681FC;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频全屏弹窗 */
|
|
|
+.kwlVideoDialog >>> .el-dialog {
|
|
|
+ background: transparent;
|
|
|
+ box-shadow: none;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoDialog >>> .el-dialog__header {
|
|
|
+ padding: 0;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoDialog >>> .el-dialog__headerbtn {
|
|
|
+ top: -2.5rem;
|
|
|
+ right: -2.5rem;
|
|
|
+ width: 2.5rem;
|
|
|
+ height: 2.5rem;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ border-radius: 50%;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoDialog >>> .el-dialog__close {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 1.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoDialog >>> .el-dialog__body {
|
|
|
+ padding: 0;
|
|
|
+ background: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoDialog >>> .el-dialog__headerbtn:hover .el-dialog__close {
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoDialogBody {
|
|
|
+ width: 100%;
|
|
|
+ background: #000;
|
|
|
+ border-radius: .5rem;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlVideoFull {
|
|
|
+ width: 100%;
|
|
|
+ height: 70vh;
|
|
|
+ display: block;
|
|
|
+ background: #000;
|
|
|
+}
|
|
|
+
|
|
|
+/* 三列 */
|
|
|
+.kwlColumns {
|
|
|
+ display: flex;
|
|
|
+ gap: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlColumn {
|
|
|
+ flex: 1;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: .75rem;
|
|
|
+ box-shadow: 0 .0625rem .1875rem rgba(0, 0, 0, 0.06);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlColTitle {
|
|
|
+ font-size: .9375rem;
|
|
|
+ font-weight: 600;
|
|
|
+ padding: .875rem 1.125rem;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlColKnow {
|
|
|
+ border-top: .375rem solid #2563eb;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlColTitleKnow {
|
|
|
+ color: #2563eb;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlColWant {
|
|
|
+ border-top: .375rem solid #F56565;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlColTitleWant {
|
|
|
+ color: #F56565;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlColLearn {
|
|
|
+ border-top: .375rem solid #48BB78;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlColTitleLearn {
|
|
|
+ color: #48BB78;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlColBody {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 10rem;
|
|
|
+ padding: 1rem;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: .5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlEmptyTip {
|
|
|
+ text-align: center;
|
|
|
+ color: #bbb;
|
|
|
+ font-size: .8125rem;
|
|
|
+ padding: 1.875rem 0;
|
|
|
+ border: .125rem dashed #e0e0e0;
|
|
|
+ border-radius: .5rem;
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlItem {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: .5rem .75rem;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: .375rem;
|
|
|
+ font-size: .8125rem;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlItemText {
|
|
|
+ flex: 1;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlItemDel {
|
|
|
+ margin-left: .5rem;
|
|
|
+ width: 1.25rem;
|
|
|
+ height: 1.25rem;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #999;
|
|
|
+ font-size: 1rem;
|
|
|
+ line-height: 1;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlItemDel:hover {
|
|
|
+ background: #eee;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlAddRow {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: .5rem;
|
|
|
+ padding: .75rem 1rem;
|
|
|
+ border-top: .0625rem solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlInput {
|
|
|
+ flex: 1;
|
|
|
+ border: none;
|
|
|
+ outline: none;
|
|
|
+ font-size: .8125rem;
|
|
|
+ padding: .375rem 0;
|
|
|
+ background: transparent;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlInput::placeholder {
|
|
|
+ color: #bbb;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlAddBtn {
|
|
|
+ width: 2rem;
|
|
|
+ height: 2rem;
|
|
|
+ border-radius: .5rem;
|
|
|
+ border: none;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 1.125rem;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: opacity 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlAddBtn:hover {
|
|
|
+ opacity: 0.85;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlAddBtnKnow {
|
|
|
+ background: #3681FC;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlAddBtnWant {
|
|
|
+ background: #F56565;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlAddBtnLearn {
|
|
|
+ background: #48BB78;
|
|
|
+}
|
|
|
+
|
|
|
+/* 学习反思 */
|
|
|
+.kwlReflection {
|
|
|
+ padding: 1.25rem 1.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlRefTitle {
|
|
|
+ font-size: 1rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1a1a1a;
|
|
|
+ margin-bottom: .75rem;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlRefAreaWrap {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlRefTextarea {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 7.5rem;
|
|
|
+ padding: .75rem 3rem .75rem .875rem;
|
|
|
+ border: .0625rem solid #e0e0e0;
|
|
|
+ border-radius: .5rem;
|
|
|
+ font-size: .875rem;
|
|
|
+ line-height: 1.375rem;
|
|
|
+ resize: vertical;
|
|
|
+ outline: none;
|
|
|
+ box-sizing: border-box;
|
|
|
+ font-family: inherit;
|
|
|
+ transition: border-color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlRefTextarea:focus {
|
|
|
+ border-color: #3681FC;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlRefTextarea::placeholder {
|
|
|
+ color: #bbb;
|
|
|
+}
|
|
|
+
|
|
|
+/* 右下角麦克风按钮 */
|
|
|
+.kwlMicBtn {
|
|
|
+ position: absolute;
|
|
|
+ right: .75rem;
|
|
|
+ bottom: .75rem;
|
|
|
+ width: 2rem;
|
|
|
+ height: 2rem;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #f5f7fa;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #888;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlMicBtn:hover {
|
|
|
+ background: #e8ecf1;
|
|
|
+ color: #3681FC;
|
|
|
+}
|
|
|
+
|
|
|
+/* 录音中:蓝色高亮 */
|
|
|
+.kwlMicBtnActive {
|
|
|
+ background: #3681FC;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.kwlMicBtnActive:hover {
|
|
|
+ background: #5B9BFC;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+</style>
|