227 lines
6.4 KiB
Vue
227 lines
6.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { aiApi, type AiExplain } from '../../api'
|
|
import { useResponsive } from '../../composables/useResponsive'
|
|
import AppCard from '../../components/base/AppCard.vue'
|
|
import AppButton from '../../components/base/AppButton.vue'
|
|
import AppIcon from '../../components/base/AppIcon.vue'
|
|
import AppBadge from '../../components/base/AppBadge.vue'
|
|
import AppPageHeader from '../../components/base/AppPageHeader.vue'
|
|
import AppLoading from '../../components/feedback/AppLoading.vue'
|
|
import AppStack from '../../components/base/AppStack.vue'
|
|
import AppError from '../../components/feedback/AppError.vue'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const { isMobile } = useResponsive()
|
|
|
|
const questionId = computed(() => String(route.query.questionId ?? ''))
|
|
const userAnswer = computed(() => String(route.query.userAnswer ?? '').toUpperCase())
|
|
const requestType = computed<'standard' | 'deep'>(() =>
|
|
route.query.requestType === 'deep' ? 'deep' : 'standard'
|
|
)
|
|
|
|
const loading = ref(true)
|
|
const error = ref('')
|
|
const explain = ref<AiExplain | null>(null)
|
|
|
|
onMounted(() => void load())
|
|
|
|
async function load() {
|
|
if (!questionId.value) {
|
|
error.value = '缺少题目参数,无法生成讲解'
|
|
loading.value = false
|
|
return
|
|
}
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
explain.value = await aiApi.explain({
|
|
questionId: questionId.value,
|
|
userAnswer: ['A', 'B', 'C', 'D'].includes(userAnswer.value)
|
|
? (userAnswer.value as 'A' | 'B' | 'C' | 'D')
|
|
: undefined,
|
|
requestType: requestType.value
|
|
})
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : 'AI 讲解加载失败'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function back() {
|
|
router.back()
|
|
}
|
|
|
|
const title = computed(() => (requestType.value === 'deep' ? 'AI 深度讲解' : 'AI 讲解'))
|
|
const isAi = computed(() => explain.value?.source === 'ai')
|
|
const mistakeText = computed(() => {
|
|
const list = explain.value?.commonMistakes ?? []
|
|
return list.length > 0 ? list : ['无明显高频错误']
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<AppStack class="ai-explain">
|
|
<AppLoading v-if="loading" fullscreen text="正在生成讲解…" />
|
|
<AppError v-else-if="error" fullscreen title="讲解加载失败" :message="error" retry @retry="load" />
|
|
|
|
<template v-else-if="explain">
|
|
<!-- 顶栏 -->
|
|
<AppPageHeader :title="title" size="detail" back>
|
|
<AppBadge :variant="isAi ? 'primary' : 'soft'">
|
|
{{ isAi ? 'AI 生成' : '题库解析回退' }}
|
|
</AppBadge>
|
|
</AppPageHeader>
|
|
|
|
<!-- 结论 -->
|
|
<AppCard title="讲解结论" icon="sparkles">
|
|
<p class="ai-explain__summary">{{ explain.summary }}</p>
|
|
<p v-if="!isAi" class="ai-explain__fallback-tip">
|
|
AI 未配置或暂时不可用时自动回退到题库标准解析,不会阻塞你的刷题流程。
|
|
</p>
|
|
</AppCard>
|
|
|
|
<!-- 分步解析 -->
|
|
<AppCard title="分步解析" icon="book-open" v-if="explain.steps.length">
|
|
<ol class="ai-explain__steps">
|
|
<li v-for="(step, i) in explain.steps" :key="i" class="ai-explain__step">
|
|
<span class="ai-explain__step-index">{{ i + 1 }}</span>
|
|
<span class="ai-explain__step-text">{{ step }}</span>
|
|
</li>
|
|
</ol>
|
|
</AppCard>
|
|
|
|
<!-- 考点 -->
|
|
<AppCard title="考点" icon="target" v-if="explain.knowledgePoints.length">
|
|
<div class="ai-explain__tags">
|
|
<AppBadge v-for="(k, i) in explain.knowledgePoints" :key="i" variant="soft">{{ k }}</AppBadge>
|
|
</div>
|
|
</AppCard>
|
|
|
|
<!-- 常见错误 -->
|
|
<AppCard title="常见错误" icon="alert">
|
|
<ul class="ai-explain__mistakes">
|
|
<li v-for="(m, i) in mistakeText" :key="i">{{ m }}</li>
|
|
</ul>
|
|
</AppCard>
|
|
|
|
<!-- 答案 -->
|
|
<AppCard title="正确答案" icon="check">
|
|
<div class="ai-explain__answer">
|
|
<strong class="ai-explain__answer-key">{{ explain.answer }}</strong>
|
|
<span class="ai-explain__answer-label">本题答案为 {{ explain.answer }}</span>
|
|
</div>
|
|
</AppCard>
|
|
|
|
<AppButton v-if="!isMobile" variant="ghost" @click="back">
|
|
<AppIcon name="chevron-left" :size="16" />返回
|
|
</AppButton>
|
|
</template>
|
|
</AppStack>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.ai-explain__summary {
|
|
margin: 0;
|
|
font-size: var(--fs-body);
|
|
line-height: var(--lh-body);
|
|
color: var(--text-primary);
|
|
white-space: pre-wrap;
|
|
}
|
|
.ai-explain__fallback-tip {
|
|
margin: var(--space-3) 0 0;
|
|
padding: var(--space-2) var(--space-3);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-soft);
|
|
font-size: var(--fs-label);
|
|
color: var(--text-secondary);
|
|
}
|
|
.ai-explain__steps {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-3);
|
|
}
|
|
.ai-explain__step {
|
|
display: flex;
|
|
gap: var(--space-3);
|
|
align-items: flex-start;
|
|
}
|
|
.ai-explain__step-index {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
flex: none;
|
|
border-radius: 50%;
|
|
background: var(--primary-soft, var(--bg-soft));
|
|
color: var(--primary);
|
|
font-size: var(--fs-label);
|
|
font-weight: var(--fw-label-bold);
|
|
}
|
|
.ai-explain__step-text {
|
|
font-size: var(--fs-subtitle);
|
|
line-height: var(--lh-subtitle);
|
|
color: var(--text-primary);
|
|
white-space: pre-wrap;
|
|
}
|
|
.ai-explain__tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
}
|
|
.ai-explain__mistakes {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
}
|
|
.ai-explain__mistakes li {
|
|
position: relative;
|
|
padding-left: var(--space-4);
|
|
font-size: var(--fs-subtitle);
|
|
line-height: var(--lh-subtitle);
|
|
color: var(--text-primary);
|
|
}
|
|
.ai-explain__mistakes li::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 8px;
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--warning);
|
|
}
|
|
.ai-explain__answer {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
}
|
|
.ai-explain__answer-key {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 44px;
|
|
height: 44px;
|
|
flex: none;
|
|
border-radius: var(--radius-md);
|
|
background: var(--success);
|
|
color: var(--on-accent);
|
|
font-family: var(--font-num);
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
}
|
|
.ai-explain__answer-label {
|
|
font-size: var(--fs-subtitle);
|
|
font-weight: var(--fw-card-title);
|
|
color: var(--text-primary);
|
|
}
|
|
</style>
|