Browse Source

feat: add task hint modal component

jimmylee 2 weeks ago
parent
commit
2e38c4b7c9
1 changed files with 352 additions and 0 deletions
  1. 352 0
      src/views/Editor/EnglishSpeaking/preview/TaskHintModal.vue

+ 352 - 0
src/views/Editor/EnglishSpeaking/preview/TaskHintModal.vue

@@ -0,0 +1,352 @@
+<template>
+  <div v-if="visible" class="modal-mask" @click.self="emit('close')">
+    <div class="modal hint-modal scale-in">
+      <div class="modal-head">
+        <h3 class="modal-title">
+          <svg
+            width="14"
+            height="14"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="#f97316"
+            stroke-width="2"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          >
+            <path d="M9 18h6" />
+            <path d="M10 22h4" />
+            <path
+              d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"
+            />
+          </svg>
+          {{ aiName ? `${aiName} 的任务提示` : '任务提示' }}
+        </h3>
+        <button class="close-btn" type="button" aria-label="关闭" @click="emit('close')">
+          <svg
+            width="14"
+            height="14"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="2"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          >
+            <line x1="18" y1="6" x2="6" y2="18" />
+            <line x1="6" y1="6" x2="18" y2="18" />
+          </svg>
+        </button>
+      </div>
+
+      <div v-if="loading" class="state-panel">
+        <p class="state-text">正在生成任务提示...</p>
+      </div>
+
+      <div v-else-if="error" class="state-panel error-panel">
+        <p class="error-text">{{ error }}</p>
+        <button class="retry-btn" type="button" @click="emit('retry')">
+          重试
+        </button>
+      </div>
+
+      <template v-else-if="hint">
+        <div class="hint-context">
+          <p class="context-label">当前问题</p>
+          <p class="context-body">{{ hint.current_question }}</p>
+        </div>
+
+        <div class="hint-section">
+          <p class="section-label">参考句子</p>
+          <div class="sentences">
+            <div
+              v-for="(sentence, index) in hint.example_sentences"
+              :key="`${sentence.english}-${index}`"
+              class="sentence-card"
+            >
+              <p class="sentence-en">{{ sentence.english }}</p>
+              <p class="sentence-zh">{{ sentence.chinese }}</p>
+            </div>
+          </div>
+        </div>
+
+        <div class="hint-section">
+          <p class="section-label">关键词汇</p>
+          <div class="vocab-grid">
+            <div
+              v-for="(vocab, index) in hint.key_vocabulary"
+              :key="`${vocab.word}-${index}`"
+              class="vocab-item"
+            >
+              <p class="vocab-word">{{ vocab.word }}</p>
+              <p class="vocab-meaning">{{ vocab.meaning }}</p>
+            </div>
+          </div>
+        </div>
+
+        <p class="hint-footer">用自己的话表达更棒哦</p>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { TaskHint } from '@/types/englishSpeaking'
+
+defineProps<{
+  visible: boolean
+  loading: boolean
+  error?: string | null
+  hint?: TaskHint | null
+  aiName?: string
+}>()
+
+const emit = defineEmits<{
+  close: []
+  retry: []
+}>()
+</script>
+
+<style lang="scss" scoped>
+.modal-mask {
+  position: fixed;
+  inset: 0;
+  z-index: 50;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 16px;
+  background: rgba(0, 0, 0, 0.3);
+  backdrop-filter: blur(2px);
+}
+
+.modal {
+  width: 100%;
+  max-height: 80vh;
+  overflow-y: auto;
+  background: #fff;
+  border-radius: 16px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
+}
+
+.hint-modal {
+  max-width: 440px;
+  min-width: 280px;
+  padding: 20px;
+}
+
+.modal-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.modal-title {
+  display: flex;
+  min-width: 0;
+  align-items: center;
+  gap: 6px;
+  margin: 0;
+  color: #111827;
+  font-size: 14px;
+  font-weight: 600;
+  line-height: 1.4;
+}
+
+.modal-title svg {
+  flex: 0 0 auto;
+}
+
+.close-btn {
+  display: flex;
+  flex: 0 0 auto;
+  align-items: center;
+  justify-content: center;
+  width: 26px;
+  height: 26px;
+  color: #6b7280;
+  cursor: pointer;
+  background: #f3f4f6;
+  border: none;
+  border-radius: 8px;
+
+  &:hover {
+    background: #e5e7eb;
+  }
+}
+
+.state-panel {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-height: 128px;
+  padding: 16px;
+  text-align: center;
+}
+
+.state-text,
+.error-text {
+  margin: 0;
+  font-size: 13px;
+  line-height: 1.6;
+}
+
+.state-text {
+  color: #6b7280;
+}
+
+.error-panel {
+  gap: 12px;
+}
+
+.error-text {
+  color: #ef4444;
+  overflow-wrap: anywhere;
+}
+
+.retry-btn {
+  min-width: 72px;
+  height: 32px;
+  padding: 0 14px;
+  color: #fff;
+  font-size: 12px;
+  font-weight: 600;
+  line-height: 1;
+  cursor: pointer;
+  background: #f97316;
+  border: none;
+  border-radius: 8px;
+
+  &:hover {
+    background: #ea580c;
+  }
+}
+
+.hint-context {
+  padding: 12px 14px;
+  margin-bottom: 16px;
+  background: #fff7ed;
+  border: 1px solid #fed7aa;
+  border-radius: 12px;
+}
+
+.context-label,
+.section-label {
+  margin: 0 0 8px;
+  font-size: 10px;
+  font-weight: 500;
+  line-height: 1.4;
+  letter-spacing: 0.03em;
+  text-transform: uppercase;
+}
+
+.context-label {
+  margin-bottom: 4px;
+  color: #f97316;
+}
+
+.context-body {
+  margin: 0;
+  color: #374151;
+  font-size: 13px;
+  line-height: 1.5;
+  overflow-wrap: anywhere;
+}
+
+.hint-section {
+  margin-bottom: 16px;
+}
+
+.section-label {
+  color: #9ca3af;
+}
+
+.sentences {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.sentence-card {
+  min-width: 0;
+  padding: 10px 12px;
+  background: #f9fafb;
+  border: 1px solid #f3f4f6;
+  border-radius: 12px;
+  transition: border-color 0.2s;
+
+  &:hover {
+    border-color: #fed7aa;
+  }
+}
+
+.sentence-en,
+.sentence-zh,
+.vocab-word,
+.vocab-meaning {
+  overflow-wrap: anywhere;
+}
+
+.sentence-en {
+  margin: 0;
+  color: #1f2937;
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+.sentence-zh {
+  margin: 2px 0 0;
+  color: #9ca3af;
+  font-size: 11px;
+  line-height: 1.5;
+}
+
+.vocab-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 8px;
+}
+
+.vocab-item {
+  min-width: 0;
+  padding: 8px 10px;
+  background: #f9fafb;
+  border: 1px solid #f3f4f6;
+  border-radius: 10px;
+}
+
+.vocab-word {
+  margin: 0;
+  color: #1f2937;
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 1.4;
+}
+
+.vocab-meaning {
+  margin: 2px 0 0;
+  color: #9ca3af;
+  font-size: 10px;
+  line-height: 1.5;
+}
+
+.hint-footer {
+  margin: 16px 0 0;
+  color: #d1d5db;
+  font-size: 11px;
+  line-height: 1.5;
+  text-align: center;
+}
+
+@media (max-width: 420px) {
+  .hint-modal {
+    padding: 16px;
+  }
+
+  .vocab-grid {
+    grid-template-columns: 1fr;
+  }
+}
+</style>