Files
ai-dimension/client/src/pages/login/login.vue
T

415 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>