Compare commits

...

16 Commits

Author SHA1 Message Date
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
38 changed files with 1191 additions and 666 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>
+21 -19
View File
@@ -198,7 +198,7 @@ onMounted(() => {
.nav-back {
font-size: 28px;
color: #fff;
color: var(--text-primary);
padding: 0 8px;
line-height: 1;
}
@@ -209,7 +209,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 +218,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 +258,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 +271,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 +298,7 @@ onMounted(() => {
.summary-text {
font-size: 14px;
line-height: 1.7;
color: rgba(255,255,255,0.85);
color: var(--text-primary);
}
// Body
@@ -304,7 +306,7 @@ onMounted(() => {
margin: 0 20px 20px;
font-size: 14px;
line-height: 1.8;
color: rgba(255,255,255,0.7);
color: var(--text-secondary);
}
// Related tags
@@ -315,7 +317,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 +325,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 +346,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>
+265 -163
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));
}
&.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.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);
}
.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;
+8 -8
View File
@@ -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
}
+111 -8
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)
}
@@ -352,25 +360,120 @@ router.post('/user/login', async (req, res) => {
logger.error('用户登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
})
/**
* 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 || '',
+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 }