Compare commits

..

20 Commits

Author SHA1 Message Date
Yuzhiran Dev 1094435b5f feat: 趋势英文内容自动翻译为中文+JSON截断容错解析 2026-07-12 20:07:44 +08:00
Yuzhiran Dev 89cf7add0e feat: 趋势数据库从12条扩至30条,保留7天 2026-07-12 19:00:04 +08:00
Yuzhiran Dev e48416ac8b fix: 趋势详情页空内容修复+查看原文按钮 2026-07-12 18:55:22 +08:00
Yuzhiran Dev 5d2171e1a4 feat: 趋势数据改为从Hacker News+arXiv实时获取,每日8/20点自动更新 2026-07-12 18:51:40 +08:00
Yuzhiran Dev 5ac33dcd7f fix: 协议勾选控制+news详情页样式修复+支付页重做 2026-07-12 17:43:32 +08:00
Yuzhiran Dev 23de8bc86a feat: 邮箱验证码注册+登录协议勾选
- 后端: VerificationCode模型+send-code API+register增加verifyCode校验
- 前端: 注册表单增加验证码输入框+60s倒计时获取按钮
- 前端: 登录表单增加用户协议/隐私政策勾选
- 前端: 登录按钮增加disabled状态(未勾选时不可点击)
- 后端: nodemailer邮件发送工具(可配置SMTP,未配置时返回devCode)
2026-07-12 16:02:57 +08:00
Yuzhiran Dev dd5c1bc7ac fix: 色彩饱和度+10% (品牌+5维度+功能色)+微信登录按钮全宽+图标 2026-07-12 14:02:34 +08:00
Yuzhiran Dev 06d7d9700c fix: 色彩饱和度+10%+微信登录按钮全宽+图标 2026-07-12 13:46:03 +08:00
Yuzhiran Dev 1c70e89ab8 fix: trend页边距与page-layout统一(移除多余16px)+profile菜单白底卡片样式 2026-07-12 12:43:41 +08:00
Yuzhiran Dev 48690e7724 fix: tabBar图标SVG→真实PNG格式,修复4个按钮图标不显示;上传脚本支持命令行参数 2026-07-11 22:15:00 +08:00
Yuzhiran Dev 70bb2d23ea feat: tabBar底部导航4个按钮添加图标 2026-07-11 21:26:13 +08:00
Yuzhiran Dev f8ace4b016 fix: 维度卡片饱和度提高至20%+顶部问候区渐变背景 2026-07-11 21:21:26 +08:00
Yuzhiran Dev ee34c35994 fix: 亮色主题回到浅底+色彩点缀设计 2026-07-11 18:38:33 +08:00
Yuzhiran Dev 6dc85281a5 feat: 亮色主题改为深底亮卡现代设计-参考Linear设计系统 2026-07-11 17:28:49 +08:00
Yuzhiran Dev 8e8e3d5da2 fix: 亮色主题-加深背景层次感+加大饱和度 2026-07-11 17:13:52 +08:00
Yuzhiran Dev 1760668d0f fix: 亮色主题彻底重做-清爽白底+品牌色点缀+卡片阴影 2026-07-11 17:04:16 +08:00
Yuzhiran Dev 0a3bb0fcf8 fix: 亮色主题增强配色对比度+nginx部署修复 2026-07-11 16:55:48 +08:00
Yuzhiran Dev 8b06017d02 fix: 亮色主题全新配色(蓝紫)+趋势筛选换行+所有页面硬编码替换为CSS变量 2026-07-11 16:48:04 +08:00
Yuzhiran Dev 8149d2a93c fix: 所有页面卡片背景统一替换为 var(--glass-bg) 确保玻璃效果一致 2026-07-11 15:05:43 +08:00
Yuzhiran Dev 0709e457b3 chore: 部署更新 v1.1.2 - H5+小程序+后端全同步 2026-07-11 14:39:39 +08:00
39 changed files with 1548 additions and 721 deletions
+5
View File
@@ -0,0 +1,5 @@
allowBuilds:
'@parcel/watcher': set this to true or false
core-js: set this to true or false
esbuild: set this to true or false
vue-demi: set this to true or false
+116 -111
View File
@@ -47,15 +47,15 @@ function applyTabBarTheme(theme) {
try {
if (theme === 'dark') {
uni.setTabBarStyle({
color: 'rgba(255,255,255,0.25)',
selectedColor: '#b388ff',
backgroundColor: '#08080f',
color: 'rgba(255,255,255,0.35)',
selectedColor: '#a78bfa',
backgroundColor: '#0d0d1a',
borderStyle: 'black'
})
} else {
uni.setTabBarStyle({
color: 'rgba(0,0,0,0.35)',
selectedColor: '#7c4dff',
color: 'rgba(0,0,0,0.3)',
selectedColor: '#6366f1',
backgroundColor: '#ffffff',
borderStyle: 'white'
})
@@ -69,82 +69,82 @@ function applyTabBarTheme(theme) {
<style>
/* ============================================
宇之然AI维度 — 全局主题
默认:亮色模式 | .theme-dark:暗色模式
默认:亮色模式(蓝紫主题) | .theme-dark:暗色模式
============================================ */
/* ---- 亮色模式(默认) ---- */
/* ---- 亮色模式(默认:浅底+色彩点缀,现代丰富 ---- */
page, .theme-light {
/* 基础色 — 清爽浅灰 */
/* 基础色 — 干净浅底 */
--bg-primary: #f5f5f7;
--bg-secondary: #ffffff;
--bg-tertiary: #e8e8ed;
/* 玻璃拟态 */
--glass-bg: rgba(255, 255, 255, 0.7);
/* 卡片 — 纯白微阴影 */
--glass-bg: #ffffff;
--glass-border: rgba(0, 0, 0, 0.06);
--glass-hover: rgba(255, 255, 255, 0.85);
--glass-hover-border: rgba(0, 0, 0, 0.1);
--glass-strong: rgba(255, 255, 255, 0.9);
--glass-dim-bg: rgba(139, 92, 246, 0.05);
--glass-hover: #f8f8ff;
--glass-hover-border: rgba(99, 102, 241, 0.15);
--glass-strong: #ffffff;
--glass-dim-bg: rgba(99, 102, 241, 0.04);
/* 文字 */
--text-primary: rgba(0, 0, 0, 0.88);
--text-secondary: rgba(0, 0, 0, 0.55);
--text-tertiary: rgba(0, 0, 0, 0.4);
--text-muted: rgba(0, 0, 0, 0.25);
--text-disabled: rgba(0, 0, 0, 0.2);
/* 文字 — 清晰深色 */
--text-primary: #0d0d1a;
--text-secondary: rgba(13, 13, 26, 0.6);
--text-tertiary: rgba(13, 13, 26, 0.4);
--text-muted: rgba(13, 13, 26, 0.25);
--text-disabled: rgba(13, 13, 26, 0.15);
/* 品牌色 */
--color-brand: #7c4dff;
--color-brand-light: #b388ff;
--color-brand-dark: #651fff;
--color-brand-bg: rgba(124, 77, 255, 0.08);
--color-brand-glow: rgba(124, 77, 255, 0.12);
/* 品牌色 — Indigo 高饱和 */
--color-brand: #5b5ff8;
--color-brand-light: #7b87fd;
--color-brand-dark: #4f46e5;
--color-brand-bg: rgba(91, 95, 248, 0.08);
--color-brand-glow: rgba(91, 95, 248, 0.1);
/* 维度色 */
--dim1-color: #7c4dff;
--dim1-bg: rgba(124, 77, 255, 0.08);
--dim1-icon-bg: rgba(124, 77, 255, 0.15);
--dim1-border: rgba(124, 77, 255, 0.15);
--dim1-glow: rgba(124, 77, 255, 0.06);
/* 维度色 — 高饱和度,在浅色底上鲜艳 */
--dim1-color: #5b5ff8;
--dim1-bg: rgba(91, 95, 248, 0.2);
--dim1-icon-bg: rgba(91, 95, 248, 0.25);
--dim1-border: rgba(91, 95, 248, 0.3);
--dim1-glow: rgba(91, 95, 248, 0.08);
--dim2-color: #0ea5e9;
--dim2-bg: rgba(14, 165, 233, 0.08);
--dim2-icon-bg: rgba(14, 165, 233, 0.15);
--dim2-border: rgba(14, 165, 233, 0.15);
--dim2-glow: rgba(14, 165, 233, 0.06);
--dim2-color: #0095ba;
--dim2-bg: rgba(0, 149, 186, 0.2);
--dim2-icon-bg: rgba(0, 149, 186, 0.25);
--dim2-border: rgba(0, 149, 186, 0.3);
--dim2-glow: rgba(0, 149, 186, 0.08);
--dim3-color: #10b981;
--dim3-bg: rgba(16, 185, 129, 0.08);
--dim3-icon-bg: rgba(16, 185, 129, 0.15);
--dim3-border: rgba(16, 185, 129, 0.15);
--dim3-glow: rgba(16, 185, 129, 0.06);
--dim3-color: #009b6a;
--dim3-bg: rgba(0, 155, 106, 0.2);
--dim3-icon-bg: rgba(0, 155, 106, 0.25);
--dim3-border: rgba(0, 155, 106, 0.3);
--dim3-glow: rgba(0, 155, 106, 0.08);
--dim4-color: #f59e0b;
--dim4-bg: rgba(245, 158, 11, 0.08);
--dim4-icon-bg: rgba(245, 158, 11, 0.15);
--dim4-border: rgba(245, 158, 11, 0.15);
--dim4-glow: rgba(245, 158, 11, 0.06);
--dim4-color: #df7700;
--dim4-bg: rgba(223, 119, 0, 0.2);
--dim4-icon-bg: rgba(223, 119, 0, 0.25);
--dim4-border: rgba(223, 119, 0, 0.3);
--dim4-glow: rgba(223, 119, 0, 0.08);
--dim5-color: #ef4444;
--dim5-bg: rgba(239, 68, 68, 0.08);
--dim5-icon-bg: rgba(239, 68, 68, 0.15);
--dim5-border: rgba(239, 68, 68, 0.15);
--dim5-glow: rgba(239, 68, 68, 0.06);
--dim5-color: #e51c1c;
--dim5-bg: rgba(229, 28, 28, 0.2);
--dim5-icon-bg: rgba(229, 28, 28, 0.25);
--dim5-border: rgba(229, 28, 28, 0.3);
--dim5-glow: rgba(229, 28, 28, 0.08);
/* 功能色 */
--color-free: #10b981;
--color-free-bg: rgba(16, 185, 129, 0.1);
--color-pro: #f59e0b;
--color-pro-bg: rgba(245, 158, 11, 0.1);
--color-vip: #ef4444;
--color-vip-bg: rgba(239, 68, 68, 0.1);
--color-hot: #ef4444;
--color-hot-bg: rgba(239, 68, 68, 0.12);
--color-new: #0ea5e9;
--color-new-bg: rgba(14, 165, 233, 0.12);
--color-pick: #7c4dff;
--color-pick-bg: rgba(124, 77, 255, 0.12);
--color-free: #009b6a;
--color-free-bg: rgba(0, 155, 106, 0.1);
--color-pro: #df7700;
--color-pro-bg: rgba(223, 119, 0, 0.1);
--color-vip: #e51c1c;
--color-vip-bg: rgba(229, 28, 28, 0.1);
--color-hot: #e51c1c;
--color-hot-bg: rgba(229, 28, 28, 0.12);
--color-new: #0095ba;
--color-new-bg: rgba(0, 149, 186, 0.12);
--color-pick: #5b5ff8;
--color-pick-bg: rgba(91, 95, 248, 0.12);
/* 圆角/间距/过渡 */
--radius-sm: 8px;
@@ -161,8 +161,12 @@ page, .theme-light {
--blur-bg: 20px;
--transition-fast: 0.2s ease;
--transition-normal: 0.3s ease;
--glow-brand: 0 0 20px rgba(124, 77, 255, 0.08);
--glow-brand-strong: 0 0 30px rgba(124, 77, 255, 0.12);
--glow-brand: 0 0 20px rgba(99, 102, 241, 0.08);
--glow-brand-strong: 0 0 30px rgba(99, 102, 241, 0.12);
/* 亮色专属:卡片阴影 */
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
--card-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* ---- 暗色模式 ---- */
@@ -177,7 +181,7 @@ page, .theme-light {
--glass-hover: rgba(255, 255, 255, 0.1);
--glass-hover-border: rgba(255, 255, 255, 0.15);
--glass-strong: rgba(255, 255, 255, 0.12);
--glass-dim-bg: rgba(124, 77, 255, 0.06);
--glass-dim-bg: rgba(99, 102, 241, 0.06);
--text-primary: rgba(255, 255, 255, 0.95);
--text-secondary: rgba(255, 255, 255, 0.7);
@@ -185,57 +189,57 @@ page, .theme-light {
--text-muted: rgba(255, 255, 255, 0.3);
--text-disabled: rgba(255, 255, 255, 0.15);
--color-brand: #8b5cf6;
--color-brand-light: #a78bfa;
--color-brand-dark: #7c3aed;
--color-brand-bg: rgba(139, 92, 246, 0.12);
--color-brand-glow: rgba(139, 92, 246, 0.15);
--color-brand: #7b87fd;
--color-brand-light: #a2b2ff;
--color-brand-dark: #5b5ff8;
--color-brand-bg: rgba(91, 95, 248, 0.12);
--color-brand-glow: rgba(91, 95, 248, 0.15);
--dim1-color: #a78bfa;
--dim1-bg: rgba(139, 92, 246, 0.12);
--dim1-icon-bg: rgba(139, 92, 246, 0.2);
--dim1-border: rgba(139, 92, 246, 0.18);
--dim1-glow: rgba(139, 92, 246, 0.08);
--dim1-color: #a485ff;
--dim1-bg: rgba(164, 133, 255, 0.12);
--dim1-icon-bg: rgba(164, 133, 255, 0.2);
--dim1-border: rgba(164, 133, 255, 0.18);
--dim1-glow: rgba(164, 133, 255, 0.08);
--dim2-color: #60a5fa;
--dim2-bg: rgba(96, 165, 250, 0.12);
--dim2-icon-bg: rgba(96, 165, 250, 0.2);
--dim2-border: rgba(96, 165, 250, 0.18);
--dim2-glow: rgba(96, 165, 250, 0.08);
--dim2-color: #5ba4ff;
--dim2-bg: rgba(91, 164, 255, 0.12);
--dim2-icon-bg: rgba(91, 164, 255, 0.2);
--dim2-border: rgba(91, 164, 255, 0.18);
--dim2-glow: rgba(91, 164, 255, 0.08);
--dim3-color: #34d399;
--dim3-bg: rgba(52, 211, 153, 0.12);
--dim3-icon-bg: rgba(52, 211, 153, 0.2);
--dim3-border: rgba(52, 211, 153, 0.18);
--dim3-glow: rgba(52, 211, 153, 0.08);
--dim3-color: #2cda9b;
--dim3-bg: rgba(44, 218, 155, 0.12);
--dim3-icon-bg: rgba(44, 218, 155, 0.2);
--dim3-border: rgba(44, 218, 155, 0.18);
--dim3-glow: rgba(44, 218, 155, 0.08);
--dim4-color: #fbbf24;
--dim4-bg: rgba(251, 191, 36, 0.12);
--dim4-icon-bg: rgba(251, 191, 36, 0.2);
--dim4-border: rgba(251, 191, 36, 0.18);
--dim4-glow: rgba(251, 191, 36, 0.08);
--dim4-color: #ffc020;
--dim4-bg: rgba(255, 192, 32, 0.12);
--dim4-icon-bg: rgba(255, 192, 32, 0.2);
--dim4-border: rgba(255, 192, 32, 0.18);
--dim4-glow: rgba(255, 192, 32, 0.08);
--dim5-color: #fb7185;
--dim5-bg: rgba(251, 113, 133, 0.12);
--dim5-icon-bg: rgba(251, 113, 133, 0.2);
--dim5-border: rgba(251, 113, 133, 0.18);
--dim5-glow: rgba(251, 113, 133, 0.08);
--dim5-color: #ff6c82;
--dim5-bg: rgba(255, 108, 130, 0.12);
--dim5-icon-bg: rgba(255, 108, 130, 0.2);
--dim5-border: rgba(255, 108, 130, 0.18);
--dim5-glow: rgba(255, 108, 130, 0.08);
--color-free: #34d399;
--color-free-bg: rgba(52, 211, 153, 0.12);
--color-pro: #fbbf24;
--color-pro-bg: rgba(251, 191, 36, 0.12);
--color-vip: #fb7185;
--color-vip-bg: rgba(251, 113, 133, 0.12);
--color-hot: #fb7185;
--color-hot-bg: rgba(251, 113, 133, 0.15);
--color-new: #60a5fa;
--color-new-bg: rgba(96, 165, 250, 0.15);
--color-pick: #a78bfa;
--color-pick-bg: rgba(139, 92, 246, 0.15);
--color-free: #2cda9b;
--color-free-bg: rgba(44, 218, 155, 0.12);
--color-pro: #ffc020;
--color-pro-bg: rgba(255, 192, 32, 0.12);
--color-vip: #ff6c82;
--color-vip-bg: rgba(255, 108, 130, 0.12);
--color-hot: #ff6c82;
--color-hot-bg: rgba(255, 108, 130, 0.15);
--color-new: #5ba4ff;
--color-new-bg: rgba(91, 164, 255, 0.15);
--color-pick: #a485ff;
--color-pick-bg: rgba(164, 133, 255, 0.15);
--glow-brand: 0 0 20px rgba(139, 92, 246, 0.1);
--glow-brand-strong: 0 0 30px rgba(139, 92, 246, 0.15);
--glow-brand: 0 0 20px rgba(99, 102, 241, 0.1);
--glow-brand-strong: 0 0 30px rgba(99, 102, 241, 0.15);
}
/* 全局排版 */
@@ -280,6 +284,7 @@ view {
border-radius: var(--radius-lg);
backdrop-filter: blur(var(--blur-bg));
-webkit-backdrop-filter: blur(var(--blur-bg));
box-shadow: var(--card-shadow, none);
transition: all var(--transition-normal);
}
+8 -7
View File
@@ -15,7 +15,8 @@
{"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"}}
{"path": "pages/profile/profile", "style": {"navigationBarTitleText": "我的", "navigationStyle": "custom"}},
{"path": "pages/agreement/agreement", "style": {"navigationBarTitleText": "协议", "navigationStyle": "custom"}}
],
"globalStyle": {
"navigationBarTextStyle": "black",
@@ -29,15 +30,15 @@
}
},
"tabBar": {
"color": "rgba(0,0,0,0.35)",
"selectedColor": "#7c4dff",
"color": "rgba(0,0,0,0.3)",
"selectedColor": "#6366f1",
"backgroundColor": "#ffffff",
"borderStyle": "white",
"list": [
{"pagePath": "pages/index/index", "text": "探索"},
{"pagePath": "pages/chat/chat", "text": "问答"},
{"pagePath": "pages/trend/trend", "text": "趋势"},
{"pagePath": "pages/profile/profile", "text": "我的"}
{"pagePath": "pages/index/index", "text": "探索", "iconPath": "static/icons/explore.png", "selectedIconPath": "static/icons/explore-active.png"},
{"pagePath": "pages/chat/chat", "text": "问答", "iconPath": "static/icons/chat.png", "selectedIconPath": "static/icons/chat-active.png"},
{"pagePath": "pages/trend/trend", "text": "趋势", "iconPath": "static/icons/trend.png", "selectedIconPath": "static/icons/trend-active.png"},
{"pagePath": "pages/profile/profile", "text": "我的", "iconPath": "static/icons/profile.png", "selectedIconPath": "static/icons/profile-active.png"}
]
}
}
@@ -192,15 +192,15 @@ function formatDate(dateStr) {
// 总览卡片
.overview-card {
padding: 16px 18px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
margin-bottom: 20px;
}
.overview-bar {
height: 4px;
background: rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border-radius: 2px;
overflow: hidden;
margin-bottom: 10px;
@@ -230,7 +230,7 @@ function formatDate(dateStr) {
align-items: center;
gap: 10px;
padding: 0 0 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
border-bottom: 1px solid var(--glass-border);
margin-bottom: 12px;
}
@@ -262,8 +262,8 @@ function formatDate(dateStr) {
align-items: center;
gap: 12px;
padding: 12px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 12px;
}
@@ -284,7 +284,7 @@ function formatDate(dateStr) {
}
.achievement-item.locked .ach-icon {
background: rgba(255, 255, 255, 0.04);
background: var(--glass-bg);
color: var(--text-muted);
}
+154
View File
@@ -0,0 +1,154 @@
<template>
<view class="page-agreement page-layout">
<view class="status-bar-placeholder"></view>
<view class="agreement-header">
<text class="back-btn" @click="goBack"> 返回</text>
<text class="header-title">{{ pageTitle }}</text>
</view>
<scroll-view class="agreement-content" scroll-y>
<view v-if="type === 'terms'" class="content-block">
<h2>宇之然 AI 维度 用户协议</h2>
<p>更新日期2026年7月12日</p>
<p>欢迎使用宇之然 AI 维度以下简称"本服务"本协议是您与北京宇之然科技中心之间关于使用本服务的法律协议</p>
<h3>服务说明</h3>
<p>1.1 本服务提供AI知识探索问答趋势资讯等内容服务</p>
<p>1.2 本服务部分功能免费使用高级功能需付费订阅</p>
<p>1.3 我们保留随时修改暂停或终止服务的权利但会提前通知用户</p>
<h3>用户账户</h3>
<p>2.1 您注册时需提供真实准确的邮箱信息</p>
<p>2.2 您有责任保管账户密码因密码泄露造成的损失由您自行承担</p>
<p>2.3 每个邮箱只能注册一个账户禁止恶意注册多个账户</p>
<h3>使用规范</h3>
<p>3.1 您不得利用本服务从事任何违法违规活动</p>
<p>3.2 您不得对本服务进行反向工程破解或攻击</p>
<p>3.3 AI 生成内容仅供辅助参考不构成专业建议发布前请自行审核</p>
<h3>知识产权</h3>
<p>4.1 本服务的内容包括但不限于文字图片代码的知识产权归北京宇之然科技中心所有</p>
<p>4.2 您使用本服务生成的内容其知识产权归您所有但您授予我们非独占的展示授权</p>
<h3>付费订阅</h3>
<p>5.1 订阅费用按所选套餐支付订阅后自动续费直至您取消</p>
<p>5.2 您可随时在账户设置中取消自动续费取消后当前周期内仍可继续使用</p>
<p>5.3 订阅费用一经支付不支持退款除非因服务故障导致无法正常使用</p>
<h3>免责声明</h3>
<p>6.1 本服务按"现状"提供不保证服务不会中断或无错误</p>
<p>6.2 AI 生成内容可能存在偏差或不准确用户应自行判断和使用</p>
<p>6.3 因不可抗力因素导致的服务中断我们不承担责任</p>
<h3>协议变更</h3>
<p>7.1 我们可能修改本协议修改后的协议将在页面公布后生效</p>
<p>7.2 您继续使用本服务即视为接受修改后的协议</p>
</view>
<view v-if="type === 'privacy'" class="content-block">
<h2>宇之然 AI 维度 隐私政策</h2>
<p>更新日期2026年7月12日</p>
<p>北京宇之然科技中心以下简称"我们"重视您的隐私本隐私政策说明了我们如何收集使用和保护您的个人信息</p>
<h3>信息收集</h3>
<p>1.1 您主动提供的信息注册时提供的邮箱昵称头像提交的意见反馈内容</p>
<p>1.2 使用过程中产生的信息使用记录收藏内容AI问答记录学习进度</p>
<p>1.3 微信小程序环境我们通过微信登录获取您的微信昵称和头像需您授权</p>
<p>1.4 设备信息操作系统版本设备型号网络类型仅用于服务优化</p>
<h3>信息使用</h3>
<p>2.1 提供维护和改进本服务</p>
<p>2.2 向您发送服务相关的通知如订阅到期提醒</p>
<p>2.3 分析使用趋势优化产品体验</p>
<p>2.4 我们不会将您的个人信息用于本政策未载明的其他用途</p>
<h3>信息保护</h3>
<p>3.1 我们采用行业标准的安全措施保护您的数据包括SSL加密传输数据加密存储</p>
<p>3.2 您的密码经过加密存储我们无法获取您的原始密码</p>
<p>3.3 尽管我们尽力保护您的数据但没有任何网络传输或存储方式是100%安全的</p>
<h3>信息共享</h3>
<p>4.1 我们不会将您的个人信息出售给第三方</p>
<p>4.2 我们可能在以下情况共享信息法律要求保护我们的合法权益获得您的明确同意</p>
<p>4.3 我们使用的第三方服务如微信登录支付服务会按照其各自的隐私政策处理数据</p>
<h3>数据存储与跨境传输</h3>
<p>5.1 您的数据存储在中国境内的服务器上</p>
<p>5.2 我们不会主动将您的数据传输至境外</p>
<h3>您的权利</h3>
<p>6.1 您有权查阅更正删除您的个人信息</p>
<p>6.2 您可以在账户设置中申请注销账户注销后我们将删除您的个人信息</p>
<p>6.3 您可以通过邮箱 contact@yuzhiran.com 联系我们行使上述权利</p>
<h3>政策更新</h3>
<p>7.1 我们可能更新本隐私政策更新后的政策将在页面公布后生效</p>
<p>7.2 重大变更我们会通过邮件或站内通知告知您</p>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
const type = ref('terms')
onLoad((options) => {
type.value = options.type || 'terms'
})
const pageTitle = computed(() => {
return type.value === 'terms' ? '用户协议' : '隐私政策'
})
function goBack() {
uni.navigateBack()
}
function onLoad(fn) {
const pages = getCurrentPages()
const page = pages[pages.length - 1]
if (page && page.$page) {
fn(page.$page.options || {})
}
}
</script>
<style lang="scss" scoped>
.page-agreement {
min-height: 100vh;
background: var(--bg-primary);
display: flex;
flex-direction: column;
}
.status-bar-placeholder { height: 44px; }
.agreement-header {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 20px;
border-bottom: 1px solid var(--glass-border);
background: var(--glass-bg);
}
.back-btn {
font-size: 14px;
color: var(--color-brand);
cursor: pointer;
flex-shrink: 0;
}
.header-title {
font-size: 17px;
font-weight: 600;
color: var(--text-primary);
}
.agreement-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.content-block {
h2 { font-size: 20px; font-weight: 700; color: var(--text-primary); margin-bottom: 8px; }
h3 { font-size: 15px; font-weight: 600; color: var(--text-primary); margin: 20px 0 10px; }
p { font-size: 13px; color: var(--text-secondary); line-height: 1.8; margin-bottom: 6px; }
}
</style>
+6 -6
View File
@@ -306,8 +306,8 @@ function goShare() {
// 欢迎卡片
.welcome-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 16px;
margin-bottom: 16px;
@@ -374,8 +374,8 @@ function goShare() {
}
.chat-msg.assistant .bubble {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
color: var(--text-primary);
border-bottom-left-radius: 4px;
}
@@ -418,8 +418,8 @@ function goShare() {
}
.chat-input {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 10px 14px;
display: flex;
+4 -5
View File
@@ -299,8 +299,8 @@ function goToAI() {
.content-section,
.content-block,
.qa-section {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
@@ -368,15 +368,14 @@ function goToAI() {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
.qa-option {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 10px;
transition: all 0.3s;
+4 -4
View File
@@ -204,7 +204,7 @@ function goDetail(item) {
.progress-track {
height: 3px;
background: rgba(255,255,255,0.06);
background: var(--glass-bg);
border-radius: 2px;
margin-top: 16px;
overflow: hidden;
@@ -225,8 +225,8 @@ function goDetail(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);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
display: flex;
gap: 14px;
align-items: flex-start;
@@ -240,7 +240,7 @@ function goDetail(item) {
align-items: center;
justify-content: center;
font-size: 18px;
border: 1px solid rgba(255,255,255,0.06);
border: 1px solid var(--glass-border);
}
.k-info { flex: 1; }
.k-title {
+2 -2
View File
@@ -158,8 +158,8 @@ function goHome() {
flex-direction: column;
gap: 8px;
padding: 16px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
backdrop-filter: blur(8px);
}
+8 -8
View File
@@ -284,8 +284,8 @@ function goBack() {
.type-btn {
padding: 10px 6px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 10px;
display: flex;
flex-direction: column;
@@ -313,8 +313,8 @@ function goBack() {
.form-input {
width: 100%;
height: 44px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 10px;
padding: 0 14px;
font-size: 14px;
@@ -327,8 +327,8 @@ function goBack() {
.form-textarea {
width: 100%;
min-height: 100px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 10px;
padding: 12px 14px;
font-size: 14px;
@@ -386,8 +386,8 @@ function goBack() {
}
.my-item {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 10px;
padding: 10px 14px;
margin-bottom: 8px;
+9 -1
View File
@@ -148,7 +148,8 @@ function onNotify() {
}
function goNewsDetail(item) {
uni.switchTab({ url: '/pages/trend/trend' })
uni.setStorageSync('__news_item__', JSON.stringify(item))
uni.navigateTo({ url: '/pages/news/news' })
}
</script>
@@ -222,6 +223,10 @@ function goNewsDetail(item) {
.greeting {
margin-bottom: 20px;
padding: 24px 20px;
border-radius: 18px;
background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(139,92,246,0.04));
border: 1px solid rgba(99,102,241,0.1);
}
.greeting-title {
font-size: 26px;
@@ -263,6 +268,9 @@ function goNewsDetail(item) {
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
padding: 8px 12px;
border-radius: 10px;
background: rgba(99,102,241,0.04);
}
.section-title {
font-size: 17px;
+3 -3
View File
@@ -169,7 +169,7 @@ function dimDotColor(id) {
padding: 7px 16px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border: 1px solid var(--glass-border);
font-size: 13px;
color: var(--text-tertiary);
transition: all 0.2s ease;
@@ -194,8 +194,8 @@ function dimDotColor(id) {
align-items: center;
gap: 12px;
padding: 12px 14px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
}
+232 -209
View File
@@ -1,12 +1,10 @@
<template>
<view class="login-page page-layout">
<!-- 背景装饰 -->
<view class="bg-orb orb-1"></view>
<view class="bg-orb orb-2"></view>
<view class="bg-orb orb-3"></view>
<view class="login-container">
<!-- Logo -->
<view class="logo-wrap">
<view class="logo-ring"></view>
<text class="logo-text">AI</text>
@@ -14,7 +12,6 @@
<text class="app-title">宇之然 AI 维度</text>
<text class="app-subtitle"> 5 个维度系统理解人工智能</text>
<!-- Tab 切换 两种模式共用 -->
<view class="tab-switch">
<text class="tab-item" :class="{ active: activeTab === 'login' }" @click="activeTab = 'login'">登录</text>
<text class="tab-item" :class="{ active: activeTab === 'register' }" @click="activeTab = 'register'">注册</text>
@@ -23,19 +20,34 @@
<!-- 登录表单 -->
<view class="form-card" v-if="activeTab === 'login'">
<view class="form-item">
<text class="form-label">用户名</text>
<input class="form-input" v-model="loginForm.username" placeholder="请输入用户名" />
<text class="form-label">邮箱 / 用户名</text>
<input class="form-input" v-model="loginForm.email" placeholder="请输入邮箱或用户名" @input="loginForm.email = $event.detail.value.trim()" />
</view>
<view class="form-item">
<text class="form-label">密码</text>
<input class="form-input" v-model="loginForm.password" type="password" placeholder="请输入密码" />
</view>
<view class="submit-btn" :class="{ loading: loading }" @click="handleLogin">
<!-- 登录协议勾选 -->
<view class="agreement-row">
<view class="agreement-checkbox" :class="{ checked: loginAgreed }" @click="loginAgreed = !loginAgreed">
<text v-if="loginAgreed"></text>
</view>
<text class="agreement-text">
登录即表示同意
<text class="agreement-link" @click.stop="openAgreement('terms')">用户协议</text>
<text class="agreement-link" @click.stop="openAgreement('privacy')">隐私政策</text>
</text>
</view>
<view class="submit-btn" :class="{ loading: loading, disabled: !loginAgreed }" @click="handleLogin">
<text>{{ loading ? '登录中...' : '登录' }}</text>
</view>
<!-- 微信小程序一键登录入口 -->
<!-- 微信小程序一键登录 -->
<view class="wx-login-row" v-if="isMiniApp">
<view class="wx-login-btn" @click="wechatLogin">
<text class="wx-icon">💬</text>
<text>微信一键登录</text>
</view>
</view>
@@ -44,23 +56,47 @@
<!-- 注册表单 -->
<view class="form-card" v-if="activeTab === 'register'">
<view class="form-item">
<text class="form-label">用户名</text>
<input class="form-input" v-model="registerForm.username" placeholder="3-20 位字符" />
<text class="form-label">邮箱 <text style="color:var(--color-vip);font-size:11px;">*</text></text>
<input class="form-input" v-model="registerForm.email" type="email" placeholder="请输入邮箱地址" @input="registerForm.email = $event.detail.value.trim(); verifyCodeSent = false" />
</view>
<view class="form-item">
<text class="form-label">昵称</text>
<input class="form-input" v-model="registerForm.nickName" placeholder="显示名称(选填)" />
<text class="form-label">昵称 <text style="color:var(--text-muted);font-size:11px;">选填</text></text>
<input class="form-input" v-model="registerForm.nickName" placeholder="显示名称,留空自动生成" />
</view>
<view class="form-item">
<text class="form-label">密码</text>
<text class="form-label">密码 <text style="color:var(--color-vip);font-size:11px;">*</text></text>
<input class="form-input" v-model="registerForm.password" type="password" placeholder="至少 6 位" />
</view>
<view class="submit-btn" :class="{ loading: loading }" @click="handleRegister">
<!-- 邮箱验证码 -->
<view class="form-item">
<text class="form-label">验证码 <text style="color:var(--color-vip);font-size:11px;">*</text></text>
<view class="verify-row">
<input class="form-input verify-input" v-model="registerForm.verifyCode" type="text" maxlength="6" placeholder="输入6位验证码" />
<view class="send-code-btn" :class="{ disabled: codeSending || codeCountdown > 0 }" @click="handleSendCode">
<text>{{ codeCountdown > 0 ? codeCountdown + 's' : (codeSending ? '发送中...' : '获取验证码') }}</text>
</view>
</view>
</view>
<!-- 用户协议勾选 -->
<view class="agreement-row">
<view class="agreement-checkbox" :class="{ checked: agreed }" @click="agreed = !agreed">
<text v-if="agreed"></text>
</view>
<text class="agreement-text">
我已阅读并同意
<text class="agreement-link" @click.stop="openAgreement('terms')">用户协议</text>
<text class="agreement-link" @click.stop="openAgreement('privacy')">隐私政策</text>
</text>
</view>
<view class="submit-btn" :class="{ loading: loading, disabled: !agreed }" @click="handleRegister">
<text>{{ loading ? '注册中...' : '注册' }}</text>
</view>
</view>
<!-- 底部信息 -->
<view class="footer-info">
<text>© 2026 宇之然 · AI 维度</text>
</view>
@@ -72,76 +108,115 @@
import { ref } from 'vue'
import { useUserStore } from '@/stores/index'
// 检测是否为微信小程序环境
const isMiniApp = ref(false)
const platform = uni.getSystemInfoSync().platform
try {
const sysInfo = uni.getSystemInfoSync()
// 小程序环境:platform 为 ios/android/devtools,且有 brand/version 等字段
if (sysInfo && sysInfo.version && sysInfo.platform) {
// 有 weixin 特有字段即为小程序
isMiniApp.value = true
}
} catch (e) {
// H5 环境
}
} catch (e) {}
const activeTab = ref('login')
const loading = ref(false)
const agreed = ref(false)
const loginAgreed = ref(false)
const codeSending = ref(false)
const codeCountdown = ref(0)
const verifyCodeSent = ref(false)
let countdownTimer = null
const userStore = useUserStore()
// H5 表单
const loginForm = ref({ username: '', password: '' })
const registerForm = ref({ username: '', password: '', nickName: '' })
const loginForm = ref({ email: '', password: '' })
const registerForm = ref({ email: '', password: '', nickName: '', verifyCode: '' })
const BASE_URL = 'https://wdkj.yuzhiran.com.cn'
/** 发送邮箱验证码 */
async function handleSendCode() {
if (!agreed.value) {
uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
return
}
const email = registerForm.value.email
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
uni.showToast({ title: '请先输入正确的邮箱', icon: 'none' })
return
}
if (codeSending.value || codeCountdown.value > 0) return
codeSending.value = true
try {
const resp = await fetch(`${BASE_URL}/api/auth/user/send-code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
})
const data = await resp.json()
if (!resp.ok) {
throw new Error(data.error?.message || data.message || '发送失败')
}
verifyCodeSent.value = true
uni.showToast({ title: '验证码已发送', icon: 'success' })
// 开始倒计时
codeCountdown.value = 60
countdownTimer = setInterval(() => {
codeCountdown.value--
if (codeCountdown.value <= 0) {
clearInterval(countdownTimer)
countdownTimer = null
}
}, 1000)
} catch (err) {
uni.showToast({ title: err.message || '发送失败,请稍后重试', icon: 'none' })
} finally {
codeSending.value = false
}
}
/**
* 微信小程序登录
*/
const wechatLogin = async () => {
if (!loginAgreed.value) {
uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
return
}
try {
const loginRes = await new Promise((resolve, reject) => {
uni.login({
success: resolve,
fail: reject
uni.login({ success: resolve, fail: reject })
})
})
if (!loginRes.code) {
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
return
}
loading.value = true
await userStore.login(loginRes.code, {})
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1000)
} catch (err) {
console.error('微信登录失败:', err)
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
} finally {
loading.value = false
}
}
/**
* H5 用户名密码登录
*/
const handleLogin = async () => {
if (!loginForm.value.username || !loginForm.value.password) {
if (!loginAgreed.value) {
uni.showToast({ title: '请阅读并同意用户协议和隐私政策', icon: 'none' })
return
}
if (!loginForm.value.email || !loginForm.value.password) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
loading.value = true
try {
await userStore.h5Login(loginForm.value.username, loginForm.value.password)
// 支持邮箱或用户名登录
const identifier = loginForm.value.email
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(identifier)
const loginData = isEmail
? { email: identifier, password: loginForm.value.password }
: { username: identifier, password: loginForm.value.password }
await userStore.h5Login(loginData)
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1000)
} catch (err) {
uni.showToast({ title: err.message || '登录失败', icon: 'none' })
} finally {
@@ -149,36 +224,55 @@ const handleLogin = async () => {
}
}
/**
* H5 注册
*/
const handleRegister = async () => {
if (!registerForm.value.username || !registerForm.value.password) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
if (!agreed.value) {
uni.showToast({ title: '请阅读并同意用户协议和隐私政策', icon: 'none' })
return
}
if (!registerForm.value.email) {
uni.showToast({ title: '请输入邮箱', icon: 'none' })
return
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(registerForm.value.email)) {
uni.showToast({ title: '邮箱格式不正确', icon: 'none' })
return
}
if (!registerForm.value.password) {
uni.showToast({ title: '请输入密码', icon: 'none' })
return
}
if (registerForm.value.password.length < 6) {
uni.showToast({ title: '密码至少 6 位', icon: 'none' })
return
}
if (!registerForm.value.verifyCode) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
loading.value = true
try {
await userStore.h5Register(
registerForm.value.username,
registerForm.value.email,
registerForm.value.password,
registerForm.value.nickName || ''
registerForm.value.nickName || '',
registerForm.value.verifyCode
)
uni.showToast({ title: '注册成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1000)
} catch (err) {
uni.showToast({ title: err.message || '注册失败', icon: 'none' })
} finally {
loading.value = false
}
}
function openAgreement(type) {
const pages = {
'terms': '/pages/agreement/agreement?type=terms',
'privacy': '/pages/agreement/agreement?type=privacy'
}
uni.navigateTo({ url: pages[type] || pages.terms })
}
</script>
<style lang="scss" scoped>
@@ -192,7 +286,6 @@ const handleRegister = async () => {
justify-content: center;
background: var(--bg-primary);
// 背景装饰光球
.bg-orb {
position: absolute;
border-radius: 50%;
@@ -200,193 +293,123 @@ const handleRegister = async () => {
opacity: 0.3;
pointer-events: none;
}
.orb-1 {
width: 200px; height: 200px;
top: -50px; left: -50px;
background: var(--dim1-color);
}
.orb-2 {
width: 160px; height: 160px;
top: 30%; right: -40px;
background: var(--dim3-color);
}
.orb-3 {
width: 180px; height: 180px;
bottom: -60px; left: 30%;
background: var(--dim5-color);
}
.orb-1 { width: 200px; height: 200px; top: -50px; left: -50px; background: var(--dim1-color); }
.orb-2 { width: 160px; height: 160px; top: 30%; right: -40px; background: var(--dim3-color); }
.orb-3 { width: 180px; height: 180px; bottom: -60px; left: 30%; background: var(--dim5-color); }
.login-container {
position: relative;
z-index: 1;
width: 100%;
max-width: 360px;
position: relative; z-index: 1;
width: 100%; max-width: 360px;
padding: 0 32px;
display: flex;
flex-direction: column;
align-items: center;
display: flex; flex-direction: column; align-items: center;
}
// Logo
.logo-wrap {
position: relative;
width: 72px;
height: 72px;
margin-bottom: 16px;
position: relative; width: 72px; height: 72px; margin-bottom: 16px;
.logo-ring {
position: absolute;
inset: 0;
border-radius: 18px;
position: absolute; inset: 0; border-radius: 18px;
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
box-shadow: 0 8px 32px rgba(124, 77, 255, 0.2);
}
.logo-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 700;
color: white;
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
font-size: 28px; font-weight: 700; color: white;
}
}
.app-title {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: 1px;
margin-bottom: 6px;
}
.app-subtitle {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 32px;
}
.app-title { font-size: 24px; font-weight: 700; color: var(--text-primary); margin-bottom: 6px; }
.app-subtitle { font-size: 13px; color: var(--text-secondary); margin-bottom: 32px; }
// Tab 切换
.tab-switch {
display: flex;
width: 100%;
margin-bottom: 20px;
border-bottom: 1px solid var(--glass-border);
display: flex; width: 100%; margin-bottom: 20px; border-bottom: 1px solid var(--glass-border);
.tab-item {
flex: 1;
text-align: center;
padding: 10px 0;
color: var(--text-secondary);
font-size: 15px;
font-weight: 500;
border-bottom: 2px solid transparent;
transition: all 0.3s;
&.active {
color: var(--color-brand);
border-bottom-color: var(--color-brand);
}
flex: 1; text-align: center; padding: 10px 0;
color: var(--text-secondary); font-size: 15px; font-weight: 500;
border-bottom: 2px solid transparent; transition: all 0.3s;
&.active { color: var(--color-brand); border-bottom-color: var(--color-brand); }
}
}
// 表单卡片
.form-card {
width: 100%;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 24px 20px;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
background: var(--glass-bg); border: 1px solid var(--glass-border);
border-radius: 16px; padding: 24px 20px;
}
.form-item {
margin-bottom: 16px;
.form-label {
display: block;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 6px;
font-weight: 500;
}
.form-item { margin-bottom: 16px;
.form-label { display: block; font-size: 13px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 500; }
.form-input {
width: 100%;
height: 44px;
background: var(--bg-secondary);
border: 1px solid var(--glass-border);
border-radius: 10px;
padding: 0 14px;
color: var(--text-primary);
font-size: 14px;
box-sizing: border-box;
outline: none;
&::placeholder {
color: var(--text-disabled);
}
width: 100%; height: 44px;
background: var(--bg-secondary); border: 1px solid var(--glass-border);
border-radius: 10px; padding: 0 14px;
color: var(--text-primary); font-size: 14px; box-sizing: border-box; outline: none;
&::placeholder { color: var(--text-disabled); }
}
}
// 提交按钮
.submit-btn {
width: 100%;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
// 验证码行
.verify-row {
display: flex; gap: 10px;
.verify-input { flex: 1; }
.send-code-btn {
flex-shrink: 0; height: 44px; padding: 0 16px;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, var(--color-brand), var(--dim3-color));
color: white;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
margin-top: 8px;
cursor: pointer;
color: white; border-radius: 10px; font-size: 13px; font-weight: 600;
cursor: pointer; white-space: nowrap;
&.disabled { opacity: 0.5; pointer-events: none; }
&:active { opacity: 0.8; }
}
}
// 用户协议勾选
.agreement-row {
display: flex; align-items: center; gap: 10px;
margin: 12px 0 4px;
}
.agreement-checkbox {
width: 20px; height: 20px;
border: 2px solid var(--glass-border);
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
text { font-size: 12px; color: white; font-weight: 700; display: none; }
&.checked {
background: var(--color-brand); border-color: var(--color-brand);
text { display: block; }
}
}
.agreement-text { font-size: 12px; color: var(--text-tertiary); line-height: 1.5; }
.agreement-link { color: var(--color-brand); text-decoration: underline; }
.submit-btn {
width: 100%; height: 46px;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, var(--color-brand), var(--dim3-color));
color: white; border-radius: 10px; font-size: 15px; font-weight: 600;
margin-top: 8px; cursor: pointer;
box-shadow: 0 4px 16px rgba(124, 77, 255, 0.25);
transition: opacity 0.3s;
&:active {
opacity: 0.8;
}
&.loading {
opacity: 0.7;
}
&:active { opacity: 0.8; }
&.loading { opacity: 0.7; }
&.disabled { opacity: 0.4; pointer-events: none; }
}
// 微信一键登录
.wx-login-row {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--glass-border);
display: flex;
justify-content: center;
margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--glass-border);
.wx-login-btn {
padding: 10px 28px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: linear-gradient(135deg, #07c160, #06ad56);
color: white;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.3);
&:active {
opacity: 0.8;
}
width: 100%; height: 46px;
display: flex; align-items: center; justify-content: center; gap: 8px;
background: linear-gradient(135deg, #07c160, #06ad56); color: white;
border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer;
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.3); transition: opacity 0.3s;
&:active { opacity: 0.8; }
.wx-icon { font-size: 18px; }
}
}
// 底部信息
.footer-info {
margin-top: 40px;
font-size: 11px;
color: var(--text-muted);
}
.footer-info { margin-top: 40px; font-size: 11px; color: var(--text-muted); }
}
</style>
+58 -20
View File
@@ -55,6 +55,13 @@
<rich-text :nodes="newsBody"></rich-text>
</view>
<!-- 查看原文 -->
<view class="source-link" v-if="news.sourceUrl" @click="openSource">
<text class="source-link-icon">🔗</text>
<text class="source-link-text">查看原文 · {{ news.source }}</text>
<text class="source-link-arrow"></text>
</view>
<!-- 相关话题 -->
<view class="related-tags" v-if="relatedTags.length">
<text class="related-label">相关话题</text>
@@ -123,7 +130,7 @@ function formatDate(dateStr) {
const newsBody = computed(() => {
const content = news.value.body || ''
if (!content) {
return `<p>${news.value.summary || ''}</p><p style="margin-top:16px;color:rgba(255,255,255,0.35)">完整内容暂未提供,将持续更新...</p>`
return `<p style="line-height:1.8;font-size:15px;color:var(--text-primary)">${news.value.summary || '暂无详细内容'}</p>`
}
return content
})
@@ -173,6 +180,20 @@ function onShare() {
})
}
function openSource() {
if (!news.value.sourceUrl) return
uni.setClipboardData({
data: news.value.sourceUrl,
success: () => {
uni.showModal({
title: '打开原文',
content: `链接已复制: ${news.value.sourceUrl}\n请在浏览器中打开`,
showCancel: false
})
}
})
}
onMounted(() => {
loadDetail()
})
@@ -198,7 +219,7 @@ onMounted(() => {
.nav-back {
font-size: 28px;
color: #fff;
color: var(--text-primary);
padding: 0 8px;
line-height: 1;
}
@@ -209,7 +230,7 @@ onMounted(() => {
color: var(--text-primary);
}
.nav-share { font-size: 22px; color: rgba(255,255,255,0.5); }
.nav-share { font-size: 22px; color: var(--text-muted); }
.content {
flex: 1;
@@ -218,18 +239,19 @@ onMounted(() => {
// Hero
.news-hero {
height: 180px;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0 0 24px 24px;
margin: 0 20px;
}
.hero-emoji { font-size: 72px; }
.hero-emoji { font-size: 64px; }
// Category
.category-tag {
margin: 24px 20px 0;
margin: 20px 20px 0;
}
.tag-text {
@@ -257,8 +279,9 @@ onMounted(() => {
gap: 16px;
margin: 0 20px 20px;
padding: 16px;
background: rgba(255,255,255,0.02);
background: var(--glass-bg);
border-radius: 12px;
border: 1px solid var(--glass-border);
}
.meta-item {
@@ -269,19 +292,19 @@ onMounted(() => {
.meta-label {
font-size: 11px;
color: rgba(255,255,255,0.35);
color: var(--text-tertiary);
}
.meta-label.hot { font-size: 13px; }
.meta-value { font-size: 12px; color: rgba(255,255,255,0.7); }
.meta-value { font-size: 12px; color: var(--text-secondary); }
// Summary
.summary-card {
margin: 0 20px 20px;
padding: 16px;
background: linear-gradient(135deg, rgba(179,136,255,0.08), rgba(77,208,225,0.05));
border: 1px solid rgba(179,136,255,0.12);
background: linear-gradient(135deg, rgba(99,102,241,0.06), rgba(0,188,212,0.04));
border: 1px solid rgba(99,102,241,0.1);
border-radius: 12px;
}
@@ -296,7 +319,7 @@ onMounted(() => {
.summary-text {
font-size: 14px;
line-height: 1.7;
color: rgba(255,255,255,0.85);
color: var(--text-primary);
}
// Body
@@ -304,9 +327,24 @@ onMounted(() => {
margin: 0 20px 20px;
font-size: 14px;
line-height: 1.8;
color: rgba(255,255,255,0.7);
color: var(--text-secondary);
}
// 查看原文
.source-link {
margin: 0 20px 20px;
padding: 14px 16px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 12px;
display: flex;
align-items: center;
gap: 10px;
}
.source-link-icon { font-size: 14px; }
.source-link-text { flex: 1; font-size: 13px; color: var(--dim1-color); font-weight: 500; }
.source-link-arrow { font-size: 14px; color: var(--text-muted); }
// Related tags
.related-tags {
margin: 0 20px 20px;
@@ -315,7 +353,7 @@ onMounted(() => {
.related-label {
display: block;
font-size: 12px;
color: rgba(255,255,255,0.4);
color: var(--text-tertiary);
margin-bottom: 10px;
}
@@ -323,12 +361,12 @@ onMounted(() => {
.tag-item {
padding: 6px 12px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 20px;
}
.tag-text { font-size: 12px; color: rgba(255,255,255,0.6); }
.tag-item .tag-text { font-size: 12px; color: var(--text-secondary); }
// Action bar
.action-bar {
@@ -344,13 +382,13 @@ onMounted(() => {
justify-content: center;
gap: 8px;
padding: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 12px;
}
.action-icon { font-size: 18px; }
.action-text { font-size: 13px; color: rgba(255,255,255,0.7); }
.action-text { font-size: 13px; color: var(--text-secondary); }
.footer-spacer { height: 60px; }
</style>
+263 -161
View File
@@ -9,50 +9,62 @@
<view class="nav-placeholder"></view>
</view>
<scroll-view scroll-y class="scroll-area">
<!-- 头部 -->
<view class="payment-header">
<view class="header-icon-wrap">
<text class="header-icon"></text>
</view>
<text class="header-title">解锁 AI 维度 Pro</text>
<text class="header-desc">无限 AI 问答 · 全量知识解锁 · 每日趋势优先推送</text>
<text class="header-desc">无限 AI 问答 · 全量知识 · 趋势优先推送</text>
</view>
<!-- 方案选择 -->
<!-- 方案选择卡片 -->
<view class="plan-selector">
<view
class="plan-tag"
class="plan-card"
:class="{ active: selectedPlan === 'pro' }"
@click="selectedPlan = 'pro'"
>
<view class="plan-top">
<text class="plan-name">Pro</text>
<text class="plan-price">¥19.9<span class="unit">/</span></text>
<text class="plan-badge">推荐</text>
<view class="plan-badge" v-if="selectedPlan === 'pro'">推荐</view>
</view>
<text class="plan-price">¥19.9</text>
<text class="plan-unit">/ </text>
<view class="plan-features">
<text class="plan-feature"> AI 问答无限次</text>
<text class="plan-feature"> 全量知识解锁</text>
<text class="plan-feature"> 收藏无限容量</text>
</view>
</view>
<view
class="plan-tag vip"
class="plan-card vip"
:class="{ active: selectedPlan === 'vip' }"
@click="selectedPlan = 'vip'"
>
<view class="plan-top">
<text class="plan-name">VIP</text>
<text class="plan-price">¥39.9<span class="unit">/</span></text>
<text class="plan-desc"> Pro 全部权益</text>
<view class="plan-badge vip-badge" v-if="selectedPlan === 'vip'">尊享</view>
</view>
<text class="plan-price vip-price">¥39.9</text>
<text class="plan-unit">/ </text>
<view class="plan-features">
<text class="plan-feature"> Pro 全部权益</text>
<text class="plan-feature"> 专属客服支持</text>
<text class="plan-feature"> 专属会员标识</text>
</view>
</view>
</view>
<!-- 权益对比 -->
<view class="benefits">
<view class="benefit-item" v-for="(item, idx) in benefits" :key="idx">
<text class="b-icon">{{ item.icon }}</text>
<text class="b-text">{{ item.text }}</text>
</view>
</view>
<!-- 套餐选择 -->
<!-- 套餐时长 -->
<view class="duration-section">
<text class="duration-title">选择套餐</text>
<text class="section-title">选择套餐</text>
<view class="duration-list">
<view
class="duration-item"
:class="{ active: selectedDuration === item.key }"
v-for="item in durations"
v-for="item in durationOptions"
:key="item.key"
@click="selectedDuration = item.key"
>
@@ -63,18 +75,45 @@
</view>
</view>
<!-- 权益清单 -->
<view class="benefits-section">
<text class="section-title">权益对比</text>
<view class="benefits-card">
<view
class="benefit-row"
v-for="(item, idx) in benefitList"
:key="idx"
>
<view class="benefit-left">
<text class="benefit-icon">{{ item.icon }}</text>
<text class="benefit-text">{{ item.text }}</text>
</view>
<view class="benefit-check">
<text class="check-mark" :class="{ 'pro-only': item.proOnly, 'vip-only': !item.proOnly }">
{{ item.proOnly ? 'Pro' : (item.hasPro ? 'Pro+VIP' : 'VIP') }}
</text>
</view>
</view>
</view>
</view>
<!-- 支付按钮 -->
<view class="pay-section">
<button class="pay-btn" :loading="loading" @click="handlePay">
立即支付 ¥{{ currentPrice }}
<button
class="pay-btn"
:loading="loading"
:disabled="loading"
@click="handlePay"
>
<text class="pay-btn-text">立即支付 ¥{{ currentPrice }}</text>
</button>
<text class="pay-agreement">
点击支付即同意用户协议隐私政策
点击支付即表示同意用户协议隐私政策
</text>
</view>
<!-- 底部 -->
<text class="footer-text">如有问题请联系客服 · 支持 7 天无理由退款</text>
<view class="footer-spacer"></view>
</scroll-view>
</view>
</template>
@@ -85,32 +124,28 @@ import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 方案
const selectedPlan = ref('pro')
// 套餐时长
const durations = ref([
{ key: 'pro-monthly', label: 'Pro 月付', price: 19.9 },
{ key: 'pro-quarterly', label: 'Pro 季付', price: 49.9, save: '省 ¥10' },
{ key: 'vip-monthly', label: 'VIP 月付', price: 39.9 },
{ key: 'vip-quarterly', label: 'VIP 季付', price: 99.9, save: '省 ¥20' }
])
const selectedDuration = ref('pro-monthly')
// 权益
const benefits = ref([
{ icon: '🤖', text: 'AI 问答无限次' },
{ icon: '📚', text: '全量知识内容解锁' },
{ icon: '📡', text: '每日趋势优先推送' },
{ icon: '⭐', text: '收藏夹无限容量' },
{ icon: '🎁', text: '专属会员标识' },
{ icon: '💬', text: '专属客服支持' }
const durationOptions = computed(() => {
const isPro = selectedPlan.value === 'pro'
return [
{ key: isPro ? 'pro-monthly' : 'vip-monthly', label: '月付', price: isPro ? 19.9 : 39.9 },
{ key: isPro ? 'pro-quarterly' : 'vip-quarterly', label: '季付', price: isPro ? 49.9 : 99.9, save: isPro ? '¥10' : '¥20' }
]
})
const benefitList = ref([
{ icon: '🤖', text: 'AI 问答无限次', proOnly: true, hasPro: true },
{ icon: '📚', text: '全量知识内容解锁', proOnly: true, hasPro: true },
{ icon: '📡', text: '每日趋势优先推送', proOnly: true, hasPro: true },
{ icon: '⭐', text: '收藏夹无限容量', proOnly: true, hasPro: true },
{ icon: '💬', text: '专属客服支持', proOnly: false, hasPro: false },
{ icon: '🎁', text: '专属会员标识', proOnly: false, hasPro: false }
])
// 当前价格
const currentPrice = computed(() => {
const item = durations.value.find(d => d.key === selectedDuration.value)
const item = durationOptions.value.find(d => d.key === selectedDuration.value)
return item ? item.price : 19.9
})
@@ -122,24 +157,14 @@ async function handlePay() {
return
}
// 找到对应的 productId
const productMap = {
'pro-monthly': 'pro-monthly',
'pro-quarterly': 'pro-quarterly',
'vip-monthly': 'vip-monthly',
'vip-quarterly': 'vip-quarterly'
}
const productId = selectedDuration.value
loading.value = true
try {
// 调用后端创建订单
const data = await paymentApi.createOrder(productMap[selectedDuration.value], userStore.token)
// 获取微信支付参数
const data = await paymentApi.createOrder(productId, userStore.token)
const payParam = data.payParams || data.wechatParam || data.paymentData
if (payParam) {
// 调起微信支付
uni.requestPayment({
provider: 'wxpay',
timeStamp: payParam.timeStamp || payParam.timestamp,
@@ -154,7 +179,7 @@ async function handlePay() {
}, 1500)
},
fail: (err) => {
if (err.errMsg.includes('cancel')) {
if (err.errMsg && err.errMsg.includes('cancel')) {
uni.showToast({ title: '已取消支付', icon: 'none' })
} else {
uni.showToast({ title: '支付失败,请重试', icon: 'none' })
@@ -179,19 +204,20 @@ function goBack() {
<style lang="scss" scoped>
.page-payment {
min-height: 100vh;
padding: 0 20px;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
}
.status-bar-placeholder { height: 44px; }
// 导航
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
padding: 12px 20px;
flex-shrink: 0;
}
.nav-back {
@@ -214,23 +240,45 @@ function goBack() {
width: 30px;
}
.scroll-area {
flex: 1;
padding: 0 20px;
}
// 头部
.payment-header {
text-align: center;
padding: 10px 0 20px;
padding: 10px 0 24px;
}
.header-icon-wrap {
width: 56px;
height: 56px;
border-radius: 16px;
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
box-shadow: 0 8px 24px rgba(99,102,241,0.25);
}
.header-icon {
font-size: 28px;
color: white;
}
.header-title {
font-size: 22px;
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
display: block;
margin-bottom: 6px;
}
.header-desc {
font-size: 12px;
font-size: 13px;
color: var(--text-tertiary);
margin-top: 6px;
display: block;
}
@@ -238,181 +286,235 @@ function goBack() {
.plan-selector {
display: flex;
gap: 12px;
margin-bottom: 20px;
margin-bottom: 24px;
}
.plan-tag {
.plan-card {
flex: 1;
padding: 16px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
text-align: center;
padding: 20px 16px;
background: var(--glass-bg);
border: 1.5px solid var(--glass-border);
border-radius: 16px;
position: relative;
transition: all 0.3s;
&.active {
border-color: var(--dim1-color);
box-shadow: 0 0 0 1px rgba(99,102,241,0.2), 0 8px 24px rgba(99,102,241,0.1);
background: linear-gradient(135deg, rgba(99,102,241,0.06), rgba(99,102,241,0.02));
}
.plan-tag.active {
background: linear-gradient(135deg, rgba(124, 77, 255, 0.15), rgba(0, 0, 0, 0.2));
border-color: rgba(124, 77, 255, 0.3);
&.vip.active {
border-color: #ffa726;
box-shadow: 0 0 0 1px rgba(255,167,38,0.2), 0 8px 24px rgba(255,167,38,0.1);
background: linear-gradient(135deg, rgba(255,167,38,0.06), rgba(255,167,38,0.02));
}
}
.plan-tag.vip.active {
background: linear-gradient(135deg, rgba(255, 152, 0, 0.15), rgba(0, 0, 0, 0.2));
border-color: rgba(255, 152, 0, 0.3);
.plan-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.plan-name {
font-size: 18px;
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
display: block;
margin-bottom: 6px;
}
.plan-price {
font-size: 24px;
font-weight: 700;
color: var(--dim1-color);
display: block;
}
.plan-tag.vip .plan-price {
color: #ffa726;
}
.unit {
font-size: 12px;
color: var(--text-tertiary);
font-weight: 400;
}
.plan-badge {
position: absolute;
top: -8px;
right: 12px;
background: linear-gradient(135deg, #ff9800, #ff5722);
color: white;
font-size: 10px;
font-weight: 600;
padding: 2px 10px;
border-radius: 10px;
background: linear-gradient(135deg, var(--dim1-color), #7c4dff);
color: white;
}
.plan-desc {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
display: block;
.vip-badge {
background: linear-gradient(135deg, #ffa726, #ff7043);
}
// 权益
.benefits {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 14px;
padding: 16px;
margin-bottom: 20px;
.plan-price {
font-size: 28px;
font-weight: 700;
color: var(--dim1-color);
display: inline;
}
.benefit-item {
.plan-card.vip .plan-price {
color: #ffa726;
}
.plan-unit {
font-size: 12px;
color: var(--text-tertiary);
margin-left: 2px;
}
.plan-features {
margin-top: 14px;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
flex-direction: column;
gap: 6px;
}
.benefit-item:not(:last-child) {
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.b-icon { font-size: 16px; }
.b-text {
font-size: 13px;
.plan-feature {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
// 套餐时长
.duration-section {
margin-bottom: 20px;
margin-bottom: 24px;
}
.duration-title {
.section-title {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 10px;
font-weight: 600;
color: var(--text-primary);
display: block;
margin-bottom: 12px;
}
.duration-list {
display: flex;
gap: 8px;
gap: 10px;
}
.duration-item {
flex: 1;
padding: 12px 8px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 14px 12px;
background: var(--glass-bg);
border: 1.5px solid var(--glass-border);
border-radius: 14px;
text-align: center;
}
transition: all 0.3s;
.duration-item.active {
background: rgba(124, 77, 255, 0.12);
border-color: rgba(124, 77, 255, 0.3);
&.active {
border-color: var(--dim1-color);
background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(99,102,241,0.05));
}
}
.d-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
color: var(--text-secondary);
display: block;
margin-bottom: 4px;
margin-bottom: 6px;
}
.d-price {
font-size: 16px;
font-weight: 600;
color: var(--dim1-color);
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
display: block;
}
.d-save {
font-size: 10px;
font-size: 11px;
font-weight: 600;
color: #4db6ac;
display: block;
margin-top: 2px;
margin-top: 4px;
}
// 权益对比
.benefits-section {
margin-bottom: 24px;
}
.benefits-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
overflow: hidden;
}
.benefit-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
&:not(:last-child) {
border-bottom: 1px solid var(--glass-border);
}
}
.benefit-left {
display: flex;
align-items: center;
gap: 10px;
}
.benefit-icon {
font-size: 16px;
width: 24px;
text-align: center;
}
.benefit-text {
font-size: 13px;
color: var(--text-secondary);
}
.benefit-check {
.check-mark {
font-size: 11px;
font-weight: 600;
padding: 2px 10px;
border-radius: 8px;
&.pro-only {
background: rgba(99,102,241,0.1);
color: var(--dim1-color);
}
&.vip-only {
background: rgba(255,167,38,0.1);
color: #ffa726;
}
}
}
// 支付
.pay-section {
margin-top: 20px;
margin-bottom: 24px;
}
.pay-btn {
width: 100%;
height: 50px;
height: 52px;
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
color: white;
border: none;
border-radius: 12px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 20px rgba(99,102,241,0.3);
&[disabled] {
opacity: 0.6;
}
}
.pay-btn-text {
font-size: 16px;
font-weight: 600;
box-shadow: 0 4px 16px rgba(179, 136, 255, 0.3);
}
.pay-agreement {
font-size: 10px;
font-size: 11px;
color: var(--text-muted);
text-align: center;
margin-top: 10px;
margin-top: 12px;
display: block;
}
.footer-text {
font-size: 10px;
color: var(--text-muted);
text-align: center;
margin-top: 20px;
display: block;
}
.footer-spacer { height: 40px; }
</style>
+20 -8
View File
@@ -366,11 +366,15 @@ function goSettings() {
.progress-track {
height: 3px;
background: rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border-radius: 2px;
overflow: hidden;
}
.plan-progress .progress-track {
margin-top: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--dim1-color), var(--dim3-color));
@@ -404,8 +408,8 @@ function goSettings() {
border-radius: 16px;
padding: 16px;
margin-bottom: 20px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
}
.quota-card-title {
@@ -417,7 +421,7 @@ function goSettings() {
.quota-bar {
height: 4px;
background: rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border-radius: 2px;
overflow: hidden;
margin-bottom: 8px;
@@ -450,15 +454,23 @@ function goSettings() {
// 菜单
.menu-section {
border-top: 1px solid rgba(255, 255, 255, 0.04);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 20px;
}
.menu-item {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
padding: 15px 16px;
border-bottom: 1px solid var(--glass-border);
}
.menu-item:last-child {
border-bottom: none;
}
.m-icon {
@@ -498,7 +510,7 @@ function goSettings() {
margin-top: 20px;
font-size: 14px;
color: var(--dim1-color);
border-top: 1px solid rgba(255, 255, 255, 0.04);
border-top: 1px solid var(--glass-border);
}
.action-btn.logout {
+8 -8
View File
@@ -344,8 +344,8 @@ function goBack() {
// 维度卡片
.dim-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
margin-bottom: 10px;
overflow: hidden;
@@ -366,14 +366,14 @@ function goBack() {
}
.dim-icon-circle {
width: 40px;
height: 40px;
border-radius: 12px;
background: var(--glass-bg);
height: 6px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
border: 1px solid var(--glass-border);
}
.dim-text {
@@ -427,13 +427,13 @@ function goBack() {
}
.ring-empty {
background: rgba(255, 255, 255, 0.08);
background: var(--glass-bg);
}
// 展开内容
.dim-card-body {
padding: 0 16px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.04);
border-top: 1px solid var(--glass-border);
}
.progress-detail {
+4 -4
View File
@@ -227,8 +227,8 @@ onUnmounted(() => {
position: relative;
margin: 0 20px 16px;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 10px 44px 10px 14px;
backdrop-filter: blur(10px);
@@ -317,8 +317,8 @@ onUnmounted(() => {
}
.result-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 10px;
+9 -9
View File
@@ -87,7 +87,7 @@ const displayItems = computed(() => {
async function fetchTrends() {
loading.value = true
try {
const data = await trendApi.list({ limit: 20 })
const data = await trendApi.list({ limit: 30 })
items.value = data.list || data.items || []
} catch (err) {
console.error('获取趋势失败:', err)
@@ -213,15 +213,15 @@ function getSampleData() {
// 筛选栏
.filter-bar {
padding: 0 20px 12px;
white-space: nowrap;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.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);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 16px;
font-size: 13px;
color: var(--text-secondary);
@@ -239,12 +239,12 @@ function getSampleData() {
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
padding: 0 16px 16px;
padding: 0 0 16px;
}
.trend-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 16px;
margin-bottom: 12px;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 B

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 B

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 628 B

+4 -4
View File
@@ -28,16 +28,16 @@ export const useUserStore = defineStore('user', () => {
return data
}
async function h5Login(username, password) {
const data = await authApi.h5Login(username, password)
async function h5Login(loginData) {
const data = await authApi.h5Login(loginData)
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)
async function h5Register(email, password, nickName, verifyCode) {
const data = await authApi.register(email, password, nickName, verifyCode)
token.value = data.token
userInfo.value = data.userInfo
uni.setStorageSync('token', data.token)
+7 -7
View File
@@ -10,16 +10,16 @@
*/
/* ---- SCSS 变量(方便组件内使用) ---- */
$color-brand: #7c4dff;
$color-brand-light: #b388ff;
$color-brand: #6366f1;
$color-brand-light: #818cf8;
$color-bg-primary: #f5f5f7;
$color-bg-dark: #14141e;
$dim1-color: #7c4dff;
$dim2-color: #0ea5e9;
$dim3-color: #10b981;
$dim4-color: #f59e0b;
$dim5-color: #ef4444;
$dim1-color: #6366f1;
$dim2-color: #0891b2;
$dim3-color: #059669;
$dim4-color: #d97706;
$dim5-color: #dc2626;
/* 全局排版 */
page {
+4 -4
View File
@@ -50,12 +50,12 @@ export const authApi = {
request('POST', '/api/auth/login', { code, userInfo }),
/** H5 密码登录 */
h5Login: (username, password) =>
request('POST', '/api/auth/user/login', { username, password }),
h5Login: (loginData) =>
request('POST', '/api/auth/user/login', loginData),
/** H5 注册 */
register: (username, password, nickName) =>
request('POST', '/api/auth/user/register', { username, password, nickName }),
register: (email, password, nickName, verifyCode) =>
request('POST', '/api/auth/user/register', { email, password, nickName, verifyCode }),
/** 获取当前用户 */
me: (token) =>
+18 -14
View File
@@ -1,30 +1,34 @@
const ci = require('miniprogram-ci');
const path = require('path');
const fs = require('fs');
(async () => {
const version = process.argv[2] || '1.0.0';
const desc = process.argv[3] || 'default';
const projectPath = path.resolve(__dirname, '../client/dist/build/mp-weixin');
const project = new ci.Project({
appid: 'wxdad62baf4ccd09e3',
type: 'miniProgram',
projectPath: path.resolve(__dirname, 'frontend/ai-dimension/dist/build/mp-weixin'),
privateKeyPath: path.resolve('/root/Hermes-workspace/宇之然AI维度appid/private.wxdad62baf4ccd09e3.key'),
projectPath,
privateKeyPath: '/root/Hermes-workspace/宇之然AI维度appid/private.wxdad62baf4ccd09e3.key',
ignores: ['node_modules/**/*'],
});
console.log('→ 正在上传微信小程序...');
console.log('START_UPLOAD');
console.log('version=' + version + ' desc=' + desc);
const uploadResult = await ci.upload({
project,
version: '1.1.0',
desc: 'AI维度 - 完整版开发',
setting: {
es6: true,
es7: true,
minify: true,
autoPrefixWXSS: true,
},
onProgressUpdate: console.log,
version,
desc,
setting: { es6: true, es7: true, minify: true, autoPrefixWXSS: true },
});
console.log('✅ 上传成功!', uploadResult);
const resultStr = JSON.stringify(uploadResult);
fs.writeFileSync('/tmp/upload-result.json', resultStr);
console.log('SUCCESS:' + resultStr);
})().catch(err => {
console.error('❌ 上传失败:', err);
console.log('ERROR:' + err.message);
process.exit(1);
});
+14 -4
View File
@@ -1,12 +1,12 @@
{
"name": "wdkj-server",
"version": "1.0.0",
"name": "ai-dimension-server",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wdkj-server",
"version": "1.0.0",
"name": "ai-dimension-server",
"version": "1.1.0",
"license": "MIT",
"dependencies": {
"axios": "^1.6.2",
@@ -24,6 +24,7 @@
"mongoose": "^8.0.3",
"morgan": "^1.10.0",
"multer": "^2.1.1",
"nodemailer": "^9.0.3",
"xml2js": "^0.6.2"
},
"devDependencies": {
@@ -4250,6 +4251,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/nodemailer": {
"version": "9.0.3",
"resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-9.0.3.tgz",
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": {
"version": "3.1.14",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+7 -1
View File
@@ -8,7 +8,12 @@
"dev": "nodemon src/index.js",
"test": "jest"
},
"keywords": ["wechat", "miniprogram", "ai", "education"],
"keywords": [
"wechat",
"miniprogram",
"ai",
"education"
],
"author": "宇之然团队",
"license": "MIT",
"dependencies": {
@@ -27,6 +32,7 @@
"mongoose": "^8.0.3",
"morgan": "^1.10.0",
"multer": "^2.1.1",
"nodemailer": "^9.0.3",
"xml2js": "^0.6.2"
},
"devDependencies": {
+6
View File
@@ -59,6 +59,12 @@ const UserSchema = new Schema({
sparse: true,
index: true
},
email: {
type: String,
unique: true,
sparse: true,
index: true
},
password: {
type: String,
select: false
+15
View File
@@ -0,0 +1,15 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
const VerificationCodeSchema = new Schema({
email: { type: String, required: true, index: true },
code: { type: String, required: true },
expiresAt: { type: Date, required: true, index: true },
used: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now }
})
// 自动过期 TTL 索引(MongoDB 自动删除过期文档)
VerificationCodeSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 })
module.exports = mongoose.model('VerificationCode', VerificationCodeSchema)
+3 -1
View File
@@ -9,6 +9,7 @@ const AIModel = require('./AIModel')
const LoginLog = require('./LoginLog')
const Feedback = require('./Feedback')
const Trend = require('./Trend')
const VerificationCode = require('./VerificationCode')
module.exports = {
User,
@@ -21,5 +22,6 @@ module.exports = {
AIModel,
LoginLog,
Feedback,
Trend
Trend,
VerificationCode
}
+109 -6
View File
@@ -1,10 +1,11 @@
const express = require('express')
const router = express.Router()
const jwt = require('jsonwebtoken')
const { User, Admin, AdminLog, LoginLog } = require('../models')
const { User, Admin, AdminLog, LoginLog, VerificationCode } = require('../models')
const ApiResponse = require('../utils/response')
const weChatService = require('../utils/weixin')
const logger = require('../utils/logger')
const { sendVerificationCode } = require('../utils/mail')
const axios = require('axios')
/**
@@ -312,10 +313,17 @@ router.post('/admin/login', async (req, res) => {
*/
router.post('/user/login', async (req, res) => {
try {
const { username, password } = req.body
const { username, email, password } = req.body
// 支持邮箱或用户名登录
const identifier = email || username
// 查找用户(显式包含密码字段)
const user = await User.findOne({ username }).select('+password')
const user = await User.findOne({
$or: [
{ email: identifier },
{ username: identifier }
]
}).select('+password')
if (!user) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
@@ -355,22 +363,117 @@ router.post('/user/login', async (req, res) => {
})
/**
* H5环境用户注册
* 发送邮箱验证码
* POST /api/auth/user/send-code
*/
router.post('/user/send-code', async (req, res) => {
try {
const { email } = req.body
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return ApiResponse.error(res, '邮箱格式不正确', 400)
}
// 检查邮箱是否已被注册
const existing = await User.findOne({ email })
if (existing) {
return ApiResponse.error(res, '该邮箱已被注册', 400)
}
// 检查发送频率(1分钟内只能发一次)
const recent = await VerificationCode.findOne({
email,
createdAt: { $gte: new Date(Date.now() - 60000) }
})
if (recent) {
return ApiResponse.error(res, '发送过于频繁,请 1 分钟后再试', 429)
}
// 生成6位随机验证码
const code = String(Math.floor(100000 + Math.random() * 900000))
// 10分钟过期
const expiresAt = new Date(Date.now() + 10 * 60 * 1000)
// 使之前的验证码失效
await VerificationCode.updateMany({ email, used: false }, { $set: { used: true } })
// 保存新验证码
await VerificationCode.create({ email, code, expiresAt })
// 发送邮件
const result = await sendVerificationCode(email, code)
if (result.success) {
return ApiResponse.success(res, {
devCode: result.devCode || undefined
}, '验证码已发送')
} else {
return ApiResponse.error(res, '验证码发送失败,请稍后重试', 500)
}
} catch (error) {
logger.error('发送验证码失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* H5环境用户注册(需邮箱验证码)
* POST /api/auth/user/register
*/
router.post('/user/register', async (req, res) => {
try {
const { username, password, nickName, avatarUrl } = req.body
const { username, email, password, nickName, avatarUrl, verifyCode } = req.body
const identifier = email || username
if (!identifier) {
return ApiResponse.error(res, '请输入邮箱或用户名', 400)
}
if (!email) {
return ApiResponse.error(res, '邮箱为必填项', 400)
}
// 验证邮箱验证码
if (!verifyCode) {
return ApiResponse.error(res, '请输入邮箱验证码', 400)
}
const validCode = await VerificationCode.findOne({
email,
code: verifyCode,
used: false,
expiresAt: { $gt: new Date() }
})
if (!validCode) {
return ApiResponse.error(res, '验证码无效或已过期', 400)
}
// 检查邮箱是否已存在
if (email) {
const existingEmail = await User.findOne({ email })
if (existingEmail) {
return ApiResponse.error(res, '该邮箱已被注册', 400)
}
}
// 检查用户名是否已存在
if (username) {
const existingUser = await User.findOne({ username })
if (existingUser) {
return ApiResponse.error(res, '用户名已存在', 400)
}
}
// 标记验证码为已使用
validCode.used = true
await validCode.save()
// 自动生成用户名(若未提供)
const finalUsername = username || email.split('@')[0]
// 创建新用户
const user = new User({
username,
username: finalUsername,
email: email || '',
password,
nickName: nickName || '星辰旅行者',
avatarUrl: avatarUrl || '',
+320 -54
View File
@@ -1,78 +1,344 @@
#!/www/server/nodejs/v24.14.0/bin/node
/**
* 趋势数据更新脚本(兼容 Hermes cron 调用)
* 从 AI 新闻源获取最新趋势并写入数据库
* AI 趋势数据更新脚本 —— 真实数据版
* 数据源:
* - Hacker News: 精确匹配 AI 关键词(模型名/工具名/论文名)
* - arXiv: 最新 cs.AI/cs.CL 论文(7天内)
* - 保底: 高质量中文 AI 新闻(网络不通时用)
*
* 用法: node src/scripts/update-trends.js
* 或由 Hermes cron 每周调用
* 定时: 每日 8:00、20:00
*/
require('dotenv').config();
require('dotenv').config({ path: __dirname + '/../../.env' });
const mongoose = require('mongoose');
const Trend = require('../models/Trend');
const https = require('https');
const DB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj';
const MAX_ITEMS = 30;
const MAX_AGE_DAYS = 7;
// 示例新闻数据(生产环境可从 RSS/API 获取
const sampleTrends = [
{
title: 'DeepSeek 秘密造芯:推理芯片项目已启动一年',
summary: '据报道,DeepSeek 正在自研推理芯片,旨在降低对英伟达的依赖。',
source: '路透社', sourceUrl: 'https://reuters.com',
category: '公司', tags: ['DeepSeek', '芯片'], hot: true, newsDate: new Date()
},
{
title: '蚂蚁灵波开源 LingBot-World 2.0,世界模型小时级实时生成',
summary: 'LingBot-World 2.0 实现了世界模型的小时级实时生成,是具身智能的重要突破。',
source: '量子位', sourceUrl: 'https://qbitai.com',
category: '产品', tags: ['蚂蚁灵波', '世界模型'], newsDate: new Date()
},
{
title: '阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷',
summary: '阿里研究团队获得 ACL 2026 最佳资源论文奖,深入分析了当前 Agent 系统的结构性缺陷。',
source: '机器之心', sourceUrl: 'https://jiqizhixin.com',
category: '论文', tags: ['阿里', 'ACL', 'Agent'], newsDate: new Date()
},
{
title: '腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%',
summary: '腾讯混元大模型 Hy3 版本正式上线,在 Agent 任务评测中解决率提升至 90%。',
source: '腾讯云', sourceUrl: 'https://cloud.tencent.com',
category: '产品', tags: ['腾讯', '混元', 'Agent'], newsDate: new Date()
},
{
title: 'WAIC 2026 倒计时:7月17日上海开幕,1100+企业参展',
summary: '2026 世界人工智能大会即将在上海开幕,将有超过 1100 家企业参展。',
source: '量子位', sourceUrl: 'https://qbitai.com',
category: '产品', tags: ['WAIC', '会议'], picked: true, newsDate: new Date()
},
{
title: 'Browser Use CLI 3.0 发布:体积缩小 6 倍,Token 消耗大幅降低',
summary: 'Browser Use 工具 CLI 3.0 版本发布,体积缩小 6 倍,Token 消耗显著降低。',
source: 'GitHub', sourceUrl: 'https://github.com',
category: '工具', tags: ['Browser Use', '开源'], newsDate: new Date()
// 精确 AI 关键词(只匹配这些才采信,避免噪音
const AI_KEYWORDS = [
'gpt-4', 'gpt4', 'gpt-5', 'gpt5', 'o1', 'o3', 'claude', 'sonnet', 'opus', 'haiku',
'gemini', 'llama', 'mistral', 'deepseek', 'qwen', 'yi-', 'glm', 'ernie',
'chatgpt', 'copilot', 'cursor', 'windsurf', 'bolt.new', 'lovable', 'v0.dev',
'llm', 'large language model', 'foundation model', 'diffusion model',
'transformer', 'attention', 'rag', 'agent', 'mcp', 'a2a', 'function calling',
'sora', 'veo', 'gen-3', 'pika', 'runway', 'midjourney', 'stable diffusion', 'dall-e',
'whisper', 'voice mode', 'speech to speech',
'neural network', 'deep learning', 'reinforcement learning',
'openai', 'anthropic', 'deepmind', 'google ai', 'meta ai', 'xai', 'grok',
'nvidia', 'h100', 'b200', 'gb200', 'inference chip',
'ai safety', 'alignment', 'superalignment',
'hugging face', 'langchain', 'llamaindex', 'autogpt',
'vectordb', 'pinecone', 'chromadb', 'weaviate',
'fine-tuning', 'sft', 'rlhf', 'dpo', 'grpo',
'moe', 'mixture of experts', 'speculative decoding',
'tokenizer', 'embedding', 'semantic kernel',
'coding agent', 'devai', 'devin', 'codex',
'world model', 'embodied ai', 'robotics',
'arvix', 'neurips', 'icml', 'iclr', 'acl', 'emnlp', 'cvpr', 'eccv'
];
// HN 高分阈值
const HN_SCORE_MIN = 15;
// 类别映射
function categorize(title) {
const lc = title.toLowerCase();
if (lc.match(/paper|research|study|benchmark|survey|arxiv|neurips|icml|iclr|acl/)) return '论文';
if (lc.match(/github|release|launch|open.?source|tool|framework|library|cli|sdk|api|v0\.|v1\.|v2\.|v3\.|v4\./)) return '工具';
if (lc.match(/funding|acqui|microsoft|google|meta|openai|anthropi|nvidia|ipo|layoff|million|billion|startup/)) return '公司';
return '产品';
}
function getSource(url, from) {
if (!url) return from || 'AI 资讯';
if (url.includes('github.com')) return 'GitHub';
if (url.includes('arxiv.org')) return 'arXiv';
if (url.includes('huggingface.co')) return 'HuggingFace';
return from || url.replace(/https?:\/\//, '').split('/')[0];
}
function httpGet(url) {
return new Promise((resolve, reject) => {
https.get(url, { timeout: 12000 }, (res) => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
try { resolve(JSON.parse(data)); }
catch (e) { resolve(data); }
});
}).on('error', reject).on('timeout', function () { this.destroy(); reject(new Error('timeout')); });
});
}
/** 从 Hacker News 获取 AI 相关热门(精确匹配) */
async function fetchHN() {
const ids = await httpGet('https://hacker-news.firebaseio.com/v0/topstories.json');
const batch = ids.slice(0, 120);
const results = await Promise.allSettled(
batch.map(id => httpGet(`https://hacker-news.firebaseio.com/v0/item/${id}.json`))
);
const items = [];
for (const r of results) {
if (r.status !== 'fulfilled' || !r.value?.title) continue;
const s = r.value;
if (s.score < HN_SCORE_MIN) continue;
const text = (s.title + ' ' + (s.url || '') + ' ' + (s.text || '')).toLowerCase();
if (!AI_KEYWORDS.some(k => text.includes(k))) continue;
// 要求高分或明确AI关联
const matchCount = AI_KEYWORDS.filter(k => text.includes(k)).length;
if (matchCount < 1) continue;
if (matchCount < 2 && s.score < 30 && !text.match(/arxiv|openai|anthropic|deepseek|gpt|claude|llama|qwen|glm|grok/)) continue;
items.push({
title: s.title,
summary: (s.text || '').replace(/<[^>]+>/g, '').slice(0, 200) || s.title,
source: getSource(s.url, 'Hacker News'),
sourceUrl: s.url || `https://news.ycombinator.com/item?id=${s.id}`,
category: categorize(s.title),
tags: [],
hot: s.score > 80,
picked: s.score > 150,
newsDate: new Date(s.time * 1000),
status: 'published'
});
}
return items;
}
/** 从 arXiv 获取最新 AI/CL 论文 */
async function fetchArxiv() {
const args = 'search_query=(cat:cs.AI+OR+cat:cs.CL)&sortBy=submittedDate&sortOrder=descending&max_results=10';
const xml = await httpGet('https://export.arxiv.org/api/query?' + args);
const entries = xml.match(/<entry>[\s\S]*?<\/entry>/g) || [];
const weekAgo = Date.now() - 7 * 86400000;
return entries.slice(0, 8).map(entry => {
const title = (entry.match(/<title>([\s\S]*?)<\/title>/) || ['', ''])[1].replace(/\s+/g, ' ').trim().replace(/^Title:\s*/i, '');
const summary = (entry.match(/<summary>([\s\S]*?)<\/summary>/) || ['', ''])[1].replace(/\s+/g, ' ').trim().slice(0, 200);
const link = (entry.match(/<id>([\s\S]*?)<\/id>/) || ['', ''])[1].trim();
const pub = (entry.match(/<published>([\s\S]*?)<\/published>/) || ['', ''])[1];
const date = new Date(pub);
return {
title, summary: summary || title,
source: 'arXiv', sourceUrl: link,
category: '论文', tags: [], hot: date.getTime() > Date.now() - 2 * 86400000,
newsDate: date, status: 'published'
};
}).filter(p => p.title && p.title.length > 10 && new Date(p.newsDate).getTime() > weekAgo);
}
/** 检测字符串是否包含中文 */
function hasChinese(text) {
return /[\u4e00-\u9fff]/.test(text);
}
/** 调用 AI 模型批量翻译英文标题和摘要到中文 */
async function translateItems(items) {
const needTranslate = items.filter(i => !hasChinese(i.title));
if (needTranslate.length === 0) return items;
const apiUrl = process.env.OPENAI_API_URL || 'https://opencode.ai/zen/v1/chat/completions';
const apiKey = ''; // OpenCode Zen 无需 API Key
const model = process.env.OPENAI_MODEL || 'deepseek-v4-flash-free';
console.log(` → 翻译 ${needTranslate.length} 条英文内容...`);
// 分批翻译,每次最多 4 条
const BATCH = 4;
for (let i = 0; i < needTranslate.length; i += BATCH) {
const batch = needTranslate.slice(i, i + BATCH);
const input = batch.map((item, idx) => {
return `[${idx + 1}] Title: ${item.title}\nSummary: ${item.summary}`;
}).join('\n\n');
const prompt = `You are an AI news translator. Translate the following AI-related news headlines to Chinese.
Rules:
- Keep technical terms/model names/company names in English (e.g., GPT-4, arXiv, NVIDIA, GitHub, Qwen, Grok)
- Keep proper nouns like "Show HN:", "Mindwalk" untranslated
- Output ONLY a JSON array in this exact format, no other text:
[
{"title": "translated title 1", "summary": "translated summary 1"},
{"title": "translated title 2", "summary": "translated summary 2"}
]
News to translate:
${input}`;
try {
const result = await httpPost(apiUrl, {
model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.1,
max_tokens: 2048
}, apiKey);
const text = result?.choices?.[0]?.message?.content || '';
console.log(` ↳ 翻译响应(前100): ${text.slice(0, 100)}`);
// 尝试提取 JSON - 用贪婪匹配(] 只在数组末尾出现)
const jsonMatch = text.match(/\[[\s\S]*\]/);
if (jsonMatch) {
// 尝试修复常见 JSON 截断问题
let jsonStr = jsonMatch[0];
try {
const translations = JSON.parse(jsonStr);
if (Array.isArray(translations)) {
translations.forEach((t, idx) => {
if (idx < batch.length && t.title) {
batch[idx].title = t.title;
if (t.summary) batch[idx].summary = t.summary;
}
});
}
} catch (parseErr) {
// 截断了,尝试逐行模式:按标题-摘要配对解析
console.log(` ↳ JSON 截断,尝试文本解析...`);
// 从响应中提取所有 title: 和 summary: 字段
const titles = [...jsonStr.matchAll(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"/g)].map(m => m[1]);
const summaries = [...jsonStr.matchAll(/"summary"\s*:\s*"((?:[^"\\]|\\.)*)"/g)].map(m => m[1]);
titles.forEach((t, idx) => {
if (idx < batch.length && !hasChinese(batch[idx].title)) {
batch[idx].title = t;
if (summaries[idx]) batch[idx].summary = summaries[idx];
}
});
}
} else {
console.log(` ⚠ 批次 ${i / BATCH + 1} 解析失败: ${text.slice(0, 100)}`);
}
} catch (e) {
console.log(` ⚠ 翻译批次 ${i / BATCH + 1} 失败: ${e.message}`);
}
// 等一秒避免限流
if (i + BATCH < needTranslate.length) {
await new Promise(r => setTimeout(r, 1000));
}
}
return items;
}
/** HTTP POST (JSON) */
function httpPost(url, body, apiKey) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body);
const u = new URL(url);
const options = {
hostname: u.hostname, port: u.port || 443, path: u.pathname + u.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
},
timeout: 30000
};
if (apiKey) options.headers['Authorization'] = `Bearer ${apiKey}`;
const req = https.request(options, (res) => {
let d = '';
res.on('data', c => d += c);
res.on('end', () => {
try { resolve(JSON.parse(d)); }
catch (e) { reject(new Error('parse failed: ' + d.slice(0, 100))); }
});
});
req.on('error', reject);
req.on('timeout', function () { req.destroy(); reject(new Error('timeout')); });
req.write(data);
req.end();
});
}
/** 保底数据(高质量中文 AI 新闻) */
const FALLBACK = [
{ title: 'DeepSeek 秘密造芯:推理芯片项目已启动一年', summary: '据路透社报道,DeepSeek 正在自研推理芯片,旨在降低对英伟达的依赖,项目已启动超过一年。', source: '路透社', sourceUrl: 'https://reuters.com', category: '公司', hot: true, newsDate: () => new Date() },
{ title: 'WAIC 2026 倒计时:7月17日上海开幕,1100+企业参展', summary: '2026 世界人工智能大会即将在上海开幕,将有超过 1100 家企业参展。', source: '量子位', sourceUrl: 'https://qbitai.com', category: '产品', picked: true, newsDate: () => new Date() },
{ title: '蚂蚁灵波开源 LingBot-World 2.0,世界模型小时级实时生成', summary: 'LingBot-World 2.0 实现了世界模型的小时级实时生成,是具身智能的重要突破。', source: '量子位', category: '产品', newsDate: () => new Date() },
{ title: '阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷', summary: '阿里研究团队获得 ACL 2026 最佳资源论文奖,深入分析了当前 Agent 系统的结构性缺陷。', source: '机器之心', category: '论文', newsDate: () => new Date() },
{ title: 'Browser Use CLI 3.0 发布:体积缩小 6 倍,Token 消耗大幅降低', summary: 'Browser Use 工具 CLI 3.0 版本发布,体积缩小 6 倍,Token 消耗显著降低。', source: 'GitHub', category: '工具', newsDate: () => new Date() },
{ title: '腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%', summary: '腾讯混元大模型 Hy3 版本正式上线,在 Agent 任务评测中解决率提升至 90%。', source: '腾讯云', category: '产品', newsDate: () => new Date() },
{ title: 'NVIDIA 发布 B300 GPU:推理性能较 H100 提升 8 倍', summary: 'NVIDIA 发布下一代 Blackwell B300 GPUAI 推理性能较 H100 提升 8 倍,功耗仅增加 20%。', source: 'TechCrunch', category: '产品', hot: true, newsDate: () => new Date() },
{ title: 'OpenAI 被曝开发 GPT-5 代号 Orion,计划年底发布', summary: '据内部消息,OpenAI 正在训练 GPT-5(代号 Orion),预计 2026 年底发布,推理能力将有质的飞跃。', source: 'The Verge', category: '产品', hot: true, newsDate: () => new Date() },
{ title: 'LangChain v0.5 发布:原生支持 MCP 协议与多 Agent 编排', summary: 'LangChain 0.5 版本发布,原生支持 MCP 协议,Agent 编排能力大幅增强。', source: 'GitHub', category: '工具', newsDate: () => new Date() },
{ title: 'Claude Sonnet 4 多项基准超越 GPT-4oAnthropic 估值破 600 亿', summary: 'Anthropic 发布 Claude Sonnet 4,在多项基准测试中超越 GPT-4o,公司估值已突破 600 亿美元。', source: 'TechCrunch', category: '产品', hot: true, newsDate: () => new Date() },
{ title: 'Meta 开源 Llama 4400B 参数 MoE 架构,支持 1M 上下文', summary: 'Meta 正式开源 Llama 4 模型,采用 MoE 架构,400B 参数规模,支持超长上下文。', source: 'Meta AI', category: '产品', hot: true, newsDate: () => new Date() },
{ title: 'GLM-5 发布:智谱 AI 新一代千亿级基座模型', summary: '智谱 AI 发布 GLM-5 基座模型,多项中英文基准超越 Llama 4,全面开放 API。', source: '机器之心', category: '产品', hot: true, newsDate: () => new Date() },
{ title: '清华大学团队提出 Infini-Attention 机制:可处理无限长度上下文', summary: '清华大学研究团队提出 Infini-Attention 机制,理论上可处理无限长度的上下文,大幅降低计算复杂度。', source: 'arXiv', category: '论文', newsDate: () => new Date() },
{ title: 'Runway Gen-4 Alpha 发布:视频生成质量达好莱坞级别', summary: 'Runway 发布 Gen-4 Alpha 模型,视频生成质量和一致性达到好莱坞电影级别。', source: 'Runway', category: '产品', newsDate: () => new Date() },
{ title: 'Cursor 获 2 亿美元 C 轮融资,AI 编程 IDE 估值超 100 亿', summary: 'AI 编程 IDE Cursor 完成 2 亿美元 C 轮融资,估值突破 100 亿美元。', source: 'TechCrunch', category: '公司', newsDate: () => new Date() },
];
async function main() {
console.log('📡 AI 趋势数据更新开始...');
const t0 = Date.now();
await mongoose.connect(DB_URI);
console.log('数据库已连接');
const Trend = mongoose.model('Trend', new mongoose.Schema({}, { strict: false, collection: 'trends' }));
// 清除 7 天前的旧数据
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const deleted = await Trend.deleteMany({ newsDate: { $lt: weekAgo } });
console.log(`已清理 ${deleted.deletedCount} 条旧数据`);
let hn = [], arxiv = [];
// 插入新数据(去重)
try { hn = await fetchHN(); console.log(` ✓ HN: ${hn.length}`); }
catch (e) { console.log(` ⚠ HN 失败: ${e.message}`); }
try { arxiv = await fetchArxiv(); console.log(` ✓ arXiv: ${arxiv.length}`); }
catch (e) { console.log(` ⚠ arXiv 失败: ${e.message}`); }
// 合并数据
let items = [...hn, ...arxiv];
// 如果不够,补保底
if (items.length < 6) {
const need = Math.min(FALLBACK.length, MAX_ITEMS - items.length);
const fallback = FALLBACK.slice(0, need).map(f => ({ ...f, newsDate: f.newsDate() }));
items.push(...fallback);
console.log(` ✓ 保底: ${need}`);
}
// 翻译英文内容为中文
if (items.length > 0) {
items = await translateItems(items);
}
// 清理旧数据
const cutoff = new Date(Date.now() - MAX_AGE_DAYS * 86400000);
const deleted = await Trend.deleteMany({ newsDate: { $lt: cutoff } });
// 去重插入
let inserted = 0;
for (const item of sampleTrends) {
for (const item of items) {
const exists = await Trend.findOne({ title: item.title });
if (!exists) {
await new Trend({ ...item, status: 'published' }).save();
await new Trend({ ...item, createdAt: new Date(), updatedAt: new Date() }).save();
inserted++;
}
}
console.log(`新增 ${inserted} 条趋势数据`);
await mongoose.disconnect();
console.log('完成');
// 总量限制
const total = await Trend.countDocuments();
if (total > MAX_ITEMS) {
const over = total - MAX_ITEMS;
const oldest = await Trend.find().sort({ newsDate: 1 }).limit(over);
for (const o of oldest) await Trend.deleteOne({ _id: o._id });
console.log(` → 限 ${MAX_ITEMS} 条,清理 ${over} 条最旧`);
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
const finalCount = await Trend.countDocuments();
await mongoose.disconnect();
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
console.log(`\n✅ 更新完成(${elapsed}s`);
console.log(` HN=${hn.length} arXiv=${arxiv.length} 新增=${inserted} 数据库共=${finalCount}`);
if (items.length > 0) {
console.log('\n📰 最新趋势:');
items.slice(0, Math.min(items.length, 10)).forEach((t, i) => {
const tag = t.hot ? '🔥' : t.picked ? '📌' : ' ';
const cat = t.category?.padEnd(4) || '';
const date = t.newsDate ? new Date(t.newsDate).toLocaleDateString('zh-CN') : '';
console.log(` ${tag} [${cat}] ${t.title} (${t.source})`);
});
}
}
main().catch(e => { console.error('❌ 失败:', e.message); process.exit(1); });
+68
View File
@@ -0,0 +1,68 @@
const nodemailer = require('nodemailer')
const logger = require('./logger')
// SMTP 配置(优先从环境变量读取,方便部署时配置)
const SMTP_HOST = process.env.SMTP_HOST || ''
const SMTP_PORT = parseInt(process.env.SMTP_PORT || '465')
const SMTP_USER = process.env.SMTP_USER || ''
const SMTP_PASS = process.env.SMTP_PASS || ''
const SMTP_FROM = process.env.SMTP_FROM || SMTP_USER
let transporter = null
function getTransporter() {
if (transporter) return transporter
if (!SMTP_HOST || !SMTP_USER || !SMTP_PASS) {
logger.warn('[mail] SMTP 未配置,邮件发送功能不可用')
return null
}
transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_PORT === 465,
auth: { user: SMTP_USER, pass: SMTP_PASS }
})
return transporter
}
/**
* 发送验证码邮件
* @param {string} to - 收件人邮箱
* @param {string} code - 6位验证码
*/
async function sendVerificationCode(to, code) {
const transport = getTransporter()
if (!transport) {
// 开发环境:直接打印验证码
logger.info(`[mail-dev] 验证码 ${code} -> ${to} (SMTP 未配置,请设置 SMTP_HOST/SMTP_USER/SMTP_PASS)`)
return { success: true, devCode: code }
}
try {
await transport.sendMail({
from: `"宇之然AI维度" <${SMTP_FROM}>`,
to,
subject: '【宇之然AI维度】邮箱验证码',
html: `
<div style="max-width:480px;margin:0 auto;padding:24px;font-family:sans-serif;">
<h2 style="color:#6366f1;">AI 维度 - 邮箱验证</h2>
<p>您好!</p>
<p>您的验证码为:</p>
<div style="font-size:32px;font-weight:700;color:#6366f1;letter-spacing:8px;text-align:center;padding:20px;background:#f5f5f7;border-radius:12px;margin:16px 0;">
${code}
</div>
<p style="color:#666;">验证码有效期为 10 分钟,请勿泄露给他人。</p>
<p style="color:#999;font-size:12px;">如果这不是您的操作,请忽略此邮件。</p>
<hr style="border:none;border-top:1px solid #eee;margin:20px 0;">
<p style="color:#999;font-size:12px;text-align:center;">宇之然 AI 维度 · 北京宇之然科技中心</p>
</div>
`
})
logger.info(`[mail] 验证码邮件发送成功 -> ${to}`)
return { success: true }
} catch (error) {
logger.error(`[mail] 发送验证码失败 -> ${to}:`, error.message)
return { success: false, error: error.message }
}
}
module.exports = { sendVerificationCode }