af57f1a395
- 添加全局 .page-layout 工具类(统一padding/背景/min-height) - 所有16个页面根元素加上 page-layout 类 - 问答页面移除登录拦截:未登录可看欢迎卡片和问题建议,使用时再跳登录 - 输入区未登录时显示品牌色登录引导按钮 - 补全 detail/search/progress/feedback 缺漏的 status-bar-placeholder
449 lines
11 KiB
Vue
449 lines
11 KiB
Vue
<template>
|
||
<view class="detail-page page-layout">
|
||
<!-- 状态栏占位 -->
|
||
<view class="status-bar-placeholder"></view>
|
||
<!-- 顶部导航 -->
|
||
<view class="nav-bar">
|
||
<view class="nav-back" @click="goBack">‹</view>
|
||
<text class="nav-title">AI 知识</text>
|
||
<view class="nav-collect" @click="toggleCollect">
|
||
<text v-if="isCollected" class="icon icon-collected">★</text>
|
||
<text v-else class="icon">☆</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 知识头部 -->
|
||
<view class="detail-header">
|
||
<view class="dim-tag" :style="{ background: dimColor }">
|
||
<text class="dim-name">{{ dimensionName }}</text>
|
||
</view>
|
||
<text class="detail-title">{{ detail?.title || '加载中...' }}</text>
|
||
<view class="meta-row">
|
||
<text class="meta-item">📖 {{ viewCountText }}</text>
|
||
<text class="meta-item">⭐ {{ collectionCount }}</text>
|
||
<text class="meta-item" v-if="detail?.category">
|
||
{{ categoryLabel }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 标签 -->
|
||
<view class="tag-row" v-if="detail?.tags?.length">
|
||
<text class="tag" v-for="tag in detail.tags" :key="tag">#{{ tag }}</text>
|
||
</view>
|
||
|
||
<!-- 内容区域 -->
|
||
<scroll-view class="content-scroll" scroll-y>
|
||
<!-- 分段内容(如果存在 sections) -->
|
||
<view class="content-section" v-if="detail?.sections?.length">
|
||
<view class="section-block" v-for="(section, idx) in detail.sections" :key="idx">
|
||
<text class="section-title">{{ section.title }}</text>
|
||
<text class="section-content">{{ section.content }}</text>
|
||
<view class="section-images" v-if="section.images?.length">
|
||
<image
|
||
class="section-img"
|
||
v-for="(img, i) in section.images"
|
||
:key="i"
|
||
:src="img"
|
||
mode="aspectFill"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 普通内容 -->
|
||
<view class="content-block" v-else>
|
||
<text class="content-text">{{ detail?.content || '暂无内容' }}</text>
|
||
</view>
|
||
|
||
<!-- 问答区域 -->
|
||
<view class="qa-section" v-if="detail?.category === 'question' && detail?.question">
|
||
<view class="qa-question">
|
||
<text class="qa-label">问题</text>
|
||
<text class="qa-text">{{ detail.question }}</text>
|
||
</view>
|
||
<view class="qa-options" v-if="detail?.options?.length">
|
||
<view
|
||
class="qa-option"
|
||
v-for="(opt, idx) in detail.options"
|
||
:key="idx"
|
||
:class="{ correct: selectedOption === idx && opt.isCorrect }"
|
||
@click="selectedOption = idx"
|
||
>
|
||
<text class="option-letter">{{ String.fromCharCode(65 + idx) }}</text>
|
||
<text class="option-text">{{ opt.text }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="qa-answer" v-if="selectedOption !== null">
|
||
<text class="answer-label">
|
||
{{ detail.options?.[selectedOption]?.isCorrect ? '✅ 正确' : '❌ 错误' }}
|
||
</text>
|
||
<text class="answer-text" v-if="detail.answer">{{ detail.answer }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部 AI 问答入口 -->
|
||
<view class="ai-banner" @click="goToAI">
|
||
<text class="ai-icon">🤖</text>
|
||
<text class="ai-text">还有疑问?问问 AI 助手</text>
|
||
<text class="ai-arrow">→</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { knowledgeApi } from '@/utils/api'
|
||
import { useUserStore } from '@/stores/user'
|
||
import { DIMENSIONS } from '@/utils/dimensions'
|
||
|
||
const detailId = ref('')
|
||
const detail = ref(null)
|
||
const isCollected = ref(false)
|
||
const selectedOption = ref(null)
|
||
const userStore = useUserStore()
|
||
|
||
// 从路由参数获取 ID
|
||
onMounted(() => {
|
||
const pages = getCurrentPages()
|
||
const currentPage = pages[pages.length - 1]
|
||
const query = currentPage.$page?.query || {}
|
||
detailId.value = query.id || ''
|
||
|
||
if (detailId.value) {
|
||
fetchDetail()
|
||
}
|
||
})
|
||
|
||
// 维度信息
|
||
const dimColor = computed(() => {
|
||
const dim = detail.value?.dim
|
||
if (!dim) return 'var(--dim1-color)'
|
||
const dimInfo = DIMENSIONS.find(d => d.id === dim)
|
||
return dimInfo ? dimInfo.color : 'var(--dim1-color)'
|
||
})
|
||
const dimensionName = computed(() => {
|
||
const dim = detail.value?.dim
|
||
if (!dim) return ''
|
||
const dimInfo = DIMENSIONS.find(d => d.id === dim)
|
||
return dimInfo ? dimInfo.name : ''
|
||
})
|
||
|
||
// 分类标签
|
||
const categoryLabel = computed(() => {
|
||
const map = {
|
||
concept: '📚 概念',
|
||
history: '📜 历史',
|
||
application: '🛠️ 应用',
|
||
question: '❓ 问答'
|
||
}
|
||
return map[detail.value?.category] || ''
|
||
})
|
||
|
||
const viewCountText = computed(() => {
|
||
const count = detail.value?.viewCount || 0
|
||
return count >= 10000 ? (count / 10000).toFixed(1) + '万' : count
|
||
})
|
||
const collectionCount = computed(() => detail.value?.collectionCount || 0)
|
||
|
||
async function fetchDetail() {
|
||
try {
|
||
detail.value = await knowledgeApi.detail(detailId.value)
|
||
// 检查是否已收藏
|
||
if (userStore.isLoggedIn) {
|
||
const collected = await knowledgeApi.collected(userStore.token)
|
||
isCollected.value = collected.some(item => item.id === detailId.value)
|
||
}
|
||
} catch (err) {
|
||
console.error('获取知识详情失败:', err)
|
||
uni.showToast({ title: '加载失败,请重试', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function toggleCollect() {
|
||
if (!userStore.isLoggedIn) {
|
||
uni.navigateTo({ url: '/pages/login/login' })
|
||
return
|
||
}
|
||
try {
|
||
if (isCollected.value) {
|
||
await knowledgeApi.uncollect(detailId.value, userStore.token)
|
||
isCollected.value = false
|
||
uni.showToast({ title: '已取消收藏', icon: 'none' })
|
||
} else {
|
||
await knowledgeApi.collect(detailId.value, userStore.token)
|
||
isCollected.value = true
|
||
uni.showToast({ title: '收藏成功', icon: 'success' })
|
||
}
|
||
} catch (err) {
|
||
uni.showToast({ title: err.message || '操作失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
function goBack() {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
function goToAI() {
|
||
uni.switchTab({ url: '/pages/chat/chat' })
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.detail-page {
|
||
width: 100%;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--bg-primary);
|
||
}
|
||
|
||
// 顶部导航
|
||
.nav-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 50px 20px 12px;
|
||
flex-shrink: 0;
|
||
|
||
.nav-back {
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22px;
|
||
color: var(--text-primary);
|
||
}
|
||
.nav-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
.nav-collect {
|
||
width: 30px;
|
||
display: flex;
|
||
justify-content: center;
|
||
.icon {
|
||
font-size: 18px;
|
||
color: var(--text-secondary);
|
||
}
|
||
.icon-collected {
|
||
color: var(--dim5-color);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 知识头部
|
||
.detail-header {
|
||
padding: 0 20px 16px;
|
||
flex-shrink: 0;
|
||
|
||
.dim-tag {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: white;
|
||
margin-bottom: 10px;
|
||
opacity: 0.9;
|
||
|
||
.dim-name { color: white; }
|
||
}
|
||
|
||
.detail-title {
|
||
display: block;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
line-height: 1.4;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.meta-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
|
||
.meta-item { display: flex; align-items: center; }
|
||
}
|
||
}
|
||
|
||
// 标签
|
||
.tag-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 0 20px 16px;
|
||
flex-shrink: 0;
|
||
|
||
.tag {
|
||
font-size: 12px;
|
||
color: var(--dim1-color);
|
||
background: rgba(179, 136, 255, 0.1);
|
||
padding: 4px 10px;
|
||
border-radius: 10px;
|
||
}
|
||
}
|
||
|
||
// 内容滚动区
|
||
.content-scroll {
|
||
flex: 1;
|
||
padding: 0 20px;
|
||
overflow-y: auto;
|
||
padding-bottom: 80px;
|
||
|
||
.content-section,
|
||
.content-block,
|
||
.qa-section {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.section-block {
|
||
margin-bottom: 16px;
|
||
|
||
&:last-child { margin-bottom: 0; }
|
||
|
||
.section-title {
|
||
display: block;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--dim1-color);
|
||
margin-bottom: 8px;
|
||
}
|
||
.section-content {
|
||
display: block;
|
||
font-size: 14px;
|
||
color: var(--text-primary);
|
||
line-height: 1.8;
|
||
}
|
||
.section-images {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
|
||
.section-img {
|
||
width: 100%;
|
||
height: 200px;
|
||
border-radius: 8px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
}
|
||
}
|
||
|
||
.content-text {
|
||
font-size: 14px;
|
||
color: var(--text-primary);
|
||
line-height: 1.8;
|
||
}
|
||
}
|
||
|
||
// 问答区域
|
||
.qa-section {
|
||
.qa-question {
|
||
.qa-label {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 6px;
|
||
}
|
||
.qa-text {
|
||
display: block;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
|
||
.qa-options {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
|
||
.qa-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 10px;
|
||
transition: all 0.3s;
|
||
|
||
&.correct {
|
||
background: rgba(34, 197, 94, 0.15);
|
||
border-color: rgba(34, 197, 94, 0.4);
|
||
}
|
||
|
||
.option-letter {
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 50%;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
.option-text {
|
||
font-size: 14px;
|
||
color: var(--text-primary);
|
||
}
|
||
}
|
||
}
|
||
|
||
.qa-answer {
|
||
margin-top: 12px;
|
||
padding: 12px;
|
||
background: rgba(179, 136, 255, 0.1);
|
||
border-radius: 10px;
|
||
|
||
.answer-label {
|
||
display: block;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
margin-bottom: 6px;
|
||
}
|
||
.answer-text {
|
||
display: block;
|
||
font-size: 13px;
|
||
color: var(--text-primary);
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
}
|
||
|
||
// AI 底部入口
|
||
.ai-banner {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 20px;
|
||
right: 20px;
|
||
height: 48px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
|
||
border-radius: 12px;
|
||
color: white;
|
||
box-shadow: 0 4px 16px rgba(179, 136, 255, 0.3);
|
||
|
||
.ai-icon { font-size: 18px; }
|
||
.ai-text { font-size: 14px; font-weight: 600; }
|
||
.ai-arrow { font-size: 14px; }
|
||
}
|
||
</style>
|