Compare commits
20 Commits
eeeed94702
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1094435b5f | |||
| 89cf7add0e | |||
| e48416ac8b | |||
| 5d2171e1a4 | |||
| 5ac33dcd7f | |||
| 23de8bc86a | |||
| dd5c1bc7ac | |||
| 06d7d9700c | |||
| 1c70e89ab8 | |||
| 48690e7724 | |||
| 70bb2d23ea | |||
| f8ace4b016 | |||
| ee34c35994 | |||
| 6dc85281a5 | |||
| 8e8e3d5da2 | |||
| 1760668d0f | |||
| 0a3bb0fcf8 | |||
| 8b06017d02 | |||
| 8149d2a93c | |||
| 0709e457b3 |
@@ -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
|
||||||
@@ -47,15 +47,15 @@ function applyTabBarTheme(theme) {
|
|||||||
try {
|
try {
|
||||||
if (theme === 'dark') {
|
if (theme === 'dark') {
|
||||||
uni.setTabBarStyle({
|
uni.setTabBarStyle({
|
||||||
color: 'rgba(255,255,255,0.25)',
|
color: 'rgba(255,255,255,0.35)',
|
||||||
selectedColor: '#b388ff',
|
selectedColor: '#a78bfa',
|
||||||
backgroundColor: '#08080f',
|
backgroundColor: '#0d0d1a',
|
||||||
borderStyle: 'black'
|
borderStyle: 'black'
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
uni.setTabBarStyle({
|
uni.setTabBarStyle({
|
||||||
color: 'rgba(0,0,0,0.35)',
|
color: 'rgba(0,0,0,0.3)',
|
||||||
selectedColor: '#7c4dff',
|
selectedColor: '#6366f1',
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
borderStyle: 'white'
|
borderStyle: 'white'
|
||||||
})
|
})
|
||||||
@@ -69,82 +69,82 @@ function applyTabBarTheme(theme) {
|
|||||||
<style>
|
<style>
|
||||||
/* ============================================
|
/* ============================================
|
||||||
宇之然AI维度 — 全局主题
|
宇之然AI维度 — 全局主题
|
||||||
默认:亮色模式 | .theme-dark:暗色模式
|
默认:亮色模式(蓝紫主题) | .theme-dark:暗色模式
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
/* ---- 亮色模式(默认) ---- */
|
/* ---- 亮色模式(默认:浅底+色彩点缀,现代丰富) ---- */
|
||||||
page, .theme-light {
|
page, .theme-light {
|
||||||
/* 基础色 — 清爽浅灰 */
|
/* 基础色 — 干净浅底 */
|
||||||
--bg-primary: #f5f5f7;
|
--bg-primary: #f5f5f7;
|
||||||
--bg-secondary: #ffffff;
|
--bg-secondary: #ffffff;
|
||||||
--bg-tertiary: #e8e8ed;
|
--bg-tertiary: #e8e8ed;
|
||||||
|
|
||||||
/* 玻璃拟态 */
|
/* 卡片 — 纯白微阴影 */
|
||||||
--glass-bg: rgba(255, 255, 255, 0.7);
|
--glass-bg: #ffffff;
|
||||||
--glass-border: rgba(0, 0, 0, 0.06);
|
--glass-border: rgba(0, 0, 0, 0.06);
|
||||||
--glass-hover: rgba(255, 255, 255, 0.85);
|
--glass-hover: #f8f8ff;
|
||||||
--glass-hover-border: rgba(0, 0, 0, 0.1);
|
--glass-hover-border: rgba(99, 102, 241, 0.15);
|
||||||
--glass-strong: rgba(255, 255, 255, 0.9);
|
--glass-strong: #ffffff;
|
||||||
--glass-dim-bg: rgba(139, 92, 246, 0.05);
|
--glass-dim-bg: rgba(99, 102, 241, 0.04);
|
||||||
|
|
||||||
/* 文字 */
|
/* 文字 — 清晰深色 */
|
||||||
--text-primary: rgba(0, 0, 0, 0.88);
|
--text-primary: #0d0d1a;
|
||||||
--text-secondary: rgba(0, 0, 0, 0.55);
|
--text-secondary: rgba(13, 13, 26, 0.6);
|
||||||
--text-tertiary: rgba(0, 0, 0, 0.4);
|
--text-tertiary: rgba(13, 13, 26, 0.4);
|
||||||
--text-muted: rgba(0, 0, 0, 0.25);
|
--text-muted: rgba(13, 13, 26, 0.25);
|
||||||
--text-disabled: rgba(0, 0, 0, 0.2);
|
--text-disabled: rgba(13, 13, 26, 0.15);
|
||||||
|
|
||||||
/* 品牌色 */
|
/* 品牌色 — Indigo 高饱和 */
|
||||||
--color-brand: #7c4dff;
|
--color-brand: #5b5ff8;
|
||||||
--color-brand-light: #b388ff;
|
--color-brand-light: #7b87fd;
|
||||||
--color-brand-dark: #651fff;
|
--color-brand-dark: #4f46e5;
|
||||||
--color-brand-bg: rgba(124, 77, 255, 0.08);
|
--color-brand-bg: rgba(91, 95, 248, 0.08);
|
||||||
--color-brand-glow: rgba(124, 77, 255, 0.12);
|
--color-brand-glow: rgba(91, 95, 248, 0.1);
|
||||||
|
|
||||||
/* 维度色 */
|
/* 维度色 — 高饱和度,在浅色底上鲜艳 */
|
||||||
--dim1-color: #7c4dff;
|
--dim1-color: #5b5ff8;
|
||||||
--dim1-bg: rgba(124, 77, 255, 0.08);
|
--dim1-bg: rgba(91, 95, 248, 0.2);
|
||||||
--dim1-icon-bg: rgba(124, 77, 255, 0.15);
|
--dim1-icon-bg: rgba(91, 95, 248, 0.25);
|
||||||
--dim1-border: rgba(124, 77, 255, 0.15);
|
--dim1-border: rgba(91, 95, 248, 0.3);
|
||||||
--dim1-glow: rgba(124, 77, 255, 0.06);
|
--dim1-glow: rgba(91, 95, 248, 0.08);
|
||||||
|
|
||||||
--dim2-color: #0ea5e9;
|
--dim2-color: #0095ba;
|
||||||
--dim2-bg: rgba(14, 165, 233, 0.08);
|
--dim2-bg: rgba(0, 149, 186, 0.2);
|
||||||
--dim2-icon-bg: rgba(14, 165, 233, 0.15);
|
--dim2-icon-bg: rgba(0, 149, 186, 0.25);
|
||||||
--dim2-border: rgba(14, 165, 233, 0.15);
|
--dim2-border: rgba(0, 149, 186, 0.3);
|
||||||
--dim2-glow: rgba(14, 165, 233, 0.06);
|
--dim2-glow: rgba(0, 149, 186, 0.08);
|
||||||
|
|
||||||
--dim3-color: #10b981;
|
--dim3-color: #009b6a;
|
||||||
--dim3-bg: rgba(16, 185, 129, 0.08);
|
--dim3-bg: rgba(0, 155, 106, 0.2);
|
||||||
--dim3-icon-bg: rgba(16, 185, 129, 0.15);
|
--dim3-icon-bg: rgba(0, 155, 106, 0.25);
|
||||||
--dim3-border: rgba(16, 185, 129, 0.15);
|
--dim3-border: rgba(0, 155, 106, 0.3);
|
||||||
--dim3-glow: rgba(16, 185, 129, 0.06);
|
--dim3-glow: rgba(0, 155, 106, 0.08);
|
||||||
|
|
||||||
--dim4-color: #f59e0b;
|
--dim4-color: #df7700;
|
||||||
--dim4-bg: rgba(245, 158, 11, 0.08);
|
--dim4-bg: rgba(223, 119, 0, 0.2);
|
||||||
--dim4-icon-bg: rgba(245, 158, 11, 0.15);
|
--dim4-icon-bg: rgba(223, 119, 0, 0.25);
|
||||||
--dim4-border: rgba(245, 158, 11, 0.15);
|
--dim4-border: rgba(223, 119, 0, 0.3);
|
||||||
--dim4-glow: rgba(245, 158, 11, 0.06);
|
--dim4-glow: rgba(223, 119, 0, 0.08);
|
||||||
|
|
||||||
--dim5-color: #ef4444;
|
--dim5-color: #e51c1c;
|
||||||
--dim5-bg: rgba(239, 68, 68, 0.08);
|
--dim5-bg: rgba(229, 28, 28, 0.2);
|
||||||
--dim5-icon-bg: rgba(239, 68, 68, 0.15);
|
--dim5-icon-bg: rgba(229, 28, 28, 0.25);
|
||||||
--dim5-border: rgba(239, 68, 68, 0.15);
|
--dim5-border: rgba(229, 28, 28, 0.3);
|
||||||
--dim5-glow: rgba(239, 68, 68, 0.06);
|
--dim5-glow: rgba(229, 28, 28, 0.08);
|
||||||
|
|
||||||
/* 功能色 */
|
/* 功能色 */
|
||||||
--color-free: #10b981;
|
--color-free: #009b6a;
|
||||||
--color-free-bg: rgba(16, 185, 129, 0.1);
|
--color-free-bg: rgba(0, 155, 106, 0.1);
|
||||||
--color-pro: #f59e0b;
|
--color-pro: #df7700;
|
||||||
--color-pro-bg: rgba(245, 158, 11, 0.1);
|
--color-pro-bg: rgba(223, 119, 0, 0.1);
|
||||||
--color-vip: #ef4444;
|
--color-vip: #e51c1c;
|
||||||
--color-vip-bg: rgba(239, 68, 68, 0.1);
|
--color-vip-bg: rgba(229, 28, 28, 0.1);
|
||||||
--color-hot: #ef4444;
|
--color-hot: #e51c1c;
|
||||||
--color-hot-bg: rgba(239, 68, 68, 0.12);
|
--color-hot-bg: rgba(229, 28, 28, 0.12);
|
||||||
--color-new: #0ea5e9;
|
--color-new: #0095ba;
|
||||||
--color-new-bg: rgba(14, 165, 233, 0.12);
|
--color-new-bg: rgba(0, 149, 186, 0.12);
|
||||||
--color-pick: #7c4dff;
|
--color-pick: #5b5ff8;
|
||||||
--color-pick-bg: rgba(124, 77, 255, 0.12);
|
--color-pick-bg: rgba(91, 95, 248, 0.12);
|
||||||
|
|
||||||
/* 圆角/间距/过渡 */
|
/* 圆角/间距/过渡 */
|
||||||
--radius-sm: 8px;
|
--radius-sm: 8px;
|
||||||
@@ -161,8 +161,12 @@ page, .theme-light {
|
|||||||
--blur-bg: 20px;
|
--blur-bg: 20px;
|
||||||
--transition-fast: 0.2s ease;
|
--transition-fast: 0.2s ease;
|
||||||
--transition-normal: 0.3s ease;
|
--transition-normal: 0.3s ease;
|
||||||
--glow-brand: 0 0 20px rgba(124, 77, 255, 0.08);
|
--glow-brand: 0 0 20px rgba(99, 102, 241, 0.08);
|
||||||
--glow-brand-strong: 0 0 30px rgba(124, 77, 255, 0.12);
|
--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: rgba(255, 255, 255, 0.1);
|
||||||
--glass-hover-border: rgba(255, 255, 255, 0.15);
|
--glass-hover-border: rgba(255, 255, 255, 0.15);
|
||||||
--glass-strong: rgba(255, 255, 255, 0.12);
|
--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-primary: rgba(255, 255, 255, 0.95);
|
||||||
--text-secondary: rgba(255, 255, 255, 0.7);
|
--text-secondary: rgba(255, 255, 255, 0.7);
|
||||||
@@ -185,57 +189,57 @@ page, .theme-light {
|
|||||||
--text-muted: rgba(255, 255, 255, 0.3);
|
--text-muted: rgba(255, 255, 255, 0.3);
|
||||||
--text-disabled: rgba(255, 255, 255, 0.15);
|
--text-disabled: rgba(255, 255, 255, 0.15);
|
||||||
|
|
||||||
--color-brand: #8b5cf6;
|
--color-brand: #7b87fd;
|
||||||
--color-brand-light: #a78bfa;
|
--color-brand-light: #a2b2ff;
|
||||||
--color-brand-dark: #7c3aed;
|
--color-brand-dark: #5b5ff8;
|
||||||
--color-brand-bg: rgba(139, 92, 246, 0.12);
|
--color-brand-bg: rgba(91, 95, 248, 0.12);
|
||||||
--color-brand-glow: rgba(139, 92, 246, 0.15);
|
--color-brand-glow: rgba(91, 95, 248, 0.15);
|
||||||
|
|
||||||
--dim1-color: #a78bfa;
|
--dim1-color: #a485ff;
|
||||||
--dim1-bg: rgba(139, 92, 246, 0.12);
|
--dim1-bg: rgba(164, 133, 255, 0.12);
|
||||||
--dim1-icon-bg: rgba(139, 92, 246, 0.2);
|
--dim1-icon-bg: rgba(164, 133, 255, 0.2);
|
||||||
--dim1-border: rgba(139, 92, 246, 0.18);
|
--dim1-border: rgba(164, 133, 255, 0.18);
|
||||||
--dim1-glow: rgba(139, 92, 246, 0.08);
|
--dim1-glow: rgba(164, 133, 255, 0.08);
|
||||||
|
|
||||||
--dim2-color: #60a5fa;
|
--dim2-color: #5ba4ff;
|
||||||
--dim2-bg: rgba(96, 165, 250, 0.12);
|
--dim2-bg: rgba(91, 164, 255, 0.12);
|
||||||
--dim2-icon-bg: rgba(96, 165, 250, 0.2);
|
--dim2-icon-bg: rgba(91, 164, 255, 0.2);
|
||||||
--dim2-border: rgba(96, 165, 250, 0.18);
|
--dim2-border: rgba(91, 164, 255, 0.18);
|
||||||
--dim2-glow: rgba(96, 165, 250, 0.08);
|
--dim2-glow: rgba(91, 164, 255, 0.08);
|
||||||
|
|
||||||
--dim3-color: #34d399;
|
--dim3-color: #2cda9b;
|
||||||
--dim3-bg: rgba(52, 211, 153, 0.12);
|
--dim3-bg: rgba(44, 218, 155, 0.12);
|
||||||
--dim3-icon-bg: rgba(52, 211, 153, 0.2);
|
--dim3-icon-bg: rgba(44, 218, 155, 0.2);
|
||||||
--dim3-border: rgba(52, 211, 153, 0.18);
|
--dim3-border: rgba(44, 218, 155, 0.18);
|
||||||
--dim3-glow: rgba(52, 211, 153, 0.08);
|
--dim3-glow: rgba(44, 218, 155, 0.08);
|
||||||
|
|
||||||
--dim4-color: #fbbf24;
|
--dim4-color: #ffc020;
|
||||||
--dim4-bg: rgba(251, 191, 36, 0.12);
|
--dim4-bg: rgba(255, 192, 32, 0.12);
|
||||||
--dim4-icon-bg: rgba(251, 191, 36, 0.2);
|
--dim4-icon-bg: rgba(255, 192, 32, 0.2);
|
||||||
--dim4-border: rgba(251, 191, 36, 0.18);
|
--dim4-border: rgba(255, 192, 32, 0.18);
|
||||||
--dim4-glow: rgba(251, 191, 36, 0.08);
|
--dim4-glow: rgba(255, 192, 32, 0.08);
|
||||||
|
|
||||||
--dim5-color: #fb7185;
|
--dim5-color: #ff6c82;
|
||||||
--dim5-bg: rgba(251, 113, 133, 0.12);
|
--dim5-bg: rgba(255, 108, 130, 0.12);
|
||||||
--dim5-icon-bg: rgba(251, 113, 133, 0.2);
|
--dim5-icon-bg: rgba(255, 108, 130, 0.2);
|
||||||
--dim5-border: rgba(251, 113, 133, 0.18);
|
--dim5-border: rgba(255, 108, 130, 0.18);
|
||||||
--dim5-glow: rgba(251, 113, 133, 0.08);
|
--dim5-glow: rgba(255, 108, 130, 0.08);
|
||||||
|
|
||||||
--color-free: #34d399;
|
--color-free: #2cda9b;
|
||||||
--color-free-bg: rgba(52, 211, 153, 0.12);
|
--color-free-bg: rgba(44, 218, 155, 0.12);
|
||||||
--color-pro: #fbbf24;
|
--color-pro: #ffc020;
|
||||||
--color-pro-bg: rgba(251, 191, 36, 0.12);
|
--color-pro-bg: rgba(255, 192, 32, 0.12);
|
||||||
--color-vip: #fb7185;
|
--color-vip: #ff6c82;
|
||||||
--color-vip-bg: rgba(251, 113, 133, 0.12);
|
--color-vip-bg: rgba(255, 108, 130, 0.12);
|
||||||
--color-hot: #fb7185;
|
--color-hot: #ff6c82;
|
||||||
--color-hot-bg: rgba(251, 113, 133, 0.15);
|
--color-hot-bg: rgba(255, 108, 130, 0.15);
|
||||||
--color-new: #60a5fa;
|
--color-new: #5ba4ff;
|
||||||
--color-new-bg: rgba(96, 165, 250, 0.15);
|
--color-new-bg: rgba(91, 164, 255, 0.15);
|
||||||
--color-pick: #a78bfa;
|
--color-pick: #a485ff;
|
||||||
--color-pick-bg: rgba(139, 92, 246, 0.15);
|
--color-pick-bg: rgba(164, 133, 255, 0.15);
|
||||||
|
|
||||||
--glow-brand: 0 0 20px rgba(139, 92, 246, 0.1);
|
--glow-brand: 0 0 20px rgba(99, 102, 241, 0.1);
|
||||||
--glow-brand-strong: 0 0 30px rgba(139, 92, 246, 0.15);
|
--glow-brand-strong: 0 0 30px rgba(99, 102, 241, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 全局排版 */
|
/* 全局排版 */
|
||||||
@@ -280,6 +284,7 @@ view {
|
|||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
backdrop-filter: blur(var(--blur-bg));
|
backdrop-filter: blur(var(--blur-bg));
|
||||||
-webkit-backdrop-filter: blur(var(--blur-bg));
|
-webkit-backdrop-filter: blur(var(--blur-bg));
|
||||||
|
box-shadow: var(--card-shadow, none);
|
||||||
transition: all var(--transition-normal);
|
transition: all var(--transition-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
{"path": "pages/dimension/dimension", "style": {"navigationBarTitleText": "", "navigationStyle": "custom"}},
|
{"path": "pages/dimension/dimension", "style": {"navigationBarTitleText": "", "navigationStyle": "custom"}},
|
||||||
{"path": "pages/chat/chat", "style": {"navigationBarTitleText": "AI 问答", "navigationStyle": "custom"}},
|
{"path": "pages/chat/chat", "style": {"navigationBarTitleText": "AI 问答", "navigationStyle": "custom"}},
|
||||||
{"path": "pages/trend/trend", "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": {
|
"globalStyle": {
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
@@ -29,15 +30,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
"color": "rgba(0,0,0,0.35)",
|
"color": "rgba(0,0,0,0.3)",
|
||||||
"selectedColor": "#7c4dff",
|
"selectedColor": "#6366f1",
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#ffffff",
|
||||||
"borderStyle": "white",
|
"borderStyle": "white",
|
||||||
"list": [
|
"list": [
|
||||||
{"pagePath": "pages/index/index", "text": "探索"},
|
{"pagePath": "pages/index/index", "text": "探索", "iconPath": "static/icons/explore.png", "selectedIconPath": "static/icons/explore-active.png"},
|
||||||
{"pagePath": "pages/chat/chat", "text": "问答"},
|
{"pagePath": "pages/chat/chat", "text": "问答", "iconPath": "static/icons/chat.png", "selectedIconPath": "static/icons/chat-active.png"},
|
||||||
{"pagePath": "pages/trend/trend", "text": "趋势"},
|
{"pagePath": "pages/trend/trend", "text": "趋势", "iconPath": "static/icons/trend.png", "selectedIconPath": "static/icons/trend-active.png"},
|
||||||
{"pagePath": "pages/profile/profile", "text": "我的"}
|
{"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 {
|
.overview-card {
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-bar {
|
.overview-bar {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: var(--glass-bg);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -230,7 +230,7 @@ function formatDate(dateStr) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 0 0 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;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +262,8 @@ function formatDate(dateStr) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ function formatDate(dateStr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.achievement-item.locked .ach-icon {
|
.achievement-item.locked .ach-icon {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--glass-bg);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -306,8 +306,8 @@ function goShare() {
|
|||||||
|
|
||||||
// 欢迎卡片
|
// 欢迎卡片
|
||||||
.welcome-card {
|
.welcome-card {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -374,8 +374,8 @@ function goShare() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-msg.assistant .bubble {
|
.chat-msg.assistant .bubble {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -418,8 +418,8 @@ function goShare() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -299,8 +299,8 @@ function goToAI() {
|
|||||||
.content-section,
|
.content-section,
|
||||||
.content-block,
|
.content-block,
|
||||||
.qa-section {
|
.qa-section {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
@@ -368,15 +368,14 @@ function goToAI() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
|
||||||
|
|
||||||
.qa-option {
|
.qa-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ function goDetail(item) {
|
|||||||
|
|
||||||
.progress-track {
|
.progress-track {
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: rgba(255,255,255,0.06);
|
background: var(--glass-bg);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -225,8 +225,8 @@ function goDetail(item) {
|
|||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
background: rgba(255,255,255,0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255,255,255,0.05);
|
border: 1px solid var(--glass-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -240,7 +240,7 @@ function goDetail(item) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
border: 1px solid rgba(255,255,255,0.06);
|
border: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
.k-info { flex: 1; }
|
.k-info { flex: 1; }
|
||||||
.k-title {
|
.k-title {
|
||||||
|
|||||||
@@ -158,8 +158,8 @@ function goHome() {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,8 +284,8 @@ function goBack() {
|
|||||||
|
|
||||||
.type-btn {
|
.type-btn {
|
||||||
padding: 10px 6px;
|
padding: 10px 6px;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -313,8 +313,8 @@ function goBack() {
|
|||||||
.form-input {
|
.form-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -327,8 +327,8 @@ function goBack() {
|
|||||||
.form-textarea {
|
.form-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -386,8 +386,8 @@ function goBack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.my-item {
|
.my-item {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ function onNotify() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goNewsDetail(item) {
|
function goNewsDetail(item) {
|
||||||
uni.switchTab({ url: '/pages/trend/trend' })
|
uni.setStorageSync('__news_item__', JSON.stringify(item))
|
||||||
|
uni.navigateTo({ url: '/pages/news/news' })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -222,6 +223,10 @@ function goNewsDetail(item) {
|
|||||||
|
|
||||||
.greeting {
|
.greeting {
|
||||||
margin-bottom: 20px;
|
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 {
|
.greeting-title {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
@@ -263,6 +268,9 @@ function goNewsDetail(item) {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(99,102,241,0.04);
|
||||||
}
|
}
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ function dimDotColor(id) {
|
|||||||
padding: 7px 16px;
|
padding: 7px 16px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
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;
|
font-size: 13px;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -194,8 +194,8 @@ function dimDotColor(id) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="login-page page-layout">
|
<view class="login-page page-layout">
|
||||||
<!-- 背景装饰 -->
|
|
||||||
<view class="bg-orb orb-1"></view>
|
<view class="bg-orb orb-1"></view>
|
||||||
<view class="bg-orb orb-2"></view>
|
<view class="bg-orb orb-2"></view>
|
||||||
<view class="bg-orb orb-3"></view>
|
<view class="bg-orb orb-3"></view>
|
||||||
|
|
||||||
<view class="login-container">
|
<view class="login-container">
|
||||||
<!-- Logo -->
|
|
||||||
<view class="logo-wrap">
|
<view class="logo-wrap">
|
||||||
<view class="logo-ring"></view>
|
<view class="logo-ring"></view>
|
||||||
<text class="logo-text">AI</text>
|
<text class="logo-text">AI</text>
|
||||||
@@ -14,7 +12,6 @@
|
|||||||
<text class="app-title">宇之然 AI 维度</text>
|
<text class="app-title">宇之然 AI 维度</text>
|
||||||
<text class="app-subtitle">从 5 个维度系统理解人工智能</text>
|
<text class="app-subtitle">从 5 个维度系统理解人工智能</text>
|
||||||
|
|
||||||
<!-- Tab 切换 — 两种模式共用 -->
|
|
||||||
<view class="tab-switch">
|
<view class="tab-switch">
|
||||||
<text class="tab-item" :class="{ active: activeTab === 'login' }" @click="activeTab = 'login'">登录</text>
|
<text class="tab-item" :class="{ active: activeTab === 'login' }" @click="activeTab = 'login'">登录</text>
|
||||||
<text class="tab-item" :class="{ active: activeTab === 'register' }" @click="activeTab = 'register'">注册</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-card" v-if="activeTab === 'login'">
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="form-label">用户名</text>
|
<text class="form-label">邮箱 / 用户名</text>
|
||||||
<input class="form-input" v-model="loginForm.username" placeholder="请输入用户名" />
|
<input class="form-input" v-model="loginForm.email" placeholder="请输入邮箱或用户名" @input="loginForm.email = $event.detail.value.trim()" />
|
||||||
</view>
|
</view>
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="form-label">密码</text>
|
<text class="form-label">密码</text>
|
||||||
<input class="form-input" v-model="loginForm.password" type="password" placeholder="请输入密码" />
|
<input class="form-input" v-model="loginForm.password" type="password" placeholder="请输入密码" />
|
||||||
</view>
|
</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>
|
<text>{{ loading ? '登录中...' : '登录' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<!-- 微信小程序一键登录入口 -->
|
<!-- 微信小程序一键登录 -->
|
||||||
<view class="wx-login-row" v-if="isMiniApp">
|
<view class="wx-login-row" v-if="isMiniApp">
|
||||||
<view class="wx-login-btn" @click="wechatLogin">
|
<view class="wx-login-btn" @click="wechatLogin">
|
||||||
|
<text class="wx-icon">💬</text>
|
||||||
<text>微信一键登录</text>
|
<text>微信一键登录</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -44,23 +56,47 @@
|
|||||||
<!-- 注册表单 -->
|
<!-- 注册表单 -->
|
||||||
<view class="form-card" v-if="activeTab === 'register'">
|
<view class="form-card" v-if="activeTab === 'register'">
|
||||||
<view class="form-item">
|
<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.username" placeholder="3-20 位字符" />
|
<input class="form-input" v-model="registerForm.email" type="email" placeholder="请输入邮箱地址" @input="registerForm.email = $event.detail.value.trim(); verifyCodeSent = false" />
|
||||||
</view>
|
</view>
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="form-label">昵称</text>
|
<text class="form-label">昵称 <text style="color:var(--text-muted);font-size:11px;">(选填)</text></text>
|
||||||
<input class="form-input" v-model="registerForm.nickName" placeholder="显示名称(选填)" />
|
<input class="form-input" v-model="registerForm.nickName" placeholder="显示名称,留空自动生成" />
|
||||||
</view>
|
</view>
|
||||||
<view class="form-item">
|
<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 位" />
|
<input class="form-input" v-model="registerForm.password" type="password" placeholder="至少 6 位" />
|
||||||
</view>
|
</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>
|
<text>{{ loading ? '注册中...' : '注册' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部信息 -->
|
|
||||||
<view class="footer-info">
|
<view class="footer-info">
|
||||||
<text>© 2026 宇之然 · AI 维度</text>
|
<text>© 2026 宇之然 · AI 维度</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -72,76 +108,115 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useUserStore } from '@/stores/index'
|
import { useUserStore } from '@/stores/index'
|
||||||
|
|
||||||
// 检测是否为微信小程序环境
|
|
||||||
const isMiniApp = ref(false)
|
const isMiniApp = ref(false)
|
||||||
const platform = uni.getSystemInfoSync().platform
|
|
||||||
try {
|
try {
|
||||||
const sysInfo = uni.getSystemInfoSync()
|
const sysInfo = uni.getSystemInfoSync()
|
||||||
// 小程序环境:platform 为 ios/android/devtools,且有 brand/version 等字段
|
|
||||||
if (sysInfo && sysInfo.version && sysInfo.platform) {
|
if (sysInfo && sysInfo.version && sysInfo.platform) {
|
||||||
// 有 weixin 特有字段即为小程序
|
|
||||||
isMiniApp.value = true
|
isMiniApp.value = true
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// H5 环境
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeTab = ref('login')
|
const activeTab = ref('login')
|
||||||
const loading = ref(false)
|
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()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// H5 表单
|
const loginForm = ref({ email: '', password: '' })
|
||||||
const loginForm = ref({ username: '', password: '' })
|
const registerForm = ref({ email: '', password: '', nickName: '', verifyCode: '' })
|
||||||
const registerForm = ref({ username: '', password: '', nickName: '' })
|
|
||||||
|
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 () => {
|
const wechatLogin = async () => {
|
||||||
|
if (!loginAgreed.value) {
|
||||||
|
uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const loginRes = await new Promise((resolve, reject) => {
|
const loginRes = await new Promise((resolve, reject) => {
|
||||||
uni.login({
|
uni.login({ success: resolve, fail: reject })
|
||||||
success: resolve,
|
|
||||||
fail: reject
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
if (!loginRes.code) {
|
if (!loginRes.code) {
|
||||||
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await userStore.login(loginRes.code, {})
|
await userStore.login(loginRes.code, {})
|
||||||
|
|
||||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||||
setTimeout(() => {
|
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1000)
|
||||||
uni.switchTab({ url: '/pages/index/index' })
|
|
||||||
}, 1000)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('微信登录失败:', err)
|
|
||||||
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* H5 用户名密码登录
|
|
||||||
*/
|
|
||||||
const handleLogin = async () => {
|
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' })
|
uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
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' })
|
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||||
setTimeout(() => {
|
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1000)
|
||||||
uni.switchTab({ url: '/pages/index/index' })
|
|
||||||
}, 1000)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
uni.showToast({ title: err.message || '登录失败', icon: 'none' })
|
uni.showToast({ title: err.message || '登录失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
@@ -149,36 +224,55 @@ const handleLogin = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* H5 注册
|
|
||||||
*/
|
|
||||||
const handleRegister = async () => {
|
const handleRegister = async () => {
|
||||||
if (!registerForm.value.username || !registerForm.value.password) {
|
if (!agreed.value) {
|
||||||
uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
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
|
return
|
||||||
}
|
}
|
||||||
if (registerForm.value.password.length < 6) {
|
if (registerForm.value.password.length < 6) {
|
||||||
uni.showToast({ title: '密码至少 6 位', icon: 'none' })
|
uni.showToast({ title: '密码至少 6 位', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!registerForm.value.verifyCode) {
|
||||||
|
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await userStore.h5Register(
|
await userStore.h5Register(
|
||||||
registerForm.value.username,
|
registerForm.value.email,
|
||||||
registerForm.value.password,
|
registerForm.value.password,
|
||||||
registerForm.value.nickName || ''
|
registerForm.value.nickName || '',
|
||||||
|
registerForm.value.verifyCode
|
||||||
)
|
)
|
||||||
uni.showToast({ title: '注册成功', icon: 'success' })
|
uni.showToast({ title: '注册成功', icon: 'success' })
|
||||||
setTimeout(() => {
|
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1000)
|
||||||
uni.switchTab({ url: '/pages/index/index' })
|
|
||||||
}, 1000)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
uni.showToast({ title: err.message || '注册失败', icon: 'none' })
|
uni.showToast({ title: err.message || '注册失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -192,7 +286,6 @@ const handleRegister = async () => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
|
|
||||||
// 背景装饰光球
|
|
||||||
.bg-orb {
|
.bg-orb {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -200,193 +293,123 @@ const handleRegister = async () => {
|
|||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.orb-1 {
|
.orb-1 { width: 200px; height: 200px; top: -50px; left: -50px; background: var(--dim1-color); }
|
||||||
width: 200px; height: 200px;
|
.orb-2 { width: 160px; height: 160px; top: 30%; right: -40px; background: var(--dim3-color); }
|
||||||
top: -50px; left: -50px;
|
.orb-3 { width: 180px; height: 180px; bottom: -60px; left: 30%; background: var(--dim5-color); }
|
||||||
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 {
|
.login-container {
|
||||||
position: relative;
|
position: relative; z-index: 1;
|
||||||
z-index: 1;
|
width: 100%; max-width: 360px;
|
||||||
width: 100%;
|
|
||||||
max-width: 360px;
|
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
display: flex;
|
display: flex; flex-direction: column; align-items: center;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo
|
|
||||||
.logo-wrap {
|
.logo-wrap {
|
||||||
position: relative;
|
position: relative; width: 72px; height: 72px; margin-bottom: 16px;
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
.logo-ring {
|
.logo-ring {
|
||||||
position: absolute;
|
position: absolute; inset: 0; border-radius: 18px;
|
||||||
inset: 0;
|
|
||||||
border-radius: 18px;
|
|
||||||
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
|
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
|
||||||
box-shadow: 0 8px 32px rgba(124, 77, 255, 0.2);
|
box-shadow: 0 8px 32px rgba(124, 77, 255, 0.2);
|
||||||
}
|
}
|
||||||
.logo-text {
|
.logo-text {
|
||||||
position: absolute;
|
position: absolute; inset: 0;
|
||||||
inset: 0;
|
display: flex; align-items: center; justify-content: center;
|
||||||
display: flex;
|
font-size: 28px; font-weight: 700; color: white;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-title {
|
.app-title { font-size: 24px; font-weight: 700; color: var(--text-primary); margin-bottom: 6px; }
|
||||||
font-size: 24px;
|
.app-subtitle { font-size: 13px; color: var(--text-secondary); margin-bottom: 32px; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab 切换
|
|
||||||
.tab-switch {
|
.tab-switch {
|
||||||
display: flex;
|
display: flex; width: 100%; margin-bottom: 20px; border-bottom: 1px solid var(--glass-border);
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 1px solid var(--glass-border);
|
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
flex: 1;
|
flex: 1; text-align: center; padding: 10px 0;
|
||||||
text-align: center;
|
color: var(--text-secondary); font-size: 15px; font-weight: 500;
|
||||||
padding: 10px 0;
|
border-bottom: 2px solid transparent; transition: all 0.3s;
|
||||||
color: var(--text-secondary);
|
&.active { color: var(--color-brand); border-bottom-color: var(--color-brand); }
|
||||||
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 {
|
.form-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg); border: 1px solid var(--glass-border);
|
||||||
border: 1px solid var(--glass-border);
|
border-radius: 16px; padding: 24px 20px;
|
||||||
border-radius: 16px;
|
|
||||||
padding: 24px 20px;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-item {
|
.form-item { margin-bottom: 16px;
|
||||||
margin-bottom: 16px;
|
.form-label { display: block; font-size: 13px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 500; }
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.form-input {
|
.form-input {
|
||||||
width: 100%;
|
width: 100%; height: 44px;
|
||||||
height: 44px;
|
background: var(--bg-secondary); border: 1px solid var(--glass-border);
|
||||||
background: var(--bg-secondary);
|
border-radius: 10px; padding: 0 14px;
|
||||||
border: 1px solid var(--glass-border);
|
color: var(--text-primary); font-size: 14px; box-sizing: border-box; outline: none;
|
||||||
border-radius: 10px;
|
&::placeholder { color: var(--text-disabled); }
|
||||||
padding: 0 14px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 14px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--text-disabled);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交按钮
|
// 验证码行
|
||||||
.submit-btn {
|
.verify-row {
|
||||||
width: 100%;
|
display: flex; gap: 10px;
|
||||||
height: 46px;
|
.verify-input { flex: 1; }
|
||||||
display: flex;
|
.send-code-btn {
|
||||||
align-items: center;
|
flex-shrink: 0; height: 44px; padding: 0 16px;
|
||||||
justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
background: linear-gradient(135deg, var(--color-brand), var(--dim3-color));
|
background: linear-gradient(135deg, var(--color-brand), var(--dim3-color));
|
||||||
color: white;
|
color: white; border-radius: 10px; font-size: 13px; font-weight: 600;
|
||||||
border-radius: 10px;
|
cursor: pointer; white-space: nowrap;
|
||||||
font-size: 15px;
|
&.disabled { opacity: 0.5; pointer-events: none; }
|
||||||
font-weight: 600;
|
&:active { opacity: 0.8; }
|
||||||
margin-top: 8px;
|
}
|
||||||
cursor: pointer;
|
}
|
||||||
|
|
||||||
|
// 用户协议勾选
|
||||||
|
.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);
|
box-shadow: 0 4px 16px rgba(124, 77, 255, 0.25);
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
|
&:active { opacity: 0.8; }
|
||||||
&:active {
|
&.loading { opacity: 0.7; }
|
||||||
opacity: 0.8;
|
&.disabled { opacity: 0.4; pointer-events: none; }
|
||||||
}
|
|
||||||
&.loading {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 微信一键登录
|
|
||||||
.wx-login-row {
|
.wx-login-row {
|
||||||
margin-top: 16px;
|
margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--glass-border);
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid var(--glass-border);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.wx-login-btn {
|
.wx-login-btn {
|
||||||
padding: 10px 28px;
|
width: 100%; height: 46px;
|
||||||
display: flex;
|
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
align-items: center;
|
background: linear-gradient(135deg, #07c160, #06ad56); color: white;
|
||||||
justify-content: center;
|
border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer;
|
||||||
gap: 8px;
|
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.3); transition: opacity 0.3s;
|
||||||
background: linear-gradient(135deg, #07c160, #06ad56);
|
&:active { opacity: 0.8; }
|
||||||
color: white;
|
.wx-icon { font-size: 18px; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部信息
|
.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>
|
</style>
|
||||||
@@ -55,6 +55,13 @@
|
|||||||
<rich-text :nodes="newsBody"></rich-text>
|
<rich-text :nodes="newsBody"></rich-text>
|
||||||
</view>
|
</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">
|
<view class="related-tags" v-if="relatedTags.length">
|
||||||
<text class="related-label">相关话题</text>
|
<text class="related-label">相关话题</text>
|
||||||
@@ -123,7 +130,7 @@ function formatDate(dateStr) {
|
|||||||
const newsBody = computed(() => {
|
const newsBody = computed(() => {
|
||||||
const content = news.value.body || ''
|
const content = news.value.body || ''
|
||||||
if (!content) {
|
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
|
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(() => {
|
onMounted(() => {
|
||||||
loadDetail()
|
loadDetail()
|
||||||
})
|
})
|
||||||
@@ -198,7 +219,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.nav-back {
|
.nav-back {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: #fff;
|
color: var(--text-primary);
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -209,7 +230,7 @@ onMounted(() => {
|
|||||||
color: var(--text-primary);
|
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 {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -218,18 +239,19 @@ onMounted(() => {
|
|||||||
|
|
||||||
// Hero
|
// Hero
|
||||||
.news-hero {
|
.news-hero {
|
||||||
height: 180px;
|
height: 160px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 0 0 24px 24px;
|
border-radius: 0 0 24px 24px;
|
||||||
|
margin: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-emoji { font-size: 72px; }
|
.hero-emoji { font-size: 64px; }
|
||||||
|
|
||||||
// Category
|
// Category
|
||||||
.category-tag {
|
.category-tag {
|
||||||
margin: 24px 20px 0;
|
margin: 20px 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-text {
|
.tag-text {
|
||||||
@@ -257,8 +279,9 @@ onMounted(() => {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin: 0 20px 20px;
|
margin: 0 20px 20px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: rgba(255,255,255,0.02);
|
background: var(--glass-bg);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-item {
|
.meta-item {
|
||||||
@@ -269,19 +292,19 @@ onMounted(() => {
|
|||||||
|
|
||||||
.meta-label {
|
.meta-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: rgba(255,255,255,0.35);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-label.hot { font-size: 13px; }
|
.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
|
||||||
.summary-card {
|
.summary-card {
|
||||||
margin: 0 20px 20px;
|
margin: 0 20px 20px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: linear-gradient(135deg, rgba(179,136,255,0.08), rgba(77,208,225,0.05));
|
background: linear-gradient(135deg, rgba(99,102,241,0.06), rgba(0,188,212,0.04));
|
||||||
border: 1px solid rgba(179,136,255,0.12);
|
border: 1px solid rgba(99,102,241,0.1);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +319,7 @@ onMounted(() => {
|
|||||||
.summary-text {
|
.summary-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: rgba(255,255,255,0.85);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
@@ -304,9 +327,24 @@ onMounted(() => {
|
|||||||
margin: 0 20px 20px;
|
margin: 0 20px 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.8;
|
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
|
||||||
.related-tags {
|
.related-tags {
|
||||||
margin: 0 20px 20px;
|
margin: 0 20px 20px;
|
||||||
@@ -315,7 +353,7 @@ onMounted(() => {
|
|||||||
.related-label {
|
.related-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(255,255,255,0.4);
|
color: var(--text-tertiary);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,12 +361,12 @@ onMounted(() => {
|
|||||||
|
|
||||||
.tag-item {
|
.tag-item {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: rgba(255,255,255,0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255,255,255,0.08);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 20px;
|
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
|
||||||
.action-bar {
|
.action-bar {
|
||||||
@@ -344,13 +382,13 @@ onMounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
background: rgba(255,255,255,0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255,255,255,0.08);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-icon { font-size: 18px; }
|
.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; }
|
.footer-spacer { height: 60px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,50 +9,62 @@
|
|||||||
<view class="nav-placeholder"></view>
|
<view class="nav-placeholder"></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="scroll-area">
|
||||||
<!-- 头部 -->
|
<!-- 头部 -->
|
||||||
<view class="payment-header">
|
<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-title">解锁 AI 维度 Pro</text>
|
||||||
<text class="header-desc">无限 AI 问答 · 全量知识解锁 · 每日趋势优先推送</text>
|
<text class="header-desc">无限 AI 问答 · 全量知识 · 趋势优先推送</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 方案选择 -->
|
<!-- 方案选择卡片 -->
|
||||||
<view class="plan-selector">
|
<view class="plan-selector">
|
||||||
<view
|
<view
|
||||||
class="plan-tag"
|
class="plan-card"
|
||||||
:class="{ active: selectedPlan === 'pro' }"
|
:class="{ active: selectedPlan === 'pro' }"
|
||||||
@click="selectedPlan = 'pro'"
|
@click="selectedPlan = 'pro'"
|
||||||
>
|
>
|
||||||
|
<view class="plan-top">
|
||||||
<text class="plan-name">Pro</text>
|
<text class="plan-name">Pro</text>
|
||||||
<text class="plan-price">¥19.9<span class="unit">/月</span></text>
|
<view class="plan-badge" v-if="selectedPlan === 'pro'">推荐</view>
|
||||||
<text class="plan-badge">推荐</text>
|
</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>
|
||||||
<view
|
<view
|
||||||
class="plan-tag vip"
|
class="plan-card vip"
|
||||||
:class="{ active: selectedPlan === 'vip' }"
|
:class="{ active: selectedPlan === 'vip' }"
|
||||||
@click="selectedPlan = 'vip'"
|
@click="selectedPlan = 'vip'"
|
||||||
>
|
>
|
||||||
|
<view class="plan-top">
|
||||||
<text class="plan-name">VIP</text>
|
<text class="plan-name">VIP</text>
|
||||||
<text class="plan-price">¥39.9<span class="unit">/月</span></text>
|
<view class="plan-badge vip-badge" v-if="selectedPlan === 'vip'">尊享</view>
|
||||||
<text class="plan-desc">含 Pro 全部权益</text>
|
</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>
|
</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">
|
<view class="duration-section">
|
||||||
<text class="duration-title">选择套餐</text>
|
<text class="section-title">选择套餐</text>
|
||||||
<view class="duration-list">
|
<view class="duration-list">
|
||||||
<view
|
<view
|
||||||
class="duration-item"
|
class="duration-item"
|
||||||
:class="{ active: selectedDuration === item.key }"
|
:class="{ active: selectedDuration === item.key }"
|
||||||
v-for="item in durations"
|
v-for="item in durationOptions"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
@click="selectedDuration = item.key"
|
@click="selectedDuration = item.key"
|
||||||
>
|
>
|
||||||
@@ -63,18 +75,45 @@
|
|||||||
</view>
|
</view>
|
||||||
</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">
|
<view class="pay-section">
|
||||||
<button class="pay-btn" :loading="loading" @click="handlePay">
|
<button
|
||||||
立即支付 ¥{{ currentPrice }}
|
class="pay-btn"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="handlePay"
|
||||||
|
>
|
||||||
|
<text class="pay-btn-text">立即支付 ¥{{ currentPrice }}</text>
|
||||||
</button>
|
</button>
|
||||||
<text class="pay-agreement">
|
<text class="pay-agreement">
|
||||||
点击支付即同意《用户协议》和《隐私政策》
|
点击支付即表示同意《用户协议》和《隐私政策》
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部 -->
|
<view class="footer-spacer"></view>
|
||||||
<text class="footer-text">如有问题请联系客服 · 支持 7 天无理由退款</text>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -85,32 +124,28 @@ import { useUserStore } from '@/stores/user'
|
|||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 方案
|
|
||||||
const selectedPlan = ref('pro')
|
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 selectedDuration = ref('pro-monthly')
|
||||||
|
|
||||||
// 权益
|
const durationOptions = computed(() => {
|
||||||
const benefits = ref([
|
const isPro = selectedPlan.value === 'pro'
|
||||||
{ icon: '🤖', text: 'AI 问答无限次' },
|
return [
|
||||||
{ icon: '📚', text: '全量知识内容解锁' },
|
{ key: isPro ? 'pro-monthly' : 'vip-monthly', label: '月付', price: isPro ? 19.9 : 39.9 },
|
||||||
{ icon: '📡', text: '每日趋势优先推送' },
|
{ key: isPro ? 'pro-quarterly' : 'vip-quarterly', label: '季付', price: isPro ? 49.9 : 99.9, save: isPro ? '¥10' : '¥20' }
|
||||||
{ icon: '⭐', text: '收藏夹无限容量' },
|
]
|
||||||
{ icon: '🎁', text: '专属会员标识' },
|
})
|
||||||
{ icon: '💬', text: '专属客服支持' }
|
|
||||||
|
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 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
|
return item ? item.price : 19.9
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,24 +157,14 @@ async function handlePay() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到对应的 productId
|
const productId = selectedDuration.value
|
||||||
const productMap = {
|
|
||||||
'pro-monthly': 'pro-monthly',
|
|
||||||
'pro-quarterly': 'pro-quarterly',
|
|
||||||
'vip-monthly': 'vip-monthly',
|
|
||||||
'vip-quarterly': 'vip-quarterly'
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 调用后端创建订单
|
const data = await paymentApi.createOrder(productId, userStore.token)
|
||||||
const data = await paymentApi.createOrder(productMap[selectedDuration.value], userStore.token)
|
|
||||||
|
|
||||||
// 获取微信支付参数
|
|
||||||
const payParam = data.payParams || data.wechatParam || data.paymentData
|
const payParam = data.payParams || data.wechatParam || data.paymentData
|
||||||
|
|
||||||
if (payParam) {
|
if (payParam) {
|
||||||
// 调起微信支付
|
|
||||||
uni.requestPayment({
|
uni.requestPayment({
|
||||||
provider: 'wxpay',
|
provider: 'wxpay',
|
||||||
timeStamp: payParam.timeStamp || payParam.timestamp,
|
timeStamp: payParam.timeStamp || payParam.timestamp,
|
||||||
@@ -154,7 +179,7 @@ async function handlePay() {
|
|||||||
}, 1500)
|
}, 1500)
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
if (err.errMsg.includes('cancel')) {
|
if (err.errMsg && err.errMsg.includes('cancel')) {
|
||||||
uni.showToast({ title: '已取消支付', icon: 'none' })
|
uni.showToast({ title: '已取消支付', icon: 'none' })
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({ title: '支付失败,请重试', icon: 'none' })
|
uni.showToast({ title: '支付失败,请重试', icon: 'none' })
|
||||||
@@ -179,19 +204,20 @@ function goBack() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page-payment {
|
.page-payment {
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
padding: 0 20px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar-placeholder { height: 44px; }
|
.status-bar-placeholder { height: 44px; }
|
||||||
|
|
||||||
// 导航
|
|
||||||
.nav-bar {
|
.nav-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px 0;
|
padding: 12px 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-back {
|
.nav-back {
|
||||||
@@ -214,23 +240,45 @@ function goBack() {
|
|||||||
width: 30px;
|
width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
// 头部
|
// 头部
|
||||||
.payment-header {
|
.payment-header {
|
||||||
text-align: center;
|
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 {
|
.header-title {
|
||||||
font-size: 22px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
display: block;
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-desc {
|
.header-desc {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
margin-top: 6px;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,181 +286,235 @@ function goBack() {
|
|||||||
.plan-selector {
|
.plan-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-tag {
|
.plan-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px;
|
padding: 20px 16px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1.5px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 16px;
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
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 {
|
.plan-top {
|
||||||
background: linear-gradient(135deg, rgba(124, 77, 255, 0.15), rgba(0, 0, 0, 0.2));
|
display: flex;
|
||||||
border-color: rgba(124, 77, 255, 0.3);
|
align-items: center;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
.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-name {
|
.plan-name {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
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 {
|
.plan-badge {
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
right: 12px;
|
|
||||||
background: linear-gradient(135deg, #ff9800, #ff5722);
|
|
||||||
color: white;
|
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, var(--dim1-color), #7c4dff);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-desc {
|
.vip-badge {
|
||||||
font-size: 11px;
|
background: linear-gradient(135deg, #ffa726, #ff7043);
|
||||||
color: var(--text-muted);
|
|
||||||
margin-top: 4px;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 权益
|
.plan-price {
|
||||||
.benefits {
|
font-size: 28px;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
font-weight: 700;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
color: var(--dim1-color);
|
||||||
border-radius: 14px;
|
display: inline;
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 6px;
|
||||||
padding: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-item:not(:last-child) {
|
.plan-feature {
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
font-size: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.b-icon { font-size: 16px; }
|
|
||||||
|
|
||||||
.b-text {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 套餐时长
|
// 套餐时长
|
||||||
.duration-section {
|
.duration-section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-title {
|
.section-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
font-weight: 600;
|
||||||
margin-bottom: 10px;
|
color: var(--text-primary);
|
||||||
display: block;
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-list {
|
.duration-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-item {
|
.duration-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px 8px;
|
padding: 14px 12px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1.5px solid var(--glass-border);
|
||||||
border-radius: 12px;
|
border-radius: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
transition: all 0.3s;
|
||||||
|
|
||||||
.duration-item.active {
|
&.active {
|
||||||
background: rgba(124, 77, 255, 0.12);
|
border-color: var(--dim1-color);
|
||||||
border-color: rgba(124, 77, 255, 0.3);
|
background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(99,102,241,0.05));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-label {
|
.d-label {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: var(--text-secondary);
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-price {
|
.d-price {
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--dim1-color);
|
color: var(--text-primary);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-save {
|
.d-save {
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
color: #4db6ac;
|
color: #4db6ac;
|
||||||
display: block;
|
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 {
|
.pay-section {
|
||||||
margin-top: 20px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pay-btn {
|
.pay-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 52px;
|
||||||
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
|
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
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-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: 0 4px 16px rgba(179, 136, 255, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pay-agreement {
|
.pay-agreement {
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-text {
|
.footer-spacer { height: 40px; }
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -366,11 +366,15 @@ function goSettings() {
|
|||||||
|
|
||||||
.progress-track {
|
.progress-track {
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: var(--glass-bg);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plan-progress .progress-track {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, var(--dim1-color), var(--dim3-color));
|
background: linear-gradient(90deg, var(--dim1-color), var(--dim3-color));
|
||||||
@@ -404,8 +408,8 @@ function goSettings() {
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quota-card-title {
|
.quota-card-title {
|
||||||
@@ -417,7 +421,7 @@ function goSettings() {
|
|||||||
|
|
||||||
.quota-bar {
|
.quota-bar {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: var(--glass-bg);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -450,15 +454,23 @@ function goSettings() {
|
|||||||
|
|
||||||
// 菜单
|
// 菜单
|
||||||
.menu-section {
|
.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 {
|
.menu-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 14px 0;
|
padding: 15px 16px;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
border-bottom: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.m-icon {
|
.m-icon {
|
||||||
@@ -498,7 +510,7 @@ function goSettings() {
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--dim1-color);
|
color: var(--dim1-color);
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
border-top: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.logout {
|
.action-btn.logout {
|
||||||
|
|||||||
@@ -344,8 +344,8 @@ function goBack() {
|
|||||||
|
|
||||||
// 维度卡片
|
// 维度卡片
|
||||||
.dim-card {
|
.dim-card {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -366,14 +366,14 @@ function goBack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dim-icon-circle {
|
.dim-icon-circle {
|
||||||
width: 40px;
|
background: var(--glass-bg);
|
||||||
height: 40px;
|
height: 6px;
|
||||||
border-radius: 12px;
|
border-radius: 3px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dim-text {
|
.dim-text {
|
||||||
@@ -427,13 +427,13 @@ function goBack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ring-empty {
|
.ring-empty {
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: var(--glass-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 展开内容
|
// 展开内容
|
||||||
.dim-card-body {
|
.dim-card-body {
|
||||||
padding: 0 16px 16px;
|
padding: 0 16px 16px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
border-top: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-detail {
|
.progress-detail {
|
||||||
|
|||||||
@@ -227,8 +227,8 @@ onUnmounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 20px 16px;
|
margin: 0 20px 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 10px 44px 10px 14px;
|
padding: 10px 44px 10px 14px;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
@@ -317,8 +317,8 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.result-card {
|
.result-card {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const displayItems = computed(() => {
|
|||||||
async function fetchTrends() {
|
async function fetchTrends() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await trendApi.list({ limit: 20 })
|
const data = await trendApi.list({ limit: 30 })
|
||||||
items.value = data.list || data.items || []
|
items.value = data.list || data.items || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取趋势失败:', err)
|
console.error('获取趋势失败:', err)
|
||||||
@@ -213,15 +213,15 @@ function getSampleData() {
|
|||||||
// 筛选栏
|
// 筛选栏
|
||||||
.filter-bar {
|
.filter-bar {
|
||||||
padding: 0 20px 12px;
|
padding: 0 20px 12px;
|
||||||
white-space: nowrap;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-item {
|
.filter-item {
|
||||||
display: inline-block;
|
|
||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
margin-right: 8px;
|
background: var(--glass-bg);
|
||||||
background: rgba(255, 255, 255, 0.04);
|
border: 1px solid var(--glass-border);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@@ -239,12 +239,12 @@ function getSampleData() {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
padding: 0 16px 16px;
|
padding: 0 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-card {
|
.trend-card {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--glass-bg);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 291 B |
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 297 B |
|
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 691 B |
|
Before Width: | Height: | Size: 170 B After Width: | Height: | Size: 729 B |
|
Before Width: | Height: | Size: 204 B After Width: | Height: | Size: 753 B |
|
Before Width: | Height: | Size: 198 B After Width: | Height: | Size: 800 B |
|
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 587 B |
|
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 628 B |
@@ -28,16 +28,16 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function h5Login(username, password) {
|
async function h5Login(loginData) {
|
||||||
const data = await authApi.h5Login(username, password)
|
const data = await authApi.h5Login(loginData)
|
||||||
token.value = data.token
|
token.value = data.token
|
||||||
userInfo.value = data.userInfo
|
userInfo.value = data.userInfo
|
||||||
uni.setStorageSync('token', data.token)
|
uni.setStorageSync('token', data.token)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function h5Register(username, password, nickName) {
|
async function h5Register(email, password, nickName, verifyCode) {
|
||||||
const data = await authApi.register(username, password, nickName)
|
const data = await authApi.register(email, password, nickName, verifyCode)
|
||||||
token.value = data.token
|
token.value = data.token
|
||||||
userInfo.value = data.userInfo
|
userInfo.value = data.userInfo
|
||||||
uni.setStorageSync('token', data.token)
|
uni.setStorageSync('token', data.token)
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* ---- SCSS 变量(方便组件内使用) ---- */
|
/* ---- SCSS 变量(方便组件内使用) ---- */
|
||||||
$color-brand: #7c4dff;
|
$color-brand: #6366f1;
|
||||||
$color-brand-light: #b388ff;
|
$color-brand-light: #818cf8;
|
||||||
$color-bg-primary: #f5f5f7;
|
$color-bg-primary: #f5f5f7;
|
||||||
$color-bg-dark: #14141e;
|
$color-bg-dark: #14141e;
|
||||||
|
|
||||||
$dim1-color: #7c4dff;
|
$dim1-color: #6366f1;
|
||||||
$dim2-color: #0ea5e9;
|
$dim2-color: #0891b2;
|
||||||
$dim3-color: #10b981;
|
$dim3-color: #059669;
|
||||||
$dim4-color: #f59e0b;
|
$dim4-color: #d97706;
|
||||||
$dim5-color: #ef4444;
|
$dim5-color: #dc2626;
|
||||||
|
|
||||||
/* 全局排版 */
|
/* 全局排版 */
|
||||||
page {
|
page {
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ export const authApi = {
|
|||||||
request('POST', '/api/auth/login', { code, userInfo }),
|
request('POST', '/api/auth/login', { code, userInfo }),
|
||||||
|
|
||||||
/** H5 密码登录 */
|
/** H5 密码登录 */
|
||||||
h5Login: (username, password) =>
|
h5Login: (loginData) =>
|
||||||
request('POST', '/api/auth/user/login', { username, password }),
|
request('POST', '/api/auth/user/login', loginData),
|
||||||
|
|
||||||
/** H5 注册 */
|
/** H5 注册 */
|
||||||
register: (username, password, nickName) =>
|
register: (email, password, nickName, verifyCode) =>
|
||||||
request('POST', '/api/auth/user/register', { username, password, nickName }),
|
request('POST', '/api/auth/user/register', { email, password, nickName, verifyCode }),
|
||||||
|
|
||||||
/** 获取当前用户 */
|
/** 获取当前用户 */
|
||||||
me: (token) =>
|
me: (token) =>
|
||||||
|
|||||||
@@ -1,30 +1,34 @@
|
|||||||
const ci = require('miniprogram-ci');
|
const ci = require('miniprogram-ci');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
(async () => {
|
(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({
|
const project = new ci.Project({
|
||||||
appid: 'wxdad62baf4ccd09e3',
|
appid: 'wxdad62baf4ccd09e3',
|
||||||
type: 'miniProgram',
|
type: 'miniProgram',
|
||||||
projectPath: path.resolve(__dirname, 'frontend/ai-dimension/dist/build/mp-weixin'),
|
projectPath,
|
||||||
privateKeyPath: path.resolve('/root/Hermes-workspace/宇之然AI维度appid/private.wxdad62baf4ccd09e3.key'),
|
privateKeyPath: '/root/Hermes-workspace/宇之然AI维度appid/private.wxdad62baf4ccd09e3.key',
|
||||||
ignores: ['node_modules/**/*'],
|
ignores: ['node_modules/**/*'],
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('→ 正在上传微信小程序...');
|
console.log('START_UPLOAD');
|
||||||
|
console.log('version=' + version + ' desc=' + desc);
|
||||||
|
|
||||||
const uploadResult = await ci.upload({
|
const uploadResult = await ci.upload({
|
||||||
project,
|
project,
|
||||||
version: '1.1.0',
|
version,
|
||||||
desc: 'AI维度 - 完整版开发',
|
desc,
|
||||||
setting: {
|
setting: { es6: true, es7: true, minify: true, autoPrefixWXSS: true },
|
||||||
es6: true,
|
|
||||||
es7: true,
|
|
||||||
minify: true,
|
|
||||||
autoPrefixWXSS: true,
|
|
||||||
},
|
|
||||||
onProgressUpdate: console.log,
|
|
||||||
});
|
});
|
||||||
console.log('✅ 上传成功!', uploadResult);
|
|
||||||
|
const resultStr = JSON.stringify(uploadResult);
|
||||||
|
fs.writeFileSync('/tmp/upload-result.json', resultStr);
|
||||||
|
console.log('SUCCESS:' + resultStr);
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
console.error('❌ 上传失败:', err);
|
console.log('ERROR:' + err.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wdkj-server",
|
"name": "ai-dimension-server",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wdkj-server",
|
"name": "ai-dimension-server",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"mongoose": "^8.0.3",
|
"mongoose": "^8.0.3",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^2.1.1",
|
"multer": "^2.1.1",
|
||||||
|
"nodemailer": "^9.0.3",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -4250,6 +4251,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.14",
|
"version": "3.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
||||||
|
|||||||
@@ -8,7 +8,12 @@
|
|||||||
"dev": "nodemon src/index.js",
|
"dev": "nodemon src/index.js",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"keywords": ["wechat", "miniprogram", "ai", "education"],
|
"keywords": [
|
||||||
|
"wechat",
|
||||||
|
"miniprogram",
|
||||||
|
"ai",
|
||||||
|
"education"
|
||||||
|
],
|
||||||
"author": "宇之然团队",
|
"author": "宇之然团队",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -27,6 +32,7 @@
|
|||||||
"mongoose": "^8.0.3",
|
"mongoose": "^8.0.3",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^2.1.1",
|
"multer": "^2.1.1",
|
||||||
|
"nodemailer": "^9.0.3",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ const UserSchema = new Schema({
|
|||||||
sparse: true,
|
sparse: true,
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
unique: true,
|
||||||
|
sparse: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
password: {
|
password: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -9,6 +9,7 @@ const AIModel = require('./AIModel')
|
|||||||
const LoginLog = require('./LoginLog')
|
const LoginLog = require('./LoginLog')
|
||||||
const Feedback = require('./Feedback')
|
const Feedback = require('./Feedback')
|
||||||
const Trend = require('./Trend')
|
const Trend = require('./Trend')
|
||||||
|
const VerificationCode = require('./VerificationCode')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
User,
|
User,
|
||||||
@@ -21,5 +22,6 @@ module.exports = {
|
|||||||
AIModel,
|
AIModel,
|
||||||
LoginLog,
|
LoginLog,
|
||||||
Feedback,
|
Feedback,
|
||||||
Trend
|
Trend,
|
||||||
|
VerificationCode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const jwt = require('jsonwebtoken')
|
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 ApiResponse = require('../utils/response')
|
||||||
const weChatService = require('../utils/weixin')
|
const weChatService = require('../utils/weixin')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
|
const { sendVerificationCode } = require('../utils/mail')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -312,10 +313,17 @@ router.post('/admin/login', async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.post('/user/login', async (req, res) => {
|
router.post('/user/login', async (req, res) => {
|
||||||
try {
|
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) {
|
if (!user) {
|
||||||
return ApiResponse.error(res, '用户名或密码错误', 401)
|
return ApiResponse.error(res, '用户名或密码错误', 401)
|
||||||
}
|
}
|
||||||
@@ -352,25 +360,120 @@ router.post('/user/login', async (req, res) => {
|
|||||||
logger.error('用户登录失败:', error)
|
logger.error('用户登录失败:', error)
|
||||||
return ApiResponse.serverError(res, error.message)
|
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
|
* POST /api/auth/user/register
|
||||||
*/
|
*/
|
||||||
router.post('/user/register', async (req, res) => {
|
router.post('/user/register', async (req, res) => {
|
||||||
try {
|
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 })
|
const existingUser = await User.findOne({ username })
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return ApiResponse.error(res, '用户名已存在', 400)
|
return ApiResponse.error(res, '用户名已存在', 400)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记验证码为已使用
|
||||||
|
validCode.used = true
|
||||||
|
await validCode.save()
|
||||||
|
|
||||||
|
// 自动生成用户名(若未提供)
|
||||||
|
const finalUsername = username || email.split('@')[0]
|
||||||
|
|
||||||
// 创建新用户
|
// 创建新用户
|
||||||
const user = new User({
|
const user = new User({
|
||||||
username,
|
username: finalUsername,
|
||||||
|
email: email || '',
|
||||||
password,
|
password,
|
||||||
nickName: nickName || '星辰旅行者',
|
nickName: nickName || '星辰旅行者',
|
||||||
avatarUrl: avatarUrl || '',
|
avatarUrl: avatarUrl || '',
|
||||||
|
|||||||
@@ -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
|
* 用法: 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 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 DB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj';
|
||||||
|
const MAX_ITEMS = 30;
|
||||||
|
const MAX_AGE_DAYS = 7;
|
||||||
|
|
||||||
// 示例新闻数据(生产环境可从 RSS/API 获取)
|
// 精确 AI 关键词(只匹配这些才采信,避免噪音)
|
||||||
const sampleTrends = [
|
const AI_KEYWORDS = [
|
||||||
{
|
'gpt-4', 'gpt4', 'gpt-5', 'gpt5', 'o1', 'o3', 'claude', 'sonnet', 'opus', 'haiku',
|
||||||
title: 'DeepSeek 秘密造芯:推理芯片项目已启动一年',
|
'gemini', 'llama', 'mistral', 'deepseek', 'qwen', 'yi-', 'glm', 'ernie',
|
||||||
summary: '据报道,DeepSeek 正在自研推理芯片,旨在降低对英伟达的依赖。',
|
'chatgpt', 'copilot', 'cursor', 'windsurf', 'bolt.new', 'lovable', 'v0.dev',
|
||||||
source: '路透社', sourceUrl: 'https://reuters.com',
|
'llm', 'large language model', 'foundation model', 'diffusion model',
|
||||||
category: '公司', tags: ['DeepSeek', '芯片'], hot: true, newsDate: new Date()
|
'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',
|
||||||
title: '蚂蚁灵波开源 LingBot-World 2.0,世界模型小时级实时生成',
|
'neural network', 'deep learning', 'reinforcement learning',
|
||||||
summary: 'LingBot-World 2.0 实现了世界模型的小时级实时生成,是具身智能的重要突破。',
|
'openai', 'anthropic', 'deepmind', 'google ai', 'meta ai', 'xai', 'grok',
|
||||||
source: '量子位', sourceUrl: 'https://qbitai.com',
|
'nvidia', 'h100', 'b200', 'gb200', 'inference chip',
|
||||||
category: '产品', tags: ['蚂蚁灵波', '世界模型'], newsDate: new Date()
|
'ai safety', 'alignment', 'superalignment',
|
||||||
},
|
'hugging face', 'langchain', 'llamaindex', 'autogpt',
|
||||||
{
|
'vectordb', 'pinecone', 'chromadb', 'weaviate',
|
||||||
title: '阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷',
|
'fine-tuning', 'sft', 'rlhf', 'dpo', 'grpo',
|
||||||
summary: '阿里研究团队获得 ACL 2026 最佳资源论文奖,深入分析了当前 Agent 系统的结构性缺陷。',
|
'moe', 'mixture of experts', 'speculative decoding',
|
||||||
source: '机器之心', sourceUrl: 'https://jiqizhixin.com',
|
'tokenizer', 'embedding', 'semantic kernel',
|
||||||
category: '论文', tags: ['阿里', 'ACL', 'Agent'], newsDate: new Date()
|
'coding agent', 'devai', 'devin', 'codex',
|
||||||
},
|
'world model', 'embodied ai', 'robotics',
|
||||||
{
|
'arvix', 'neurips', 'icml', 'iclr', 'acl', 'emnlp', 'cvpr', 'eccv'
|
||||||
title: '腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%',
|
];
|
||||||
summary: '腾讯混元大模型 Hy3 版本正式上线,在 Agent 任务评测中解决率提升至 90%。',
|
|
||||||
source: '腾讯云', sourceUrl: 'https://cloud.tencent.com',
|
// HN 高分阈值
|
||||||
category: '产品', tags: ['腾讯', '混元', 'Agent'], newsDate: new Date()
|
const HN_SCORE_MIN = 15;
|
||||||
},
|
|
||||||
{
|
// 类别映射
|
||||||
title: 'WAIC 2026 倒计时:7月17日上海开幕,1100+企业参展',
|
function categorize(title) {
|
||||||
summary: '2026 世界人工智能大会即将在上海开幕,将有超过 1100 家企业参展。',
|
const lc = title.toLowerCase();
|
||||||
source: '量子位', sourceUrl: 'https://qbitai.com',
|
if (lc.match(/paper|research|study|benchmark|survey|arxiv|neurips|icml|iclr|acl/)) return '论文';
|
||||||
category: '产品', tags: ['WAIC', '会议'], picked: true, newsDate: new Date()
|
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 '产品';
|
||||||
title: 'Browser Use CLI 3.0 发布:体积缩小 6 倍,Token 消耗大幅降低',
|
}
|
||||||
summary: 'Browser Use 工具 CLI 3.0 版本发布,体积缩小 6 倍,Token 消耗显著降低。',
|
|
||||||
source: 'GitHub', sourceUrl: 'https://github.com',
|
function getSource(url, from) {
|
||||||
category: '工具', tags: ['Browser Use', '开源'], newsDate: new Date()
|
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 GPU,AI 推理性能较 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-4o,Anthropic 估值破 600 亿', summary: 'Anthropic 发布 Claude Sonnet 4,在多项基准测试中超越 GPT-4o,公司估值已突破 600 亿美元。', source: 'TechCrunch', category: '产品', hot: true, newsDate: () => new Date() },
|
||||||
|
{ title: 'Meta 开源 Llama 4:400B 参数 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() {
|
async function main() {
|
||||||
|
console.log('📡 AI 趋势数据更新开始...');
|
||||||
|
const t0 = Date.now();
|
||||||
|
|
||||||
await mongoose.connect(DB_URI);
|
await mongoose.connect(DB_URI);
|
||||||
console.log('数据库已连接');
|
const Trend = mongoose.model('Trend', new mongoose.Schema({}, { strict: false, collection: 'trends' }));
|
||||||
|
|
||||||
// 清除 7 天前的旧数据
|
let hn = [], arxiv = [];
|
||||||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
||||||
const deleted = await Trend.deleteMany({ newsDate: { $lt: weekAgo } });
|
|
||||||
console.log(`已清理 ${deleted.deletedCount} 条旧数据`);
|
|
||||||
|
|
||||||
// 插入新数据(去重)
|
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;
|
let inserted = 0;
|
||||||
for (const item of sampleTrends) {
|
for (const item of items) {
|
||||||
const exists = await Trend.findOne({ title: item.title });
|
const exists = await Trend.findOne({ title: item.title });
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
await new Trend({ ...item, status: 'published' }).save();
|
await new Trend({ ...item, createdAt: new Date(), updatedAt: new Date() }).save();
|
||||||
inserted++;
|
inserted++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`新增 ${inserted} 条趋势数据`);
|
|
||||||
|
|
||||||
|
// 总量限制
|
||||||
|
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} 条最旧`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalCount = await Trend.countDocuments();
|
||||||
await mongoose.disconnect();
|
await mongoose.disconnect();
|
||||||
console.log('完成');
|
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); });
|
main().catch(e => { console.error('❌ 失败:', e.message); process.exit(1); });
|
||||||
@@ -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 }
|
||||||