upload project source code

This commit is contained in:
2026-04-30 18:49:43 +08:00
commit 9b394ba682
2277 changed files with 660945 additions and 0 deletions

View File

@@ -0,0 +1,661 @@
<template>
<view class="login-screen">
<!-- 背景装饰 -->
<view class="login-bg">
<view class="login-bg-pattern"></view>
</view>
<!-- 主要内容 -->
<view class="login-content">
<!-- Logo/标题区域 -->
<view class="login-header">
<view class="login-logo">
<text class="login-logo-text">壹梵</text>
</view>
<text class="login-title">壹梵起名</text>
<text class="login-subtitle">传承千年文化 · 赋予美好寓意</text>
</view>
<!-- 登录/注册表单 -->
<view class="login-form-wrapper">
<!-- 标签切换 -->
<view class="login-tabs">
<view class="login-tab" :class="{ active: currentTab === 'login' }" @click="currentTab = 'login'">
<text class="login-tab-text">登录</text>
</view>
<view class="login-tab" :class="{ active: currentTab === 'register' }" @click="currentTab = 'register'">
<text class="login-tab-text">注册</text>
</view>
</view>
<!-- 登录表单 -->
<view v-if="currentTab === 'login'" class="login-form">
<view class="login-form-item">
<text class="login-form-label">手机号</text>
<input v-model="loginForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号" maxlength="11" />
</view>
<view class="login-form-item">
<text class="login-form-label">密码</text>
<input v-model="loginForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请输入密码" />
<view class="login-form-eye" @click="showPassword = !showPassword">
<text>{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="login-forgot" @click="currentTab = 'forgot'">
<text class="login-forgot-text">忘记密码</text>
</view>
<button class="login-btn login-btn-primary" :disabled="!canLogin || loading" @click="handleLogin">
<text class="login-btn-text">{{ loading ? '登录中...' : '登录' }}</text>
</button>
</view>
<!-- 注册表单 -->
<view v-if="currentTab === 'register'" class="login-form">
<view class="login-form-item">
<text class="login-form-label">手机号</text>
<input v-model="registerForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号"
maxlength="11" />
</view>
<view class="login-form-item">
<text class="login-form-label">验证码</text>
<view class="login-form-code">
<input v-model="registerForm.code" type="tel" class="login-form-input" placeholder="请输入验证码"
maxlength="6" />
<button class="login-code-btn" :disabled="!canSendCode || countdown > 0"
@click="handleSendCode('register')">
<text class="login-code-text">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</text>
</button>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">密码</text>
<input v-model="registerForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请输入密码6-20位" />
<view class="login-form-eye" @click="showPassword = !showPassword">
<text>{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">确认密码</text>
<input v-model="registerForm.repassword" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请再次输入密码" />
</view>
<button class="login-btn login-btn-primary" :disabled="loading" @click="handleRegister">
<text class="login-btn-text">{{ loading ? '注册中...' : '注册' }}</text>
</button>
</view>
<!-- 忘记密码表单 -->
<view v-if="currentTab === 'forgot'" class="login-form">
<view class="login-form-item">
<text class="login-form-label">手机号</text>
<input v-model="forgotForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号"
maxlength="11" />
</view>
<view class="login-form-item">
<text class="login-form-label">验证码</text>
<view class="login-form-code">
<input v-model="forgotForm.code" type="tel" class="login-form-input" placeholder="请输入验证码" maxlength="6" />
<button class="login-code-btn" :disabled="!canSendCodeForgot || countdown > 0"
@click="handleSendCode('forgot')">
<text class="login-code-text">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</text>
</button>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">新密码</text>
<input v-model="forgotForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请输入新密码6-20位" />
<view class="login-form-eye" @click="showPassword = !showPassword">
<text>{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">确认密码</text>
<input v-model="forgotForm.repassword" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请再次输入新密码" />
</view>
<button class="login-btn login-btn-primary" :disabled="!canResetPassword || loading"
@click="handleResetPassword">
<text class="login-btn-text">{{ loading ? '重置中...' : '重置密码' }}</text>
</button>
<view class="login-back" @click="currentTab = 'login'">
<text class="login-back-text">返回登录</text>
</view>
</view>
</view>
<!-- 协议 -->
<view class="login-agreement">
<text class="login-agreement-text">
登录即表示同意
<text class="login-agreement-link" @click="handleNavigateToAgreement">用户协议</text>
<text class="login-agreement-link" @click="handleNavigateToPrivacy">隐私政策</text>
</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { useRouter } from 'vue-router';
import { userApi } from '@/api';
import type { MobileLoginResponse, MobileRegisterResponse, ForgotPasswordResponse } from '@/api/types';
import { showToast } from '@/utils/uni-compat';
const router = useRouter();
const emit = defineEmits<{
success: [data: MobileLoginResponse | MobileRegisterResponse | ForgotPasswordResponse];
}>();
const loading = ref(false);
const currentTab = ref<'login' | 'register' | 'forgot'>('login');
const showPassword = ref(false);
const countdown = ref(0);
let countdownTimer: number | null = null;
// 登录表单
const loginForm = reactive({
mobile: '',
password: '',
});
// 注册表单
const registerForm = reactive({
mobile: '',
code: '',
password: '',
repassword: '',
});
// 忘记密码表单
const forgotForm = reactive({
mobile: '',
code: '',
password: '',
repassword: '',
});
// 验证手机号
const isValidMobile = (mobile: string) => {
return /^1[3-9]\d{9}$/.test(mobile);
};
// 验证密码
const isValidPassword = (password: string) => {
return password.length >= 6 && password.length <= 20;
};
// 是否可以登录(仅根据 loading 控制按钮,去掉其他前置校验)
const canLogin = computed(() => {
return true;
});
// 是否可以注册
const canRegister = computed(() => {
return (
isValidMobile(registerForm.mobile) &&
registerForm.code.length === 6 &&
isValidPassword(registerForm.password) &&
registerForm.password === registerForm.repassword
);
});
// 是否可以发送验证码(注册)
const canSendCode = computed(() => {
return isValidMobile(registerForm.mobile);
});
// 是否可以发送验证码(忘记密码)
const canSendCodeForgot = computed(() => {
return isValidMobile(forgotForm.mobile);
});
// 是否可以重置密码
const canResetPassword = computed(() => {
return (
isValidMobile(forgotForm.mobile) &&
forgotForm.code.length === 6 &&
isValidPassword(forgotForm.password) &&
forgotForm.password === forgotForm.repassword
);
});
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
if (countdownTimer) {
clearInterval(countdownTimer);
}
countdownTimer = window.setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
}
}, 1000);
};
// 发送验证码
const handleSendCode = async (type: 'register' | 'forgot') => {
const mobile = type === 'register' ? registerForm.mobile : forgotForm.mobile;
if (!isValidMobile(mobile)) {
showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
try {
await userApi.sendSmsCode(mobile);
showToast({ title: '验证码已发送', icon: 'success' });
startCountdown();
} catch (error: any) {
showToast({ title: error.msg || '发送失败,请重试', icon: 'none' });
}
};
// 登录
const handleLogin = async () => {
if (!canLogin.value || loading.value) return;
try {
loading.value = true;
const result = await userApi.mobileLogin({
mobile: loginForm.mobile,
password: loginForm.password,
});
showToast({ title: '登录成功', icon: 'success' });
emit('success', result);
} catch (error: any) {
showToast({ title: error.msg || '登录失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 注册
const handleRegister = async () => {
if (!canRegister.value || loading.value) return;
if (registerForm.password !== registerForm.repassword) {
showToast({ title: '两次密码输入不一致', icon: 'none' });
return;
}
try {
loading.value = true;
const result = await userApi.mobileRegister({
mobile: registerForm.mobile,
password: registerForm.password,
repassword: registerForm.repassword,
verification_code: registerForm.code,
});
showToast({ title: '注册成功', icon: 'success' });
emit('success', result);
} catch (error: any) {
showToast({ title: error.msg || '注册失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 重置密码
const handleResetPassword = async () => {
if (!canResetPassword.value || loading.value) return;
if (forgotForm.password !== forgotForm.repassword) {
showToast({ title: '两次密码输入不一致', icon: 'none' });
return;
}
try {
loading.value = true;
const result = await userApi.forgotPassword({
mobile: forgotForm.mobile,
password: forgotForm.password,
repassword: forgotForm.repassword,
verification_code: forgotForm.code,
});
showToast({ title: '密码重置成功', icon: 'success' });
emit('success', result);
} catch (error: any) {
showToast({ title: error.msg || '重置失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 导航到用户协议
const handleNavigateToAgreement = () => {
router.push('/user-agreement');
};
// 导航到隐私政策
const handleNavigateToPrivacy = () => {
router.push('/privacy-policy');
};
</script>
<style scoped>
.login-screen {
position: relative;
width: 100%;
min-height: 100vh;
background: #fdfbf7 url("https://www.transparenttextures.com/patterns/rice-paper.png");
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.login-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
overflow: hidden;
}
.login-bg-pattern {
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 30% 30%, rgba(139, 35, 35, 0.03) 0%, transparent 50%);
animation: rotate 60s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.login-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 30px;
width: 100%;
max-width: 500px;
}
.login-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
}
.login-logo {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #8b2323 0%, #9c2a2a 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(139, 35, 35, 0.3);
}
.login-logo-text {
font-size: 32px;
font-weight: bold;
color: #fdfbf7;
font-family: SimSun, "Songti SC", serif;
}
.login-title {
font-size: 28px;
font-weight: 500;
color: #2c2c2c;
margin-bottom: 8px;
font-family: SimSun, "Songti SC", serif;
letter-spacing: 0.2em;
}
.login-subtitle {
font-size: 14px;
color: #8a8a8a;
font-family: SimSun, "Songti SC", serif;
}
.login-form-wrapper {
width: 100%;
background: rgba(255, 255, 255, 0.8);
border-radius: 16px;
padding: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #eaddcf;
}
.login-tabs {
display: flex;
margin-bottom: 30px;
border-bottom: 2px solid #eaddcf;
}
.login-tab {
flex: 1;
padding: 12px 0;
text-align: center;
cursor: pointer;
position: relative;
transition: all 0.3s;
}
.login-tab.active {
color: #8b2323;
}
.login-tab.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #8b2323;
}
.login-tab-text {
font-size: 16px;
font-weight: 500;
font-family: SimSun, "Songti SC", serif;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.login-form-item {
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
}
.login-form-label {
font-size: 14px;
color: #2c2c2c;
font-weight: 500;
font-family: SimSun, "Songti SC", serif;
}
.login-form-input {
width: 100%;
height: 44px;
padding: 0 16px;
background: #fff;
border: 1px solid #dcd3c9;
border-radius: 8px;
font-size: 14px;
color: #2c2c2c;
font-family: SimSun, "Songti SC", serif;
box-sizing: border-box;
}
.login-form-input:focus {
border-color: #8b2323;
outline: none;
}
.login-form-eye {
position: absolute;
right: 16px;
bottom: 12px;
cursor: pointer;
font-size: 18px;
}
.login-form-code {
display: flex;
gap: 10px;
align-items: center;
}
.login-form-code .login-form-input {
flex: 1;
}
.login-code-btn {
flex-shrink: 0;
height: 44px;
padding: 0 16px;
background: #8b2323;
color: #fff;
border: none;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
white-space: nowrap;
font-family: SimSun, "Songti SC", serif;
}
.login-code-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-code-text {
font-size: 13px;
}
.login-forgot {
text-align: right;
margin-top: -10px;
cursor: pointer;
}
.login-forgot-text {
font-size: 13px;
color: #8b2323;
font-family: SimSun, "Songti SC", serif;
}
.login-back {
text-align: center;
margin-top: 10px;
cursor: pointer;
}
.login-back-text {
font-size: 14px;
color: #8b2323;
font-family: SimSun, "Songti SC", serif;
}
.login-btn {
width: 100%;
height: 48px;
border-radius: 24px;
border: none;
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
font-family: SimSun, "Songti SC", serif;
cursor: pointer;
margin-top: 10px;
}
.login-btn-primary {
background: linear-gradient(135deg, #8b2323 0%, #9c2a2a 100%);
color: #fff;
}
.login-btn-primary:active {
background: linear-gradient(135deg, #701c1c 0%, #8b2323 100%);
transform: scale(0.98);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-btn-text {
font-size: 16px;
}
.login-agreement {
margin-top: 30px;
text-align: center;
}
.login-agreement-text {
font-size: 12px;
color: #8a8a8a;
font-family: SimSun, "Songti SC", serif;
}
.login-agreement-link {
color: #8b2323;
cursor: pointer;
text-decoration: underline;
}
.login-agreement-link:hover {
opacity: 0.8;
}
</style>