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:
Yuzhiran Dev
2026-07-11 12:45:37 +08:00
parent 66ac576082
commit c1d6dd3b29
106 changed files with 17436 additions and 2311 deletions
+446
View File
@@ -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>