refactor: 项目架构重构 - 目录标准化 + 清理冗余
- 目录重命名: backend/wdkj-server/ → server/, frontend/ai-dimension/ → client/ - 删除 20+ 冗余文件(WDKJ 旧脚本、Windows 脚本、过时文档、设计稿) - 更新 package.json 元数据(移除 wdkj 命名) - 完善三级 .gitignore(根 + server + client) - 重写 README.md 和 CHANGELOG.md - 工具脚本移至 scripts/
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
<template>
|
||||
<view class="detail-page">
|
||||
<!-- 顶部导航 -->
|
||||
<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>
|
||||
Reference in New Issue
Block a user