🎉 feat: initialize 宇之然AI维度 project
- uni-app Vue3 frontend with dark glassmorphism theme - 5 AI dimensions: Origin / Development / Current / Learning / Trend - AI Chat system prompt updated from geometry to AI - 23 AI knowledge articles initialized in DB - Trend API + cron job for daily news - Product pricing (Pro ¥19.9, VIP ¥39.9) - Pinia stores + API utilities - AGENTS.md documentation
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<title>宇之然AI维度 - AI 知识探索社区</title>
|
||||
<meta name="description" content="宇之然AI维度 — 从5个维度系统理解人工智能:AI起源、发展、当前格局、学习工具、趋势跟踪。" />
|
||||
<meta name="keywords" content="宇之然AI维度,AI知识,人工智能学习,AI科普,AI趋势,大模型学习" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "ai-dimension",
|
||||
"version": "1.0.0",
|
||||
"description": "宇之然AI维度 - 微信小程序",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev:mp-weixin": "uni -p mp-weixin",
|
||||
"build:mp-weixin": "uni build -p mp-weixin",
|
||||
"dev:h5": "uni",
|
||||
"build:h5": "uni build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4060620250520001",
|
||||
"@dcloudio/uni-components": "3.0.0-4060620250520001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4060620250520001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
"@dcloudio/uni-automator": "3.0.0-4060620250520001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4060620250520001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': set this to true or false
|
||||
core-js: set this to true or false
|
||||
core-js-pure: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
vue-demi: set this to true or false
|
||||
minimumReleaseAgeExclude:
|
||||
- '@dcloudio/uni-mp-weixin@2.0.2-5010520260709001'
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<view class="app-root">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
||||
|
||||
onLaunch(() => {
|
||||
console.log('[AI维度] App launched')
|
||||
// 初始化主题
|
||||
initTheme()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
console.log('[AI维度] App shown')
|
||||
})
|
||||
|
||||
onHide(() => {
|
||||
console.log('[AI维度] App hidden')
|
||||
})
|
||||
|
||||
function initTheme() {
|
||||
// 从本地存储读取主题偏好
|
||||
const theme = uni.getStorageSync('theme') || 'dark'
|
||||
applyTheme(theme)
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.add('theme-light')
|
||||
} else {
|
||||
document.documentElement.classList.remove('theme-light')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 全局样式 */
|
||||
page {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
view {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 滚动条隐藏 */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<view
|
||||
class="dim-card-comp"
|
||||
:class="{ 'dim-full': dim.isFull }"
|
||||
:style="{ background: dim.bg }"
|
||||
@tap="$emit('tap', dim)"
|
||||
>
|
||||
<view class="dim-icon" :style="{ background: dim.iconBg, color: dim.color }">
|
||||
<text>{{ dim.icon }}</text>
|
||||
</view>
|
||||
<text class="dim-name" :style="{ color: dim.color }">{{ dim.name }}</text>
|
||||
<text class="dim-desc">{{ dim.desc }}</text>
|
||||
<text class="dim-count">
|
||||
{{ dim.isFull ? '今日已更新 8 条' : `${dim.articleCount} 篇 · ${dim.quizCount} 个问答` }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
dim: { type: Object, required: true }
|
||||
})
|
||||
defineEmits(['tap'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dim-card-comp {
|
||||
border-radius: 18px;
|
||||
padding: 20px 16px;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
.dim-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dim-icon {
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.dim-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
display: block;
|
||||
}
|
||||
.dim-desc {
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
}
|
||||
.dim-count {
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.15);
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<view
|
||||
class="news-card-comp"
|
||||
@tap="$emit('tap', item)"
|
||||
>
|
||||
<view class="news-tag" :class="item.tagClass">{{ item.tag }}</view>
|
||||
<text class="news-title">{{ item.title }}</text>
|
||||
<view class="news-meta">
|
||||
<text>{{ item.source }}</text>
|
||||
<text>{{ item.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
item: { type: Object, required: true }
|
||||
})
|
||||
defineEmits(['tap'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.news-card-comp {
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.news-tag {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.news-tag.hot { background: rgba(239,83,80,0.12); color: #ef5350; }
|
||||
.news-tag.new { background: rgba(0,188,212,0.12); color: #4dd0e1; }
|
||||
.news-tag.pick { background: rgba(124,77,255,0.12); color: #b388ff; }
|
||||
|
||||
.news-title {
|
||||
font-size: 13px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
return { app }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "宇之然AI维度",
|
||||
"appid": "wxdad62baf4ccd09e3",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"description": "AI 知识探索社区 — 从5个维度系统理解人工智能",
|
||||
"uni-app": {
|
||||
"compilerVersion": "3.0.0"
|
||||
},
|
||||
"mp-weixin": {
|
||||
"appid": "wxdad62baf4ccd09e3",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true
|
||||
},
|
||||
"usingComponents": true,
|
||||
"permission": {},
|
||||
"requiredPrivateInfos": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"pages": [
|
||||
{"path": "pages/index/index", "style": {"navigationBarTitleText": "AI 维度", "navigationStyle": "custom"}},
|
||||
{"path": "pages/dimension/dimension", "style": {"navigationBarTitleText": "", "navigationStyle": "custom"}},
|
||||
{"path": "pages/chat/chat", "style": {"navigationBarTitleText": "AI 问答", "navigationStyle": "custom"}},
|
||||
{"path": "pages/trend/trend", "style": {"navigationBarTitleText": "AI 趋势", "navigationStyle": "custom"}},
|
||||
{"path": "pages/profile/profile", "style": {"navigationBarTitleText": "我的", "navigationStyle": "custom"}}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "宇之然AI维度",
|
||||
"navigationBarBackgroundColor": "#050508",
|
||||
"backgroundColor": "#050508",
|
||||
"backgroundColorTop": "#050508",
|
||||
"backgroundColorBottom": "#050508",
|
||||
"app-plus": {
|
||||
"background": "#050508"
|
||||
}
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "rgba(255,255,255,0.25)",
|
||||
"selectedColor": "#b388ff",
|
||||
"backgroundColor": "#08080f",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{"pagePath": "pages/index/index", "text": "探索"},
|
||||
{"pagePath": "pages/chat/chat", "text": "问答"},
|
||||
{"pagePath": "pages/trend/trend", "text": "趋势"},
|
||||
{"pagePath": "pages/profile/profile", "text": "我的"}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<view class="page-chat">
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<view class="chat-header">
|
||||
<view class="chat-avatar">∞</view>
|
||||
<view class="chat-info">
|
||||
<text class="chat-name">AI 助手</text>
|
||||
<text class="chat-status">DeepSeek V4 · 在线</text>
|
||||
</view>
|
||||
<view class="chat-quota">剩余 {{ quota }} 次</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view class="msg-list" scroll-y :scroll-into-view="scrollTo">
|
||||
<view v-for="(msg, idx) in messages" :key="idx" :id="'msg-' + idx" class="chat-msg" :class="msg.role">
|
||||
<view v-if="msg.role === 'assistant'" class="mini-avatar">∞</view>
|
||||
<view class="msg-content">
|
||||
<view class="bubble">{{ msg.content }}</view>
|
||||
<text class="msg-time">{{ msg.role === 'assistant' ? 'AI 助手' : '我' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="chat-msg ai">
|
||||
<view class="mini-avatar">∞</view>
|
||||
<view class="msg-content">
|
||||
<view class="bubble loading-dots">
|
||||
<text class="dot">.</text><text class="dot">.</text><text class="dot">.</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区 -->
|
||||
<view class="chat-input-area">
|
||||
<view class="chat-input">
|
||||
<text class="add-btn">➕</text>
|
||||
<input
|
||||
v-model="question"
|
||||
class="input-field"
|
||||
placeholder="输入你的 AI 问题..."
|
||||
:disabled="loading"
|
||||
@confirm="sendMessage"
|
||||
confirm-type="send"
|
||||
/>
|
||||
<view class="send-btn" @tap="sendMessage">
|
||||
<text>➤</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { aiChatApi } from '@/utils/api'
|
||||
|
||||
const question = ref('')
|
||||
const messages = ref([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '你好!我是 AI 维度助手,你可以问我任何关于人工智能的问题,比如:\n\n💡 "Transformer 的核心原理是什么?"\n💡 "RAG 和微调有什么区别?"\n💡 "2026 年最值得关注的 AI 趋势?"'
|
||||
}
|
||||
])
|
||||
const loading = ref(false)
|
||||
const quota = ref(5)
|
||||
const scrollTo = ref('')
|
||||
|
||||
const token = computed(() => uni.getStorageSync('token'))
|
||||
|
||||
async function sendMessage() {
|
||||
const q = question.value.trim()
|
||||
if (!q || loading.value) return
|
||||
|
||||
// 先显示用户消息
|
||||
messages.value.push({ role: 'user', content: q })
|
||||
question.value = ''
|
||||
scrollToLast()
|
||||
|
||||
// 检查登录
|
||||
if (!token.value) {
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: '请先登录后使用 AI 问答功能'
|
||||
})
|
||||
scrollToLast()
|
||||
return
|
||||
}
|
||||
|
||||
// 调 AI 接口
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await aiChatApi.ask(q, 'general', token.value)
|
||||
messages.value.push({ role: 'assistant', content: data.answer })
|
||||
quota.value = data.remainingQuota
|
||||
} catch (e) {
|
||||
// 降级:本地模拟回复
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: `关于「${q}」的问题,这是一个很好的切入点!建议你从对应的 AI 维度开始系统学习。\n\n💡 试试搜索知识库中的相关文章。`
|
||||
})
|
||||
if (quota.value > 0) quota.value--
|
||||
} finally {
|
||||
loading.value = false
|
||||
scrollToLast()
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToLast() {
|
||||
setTimeout(() => {
|
||||
scrollTo.value = 'msg-' + (messages.value.length - 1)
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-chat {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 20px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.status-bar-placeholder { height: 44px; }
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0 20px;
|
||||
}
|
||||
|
||||
.chat-avatar, .mini-avatar {
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, rgba(124,77,255,0.3), rgba(0,188,212,0.2));
|
||||
border: 1px solid rgba(124,77,255,0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mini-avatar {
|
||||
width: 32px; height: 32px;
|
||||
font-size: 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.chat-info { flex: 1; }
|
||||
.chat-name {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
.chat-status {
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chat-quota {
|
||||
background: rgba(124,77,255,0.1);
|
||||
border: 1px solid rgba(124,77,255,0.15);
|
||||
border-radius: 20px;
|
||||
padding: 6px 14px;
|
||||
color: #b388ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.msg-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.chat-msg.user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.msg-content {
|
||||
max-width: 82%;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.chat-msg.ai .bubble {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
color: rgba(255,255,255,0.8);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.chat-msg.user .bubble {
|
||||
background: linear-gradient(135deg, rgba(124,77,255,0.3), rgba(124,77,255,0.15));
|
||||
border: 1px solid rgba(124,77,255,0.15);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.msg-time {
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.15);
|
||||
padding-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
.chat-msg.ai .msg-time { padding-left: 0; }
|
||||
.chat-msg.user .msg-time { text-align: right; }
|
||||
|
||||
.loading-dots { display: flex; gap: 2px; }
|
||||
.dot {
|
||||
animation: blink 1.4s infinite;
|
||||
font-size: 24px;
|
||||
line-height: 0.5;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
.dot:nth-child(2) { animation-delay: 0.2s; }
|
||||
.dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes blink {
|
||||
0%, 20% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 12px 0 30px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 14px;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.add-btn { color: rgba(255,255,255,0.2); font-size: 18px; }
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.8);
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #7c4dff, #b388ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 20px rgba(124,77,255,0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<view class="page-dim">
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<view class="back-btn" @tap="goBack">
|
||||
<text>← 返回</text>
|
||||
</view>
|
||||
|
||||
<!-- 维度横幅 -->
|
||||
<view class="dim-banner" :style="{ borderColor: currentDim?.border }">
|
||||
<text class="dim-banner-icon">{{ currentDim?.icon }}</text>
|
||||
<text class="dim-banner-title">{{ currentDim?.name }}</text>
|
||||
<text class="dim-banner-desc">{{ bannerDesc }}</text>
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
|
||||
</view>
|
||||
<text class="progress-label">探索进度 {{ progress }}% · 已读 {{ readCount }}/{{ totalCount }} 篇</text>
|
||||
</view>
|
||||
|
||||
<!-- 知识列表 -->
|
||||
<view v-for="item in knowledgeList" :key="item._id" class="knowledge-item" @tap="goDetail(item)">
|
||||
<view class="k-icon" :style="getItemStyle(item)">
|
||||
<text>{{ item.icon || '📄' }}</text>
|
||||
</view>
|
||||
<view class="k-info">
|
||||
<text class="k-title">{{ item.title }}</text>
|
||||
<text class="k-desc">{{ item.desc }}</text>
|
||||
<view class="k-meta">
|
||||
<text>📖 {{ item.readTime || 5 }} 分钟</text>
|
||||
<text v-if="item.collected">⭐ 已收藏</text>
|
||||
<text v-else>🔖 {{ item.collectCount || 0 }} 人收藏</text>
|
||||
<text class="k-badge" :class="item.isPremium ? 'pro' : 'free'">
|
||||
{{ item.isPremium ? 'Pro' : '免费' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="loading-state">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getDimension } from '@/utils/dimensions'
|
||||
import { knowledgeApi } from '@/utils/api'
|
||||
|
||||
const dimId = ref(1)
|
||||
const currentDim = computed(() => getDimension(dimId.value))
|
||||
const knowledgeList = ref([])
|
||||
const loading = ref(false)
|
||||
const progress = ref(35)
|
||||
const readCount = ref(4)
|
||||
const totalCount = ref(12)
|
||||
|
||||
const bannerDesc = computed(() => {
|
||||
const descs = {
|
||||
1: '从 1950 年图灵测试到 2026 年通用人工智能,追溯 70 年演进之路',
|
||||
2: '从深度学习到 Transformer,从 MoE 到多模态,理解每一次技术跃迁',
|
||||
3: '大模型格局、Agent 生态、具身智能——2026 年 AI 世界全景图',
|
||||
4: '提示词工程、RAG 系统、模型微调、工具链——从入门到上手',
|
||||
5: '每日追踪 AI 前沿动态,不错过每一个重要时刻'
|
||||
}
|
||||
return descs[dimId.value] || '探索人工智能的精彩世界'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const page = pages[pages.length - 1]
|
||||
if (page.options?.id) {
|
||||
dimId.value = parseInt(page.options.id)
|
||||
}
|
||||
loadKnowledge()
|
||||
})
|
||||
|
||||
async function loadKnowledge() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await knowledgeApi.list({ dim: dimId.value })
|
||||
knowledgeList.value = data.map(item => ({
|
||||
...item,
|
||||
icon: getDimIcon(item.dim),
|
||||
desc: item.content?.substring(0, 40) + '...' || '',
|
||||
readTime: Math.ceil((item.content?.length || 300) / 200),
|
||||
isPremium: item.isPremium,
|
||||
collected: false,
|
||||
collectCount: item.collectionCount
|
||||
}))
|
||||
} catch (e) {
|
||||
// 用示例数据填充
|
||||
knowledgeList.value = getSampleData(dimId.value)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getItemStyle(item) {
|
||||
const colors = [
|
||||
{ bg: 'rgba(124,77,255,0.1)', color: '#b388ff' },
|
||||
{ bg: 'rgba(0,188,212,0.1)', color: '#4dd0e1' },
|
||||
{ bg: 'rgba(0,200,150,0.1)', color: '#4db6ac' },
|
||||
{ bg: 'rgba(255,193,7,0.1)', color: '#ffd54f' },
|
||||
{ bg: 'rgba(239,83,80,0.1)', color: '#ef5350' }
|
||||
]
|
||||
const c = colors[(item.dim || 1) - 1] || colors[0]
|
||||
return {
|
||||
background: c.bg,
|
||||
color: c.color,
|
||||
borderColor: c.bg
|
||||
}
|
||||
}
|
||||
|
||||
function getDimIcon(dim) {
|
||||
const icons = ['📜', '🏛', '♟', '🧠', '⚡']
|
||||
return icons[(dim || 1) - 1]
|
||||
}
|
||||
|
||||
function getSampleData(dim) {
|
||||
const samples = {
|
||||
1: [
|
||||
{ title: '图灵测试:人工智能的哲学起点', content: '1950 年,艾伦·图灵提出"机器能思考吗?"' },
|
||||
{ title: '达特茅斯会议:AI 的诞生时刻', content: '1956 年,一群科学家正式命名了"人工智能"' },
|
||||
{ title: '深蓝 vs 卡斯帕罗夫', content: '1997 年,IBM 深蓝首次击败国际象棋世界冠军' },
|
||||
],
|
||||
2: [
|
||||
{ title: '深度学习革命', content: '从神经网络到深度学习的演进之路' },
|
||||
{ title: 'Transformer 架构详解', content: '2017 年 Google 提出的革命性架构' },
|
||||
],
|
||||
3: [
|
||||
{ title: '2026 年大模型格局', content: 'GPT、Claude、Gemini、DeepSeek 全面对比' },
|
||||
{ title: 'Agent 智能体生态', content: '从 Chatbot 到自主 Agent 的进化' },
|
||||
],
|
||||
4: [
|
||||
{ title: '提示词工程入门', content: '如何写出高质量的 prompt' },
|
||||
{ title: 'RAG 系统搭建指南', content: '检索增强生成从零到一' },
|
||||
],
|
||||
5: [
|
||||
{ title: 'DeepSeek 自研推理芯片', content: '降低对英伟达依赖的重要一步' },
|
||||
{ title: 'WAIC 2026 即将开幕', content: '7 月 17 日上海,1100+ 企业参展' },
|
||||
]
|
||||
}
|
||||
const data = samples[dim] || samples[1]
|
||||
return data.map((item, i) => ({
|
||||
...item,
|
||||
_id: `sample-${dim}-${i}`,
|
||||
dim,
|
||||
icon: getDimIcon(dim),
|
||||
readTime: 5 + i * 2,
|
||||
isPremium: i >= 3,
|
||||
collected: i === 0,
|
||||
collectionCount: 10 - i * 2
|
||||
}))
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/dimension/detail?id=${item._id}`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-dim {
|
||||
min-height: 100vh;
|
||||
padding: 0 20px 40px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.status-bar-placeholder { height: 44px; }
|
||||
|
||||
.back-btn {
|
||||
color: rgba(255,255,255,0.4);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 0 14px;
|
||||
}
|
||||
|
||||
.dim-banner {
|
||||
border-radius: 18px;
|
||||
padding: 24px 20px;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(135deg, rgba(124,77,255,0.1), rgba(0,0,0,0.3));
|
||||
border: 1px solid rgba(124,77,255,0.12);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dim-banner-icon { font-size: 28px; display: block; margin-bottom: 8px; }
|
||||
.dim-banner-title { font-size: 22px; color: #fff; display: block; margin-bottom: 6px; }
|
||||
.dim-banner-desc {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
line-height: 1.6;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
height: 3px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-radius: 2px;
|
||||
margin-top: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #b388ff, #7c4dff);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.progress-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.2);
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.knowledge-item {
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
margin-bottom: 10px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.k-icon {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
}
|
||||
.k-info { flex: 1; }
|
||||
.k-title {
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.k-desc {
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
.k-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.15);
|
||||
}
|
||||
.k-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.k-badge.free { background: rgba(76,175,80,0.12); color: #66bb6a; }
|
||||
.k-badge.pro { background: rgba(255,152,0,0.12); color: #ffa726; }
|
||||
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: rgba(255,255,255,0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<view class="page-home">
|
||||
<!-- 状态栏占位 -->
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<view class="header">
|
||||
<view class="brand">
|
||||
<view class="brand-icon">✦</view>
|
||||
<view class="brand-text">
|
||||
<text class="title">AI 维度</text>
|
||||
<text class="subtitle">宇之然 · 探索</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
<view class="icon-btn" @tap="onNotify">
|
||||
<text>🔔</text>
|
||||
</view>
|
||||
<view class="icon-btn" @tap="goProfile">
|
||||
<text>👤</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 问候语 -->
|
||||
<view class="greeting">
|
||||
<text class="greeting-title">探索 </text>
|
||||
<text class="greeting-highlight">人工智能</text>
|
||||
<text class="greeting-desc">从 5 个维度,系统理解 AI 的过去、现在与未来</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar" @tap="onSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
<text class="search-placeholder">搜索 AI 知识...</text>
|
||||
</view>
|
||||
|
||||
<!-- 五维度网格 -->
|
||||
<view class="section-header">
|
||||
<text class="section-title">✦ 五个维度</text>
|
||||
</view>
|
||||
<view class="dim-grid">
|
||||
<view
|
||||
v-for="dim in dimensions"
|
||||
:key="dim.id"
|
||||
class="dim-card"
|
||||
:class="{ 'dim-full': dim.isFull }"
|
||||
:style="{ background: dim.bg }"
|
||||
@tap="goDimension(dim)"
|
||||
>
|
||||
<view class="dim-icon" :style="{ background: dim.iconBg, color: dim.color }">
|
||||
<text>{{ dim.icon }}</text>
|
||||
</view>
|
||||
<text class="dim-name" :style="{ color: dim.color }">{{ dim.name }}</text>
|
||||
<text class="dim-desc">{{ dim.desc }}</text>
|
||||
<text class="dim-count">
|
||||
{{ dim.isFull ? '今日已更新 8 条' : `${dim.articleCount} 篇 · ${dim.quizCount} 个问答` }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日快讯 -->
|
||||
<view class="section-header">
|
||||
<text class="section-title">📡 今日快讯</text>
|
||||
<text class="section-more" @tap="goTrend">查看全部 →</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="(item, idx) in newsList"
|
||||
:key="idx"
|
||||
class="news-card"
|
||||
@tap="goNewsDetail(item)"
|
||||
>
|
||||
<view class="news-tag" :class="item.tagClass">{{ item.tag }}</view>
|
||||
<text class="news-title">{{ item.title }}</text>
|
||||
<view class="news-meta">
|
||||
<text>{{ item.source }}</text>
|
||||
<text>{{ item.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { DIMENSIONS } from '@/utils/dimensions'
|
||||
import { trendApi } from '@/utils/api'
|
||||
|
||||
const dimensions = ref(DIMENSIONS)
|
||||
|
||||
const newsList = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
fetchNews()
|
||||
})
|
||||
|
||||
async function fetchNews() {
|
||||
try {
|
||||
const data = await trendApi.list({ limit: 5 })
|
||||
newsList.value = data.list.map(item => ({
|
||||
tag: item.hot ? '🔥 热门' : item.picked ? '📌 精选' : '🚀 最新',
|
||||
tagClass: item.hot ? 'hot' : item.picked ? 'pick' : 'new',
|
||||
title: item.title,
|
||||
source: item.source,
|
||||
time: formatTime(item.newsDate),
|
||||
_id: item._id
|
||||
}))
|
||||
} catch (e) {
|
||||
// 降级:本地数据
|
||||
newsList.value = [
|
||||
{ tag: '🔥 热门', tagClass: 'hot', title: 'DeepSeek 被曝自研推理芯片', source: '量子位', time: '2 小时前' },
|
||||
{ tag: '🚀 最新', tagClass: 'new', title: '蚂蚁灵波开源 LingBot-World 2.0', source: '量子位', time: '4 小时前' },
|
||||
{ tag: '📌 精选', tagClass: 'pick', title: '阿里获 ACL 2026 最佳资源论文奖', source: '机器之心', time: '6 小时前' }
|
||||
]
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(dateStr) {
|
||||
const diff = Date.now() - new Date(dateStr).getTime()
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
if (hours < 1) return '刚刚'
|
||||
if (hours < 24) return `${hours} 小时前`
|
||||
return `${Math.floor(hours / 24)} 天前`
|
||||
}
|
||||
|
||||
function goDimension(dim) {
|
||||
uni.navigateTo({ url: `/pages/dimension/dimension?id=${dim.id}&name=${dim.name}` })
|
||||
}
|
||||
|
||||
function goTrend() {
|
||||
uni.switchTab({ url: '/pages/trend/trend' })
|
||||
}
|
||||
|
||||
function goProfile() {
|
||||
uni.switchTab({ url: '/pages/profile/profile' })
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
uni.showToast({ title: '搜索功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function onNotify() {
|
||||
uni.showToast({ title: '暂无新消息', icon: 'none' })
|
||||
}
|
||||
|
||||
function goNewsDetail(item) {
|
||||
uni.switchTab({ url: '/pages/trend/trend' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-home {
|
||||
min-height: 100vh;
|
||||
padding: 0 20px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.status-bar-placeholder {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 0 20px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, rgba(124,77,255,0.3), rgba(0,188,212,0.2));
|
||||
border: 1px solid rgba(124,77,255,0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 0 20px rgba(124,77,255,0.1);
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.brand-text .title {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.brand-text .subtitle {
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.icon-btn {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.greeting-title {
|
||||
font-size: 26px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
.greeting-highlight {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #b388ff, #7c4dff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.greeting-desc {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 14px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
.search-icon { color: rgba(255,255,255,0.2); }
|
||||
.search-placeholder { color: rgba(255,255,255,0.2); font-size: 14px; }
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 17px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.section-more {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
.dim-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.dim-card {
|
||||
border-radius: 18px;
|
||||
padding: 20px 16px;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
.dim-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dim-icon {
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.dim-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
display: block;
|
||||
}
|
||||
.dim-desc {
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
}
|
||||
.dim-count {
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.15);
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.news-tag {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.news-tag.hot {
|
||||
background: rgba(239,83,80,0.12);
|
||||
color: #ef5350;
|
||||
}
|
||||
.news-tag.new {
|
||||
background: rgba(0,188,212,0.12);
|
||||
color: #4dd0e1;
|
||||
}
|
||||
.news-tag.pick {
|
||||
background: rgba(124,77,255,0.12);
|
||||
color: #b388ff;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 13px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.bottom-spacer {
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<view class="page-profile">
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<view class="profile-card">
|
||||
<view class="profile-avatar">
|
||||
<text>{{ userInfo?.avatarUrl ? '🖼' : '🧑' }}</text>
|
||||
</view>
|
||||
<text class="profile-name">{{ userInfo?.nickName || '星辰旅者' }}</text>
|
||||
<text class="profile-level">Lv.5 · 维度学徒 · 已探索 12 篇文章</text>
|
||||
</view>
|
||||
|
||||
<!-- 订阅卡片 -->
|
||||
<view class="plan-card" @tap="upgradePlan">
|
||||
<view class="plan-info">
|
||||
<text class="plan-title">✦ 免费版</text>
|
||||
<text class="plan-desc">{{ isLoggedIn ? '每日 5 次 AI 问答 · 解锁全部知识' : '登录后开始你的 AI 探索之旅' }}</text>
|
||||
</view>
|
||||
<view class="plan-btn">
|
||||
<text>{{ isLoggedIn ? '升级 Pro' : '登录/注册' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 菜单 -->
|
||||
<view class="menu-item" @tap="goFavorites">
|
||||
<view class="m-icon" :style="{ background: 'rgba(124,77,255,0.1)', color: '#b388ff' }">📚</view>
|
||||
<text class="m-label">我的收藏</text>
|
||||
<text class="m-arrow">→</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @tap="goProgress">
|
||||
<view class="m-icon" :style="{ background: 'rgba(0,188,212,0.1)', color: '#4dd0e1' }">📊</view>
|
||||
<text class="m-label">学习进度</text>
|
||||
<text class="m-arrow">→</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @tap="goAchievements">
|
||||
<view class="m-icon" :style="{ background: 'rgba(0,200,150,0.1)', color: '#4db6ac' }">🏆</view>
|
||||
<text class="m-label">成就</text>
|
||||
<text class="m-badge">3 项未解锁</text>
|
||||
<text class="m-arrow">→</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @tap="checkin">
|
||||
<view class="m-icon" :style="{ background: 'rgba(255,193,7,0.1)', color: '#ffd54f' }">📅</view>
|
||||
<text class="m-label">每日签到</text>
|
||||
<text class="m-arrow">→</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @tap="goSettings">
|
||||
<view class="m-icon" :style="{ background: 'rgba(239,83,80,0.1)', color: '#ef5350' }">⚙️</view>
|
||||
<text class="m-label">设置</text>
|
||||
<text class="m-arrow">→</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录/退出 -->
|
||||
<view v-if="isLoggedIn" class="logout-btn" @tap="handleLogout">
|
||||
<text>退出登录</text>
|
||||
</view>
|
||||
<view v-else class="logout-btn" @tap="handleLogin">
|
||||
<text>微信一键登录</text>
|
||||
</view>
|
||||
|
||||
<view class="version">宇之然AI维度 v1.0.0</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const userInfo = ref(null)
|
||||
const isLoggedIn = computed(() => !!uni.getStorageSync('token'))
|
||||
|
||||
function upgradePlan() {
|
||||
if (!isLoggedIn.value) {
|
||||
handleLogin()
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: '订阅功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (loginRes) => {
|
||||
const code = loginRes.code
|
||||
uni.getUserProfile({
|
||||
desc: '用于完善用户资料',
|
||||
success: (profileRes) => {
|
||||
const userInfo = profileRes.userInfo
|
||||
// 调用登录 API
|
||||
console.log('登录 code:', code, 'userInfo:', userInfo)
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: '登录失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: '微信登录失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.removeStorageSync('token')
|
||||
userInfo.value = null
|
||||
uni.showToast({ title: '已退出', icon: 'success' })
|
||||
}
|
||||
|
||||
function goFavorites() { uni.showToast({ title: '收藏功能开发中', icon: 'none' }) }
|
||||
function goProgress() { uni.showToast({ title: '学习进度开发中', icon: 'none' }) }
|
||||
function goAchievements() { uni.showToast({ title: '成就系统开发中', icon: 'none' }) }
|
||||
function checkin() { uni.showToast({ title: '签到功能开发中', icon: 'none' }) }
|
||||
function goSettings() { uni.showToast({ title: '设置功能开发中', icon: 'none' }) }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-profile {
|
||||
min-height: 100vh;
|
||||
padding: 0 20px 40px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.status-bar-placeholder { height: 44px; }
|
||||
|
||||
.profile-card {
|
||||
text-align: center;
|
||||
padding: 20px 0 24px;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 72px; height: 72px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(124,77,255,0.3), rgba(0,188,212,0.15));
|
||||
border: 2px solid rgba(124,77,255,0.2);
|
||||
margin: 0 auto 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
box-shadow: 0 0 40px rgba(124,77,255,0.1);
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
.profile-level {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
border-radius: 18px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, rgba(124,77,255,0.08), rgba(0,0,0,0.2));
|
||||
border: 1px solid rgba(124,77,255,0.12);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plan-info { flex: 1; }
|
||||
.plan-title {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
.plan-desc {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.plan-btn {
|
||||
background: linear-gradient(135deg, #7c4dff, #b388ff);
|
||||
padding: 10px 22px;
|
||||
border-radius: 20px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 0 20px rgba(124,77,255,0.2);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.m-icon {
|
||||
width: 34px; height: 34px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.m-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.m-arrow {
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
.m-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(239,83,80,0.12);
|
||||
color: #ef5350;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
text-align: center;
|
||||
padding: 18px 0;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.version {
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.1);
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<view class="page-trend">
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<view class="section-header" style="margin-bottom: 4px;">
|
||||
<text class="section-title">🔥 AI 趋势</text>
|
||||
<text class="section-more">订阅</text>
|
||||
</view>
|
||||
<text class="trend-desc">每日追踪 AI 前沿动态,不错过每一个重要时刻</text>
|
||||
|
||||
<!-- 分类筛选 -->
|
||||
<scroll-view class="filter-bar" scroll-x>
|
||||
<view
|
||||
v-for="(tag, idx) in tags"
|
||||
:key="idx"
|
||||
class="filter-tag"
|
||||
:class="{ active: currentTag === idx }"
|
||||
@tap="currentTag = idx"
|
||||
>
|
||||
<text>{{ tag }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 趋势卡片 -->
|
||||
<view
|
||||
v-for="(item, idx) in filteredTrends"
|
||||
:key="idx"
|
||||
class="trend-card"
|
||||
:style="{ borderColor: item.borderColor }"
|
||||
>
|
||||
<view class="trend-marker" :style="{ background: item.markerColor }"></view>
|
||||
<view class="trend-content">
|
||||
<text class="trend-tag" :style="{ background: item.tagBg, color: item.tagColor }">{{ item.tag }}</text>
|
||||
<text class="trend-title">{{ item.title }}</text>
|
||||
<text class="trend-source">{{ item.source }} · {{ item.date }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { trendApi } from '@/utils/api'
|
||||
|
||||
const tags = ['全部', '🚀 产品', '📄 论文', '🔧 工具', '🏢 公司']
|
||||
const currentTag = ref(0)
|
||||
|
||||
const trends = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
fetchTrends()
|
||||
})
|
||||
|
||||
async function fetchTrends() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await trendApi.list({ limit: 50 })
|
||||
trends.value = data.list.map(item => ({
|
||||
tag: item.hot ? '🔥 热门' : item.picked ? '📌 精选' : getCategoryTag(item.category),
|
||||
tagBg: getTagBg(item.category),
|
||||
tagColor: getTagColor(item.category),
|
||||
borderColor: `${getTagBg(item.category).replace('0.12', '0.08')}`,
|
||||
markerColor: getMarkerGradient(item.category),
|
||||
title: item.title,
|
||||
source: item.source,
|
||||
date: formatDate(item.newsDate),
|
||||
category: item.category,
|
||||
_id: item._id
|
||||
}))
|
||||
} catch (e) {
|
||||
// Fallback data
|
||||
trends.value = getFallbackData()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getCategoryTag(cat) {
|
||||
const map = { '产品': '🚀 产品', '论文': '📄 论文', '工具': '🔧 工具', '公司': '🏢 公司', '政策': '🏛 政策' }
|
||||
return map[cat] || '📌 其他'
|
||||
}
|
||||
|
||||
const tagStyleMap = {
|
||||
'产品': { bg: 'rgba(0,188,212,0.12)', color: '#4dd0e1', gradient: 'linear-gradient(180deg, #4dd0e1, #80deea)' },
|
||||
'论文': { bg: 'rgba(0,200,150,0.12)', color: '#4db6ac', gradient: 'linear-gradient(180deg, #4db6ac, #80cbc4)' },
|
||||
'工具': { bg: 'rgba(255,193,7,0.12)', color: '#ffd54f', gradient: 'linear-gradient(180deg, #ffd54f, #ffe082)' },
|
||||
'公司': { bg: 'rgba(239,83,80,0.12)', color: '#ef5350', gradient: 'linear-gradient(180deg, #ef5350, #ff8a80)' },
|
||||
'其他': { bg: 'rgba(124,77,255,0.12)', color: '#b388ff', gradient: 'linear-gradient(180deg, #b388ff, #d1c4e9)' }
|
||||
}
|
||||
|
||||
function getTagBg(cat) { return tagStyleMap[cat]?.bg || tagStyleMap['其他'].bg }
|
||||
function getTagColor(cat) { return tagStyleMap[cat]?.color || tagStyleMap['其他'].color }
|
||||
function getMarkerGradient(cat) { return tagStyleMap[cat]?.gradient || tagStyleMap['其他'].gradient }
|
||||
|
||||
function formatDate(dateStr) {
|
||||
return new Date(dateStr).toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
function getFallbackData() {
|
||||
return [
|
||||
{ tag: '🔥 热门', tagBg: 'rgba(239,83,80,0.12)', tagColor: '#ef5350', borderColor: 'rgba(239,83,80,0.08)', markerColor: 'linear-gradient(180deg, #ef5350, #ff8a80)', title: 'DeepSeek 秘密造芯', source: '路透社', date: '2026-07-10', category: '公司' },
|
||||
{ tag: '🚀 产品', tagBg: 'rgba(0,188,212,0.12)', tagColor: '#4dd0e1', borderColor: 'rgba(0,188,212,0.08)', markerColor: 'linear-gradient(180deg, #4dd0e1, #80deea)', title: '蚂蚁灵波开源 LingBot-World 2.0', source: '量子位', date: '2026-07-09', category: '产品' },
|
||||
{ tag: '📄 论文', tagBg: 'rgba(0,200,150,0.12)', tagColor: '#4db6ac', borderColor: 'rgba(0,200,150,0.08)', markerColor: 'linear-gradient(180deg, #4db6ac, #80cbc4)', title: '阿里获 ACL 2026 最佳资源论文奖', source: '机器之心', date: '2026-07-09', category: '论文' },
|
||||
]
|
||||
}
|
||||
|
||||
const filteredTrends = computed(() => {
|
||||
if (currentTag.value === 0) return trends.value
|
||||
const category = tags[currentTag.value].replace(/^[^\s]+\s/, '')
|
||||
return trends.value.filter(t => t.category === category)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-trend {
|
||||
min-height: 100vh;
|
||||
padding: 0 20px 40px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.status-bar-placeholder { height: 44px; }
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 17px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.section-more {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
.trend-desc {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 18px;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
display: inline-block;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
}
|
||||
.filter-tag.active {
|
||||
background: rgba(124,77,255,0.2);
|
||||
color: #b388ff;
|
||||
border-color: rgba(124,77,255,0.2);
|
||||
}
|
||||
.filter-tag:not(.active) {
|
||||
background: rgba(255,255,255,0.03);
|
||||
color: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.trend-card {
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.trend-marker {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 14px;
|
||||
bottom: 14px;
|
||||
width: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.trend-content {
|
||||
padding-left: 14px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.trend-tag {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.trend-title {
|
||||
font-size: 13px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.trend-source {
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.2);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bottom-spacer { height: 40px; }
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<rect x="6" y="6" width="16" height="16" rx="3" fill="#b388ff"/>
|
||||
<rect x="26" y="6" width="16" height="16" rx="3" fill="#b388ff"/>
|
||||
<rect x="6" y="26" width="16" height="16" rx="3" fill="#b388ff"/>
|
||||
<rect x="26" y="26" width="16" height="16" rx="3" fill="#b388ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<rect x="6" y="6" width="16" height="16" rx="3" fill="#444"/>
|
||||
<rect x="26" y="6" width="16" height="16" rx="3" fill="#444"/>
|
||||
<rect x="6" y="26" width="16" height="16" rx="3" fill="#444"/>
|
||||
<rect x="26" y="26" width="16" height="16" rx="3" fill="#444"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 339 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<path d="M24 4L28 16L40 18L30 26L34 40L24 32L14 40L18 26L8 18L20 16L24 4Z" fill="#b388ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 173 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<path d="M24 4L28 16L40 18L30 26L34 40L24 32L14 40L18 26L8 18L20 16L24 4Z" fill="#444"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 170 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<circle cx="24" cy="16" r="8" fill="#b388ff"/>
|
||||
<path d="M10 44C10 32 16 26 24 26C32 26 38 32 38 44H10Z" fill="#b388ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 204 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<circle cx="24" cy="16" r="8" fill="#444"/>
|
||||
<path d="M10 44C10 32 16 26 24 26C32 26 38 32 38 44H10Z" fill="#444"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 198 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<path d="M8 36L24 8L40 36H8Z" fill="#b388ff"/>
|
||||
<rect x="22" y="28" width="4" height="8" rx="1" fill="#7c4dff"/>
|
||||
<circle cx="24" cy="22" r="2" fill="#7c4dff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 244 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<path d="M8 36L24 8L40 36H8Z" fill="#444"/>
|
||||
<rect x="22" y="28" width="4" height="8" rx="1" fill="#222"/>
|
||||
<circle cx="24" cy="22" r="2" fill="#222"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 235 B |
@@ -0,0 +1,72 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { authApi, knowledgeApi, aiChatApi } from '@/utils/api'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const token = ref(uni.getStorageSync('token') || '')
|
||||
const userInfo = ref(null)
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
|
||||
async function login(code, userInfoData) {
|
||||
const data = await authApi.login(code, userInfoData)
|
||||
token.value = data.token
|
||||
userInfo.value = data.userInfo
|
||||
uni.setStorageSync('token', data.token)
|
||||
return data
|
||||
}
|
||||
|
||||
function logout() {
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
uni.removeStorageSync('token')
|
||||
}
|
||||
|
||||
return { token, userInfo, isLoggedIn, login, logout }
|
||||
})
|
||||
|
||||
export const useKnowledgeStore = defineStore('knowledge', () => {
|
||||
const list = ref([])
|
||||
const currentDetail = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchList(params = {}) {
|
||||
loading.value = true
|
||||
try {
|
||||
list.value = await knowledgeApi.list(params)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDetail(id) {
|
||||
currentDetail.value = await knowledgeApi.detail(id)
|
||||
}
|
||||
|
||||
return { list, currentDetail, loading, fetchList, fetchDetail }
|
||||
})
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const messages = ref([])
|
||||
const quota = ref({ remaining: 0, total: 0 })
|
||||
const loading = ref(false)
|
||||
|
||||
async function ask(question, token) {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await aiChatApi.ask(question, 'general', token)
|
||||
messages.value.push({ role: 'user', content: question })
|
||||
messages.value.push({ role: 'assistant', content: data.answer })
|
||||
quota.value.remaining = data.remainingQuota
|
||||
return data
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchQuota(token) {
|
||||
const data = await aiChatApi.quota(token)
|
||||
quota.value = data
|
||||
}
|
||||
|
||||
return { messages, quota, loading, ask, fetchQuota }
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
# 宇之然AI维度 — 主题系统指南
|
||||
|
||||
## 架构
|
||||
|
||||
所有主题变量定义在 `src/styles/theme.scss` 中,通过 CSS 自定义属性 (CSS Custom Properties) 实现。
|
||||
|
||||
## 核心变量层级
|
||||
|
||||
```
|
||||
CSS 自定义属性 (theme.scss)
|
||||
├── 基础色 (bg-primary, text-primary 等)
|
||||
├── 玻璃拟态 (glass-bg, glass-border 等)
|
||||
├── 品牌色 (color-brand, color-brand-light 等)
|
||||
├── 维度色 (dim1~dim5: color, bg, icon-bg, border, glow)
|
||||
├── 功能色 (free, pro, hot, new, pick)
|
||||
└── 布局 (radius, spacing, blur, transition)
|
||||
```
|
||||
|
||||
## 一键换肤
|
||||
|
||||
### 添加新主题
|
||||
|
||||
在 `theme.scss` 底部添加:
|
||||
|
||||
```scss
|
||||
.theme-xxx {
|
||||
// 只覆盖需要变化的变量
|
||||
--color-brand: #...;
|
||||
--color-brand-light: #...;
|
||||
--bg-primary: #...;
|
||||
// 维度颜色也可以单独覆盖
|
||||
--dim1-color: #...;
|
||||
}
|
||||
```
|
||||
|
||||
### 切换主题
|
||||
|
||||
```js
|
||||
// 在 App.vue 或任意页面中:
|
||||
document.documentElement.className = 'theme-xxx'
|
||||
```
|
||||
|
||||
### 维度卡颜色映射
|
||||
|
||||
每个维度通过 `utils/dimensions.js` 中的 `DIMENSIONS` 数组配置,引用 CSS 变量:
|
||||
|
||||
```js
|
||||
color: 'var(--dim1-color)',
|
||||
bg: 'var(--dim1-bg)',
|
||||
iconBg: 'var(--dim1-icon-bg)',
|
||||
```
|
||||
|
||||
## 现有主题
|
||||
|
||||
| 主题类名 | 说明 |
|
||||
|---------|------|
|
||||
| (默认) | 暗色宇宙深空 |
|
||||
| `.theme-light` | 浅色模式(预留) |
|
||||
| `.theme-christmas` | 圣诞主题(示例) |
|
||||
|
||||
## 修改指南
|
||||
|
||||
### 只改颜色
|
||||
编辑 `theme.scss` 中的 CSS 变量即可,所有组件自动生效。
|
||||
|
||||
### 改维度配置
|
||||
编辑 `utils/dimensions.js` 中的 `DIMENSIONS` 数组。
|
||||
|
||||
### 改布局间距
|
||||
编辑 `theme.scss` 中的 `--spacing-*` 和 `--radius-*` 变量。
|
||||
@@ -0,0 +1,229 @@
|
||||
/* ============================================
|
||||
宇之然AI维度 — 主题系统
|
||||
使用 CSS 自定义属性,一键切换主题
|
||||
============================================ */
|
||||
|
||||
/* ---- 主题色板 ----
|
||||
🌌 宇宙深空背景
|
||||
🟣 紫色 — 品牌色、AI 起源
|
||||
🔵 青色 — AI 发展、科技
|
||||
🟢 青绿 — AI 当前
|
||||
🟡 金色 — AI 学习
|
||||
🔴 珊瑚 — AI 趋势
|
||||
============================================ */
|
||||
|
||||
:root,
|
||||
page {
|
||||
/* === 基础色 === */
|
||||
--bg-primary: #050508;
|
||||
--bg-secondary: #08080f;
|
||||
--bg-tertiary: #0f0a1a;
|
||||
|
||||
/* === 玻璃拟态 === */
|
||||
--glass-bg: rgba(255, 255, 255, 0.03);
|
||||
--glass-border: rgba(255, 255, 255, 0.06);
|
||||
--glass-hover: rgba(255, 255, 255, 0.05);
|
||||
--glass-hover-border: rgba(255, 255, 255, 0.12);
|
||||
--glass-strong: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* === 文字 === */
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.8);
|
||||
--text-tertiary: rgba(255, 255, 255, 0.4);
|
||||
--text-muted: rgba(255, 255, 255, 0.2);
|
||||
--text-dim: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* === 品牌色 — 紫色系 === */
|
||||
--color-brand: #7c4dff;
|
||||
--color-brand-light: #b388ff;
|
||||
--color-brand-dark: #651fff;
|
||||
--color-brand-bg: rgba(124, 77, 255, 0.1);
|
||||
--color-brand-glow: rgba(124, 77, 255, 0.2);
|
||||
|
||||
/* === 维度色系 === */
|
||||
|
||||
/* dim1: AI 起源 — 紫色 */
|
||||
--dim1-color: #b388ff;
|
||||
--dim1-bg: rgba(124, 77, 255, 0.08);
|
||||
--dim1-icon-bg: rgba(124, 77, 255, 0.25);
|
||||
--dim1-border: rgba(124, 77, 255, 0.15);
|
||||
--dim1-glow: rgba(124, 77, 255, 0.1);
|
||||
|
||||
/* dim2: AI 发展 — 青色 */
|
||||
--dim2-color: #4dd0e1;
|
||||
--dim2-bg: rgba(0, 188, 212, 0.08);
|
||||
--dim2-icon-bg: rgba(0, 188, 212, 0.25);
|
||||
--dim2-border: rgba(0, 188, 212, 0.15);
|
||||
--dim2-glow: rgba(0, 188, 212, 0.1);
|
||||
|
||||
/* dim3: AI 当前 — 青绿 */
|
||||
--dim3-color: #4db6ac;
|
||||
--dim3-bg: rgba(0, 200, 150, 0.08);
|
||||
--dim3-icon-bg: rgba(0, 200, 150, 0.25);
|
||||
--dim3-border: rgba(0, 200, 150, 0.15);
|
||||
--dim3-glow: rgba(0, 200, 150, 0.1);
|
||||
|
||||
/* dim4: AI 学习 — 金色 */
|
||||
--dim4-color: #ffd54f;
|
||||
--dim4-bg: rgba(255, 193, 7, 0.08);
|
||||
--dim4-icon-bg: rgba(255, 193, 7, 0.25);
|
||||
--dim4-border: rgba(255, 193, 7, 0.15);
|
||||
--dim4-glow: rgba(255, 193, 7, 0.1);
|
||||
|
||||
/* dim5: AI 趋势 — 珊瑚 */
|
||||
--dim5-color: #ef5350;
|
||||
--dim5-bg: rgba(239, 83, 80, 0.08);
|
||||
--dim5-icon-bg: rgba(239, 83, 80, 0.25);
|
||||
--dim5-border: rgba(239, 83, 80, 0.15);
|
||||
--dim5-glow: rgba(239, 83, 80, 0.1);
|
||||
|
||||
/* === 功能色 === */
|
||||
--color-free: #66bb6a;
|
||||
--color-free-bg: rgba(76, 175, 80, 0.12);
|
||||
--color-pro: #ffa726;
|
||||
--color-pro-bg: rgba(255, 152, 0, 0.12);
|
||||
--color-hot: #ef5350;
|
||||
--color-hot-bg: rgba(239, 83, 80, 0.12);
|
||||
--color-new: #4dd0e1;
|
||||
--color-new-bg: rgba(0, 188, 212, 0.12);
|
||||
--color-pick: #b388ff;
|
||||
--color-pick-bg: rgba(124, 77, 255, 0.12);
|
||||
|
||||
/* === 布局 === */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 18px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 12px;
|
||||
--spacing-lg: 16px;
|
||||
--spacing-xl: 20px;
|
||||
--spacing-2xl: 24px;
|
||||
|
||||
/* === 玻璃态 === */
|
||||
--blur-bg: 20px;
|
||||
|
||||
/* === 过渡 === */
|
||||
--transition-fast: 0.2s ease;
|
||||
--transition-normal: 0.3s ease;
|
||||
|
||||
/* === 辉光 === */
|
||||
--glow-brand: 0 0 20px rgba(124, 77, 255, 0.1);
|
||||
--glow-brand-strong: 0 0 30px rgba(124, 77, 255, 0.2);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
主题切换 — 后期只需覆盖这里的变量
|
||||
============================================ */
|
||||
|
||||
/* 浅色主题(预留) */
|
||||
.theme-light {
|
||||
--bg-primary: #f5f5f8;
|
||||
--bg-secondary: #eeeef2;
|
||||
--bg-tertiary: #e8e4f0;
|
||||
--glass-bg: rgba(255, 255, 255, 0.6);
|
||||
--glass-border: rgba(0, 0, 0, 0.06);
|
||||
--text-primary: #1a1a24;
|
||||
--text-secondary: rgba(0, 0, 0, 0.7);
|
||||
--text-tertiary: rgba(0, 0, 0, 0.4);
|
||||
--text-muted: rgba(0, 0, 0, 0.2);
|
||||
--color-brand: #7c4dff;
|
||||
--color-brand-light: #9c7af0;
|
||||
}
|
||||
|
||||
/* 圣诞主题(示例) */
|
||||
.theme-christmas {
|
||||
--color-brand: #d32f2f;
|
||||
--color-brand-light: #ef5350;
|
||||
--dim1-color: #c62828;
|
||||
--dim2-color: #2e7d32;
|
||||
/* ... 更多覆盖 */
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
全局排版
|
||||
============================================ */
|
||||
|
||||
/* 字体抗锯齿 */
|
||||
page {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 卡片通用样式 */
|
||||
.card {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
backdrop-filter: blur(var(--blur-bg));
|
||||
-webkit-backdrop-filter: blur(var(--blur-bg));
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.card:active {
|
||||
background: var(--glass-hover);
|
||||
border-color: var(--glass-hover-border);
|
||||
}
|
||||
|
||||
/* 维度卡片通用 */
|
||||
.dim-card-base {
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--spacing-xl) var(--spacing-lg);
|
||||
backdrop-filter: blur(var(--blur-bg));
|
||||
-webkit-backdrop-filter: blur(var(--blur-bg));
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.dim-card-base:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 品牌渐变 */
|
||||
.gradient-brand {
|
||||
background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light));
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--color-brand-light), var(--color-brand));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 徽章通用 */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 搜索栏通用 */
|
||||
.search-bar {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
backdrop-filter: blur(var(--blur-bg));
|
||||
-webkit-backdrop-filter: blur(var(--blur-bg));
|
||||
}
|
||||
|
||||
/* 维度色映射工具类 */
|
||||
.dim-color-1 { color: var(--dim1-color); }
|
||||
.dim-color-2 { color: var(--dim2-color); }
|
||||
.dim-color-3 { color: var(--dim3-color); }
|
||||
.dim-color-4 { color: var(--dim4-color); }
|
||||
.dim-color-5 { color: var(--dim5-color); }
|
||||
|
||||
.dim-bg-1 { background: var(--dim1-bg); }
|
||||
.dim-bg-2 { background: var(--dim2-bg); }
|
||||
.dim-bg-3 { background: var(--dim3-bg); }
|
||||
.dim-bg-4 { background: var(--dim4-bg); }
|
||||
.dim-bg-5 { background: var(--dim5-bg); }
|
||||
@@ -0,0 +1,2 @@
|
||||
/* 宇之然AI维度 — 全局 SCSS 变量 (uni-app 自动注入) */
|
||||
@import "@/styles/theme.scss";
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 宇之然AI维度 — API 工具
|
||||
* 后端地址:wdkj.yuzhiran.com.cn (生产) / 127.0.0.1:3001 (开发)
|
||||
*/
|
||||
|
||||
const BASE_URL = 'https://wdkj.yuzhiran.com.cn'
|
||||
const DEV_URL = 'http://127.0.0.1:3001'
|
||||
|
||||
// 判断环境
|
||||
const isDev = import.meta.env.DEV
|
||||
const API_BASE = isDev ? DEV_URL : BASE_URL
|
||||
|
||||
/**
|
||||
* 通用请求封装
|
||||
*/
|
||||
async function request(method, path, data = null, token = null) {
|
||||
const url = `${API_BASE}${path}`
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
header: headers,
|
||||
timeout: 15000
|
||||
})
|
||||
|
||||
const body = res.data
|
||||
if (body.success) {
|
||||
return body.data
|
||||
}
|
||||
throw new Error(body.error || '请求失败')
|
||||
} catch (err) {
|
||||
console.error(`[API] ${method} ${path} failed:`, err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证相关
|
||||
*/
|
||||
export const authApi = {
|
||||
/** 微信小程序登录 */
|
||||
login: (code, userInfo) =>
|
||||
request('POST', '/api/auth/login', { code, userInfo }),
|
||||
|
||||
/** H5 密码登录 */
|
||||
h5Login: (username, password) =>
|
||||
request('POST', '/api/auth/user/login', { username, password }),
|
||||
|
||||
/** H5 注册 */
|
||||
register: (username, password, nickName) =>
|
||||
request('POST', '/api/auth/user/register', { username, password, nickName }),
|
||||
|
||||
/** 获取当前用户 */
|
||||
me: (token) =>
|
||||
request('GET', '/api/auth/me', null, token),
|
||||
}
|
||||
|
||||
/**
|
||||
* 知识库
|
||||
*/
|
||||
export const knowledgeApi = {
|
||||
/** 获取知识列表 */
|
||||
list: (params = {}) => {
|
||||
const query = Object.entries(params)
|
||||
.filter(([_, v]) => v !== undefined && v !== null)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('&')
|
||||
return request('GET', `/api/knowledge${query ? '?' + query : ''}`)
|
||||
},
|
||||
|
||||
/** 获取知识详情 */
|
||||
detail: (id) =>
|
||||
request('GET', `/api/knowledge/${id}`),
|
||||
|
||||
/** 收藏知识 */
|
||||
collect: (id, token) =>
|
||||
request('POST', `/api/knowledge/${id}/collect`, null, token),
|
||||
|
||||
/** 取消收藏 */
|
||||
uncollect: (id, token) =>
|
||||
request('DELETE', `/api/knowledge/collect/${id}`, null, token),
|
||||
|
||||
/** 获取收藏列表 */
|
||||
collected: (token) =>
|
||||
request('GET', '/api/knowledge/collected', null, token),
|
||||
|
||||
/** 获取维度问答 */
|
||||
questions: (dim) =>
|
||||
request('GET', `/api/knowledge/${dim}/questions`),
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 问答
|
||||
*/
|
||||
export const aiChatApi = {
|
||||
/** 获取配额 */
|
||||
quota: (token) =>
|
||||
request('GET', '/api/ai-chat/quota', null, token),
|
||||
|
||||
/** 提问 */
|
||||
ask: (question, dimension = 'general', token) =>
|
||||
request('POST', '/api/ai-chat/ask', { question, dimension }, token),
|
||||
|
||||
/** 分享获得次数 */
|
||||
shareGain: (token) =>
|
||||
request('POST', '/api/ai-chat/share-gain', null, token),
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户相关
|
||||
*/
|
||||
export const userApi = {
|
||||
/** 获取用户进度 */
|
||||
progress: (token) =>
|
||||
request('GET', '/api/user/progress', null, token),
|
||||
|
||||
/** 保存进度 */
|
||||
saveProgress: (data, token) =>
|
||||
request('POST', '/api/user/progress', data, token),
|
||||
|
||||
/** 排行榜 */
|
||||
leaderboard: (dim = null) =>
|
||||
request('GET', `/api/user/leaderboard${dim ? '?dim=' + dim : ''}`),
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付相关
|
||||
*/
|
||||
export const paymentApi = {
|
||||
/** 获取商品列表 */
|
||||
products: () =>
|
||||
request('GET', '/api/payment/products'),
|
||||
|
||||
/** 创建订单 */
|
||||
createOrder: (productId, token) =>
|
||||
request('POST', '/api/payment/create-order', { productId }, token),
|
||||
}
|
||||
|
||||
/**
|
||||
* 趋势资讯
|
||||
*/
|
||||
export const trendApi = {
|
||||
/** 获取趋势列表 */
|
||||
list: (params = {}) => {
|
||||
const query = Object.entries(params)
|
||||
.filter(([_, v]) => v !== undefined && v !== null)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('&')
|
||||
return request('GET', `/api/trend${query ? '?' + query : ''}`)
|
||||
},
|
||||
|
||||
/** 获取趋势详情 */
|
||||
detail: (id) =>
|
||||
request('GET', `/api/trend/${id}`),
|
||||
}
|
||||
|
||||
export default {
|
||||
auth: authApi,
|
||||
knowledge: knowledgeApi,
|
||||
aiChat: aiChatApi,
|
||||
user: userApi,
|
||||
payment: paymentApi
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 维度配置 — 5 个 AI 维度的元数据
|
||||
* 修改这里可以调整维度名称、颜色、图标等
|
||||
*/
|
||||
|
||||
export const DIMENSIONS = [
|
||||
{
|
||||
id: 1,
|
||||
key: 'origin',
|
||||
name: 'AI 起源',
|
||||
icon: '🌅',
|
||||
desc: '从图灵到达特茅斯,追溯 70 年',
|
||||
color: 'var(--dim1-color)',
|
||||
bg: 'var(--dim1-bg)',
|
||||
iconBg: 'var(--dim1-icon-bg)',
|
||||
border: 'var(--dim1-border)',
|
||||
glow: 'var(--dim1-glow)',
|
||||
articleCount: 12,
|
||||
quizCount: 4
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
key: 'development',
|
||||
name: 'AI 发展',
|
||||
icon: '📈',
|
||||
desc: '深度学习·Transformer·MoE',
|
||||
color: 'var(--dim2-color)',
|
||||
bg: 'var(--dim2-bg)',
|
||||
iconBg: 'var(--dim2-icon-bg)',
|
||||
border: 'var(--dim2-border)',
|
||||
glow: 'var(--dim2-glow)',
|
||||
articleCount: 18,
|
||||
quizCount: 6
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
key: 'current',
|
||||
name: 'AI 当前',
|
||||
icon: '🌐',
|
||||
desc: '大模型格局·Agent·具身智能',
|
||||
color: 'var(--dim3-color)',
|
||||
bg: 'var(--dim3-bg)',
|
||||
iconBg: 'var(--dim3-icon-bg)',
|
||||
border: 'var(--dim3-border)',
|
||||
glow: 'var(--dim3-glow)',
|
||||
articleCount: 15,
|
||||
quizCount: 5
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
key: 'learning',
|
||||
name: 'AI 学习',
|
||||
icon: '🛠',
|
||||
desc: '提示词·RAG·微调·工具链',
|
||||
color: 'var(--dim4-color)',
|
||||
bg: 'var(--dim4-bg)',
|
||||
iconBg: 'var(--dim4-icon-bg)',
|
||||
border: 'var(--dim4-border)',
|
||||
glow: 'var(--dim4-glow)',
|
||||
articleCount: 20,
|
||||
quizCount: 8
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
key: 'trend',
|
||||
name: 'AI 趋势',
|
||||
icon: '🔥',
|
||||
desc: '每日资讯·产品动态·论文快报',
|
||||
color: 'var(--dim5-color)',
|
||||
bg: 'var(--dim5-bg)',
|
||||
iconBg: 'var(--dim5-icon-bg)',
|
||||
border: 'var(--dim5-border)',
|
||||
glow: 'var(--dim5-glow)',
|
||||
articleCount: 0,
|
||||
quizCount: 0,
|
||||
isFull: true // 跨列
|
||||
}
|
||||
]
|
||||
|
||||
/** 按维度 ID 查找 */
|
||||
export function getDimension(id) {
|
||||
return DIMENSIONS.find(d => d.id === id)
|
||||
}
|
||||
|
||||
/** 按维度 key 查找 */
|
||||
export function getDimensionByKey(key) {
|
||||
return DIMENSIONS.find(d => d.key === key)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import uni from '@dcloudio/vite-plugin-uni'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [uni()],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
// 全局 SCSS 变量注入
|
||||
additionalData: `@import "@/styles/theme.scss";`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||