diff --git a/frontend/ai-dimension/src/pages.json b/frontend/ai-dimension/src/pages.json index 938b7fa..00167bf 100644 --- a/frontend/ai-dimension/src/pages.json +++ b/frontend/ai-dimension/src/pages.json @@ -1,6 +1,9 @@ { "pages": [ {"path": "pages/index/index", "style": {"navigationBarTitleText": "AI 维度", "navigationStyle": "custom"}}, + {"path": "pages/login/login", "style": {"navigationBarTitleText": "登录", "navigationStyle": "custom"}}, + {"path": "pages/detail/detail", "style": {"navigationBarTitleText": "AI 知识", "navigationStyle": "custom"}}, + {"path": "pages/payment/payment", "style": {"navigationBarTitleText": "订阅升级", "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"}}, @@ -29,4 +32,4 @@ {"pagePath": "pages/profile/profile", "text": "我的"} ] } -} \ No newline at end of file +} diff --git a/frontend/ai-dimension/src/pages/chat/chat.vue b/frontend/ai-dimension/src/pages/chat/chat.vue index 592d282..b3f7921 100644 --- a/frontend/ai-dimension/src/pages/chat/chat.vue +++ b/frontend/ai-dimension/src/pages/chat/chat.vue @@ -2,40 +2,64 @@ + - + + + AI 助手 - DeepSeek V4 · 在线 + 多模型路由 · 在线 - 剩余 {{ quota }} 次 + + + {{ quotaDisplay }} + 🎁 + + + + + + 登录后开启 AI 问答 + - - - - - {{ msg.content }} - {{ msg.role === 'assistant' ? 'AI 助手' : '我' }} + + + + 💡 + 试试问我: + + + {{ item }} + - - + + + + + + + + + - ... + + + - + - - + @@ -55,63 +79,115 @@ \ No newline at end of file + +.send-btn text { + font-size: 16px; +} + diff --git a/frontend/ai-dimension/src/pages/detail/detail.vue b/frontend/ai-dimension/src/pages/detail/detail.vue new file mode 100644 index 0000000..5f61652 --- /dev/null +++ b/frontend/ai-dimension/src/pages/detail/detail.vue @@ -0,0 +1,446 @@ + + + + + diff --git a/frontend/ai-dimension/src/pages/dimension/dimension.vue b/frontend/ai-dimension/src/pages/dimension/dimension.vue index aad80da..8c78ab2 100644 --- a/frontend/ai-dimension/src/pages/dimension/dimension.vue +++ b/frontend/ai-dimension/src/pages/dimension/dimension.vue @@ -161,7 +161,7 @@ function goBack() { function goDetail(item) { uni.navigateTo({ - url: `/pages/dimension/detail?id=${item._id}` + url: `/pages/detail/detail?id=${item._id}` }) } diff --git a/frontend/ai-dimension/src/pages/login/login.vue b/frontend/ai-dimension/src/pages/login/login.vue new file mode 100644 index 0000000..a18991f --- /dev/null +++ b/frontend/ai-dimension/src/pages/login/login.vue @@ -0,0 +1,375 @@ + + + + + diff --git a/frontend/ai-dimension/src/pages/payment/payment.vue b/frontend/ai-dimension/src/pages/payment/payment.vue new file mode 100644 index 0000000..4da9042 --- /dev/null +++ b/frontend/ai-dimension/src/pages/payment/payment.vue @@ -0,0 +1,418 @@ + + + + + diff --git a/frontend/ai-dimension/src/pages/profile/profile.vue b/frontend/ai-dimension/src/pages/profile/profile.vue index 261010b..0dd2a09 100644 --- a/frontend/ai-dimension/src/pages/profile/profile.vue +++ b/frontend/ai-dimension/src/pages/profile/profile.vue @@ -5,116 +5,257 @@ - {{ userInfo?.avatarUrl ? '🖼' : '🧑' }} + + + {{ (userStore.userInfo?.nickName || '用').charAt(0) }} + - {{ userInfo?.nickName || '星辰旅者' }} - Lv.5 · 维度学徒 · 已探索 12 篇文章 + {{ userStore.userInfo?.nickName || '游客' }} + + {{ subscriptionBadge }} · 已收藏 {{ collectedCount }} 篇 + - + - ✦ 免费版 - {{ isLoggedIn ? '每日 5 次 AI 问答 · 解锁全部知识' : '登录后开始你的 AI 探索之旅' }} + {{ planTitle }} + {{ planDesc }} + + + + + + + {{ planExpiryText }} + + - - {{ isLoggedIn ? '升级 Pro' : '登录/注册' }} + + {{ planBtnText }} - - - 📚 - 我的收藏 - + + + 🤖 AI 问答配额 + + + + + {{ quotaUsed }} / {{ quotaTotal }} + 今日使用 + + 🎁 分享得次数 - - 📊 - 学习进度 - - - - - 🏆 - 成就 - 3 项未解锁 - - - - - 📅 - 每日签到 - - - - - ⚙️ - 设置 - + + + + + 我的收藏 + {{ collectedCount }} + + + + 📊 + 学习进度 + + + + 🏆 + 成就 + + + + ℹ️ + 关于 AI 维度 + + + + ⚙️ + 设置 + + - - 退出登录 - - + 微信一键登录 + + 退出登录 + - 宇之然AI维度 v1.0.0 + 宇之然 AI 维度 v1.0.0 \ No newline at end of file + diff --git a/frontend/ai-dimension/src/pages/trend/trend.vue b/frontend/ai-dimension/src/pages/trend/trend.vue index 48e7b4e..a4cf896 100644 --- a/frontend/ai-dimension/src/pages/trend/trend.vue +++ b/frontend/ai-dimension/src/pages/trend/trend.vue @@ -2,53 +2,83 @@ - - 🔥 AI 趋势 - 订阅 + + + AI 趋势 + 每日追踪人工智能前沿动态 - 每日追踪 AI 前沿动态,不错过每一个重要时刻 - + {{ tag }} - - - - - {{ item.tag }} - {{ item.title }} - {{ item.source }} · {{ item.date }} - - + + + + + + + {{ getTagText(item) }} + + {{ item.source }} + - + + {{ item.title }} + + + + {{ item.summary }} + + + + + {{ formatDate(item.newsDate) }} + + 🔥 {{ item.hotCount }} + 👁️ {{ item.viewCount || 0 }} + + + + + + + 加载中... + + + 📡 + 暂无趋势资讯 + 稍后再来看最新 AI 动态 + + \ No newline at end of file +.card-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.card-date { + font-size: 11px; + color: rgba(255, 255, 255, 0.3); +} + +.card-stats { + display: flex; + gap: 12px; + font-size: 11px; + color: rgba(255, 255, 255, 0.3); +} + +.stat-item { + color: rgba(255, 255, 255, 0.4); +} + +// 加载/空状态 +.loading-state, +.empty-state { + text-align: center; + padding: 40px 0; +} + +.loading-state text { + color: rgba(255, 255, 255, 0.3); + font-size: 13px; +} + +.empty-icon { + font-size: 48px; + display: block; + margin-bottom: 16px; +} + +.empty-text { + font-size: 15px; + color: rgba(255, 255, 255, 0.5); + display: block; +} + +.empty-sub { + font-size: 12px; + color: rgba(255, 255, 255, 0.3); + margin-top: 6px; + display: block; +} + diff --git a/frontend/ai-dimension/src/stores/chat.js b/frontend/ai-dimension/src/stores/chat.js new file mode 100644 index 0000000..0060dd9 --- /dev/null +++ b/frontend/ai-dimension/src/stores/chat.js @@ -0,0 +1,39 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { aiChatApi } from '@/utils/api' + +export const useChatStore = defineStore('chat', () => { + const messages = ref([]) + const quota = ref({ remaining: 0, total: 0 }) + const loading = ref(false) + + async function ask(question, dimension = 'general', token) { + loading.value = true + try { + const data = await aiChatApi.ask(question, dimension, 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) { + try { + const data = await aiChatApi.quota(token) + quota.value = data + return data + } catch (err) { + console.error('[chatStore] fetchQuota failed:', err) + return null + } + } + + function clearMessages() { + messages.value = [] + } + + return { messages, quota, loading, ask, fetchQuota, clearMessages } +}) diff --git a/frontend/ai-dimension/src/stores/index.js b/frontend/ai-dimension/src/stores/index.js index 03c2372..4bd4932 100644 --- a/frontend/ai-dimension/src/stores/index.js +++ b/frontend/ai-dimension/src/stores/index.js @@ -1,72 +1,3 @@ -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 } -}) \ No newline at end of file +export { useUserStore } from './user' +export { useKnowledgeStore } from './knowledge' +export { useChatStore } from './chat' diff --git a/frontend/ai-dimension/src/stores/knowledge.js b/frontend/ai-dimension/src/stores/knowledge.js new file mode 100644 index 0000000..83d2497 --- /dev/null +++ b/frontend/ai-dimension/src/stores/knowledge.js @@ -0,0 +1,58 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { knowledgeApi } from '@/utils/api' + +export const useKnowledgeStore = defineStore('knowledge', () => { + const list = ref([]) + const currentDetail = ref(null) + const loading = ref(false) + const collected = ref([]) + + async function fetchList(params = {}) { + loading.value = true + try { + const data = await knowledgeApi.list(params) + list.value = data.list || data.items || [] + } finally { + loading.value = false + } + } + + async function fetchDetail(id) { + try { + currentDetail.value = await knowledgeApi.detail(id) + return currentDetail.value + } catch (err) { + console.error('[knowledgeStore] fetchDetail failed:', err) + return null + } + } + + async function fetchCollected(token) { + try { + collected.value = await knowledgeApi.collected(token) + return collected.value + } catch (err) { + console.error('[knowledgeStore] fetchCollected failed:', err) + return [] + } + } + + async function toggleCollect(id, token) { + try { + const item = collected.value.find(i => i.id === id) + if (item) { + await knowledgeApi.uncollect(id, token) + collected.value = collected.value.filter(i => i.id !== id) + } else { + await knowledgeApi.collect(id, token) + collected.value.push({ id }) + } + } catch (err) { + console.error('[knowledgeStore] toggleCollect failed:', err) + throw err + } + } + + return { list, currentDetail, loading, collected, fetchList, fetchDetail, fetchCollected, toggleCollect } +}) diff --git a/frontend/ai-dimension/src/stores/user.js b/frontend/ai-dimension/src/stores/user.js new file mode 100644 index 0000000..49d99ae --- /dev/null +++ b/frontend/ai-dimension/src/stores/user.js @@ -0,0 +1,108 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { authApi } from '@/utils/api' + +export const useUserStore = defineStore('user', () => { + const token = ref(uni.getStorageSync('token') || '') + const userInfo = ref(null) + const subInfo = ref(null) // 订阅信息 + const quota = ref(null) // AI 问答配额 + const isLoggedIn = computed(() => !!token.value) + const isSubscribed = computed(() => { + if (!subInfo.value) return false + const expires = new Date(subInfo.value.expiresAt) + return expires > new Date() + }) + const subscriptionLevel = computed(() => { + if (!subInfo.value) return 'free' + const expires = new Date(subInfo.value.expiresAt) + if (expires <= new Date()) return 'free' + return subInfo.value.plan || 'pro' + }) + + 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 + } + + async function h5Login(username, password) { + const data = await authApi.h5Login(username, password) + token.value = data.token + userInfo.value = data.userInfo + uni.setStorageSync('token', data.token) + return data + } + + async function h5Register(username, password, nickName) { + const data = await authApi.register(username, password, nickName) + token.value = data.token + userInfo.value = data.userInfo + uni.setStorageSync('token', data.token) + return data + } + + async function fetchMe() { + try { + const data = await authApi.me(token.value) + userInfo.value = data + return data + } catch (err) { + console.error('[userStore] fetchMe failed:', err) + return null + } + } + + async function fetchSubInfo(tokenVal) { + // 从后端获取订阅信息 - 复用 knowledgeApi 或专门接口 + // 当前后端无单独订阅接口,从 /api/user/progress 中获取 + const { userApi } = await import('@/utils/api') + try { + const progress = await userApi.progress(tokenVal) + if (progress && progress.subscription) { + subInfo.value = progress.subscription + } + } catch (err) { + console.error('[userStore] fetchSubInfo failed:', err) + } + } + + async function fetchQuota(tokenVal) { + const { aiChatApi } = await import('@/utils/api') + try { + const data = await aiChatApi.quota(tokenVal) + quota.value = data + return data + } catch (err) { + console.error('[userStore] fetchQuota failed:', err) + return null + } + } + + function logout() { + token.value = '' + userInfo.value = null + subInfo.value = null + quota.value = null + uni.removeStorageSync('token') + } + + return { + token, + userInfo, + subInfo, + quota, + isLoggedIn, + isSubscribed, + subscriptionLevel, + login, + h5Login, + h5Register, + fetchMe, + fetchSubInfo, + fetchQuota, + logout + } +}) diff --git a/frontend/ai-dimension/src/utils/error-handler.js b/frontend/ai-dimension/src/utils/error-handler.js new file mode 100644 index 0000000..edc2fe6 --- /dev/null +++ b/frontend/ai-dimension/src/utils/error-handler.js @@ -0,0 +1,64 @@ +/** + * 全局错误处理 + * 在 main.js 中调用 initErrorHandler() + */ + +export function initErrorHandler() { + // 捕获未处理的 Promise rejection + uni.onUnhandledRejection((res) => { + console.error('[Global Error]', res.reason) + uni.showToast({ title: '操作失败,请重试', icon: 'none' }) + }) +} + +/** + * 网络请求拦截器 + * 增强版 request 函数,带全局错误处理 + */ +async function request(method, path, data = null, token = null, options = {}) { + 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 + + 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: options.timeout || 15000 + }) + + // 统一错误处理 + if (res.statusCode >= 400) { + throw new Error(res.data?.error || `请求失败 (${res.statusCode})`) + } + + const body = res.data + if (body && body.success) { + return body.data + } + throw new Error(body?.error || '请求失败') + } catch (err) { + // 网络错误 + if (err.errMsg && err.errMsg.includes('timeout')) { + throw new Error('网络超时,请检查网络') + } + if (err.errMsg && err.errMsg.includes('fail')) { + throw new Error('网络连接失败') + } + console.error(`[API Error] ${method} ${path}:`, err) + throw err + } +} + +export default request