Files
ai-dimension/client/src/pages/detail/detail.vue
T
Yuzhiran Dev af57f1a395 feat: 统一页面布局 + 问答页面未登录可预览
- 添加全局 .page-layout 工具类(统一padding/背景/min-height)
- 所有16个页面根元素加上 page-layout 类
- 问答页面移除登录拦截:未登录可看欢迎卡片和问题建议,使用时再跳登录
- 输入区未登录时显示品牌色登录引导按钮
- 补全 detail/search/progress/feedback 缺漏的 status-bar-placeholder
2026-07-11 13:09:43 +08:00

449 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>