|
|
@@ -0,0 +1,508 @@
|
|
|
+<template>
|
|
|
+ <view class="login-container">
|
|
|
+ <view class="login-wrapper">
|
|
|
+ <!-- Logo 区域 -->
|
|
|
+ <view class="logo-section">
|
|
|
+ <image class="logo" src="/static/logo.png" mode="aspectFit" />
|
|
|
+ <text class="app-name">系统登录</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 登录表单 -->
|
|
|
+ <view class="form-section">
|
|
|
+ <uni-forms
|
|
|
+ ref="loginFormRef"
|
|
|
+ :model="loginForm"
|
|
|
+ :rules="loginRules"
|
|
|
+ label-position="top"
|
|
|
+ >
|
|
|
+ <!-- 用户名输入框 -->
|
|
|
+ <uni-forms-item label="用户名" name="username" required>
|
|
|
+ <uni-easyinput
|
|
|
+ v-model="loginForm.username"
|
|
|
+ placeholder="请输入用户名"
|
|
|
+ :clearable="true"
|
|
|
+ :focus="false"
|
|
|
+ @input="clearError"
|
|
|
+ >
|
|
|
+ <template #left>
|
|
|
+ <uni-icons type="person" size="20" color="#999" />
|
|
|
+ </template>
|
|
|
+ </uni-easyinput>
|
|
|
+ </uni-forms-item>
|
|
|
+
|
|
|
+ <!-- 密码输入框 -->
|
|
|
+ <uni-forms-item label="密码" name="password" required>
|
|
|
+ <uni-easyinput
|
|
|
+ v-model="loginForm.password"
|
|
|
+ type="password"
|
|
|
+ placeholder="请输入密码"
|
|
|
+ :clearable="true"
|
|
|
+ @input="clearError"
|
|
|
+ >
|
|
|
+ <template #left>
|
|
|
+ <uni-icons type="locked" size="20" color="#999" />
|
|
|
+ </template>
|
|
|
+ </uni-easyinput>
|
|
|
+ </uni-forms-item>
|
|
|
+
|
|
|
+ <!-- 验证码输入框 -->
|
|
|
+ <uni-forms-item
|
|
|
+ v-if="needCaptcha"
|
|
|
+ label="验证码"
|
|
|
+ name="code"
|
|
|
+ required
|
|
|
+ >
|
|
|
+ <view class="captcha-container">
|
|
|
+ <uni-easyinput
|
|
|
+ v-model="loginForm.code"
|
|
|
+ placeholder="请输入验证码"
|
|
|
+ :clearable="true"
|
|
|
+ class="captcha-input"
|
|
|
+ @input="clearError"
|
|
|
+ >
|
|
|
+ <template #left>
|
|
|
+ <uni-icons type="checkmarkempty" size="20" color="#999" />
|
|
|
+ </template>
|
|
|
+ </uni-easyinput>
|
|
|
+ <image
|
|
|
+ v-if="captchaImage"
|
|
|
+ :src="captchaImage"
|
|
|
+ class="captcha-image"
|
|
|
+ @click="refreshCaptcha"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </uni-forms-item>
|
|
|
+
|
|
|
+ <!-- 记住我 -->
|
|
|
+ <view class="remember-section">
|
|
|
+ <uni-data-checkbox
|
|
|
+ v-model="loginForm.rememberMe"
|
|
|
+ :localdata="rememberOptions"
|
|
|
+ mode="tag"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 错误提示 -->
|
|
|
+ <view v-if="errorMessage" class="error-message">
|
|
|
+ <uni-icons type="info" color="#f56c6c" size="16" />
|
|
|
+ <text class="error-text">{{ errorMessage }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 登录按钮 -->
|
|
|
+ <button
|
|
|
+ class="login-btn"
|
|
|
+ :class="{ 'login-btn--loading': loading }"
|
|
|
+ :disabled="loading"
|
|
|
+ @click="handleLogin"
|
|
|
+ >
|
|
|
+ <uni-icons
|
|
|
+ v-if="loading"
|
|
|
+ type="spinner-cycle"
|
|
|
+ class="loading-icon"
|
|
|
+ size="18"
|
|
|
+ color="#fff"
|
|
|
+ />
|
|
|
+ {{ loading ? '登录中...' : '登录' }}
|
|
|
+ </button>
|
|
|
+ </uni-forms>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部链接 -->
|
|
|
+ <view class="footer-section">
|
|
|
+ <text class="footer-link" @click="handleForgotPassword">忘记密码?</text>
|
|
|
+ <text class="footer-divider">|</text>
|
|
|
+ <text class="footer-link" @click="handleRegister">注册账号</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { reactive, ref, onMounted } from 'vue';
|
|
|
+import { useAuthStore } from '@/store/modules/auth';
|
|
|
+import type { LoginForm } from '@/store/modules/auth';
|
|
|
+
|
|
|
+// Store
|
|
|
+const authStore = useAuthStore();
|
|
|
+
|
|
|
+// 响应式数据
|
|
|
+const loginFormRef = ref();
|
|
|
+const loading = ref<boolean>(false);
|
|
|
+const errorMessage = ref<string>('');
|
|
|
+const needCaptcha = ref<boolean>(false);
|
|
|
+const captchaImage = ref<string>('');
|
|
|
+
|
|
|
+// 登录表单
|
|
|
+const loginForm = reactive<LoginForm>({
|
|
|
+ username: '',
|
|
|
+ password: '',
|
|
|
+ code: '',
|
|
|
+ uuid: '',
|
|
|
+ rememberMe: false
|
|
|
+});
|
|
|
+
|
|
|
+// 记住我选项
|
|
|
+const rememberOptions = [
|
|
|
+ {
|
|
|
+ text: '记住我',
|
|
|
+ value: true
|
|
|
+ }
|
|
|
+];
|
|
|
+
|
|
|
+// 表单验证规则
|
|
|
+const loginRules = {
|
|
|
+ username: [
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ errorMessage: '请输入用户名'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ minLength: 2,
|
|
|
+ maxLength: 20,
|
|
|
+ errorMessage: '用户名长度在 2 到 20 个字符'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ password: [
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ errorMessage: '请输入密码'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ minLength: 6,
|
|
|
+ maxLength: 20,
|
|
|
+ errorMessage: '密码长度在 6 到 20 个字符'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ code: [
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ errorMessage: '请输入验证码'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ minLength: 4,
|
|
|
+ maxLength: 6,
|
|
|
+ errorMessage: '验证码长度在 4 到 6 个字符'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 清除错误信息
|
|
|
+ */
|
|
|
+const clearError = (): void => {
|
|
|
+ errorMessage.value = '';
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取验证码
|
|
|
+ */
|
|
|
+const getCaptcha = async (): Promise<void> => {
|
|
|
+ try {
|
|
|
+ // 这里应该调用获取验证码的API
|
|
|
+ // const response = await useClientRequest.get('/auth/captcha');
|
|
|
+ // if (response.code === 200) {
|
|
|
+ // captchaImage.value = response.data.image;
|
|
|
+ // loginForm.uuid = response.data.uuid;
|
|
|
+ // needCaptcha.value = true;
|
|
|
+ // }
|
|
|
+
|
|
|
+ // 临时模拟
|
|
|
+ needCaptcha.value = false;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取验证码失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 刷新验证码
|
|
|
+ */
|
|
|
+const refreshCaptcha = (): void => {
|
|
|
+ getCaptcha();
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 处理登录
|
|
|
+ */
|
|
|
+const handleLogin = async (): Promise<void> => {
|
|
|
+ try {
|
|
|
+ // 表单验证
|
|
|
+ const valid = await loginFormRef.value?.validate();
|
|
|
+ if (!valid) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = true;
|
|
|
+ errorMessage.value = '';
|
|
|
+
|
|
|
+ // 调用登录
|
|
|
+ const success = await authStore.login(loginForm);
|
|
|
+
|
|
|
+ if (success) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '登录成功',
|
|
|
+ icon: 'success'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 登录成功后跳转
|
|
|
+ setTimeout(() => {
|
|
|
+ const pages = getCurrentPages();
|
|
|
+ const currentPage = pages[pages.length - 1];
|
|
|
+ const options = (currentPage as any).options || {};
|
|
|
+ const redirectUrl = options.redirect || '/pages/index/index';
|
|
|
+
|
|
|
+ uni.reLaunch({
|
|
|
+ url: redirectUrl
|
|
|
+ });
|
|
|
+ }, 1500);
|
|
|
+ } else {
|
|
|
+ errorMessage.value = authStore.loginError || '登录失败,请重试';
|
|
|
+ // 如果是验证码错误,刷新验证码
|
|
|
+ if (needCaptcha.value) {
|
|
|
+ refreshCaptcha();
|
|
|
+ loginForm.code = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('登录错误:', error);
|
|
|
+ errorMessage.value = error.message || '网络错误,请稍后重试';
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 忘记密码
|
|
|
+ */
|
|
|
+const handleForgotPassword = (): void => {
|
|
|
+ uni.showToast({
|
|
|
+ title: '功能开发中',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 注册账号
|
|
|
+ */
|
|
|
+const handleRegister = (): void => {
|
|
|
+ uni.showToast({
|
|
|
+ title: '功能开发中',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 页面加载
|
|
|
+ */
|
|
|
+onMounted(() => {
|
|
|
+ // 检查是否需要验证码
|
|
|
+ getCaptcha();
|
|
|
+
|
|
|
+ // 如果已经登录,直接跳转
|
|
|
+ if (authStore.isLoggedIn) {
|
|
|
+ uni.reLaunch({
|
|
|
+ url: '/pages/index/index'
|
|
|
+ });
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.login-container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.login-wrapper {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 600rpx;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 60rpx 40rpx;
|
|
|
+ box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.logo-section {
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 60rpx;
|
|
|
+
|
|
|
+ .logo {
|
|
|
+ width: 120rpx;
|
|
|
+ height: 120rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .app-name {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.form-section {
|
|
|
+ .captcha-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20rpx;
|
|
|
+
|
|
|
+ .captcha-input {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .captcha-image {
|
|
|
+ width: 160rpx;
|
|
|
+ height: 80rpx;
|
|
|
+ border: 1rpx solid #ddd;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .remember-section {
|
|
|
+ margin: 30rpx 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .error-message {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10rpx;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+ padding: 20rpx;
|
|
|
+ background: #fef0f0;
|
|
|
+ border: 1rpx solid #fbc4c4;
|
|
|
+ border-radius: 8rpx;
|
|
|
+
|
|
|
+ .error-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #f56c6c;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .login-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 88rpx;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 10rpx;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+
|
|
|
+ &:not([disabled]):active {
|
|
|
+ transform: translateY(2rpx);
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ &[disabled] {
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+
|
|
|
+ &--loading {
|
|
|
+ .loading-icon {
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.footer-section {
|
|
|
+ text-align: center;
|
|
|
+ margin-top: 40rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 20rpx;
|
|
|
+
|
|
|
+ .footer-link {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #667eea;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .footer-divider {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #ddd;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ from {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// uni-app 组件样式重写
|
|
|
+::v-deep .uni-forms-item {
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .uni-forms-item__label {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666;
|
|
|
+ margin-bottom: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .uni-easyinput {
|
|
|
+ .uni-easyinput__content {
|
|
|
+ height: 88rpx;
|
|
|
+ border: 1rpx solid #ddd;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ background: #fafafa;
|
|
|
+
|
|
|
+ &.uni-easyinput__content-focus {
|
|
|
+ border-color: #667eea;
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .uni-easyinput__content-input {
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #333;
|
|
|
+ padding-left: 20rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .uni-easyinput__placeholder-class {
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .uni-data-checklist {
|
|
|
+ .checklist-group {
|
|
|
+ .checklist-box {
|
|
|
+ border: none;
|
|
|
+ padding: 0;
|
|
|
+ margin: 0;
|
|
|
+
|
|
|
+ .checklist-content {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+
|
|
|
+ .checkbox__inner {
|
|
|
+ border-color: #667eea;
|
|
|
+
|
|
|
+ &.checkbox__inner--checked {
|
|
|
+ background-color: #667eea;
|
|
|
+ border-color: #667eea;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|