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,366 @@
|
||||
<template>
|
||||
<view class="page-trend">
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<!-- 顶部 -->
|
||||
<view class="trend-header">
|
||||
<text class="header-title">AI 趋势</text>
|
||||
<text class="header-desc">每天精选 AI 行业最新动态</text>
|
||||
</view>
|
||||
|
||||
<!-- 分类筛选 -->
|
||||
<view class="filter-bar">
|
||||
<view
|
||||
class="filter-item"
|
||||
:class="{ active: activeFilter === idx }"
|
||||
v-for="(tag, idx) in filters"
|
||||
:key="idx"
|
||||
@click="activeFilter = idx"
|
||||
>
|
||||
<text>{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新闻列表 -->
|
||||
<view class="trend-list">
|
||||
<view v-for="(item, idx) in displayItems" :key="idx" class="trend-card" @click="goDetail(item)">
|
||||
<!-- 头部标签 -->
|
||||
<view class="card-header">
|
||||
<text class="card-tag" :class="getTagClass(item)">
|
||||
{{ getTagText(item) }}
|
||||
</text>
|
||||
<text class="card-source">{{ item.source }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<text class="card-title">{{ item.title }}</text>
|
||||
|
||||
<!-- 摘要 -->
|
||||
<text class="card-summary" v-if="item.summary">
|
||||
{{ item.summary }}
|
||||
</text>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<view class="card-footer">
|
||||
<text class="card-date">{{ formatDate(item.newsDate) }}</text>
|
||||
<view class="card-stats">
|
||||
<text class="stat-item" v-if="item.hot">🔥 {{ item.hotCount }}</text>
|
||||
<text class="stat-item">👁️ {{ item.viewCount || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="loading-state">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="displayItems.length === 0" class="empty-state">
|
||||
<text class="empty-icon">📡</text>
|
||||
<text class="empty-text">暂无趋势资讯</text>
|
||||
<text class="empty-sub">稍后再来看最新 AI 动态</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { trendApi } from '@/utils/api'
|
||||
|
||||
const filters = ref(['全部', '🚀 产品', '📄 论文', '🔧 工具', '🏢 公司'])
|
||||
const activeFilter = ref(0)
|
||||
const items = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
fetchTrends()
|
||||
})
|
||||
|
||||
const displayItems = computed(() => {
|
||||
if (activeFilter.value === 0) return items.value
|
||||
const categoryMap = { 1: 'product', 2: 'paper', 3: 'tool', 4: 'company' }
|
||||
const category = categoryMap[activeFilter.value]
|
||||
if (!category) return items.value
|
||||
return items.value.filter(item => item.category === category)
|
||||
})
|
||||
|
||||
async function fetchTrends() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await trendApi.list({ limit: 20 })
|
||||
items.value = data.list || data.items || []
|
||||
} catch (err) {
|
||||
console.error('获取趋势失败:', err)
|
||||
// 降级:本地示例
|
||||
items.value = getSampleData()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getTagText(item) {
|
||||
if (item.hot) return '🔥 热门'
|
||||
if (item.picked) return '📌 精选'
|
||||
if (item.category === 'product') return '🚀 产品'
|
||||
if (item.category === 'paper') return '📄 论文'
|
||||
if (item.category === 'tool') return '🔧 工具'
|
||||
if (item.category === 'company') return '🏢 公司'
|
||||
return '📰 资讯'
|
||||
}
|
||||
|
||||
function getTagClass(item) {
|
||||
if (item.hot) return 'hot'
|
||||
if (item.picked) return 'pick'
|
||||
if (item.category === 'product') return 'product'
|
||||
if (item.category === 'paper') return 'paper'
|
||||
if (item.category === 'tool') return 'tool'
|
||||
return ''
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
if (hours < 1) return '刚刚'
|
||||
if (hours < 24) return `${hours} 小时前`
|
||||
return `${Math.floor(hours / 24)} 天前`
|
||||
}
|
||||
|
||||
function goDetail(item) {
|
||||
// 如果有原文链接,跳转外部
|
||||
if (item.link) {
|
||||
// 微信小程序不支持直接打开外部链接,复制链接提示
|
||||
uni.setClipboardData({
|
||||
data: item.link,
|
||||
success: () => {
|
||||
uni.showModal({
|
||||
title: '打开链接',
|
||||
content: '链接已复制,请在浏览器中打开',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.setStorageSync('__news_item__', JSON.stringify(item))
|
||||
uni.navigateTo({ url: '/pages/news/news' })
|
||||
}
|
||||
|
||||
function getSampleData() {
|
||||
return [
|
||||
{
|
||||
title: 'DeepSeek 发布 R1 推理模型',
|
||||
summary: 'DeepSeek R1 在多项基准测试中超越 GPT-4o,引发全球关注',
|
||||
source: '量子位',
|
||||
newsDate: new Date(Date.now() - 2 * 3600000).toISOString(),
|
||||
category: 'product',
|
||||
hot: true,
|
||||
hotCount: 2000,
|
||||
viewCount: 15000
|
||||
},
|
||||
{
|
||||
title: 'Google Gemini 1.5 Pro 发布',
|
||||
summary: '支持 200 万 token 上下文窗口,多模态能力大幅提升',
|
||||
source: '机器之心',
|
||||
newsDate: new Date(Date.now() - 6 * 3600000).toISOString(),
|
||||
category: 'product',
|
||||
viewCount: 8000
|
||||
},
|
||||
{
|
||||
title: 'Transformer 论文作者新方向',
|
||||
summary: '从注意力机制到状态空间模型,作者分享最新研究进展',
|
||||
source: 'PaperWeekly',
|
||||
newsDate: new Date(Date.now() - 12 * 3600000).toISOString(),
|
||||
category: 'paper',
|
||||
picked: true,
|
||||
viewCount: 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-trend {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.status-bar-placeholder { height: 44px; }
|
||||
|
||||
// 头部
|
||||
.trend-header {
|
||||
padding: 10px 20px 16px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// 筛选栏
|
||||
.filter-bar {
|
||||
padding: 0 20px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: inline-block;
|
||||
padding: 6px 14px;
|
||||
margin-right: 8px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background: rgba(124, 77, 255, 0.15);
|
||||
border-color: rgba(124, 77, 255, 0.3);
|
||||
color: var(--dim1-color);
|
||||
}
|
||||
|
||||
// 列表
|
||||
.trend-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.trend-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-tag.hot {
|
||||
background: rgba(239, 83, 80, 0.12);
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
.card-tag.pick {
|
||||
background: rgba(124, 77, 255, 0.12);
|
||||
color: #b388ff;
|
||||
}
|
||||
|
||||
.card-tag.product {
|
||||
background: rgba(0, 188, 212, 0.12);
|
||||
color: #4dd0e1;
|
||||
}
|
||||
|
||||
.card-tag.paper {
|
||||
background: rgba(179, 136, 255, 0.12);
|
||||
color: #b388ff;
|
||||
}
|
||||
|
||||
.card-tag.tool {
|
||||
background: rgba(77, 182, 172, 0.12);
|
||||
color: #4db6ac;
|
||||
}
|
||||
|
||||
.card-source {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-summary {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-date {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
// 加载/空状态
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.loading-state text {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 15px;
|
||||
color: var(--text-tertiary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-sub {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 6px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user