415 lines
15 KiB
Vue
415 lines
15 KiB
Vue
<template>
|
||
<view class="login-page page-layout">
|
||
<view class="bg-orb orb-1"></view>
|
||
<view class="bg-orb orb-2"></view>
|
||
<view class="bg-orb orb-3"></view>
|
||
|
||
<view class="login-container">
|
||
<view class="logo-wrap">
|
||
<view class="logo-ring"></view>
|
||
<text class="logo-text">AI</text>
|
||
</view>
|
||
<text class="app-title">宇之然 AI 维度</text>
|
||
<text class="app-subtitle">从 5 个维度系统理解人工智能</text>
|
||
|
||
<view class="tab-switch">
|
||
<text class="tab-item" :class="{ active: activeTab === 'login' }" @click="activeTab = 'login'">登录</text>
|
||
<text class="tab-item" :class="{ active: activeTab === 'register' }" @click="activeTab = 'register'">注册</text>
|
||
</view>
|
||
|
||
<!-- 登录表单 -->
|
||
<view class="form-card" v-if="activeTab === 'login'">
|
||
<view class="form-item">
|
||
<text class="form-label">邮箱 / 用户名</text>
|
||
<input class="form-input" v-model="loginForm.email" placeholder="请输入邮箱或用户名" @input="loginForm.email = $event.detail.value.trim()" />
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label">密码</text>
|
||
<input class="form-input" v-model="loginForm.password" type="password" placeholder="请输入密码" />
|
||
</view>
|
||
|
||
<!-- 登录协议勾选 -->
|
||
<view class="agreement-row">
|
||
<view class="agreement-checkbox" :class="{ checked: loginAgreed }" @click="loginAgreed = !loginAgreed">
|
||
<text v-if="loginAgreed">✓</text>
|
||
</view>
|
||
<text class="agreement-text">
|
||
登录即表示同意
|
||
<text class="agreement-link" @click.stop="openAgreement('terms')">《用户协议》</text>
|
||
和
|
||
<text class="agreement-link" @click.stop="openAgreement('privacy')">《隐私政策》</text>
|
||
</text>
|
||
</view>
|
||
|
||
<view class="submit-btn" :class="{ loading: loading, disabled: !loginAgreed }" @click="handleLogin">
|
||
<text>{{ loading ? '登录中...' : '登录' }}</text>
|
||
</view>
|
||
<!-- 微信小程序一键登录 -->
|
||
<view class="wx-login-row" v-if="isMiniApp">
|
||
<view class="wx-login-btn" @click="wechatLogin">
|
||
<text class="wx-icon">💬</text>
|
||
<text>微信一键登录</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 注册表单 -->
|
||
<view class="form-card" v-if="activeTab === 'register'">
|
||
<view class="form-item">
|
||
<text class="form-label">邮箱 <text style="color:var(--color-vip);font-size:11px;">*</text></text>
|
||
<input class="form-input" v-model="registerForm.email" type="email" placeholder="请输入邮箱地址" @input="registerForm.email = $event.detail.value.trim(); verifyCodeSent = false" />
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label">昵称 <text style="color:var(--text-muted);font-size:11px;">(选填)</text></text>
|
||
<input class="form-input" v-model="registerForm.nickName" placeholder="显示名称,留空自动生成" />
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label">密码 <text style="color:var(--color-vip);font-size:11px;">*</text></text>
|
||
<input class="form-input" v-model="registerForm.password" type="password" placeholder="至少 6 位" />
|
||
</view>
|
||
|
||
<!-- 邮箱验证码 -->
|
||
<view class="form-item">
|
||
<text class="form-label">验证码 <text style="color:var(--color-vip);font-size:11px;">*</text></text>
|
||
<view class="verify-row">
|
||
<input class="form-input verify-input" v-model="registerForm.verifyCode" type="text" maxlength="6" placeholder="输入6位验证码" />
|
||
<view class="send-code-btn" :class="{ disabled: codeSending || codeCountdown > 0 }" @click="handleSendCode">
|
||
<text>{{ codeCountdown > 0 ? codeCountdown + 's' : (codeSending ? '发送中...' : '获取验证码') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 用户协议勾选 -->
|
||
<view class="agreement-row">
|
||
<view class="agreement-checkbox" :class="{ checked: agreed }" @click="agreed = !agreed">
|
||
<text v-if="agreed">✓</text>
|
||
</view>
|
||
<text class="agreement-text">
|
||
我已阅读并同意
|
||
<text class="agreement-link" @click.stop="openAgreement('terms')">《用户协议》</text>
|
||
和
|
||
<text class="agreement-link" @click.stop="openAgreement('privacy')">《隐私政策》</text>
|
||
</text>
|
||
</view>
|
||
|
||
<view class="submit-btn" :class="{ loading: loading, disabled: !agreed }" @click="handleRegister">
|
||
<text>{{ loading ? '注册中...' : '注册' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="footer-info">
|
||
<text>© 2026 宇之然 · AI 维度</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { useUserStore } from '@/stores/index'
|
||
|
||
const isMiniApp = ref(false)
|
||
try {
|
||
const sysInfo = uni.getSystemInfoSync()
|
||
if (sysInfo && sysInfo.version && sysInfo.platform) {
|
||
isMiniApp.value = true
|
||
}
|
||
} catch (e) {}
|
||
|
||
const activeTab = ref('login')
|
||
const loading = ref(false)
|
||
const agreed = ref(false)
|
||
const loginAgreed = ref(false)
|
||
const codeSending = ref(false)
|
||
const codeCountdown = ref(0)
|
||
const verifyCodeSent = ref(false)
|
||
let countdownTimer = null
|
||
const userStore = useUserStore()
|
||
|
||
const loginForm = ref({ email: '', password: '' })
|
||
const registerForm = ref({ email: '', password: '', nickName: '', verifyCode: '' })
|
||
|
||
const BASE_URL = 'https://wdkj.yuzhiran.com.cn'
|
||
|
||
/** 发送邮箱验证码 */
|
||
async function handleSendCode() {
|
||
if (!agreed.value) {
|
||
uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||
return
|
||
}
|
||
const email = registerForm.value.email
|
||
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||
uni.showToast({ title: '请先输入正确的邮箱', icon: 'none' })
|
||
return
|
||
}
|
||
if (codeSending.value || codeCountdown.value > 0) return
|
||
|
||
codeSending.value = true
|
||
try {
|
||
const resp = await fetch(`${BASE_URL}/api/auth/user/send-code`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email })
|
||
})
|
||
const data = await resp.json()
|
||
if (!resp.ok) {
|
||
throw new Error(data.error?.message || data.message || '发送失败')
|
||
}
|
||
verifyCodeSent.value = true
|
||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||
// 开始倒计时
|
||
codeCountdown.value = 60
|
||
countdownTimer = setInterval(() => {
|
||
codeCountdown.value--
|
||
if (codeCountdown.value <= 0) {
|
||
clearInterval(countdownTimer)
|
||
countdownTimer = null
|
||
}
|
||
}, 1000)
|
||
} catch (err) {
|
||
uni.showToast({ title: err.message || '发送失败,请稍后重试', icon: 'none' })
|
||
} finally {
|
||
codeSending.value = false
|
||
}
|
||
}
|
||
|
||
const wechatLogin = async () => {
|
||
if (!loginAgreed.value) {
|
||
uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||
return
|
||
}
|
||
try {
|
||
const loginRes = await new Promise((resolve, reject) => {
|
||
uni.login({ success: resolve, fail: reject })
|
||
})
|
||
if (!loginRes.code) {
|
||
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||
return
|
||
}
|
||
loading.value = true
|
||
await userStore.login(loginRes.code, {})
|
||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1000)
|
||
} catch (err) {
|
||
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleLogin = async () => {
|
||
if (!loginAgreed.value) {
|
||
uni.showToast({ title: '请阅读并同意用户协议和隐私政策', icon: 'none' })
|
||
return
|
||
}
|
||
if (!loginForm.value.email || !loginForm.value.password) {
|
||
uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
// 支持邮箱或用户名登录
|
||
const identifier = loginForm.value.email
|
||
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(identifier)
|
||
const loginData = isEmail
|
||
? { email: identifier, password: loginForm.value.password }
|
||
: { username: identifier, password: loginForm.value.password }
|
||
await userStore.h5Login(loginData)
|
||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1000)
|
||
} catch (err) {
|
||
uni.showToast({ title: err.message || '登录失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleRegister = async () => {
|
||
if (!agreed.value) {
|
||
uni.showToast({ title: '请阅读并同意用户协议和隐私政策', icon: 'none' })
|
||
return
|
||
}
|
||
if (!registerForm.value.email) {
|
||
uni.showToast({ title: '请输入邮箱', icon: 'none' })
|
||
return
|
||
}
|
||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(registerForm.value.email)) {
|
||
uni.showToast({ title: '邮箱格式不正确', icon: 'none' })
|
||
return
|
||
}
|
||
if (!registerForm.value.password) {
|
||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||
return
|
||
}
|
||
if (registerForm.value.password.length < 6) {
|
||
uni.showToast({ title: '密码至少 6 位', icon: 'none' })
|
||
return
|
||
}
|
||
if (!registerForm.value.verifyCode) {
|
||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
await userStore.h5Register(
|
||
registerForm.value.email,
|
||
registerForm.value.password,
|
||
registerForm.value.nickName || '',
|
||
registerForm.value.verifyCode
|
||
)
|
||
uni.showToast({ title: '注册成功', icon: 'success' })
|
||
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1000)
|
||
} catch (err) {
|
||
uni.showToast({ title: err.message || '注册失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function openAgreement(type) {
|
||
const pages = {
|
||
'terms': '/pages/agreement/agreement?type=terms',
|
||
'privacy': '/pages/agreement/agreement?type=privacy'
|
||
}
|
||
uni.navigateTo({ url: pages[type] || pages.terms })
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.login-page {
|
||
position: relative;
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--bg-primary);
|
||
|
||
.bg-orb {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(60px);
|
||
opacity: 0.3;
|
||
pointer-events: none;
|
||
}
|
||
.orb-1 { width: 200px; height: 200px; top: -50px; left: -50px; background: var(--dim1-color); }
|
||
.orb-2 { width: 160px; height: 160px; top: 30%; right: -40px; background: var(--dim3-color); }
|
||
.orb-3 { width: 180px; height: 180px; bottom: -60px; left: 30%; background: var(--dim5-color); }
|
||
|
||
.login-container {
|
||
position: relative; z-index: 1;
|
||
width: 100%; max-width: 360px;
|
||
padding: 0 32px;
|
||
display: flex; flex-direction: column; align-items: center;
|
||
}
|
||
|
||
.logo-wrap {
|
||
position: relative; width: 72px; height: 72px; margin-bottom: 16px;
|
||
.logo-ring {
|
||
position: absolute; inset: 0; border-radius: 18px;
|
||
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
|
||
box-shadow: 0 8px 32px rgba(124, 77, 255, 0.2);
|
||
}
|
||
.logo-text {
|
||
position: absolute; inset: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 28px; font-weight: 700; color: white;
|
||
}
|
||
}
|
||
|
||
.app-title { font-size: 24px; font-weight: 700; color: var(--text-primary); margin-bottom: 6px; }
|
||
.app-subtitle { font-size: 13px; color: var(--text-secondary); margin-bottom: 32px; }
|
||
|
||
.tab-switch {
|
||
display: flex; width: 100%; margin-bottom: 20px; border-bottom: 1px solid var(--glass-border);
|
||
.tab-item {
|
||
flex: 1; text-align: center; padding: 10px 0;
|
||
color: var(--text-secondary); font-size: 15px; font-weight: 500;
|
||
border-bottom: 2px solid transparent; transition: all 0.3s;
|
||
&.active { color: var(--color-brand); border-bottom-color: var(--color-brand); }
|
||
}
|
||
}
|
||
|
||
.form-card {
|
||
width: 100%;
|
||
background: var(--glass-bg); border: 1px solid var(--glass-border);
|
||
border-radius: 16px; padding: 24px 20px;
|
||
}
|
||
|
||
.form-item { margin-bottom: 16px;
|
||
.form-label { display: block; font-size: 13px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 500; }
|
||
.form-input {
|
||
width: 100%; height: 44px;
|
||
background: var(--bg-secondary); border: 1px solid var(--glass-border);
|
||
border-radius: 10px; padding: 0 14px;
|
||
color: var(--text-primary); font-size: 14px; box-sizing: border-box; outline: none;
|
||
&::placeholder { color: var(--text-disabled); }
|
||
}
|
||
}
|
||
|
||
// 验证码行
|
||
.verify-row {
|
||
display: flex; gap: 10px;
|
||
.verify-input { flex: 1; }
|
||
.send-code-btn {
|
||
flex-shrink: 0; height: 44px; padding: 0 16px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: linear-gradient(135deg, var(--color-brand), var(--dim3-color));
|
||
color: white; border-radius: 10px; font-size: 13px; font-weight: 600;
|
||
cursor: pointer; white-space: nowrap;
|
||
&.disabled { opacity: 0.5; pointer-events: none; }
|
||
&:active { opacity: 0.8; }
|
||
}
|
||
}
|
||
|
||
// 用户协议勾选
|
||
.agreement-row {
|
||
display: flex; align-items: center; gap: 10px;
|
||
margin: 12px 0 4px;
|
||
}
|
||
.agreement-checkbox {
|
||
width: 20px; height: 20px;
|
||
border: 2px solid var(--glass-border);
|
||
border-radius: 4px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0;
|
||
transition: all 0.2s;
|
||
text { font-size: 12px; color: white; font-weight: 700; display: none; }
|
||
&.checked {
|
||
background: var(--color-brand); border-color: var(--color-brand);
|
||
text { display: block; }
|
||
}
|
||
}
|
||
.agreement-text { font-size: 12px; color: var(--text-tertiary); line-height: 1.5; }
|
||
.agreement-link { color: var(--color-brand); text-decoration: underline; }
|
||
|
||
.submit-btn {
|
||
width: 100%; height: 46px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: linear-gradient(135deg, var(--color-brand), var(--dim3-color));
|
||
color: white; border-radius: 10px; font-size: 15px; font-weight: 600;
|
||
margin-top: 8px; cursor: pointer;
|
||
box-shadow: 0 4px 16px rgba(124, 77, 255, 0.25);
|
||
transition: opacity 0.3s;
|
||
&:active { opacity: 0.8; }
|
||
&.loading { opacity: 0.7; }
|
||
&.disabled { opacity: 0.4; pointer-events: none; }
|
||
}
|
||
|
||
.wx-login-row {
|
||
margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--glass-border);
|
||
.wx-login-btn {
|
||
width: 100%; height: 46px;
|
||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||
background: linear-gradient(135deg, #07c160, #06ad56); color: white;
|
||
border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer;
|
||
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.3); transition: opacity 0.3s;
|
||
&:active { opacity: 0.8; }
|
||
.wx-icon { font-size: 18px; }
|
||
}
|
||
}
|
||
|
||
.footer-info { margin-top: 40px; font-size: 11px; color: var(--text-muted); }
|
||
}
|
||
</style> |