login.vue 10.4 KB
<template>
  <view class="login-wrapper">
    <!-- 1. Logo 及品牌信息 -->
    <view class="logo-section">
      <view class="logo-box">
        <!-- 渐变绿火焰 SVG 矢量图,无需本地图片 -->
        <svg viewBox="0 0 24 24" class="logo-svg">
          <path
            d="M12 2C12 2 6 7.5 6 13C6 16.8 9.1 20 12 20C14.9 20 18 16.8 18 13C18 7.5 12 2 12 2ZM12 16C10.3 16 9 14.7 9 13C9 10.5 12 7.5 12 7.5C12 7.5 15 10.5 15 13C15 14.7 13.7 16 12 16Z"
            fill="url(#flameGradient)"
          />
          <defs>
            <linearGradient id="flameGradient" x1="0%" y1="0%" x2="100%" y2="100%">
              <stop offset="0%" stop-color="#8fc31f" />
              <stop offset="100%" stop-color="#00b074" />
            </linearGradient>
          </defs>
        </svg>
      </view>
      <view class="brand-title">FitFlow 悦动健身</view>
      <view class="brand-slogan">健康极简 · 开启你的蜕变时刻</view>
    </view>

    <!-- 2. 登录方式 Tab 切换 -->
    <view class="tab-container">
      <view class="tab-item" :class="{ active: activeTab === 'sms' }" @click="activeTab = 'sms'">
        <u-icon
          name="phone"
          size="18"
          :color="activeTab === 'sms' ? '#0a1931' : '#7a8290'"
          class="tab-icon"
        />
        <text>免密登录</text>
      </view>
      <view
        class="tab-item"
        :class="{ active: activeTab === 'password' }"
        @click="activeTab = 'password'"
      >
        <u-icon
          name="lock"
          size="18"
          :color="activeTab === 'password' ? '#0a1931' : '#7a8290'"
          class="tab-icon"
        />
        <text>密码登录</text>
      </view>
    </view>

    <!-- 3. 表单区域 -->
    <view class="form-container">
      <!-- 手机号码 -->
      <view class="form-item-wrapper">
        <text class="form-label">手机号码</text>
        <view class="input-box">
          <u-icon name="phone" size="20" color="#a1a8b3" class="input-icon" />
          <input
            type="number"
            v-model="phoneNumber"
            placeholder="请输入您的手机号"
            placeholder-style="color: #a1a8b3"
            maxlength="11"
            class="native-input"
          />
        </view>
      </view>

      <!-- 验证码 / 密码(支持免密与密码登录切换) -->
      <view class="form-item-wrapper" v-if="activeTab === 'sms'">
        <text class="form-label">验证码</text>
        <view class="code-row">
          <view class="input-box flex-1">
            <u-icon name="chat" size="20" color="#a1a8b3" class="input-icon" />
            <input
              type="number"
              v-model="verifyCode"
              placeholder="6位验证码"
              placeholder-style="color: #a1a8b3"
              maxlength="6"
              class="native-input"
            />
          </view>
          <!-- 获取验证码按钮 -->
          <view class="code-btn" :class="{ disabled: countdown > 0 }" @click="handleGetCode">
            <text>{{ countdown > 0 ? `${countdown}s 后重试` : '获取验证码' }}</text>
          </view>
        </view>
      </view>

      <!-- 密码输入框(密码登录模式时显示) -->
      <view class="form-item-wrapper" v-else>
        <text class="form-label">登录密码</text>
        <view class="input-box">
          <u-icon name="lock" size="20" color="#a1a8b3" class="input-icon" />
          <input
            type="password"
            v-model="password"
            placeholder="请输入您的密码"
            placeholder-style="color: #a1a8b3"
            class="native-input"
          />
        </view>
      </view>
    </view>

    <!-- 4. 用户协议与隐私政策 -->
    <view class="agreement-container" @click="isAgreed = !isAgreed">
      <view class="checkbox-box" :class="{ checked: isAgreed }">
        <u-icon v-if="isAgreed" name="checkbox-mark" size="12" color="#ffffff" />
      </view>
      <view class="agreement-text">
        我已阅读并同意 FitFlow
        <text class="link-text" @click.stop="goToAgreement('service')">《用户服务协议》</text>

        <text class="link-text" @click.stop="goToAgreement('privacy')">《隐私政策》</text>
      </view>
    </view>

    <!-- 5. 立即登录按钮 -->
    <view class="submit-btn" @click="handleLogin">
      <text>立即登录</text>
      <u-icon name="arrow-right" size="14" color="#ffffff" class="btn-arrow" />
    </view>
  </view>
</template>

<script setup>
  import { ref } from 'vue';

  const activeTab = ref('sms'); // sms: 免密, password: 密码
  const phoneNumber = ref('');
  const verifyCode = ref('');
  const password = ref('');
  const isAgreed = ref(false);

  // 验证码倒计时逻辑
  const countdown = ref(0);
  const handleGetCode = () => {
    if (countdown.value > 0) return;
    if (!phoneNumber.value || phoneNumber.value.length !== 11) {
      uni.showToast({ title: '请输入正确的手机号码', icon: 'none' });
      return;
    }

    uni.showToast({ title: '验证码已发送', icon: 'success' });
    countdown.value = 60;
    const timer = setInterval(() => {
      countdown.value--;
      if (countdown.value <= 0) {
        clearInterval(timer);
      }
    }, 1000);
  };

  // 登录提交事件
  const handleLogin = () => {
    if (!isAgreed.value) {
      uni.showToast({ title: '请先阅读并同意用户协议与隐私政策', icon: 'none' });
      return;
    }
    if (!phoneNumber.value) {
      uni.showToast({ title: '请输入手机号码', icon: 'none' });
      return;
    }

    uni.showToast({ title: '登录成功', icon: 'success' });
  };

  // 协议点击跳转
  const goToAgreement = (type) => {
    uni.showToast({
      title: `跳转至${type === 'service' ? '用户协议' : '隐私政策'}`,
      icon: 'none',
    });
  };
</script>

<style lang="scss" scoped>
  .login-wrapper {
    min-height: 100vh;
    background-color: #ffffff;
    padding: 80rpx 50rpx;
    box-sizing: border-box;

    // 1. Logo
    .logo-section {
      display: flex;
      flex-direction: column;
      align-items: center;
      margin-bottom: 70rpx;
      margin-top: 40rpx;

      .logo-box {
        width: 140rpx;
        height: 140rpx;
        border-radius: 40rpx;
        background: #ffffff;
        border: 3rpx solid #d2ee9e;
        box-shadow: 0 16rpx 40rpx rgba(111, 214, 32, 0.12);
        display: flex;
        justify-content: center;
        align-items: center;
        margin-bottom: 30rpx;

        .logo-svg {
          width: 76rpx;
          height: 76rpx;
        }
      }

      .brand-title {
        font-size: 46rpx;
        font-weight: bold;
        color: #0a1931;
        letter-spacing: 2rpx;
      }

      .brand-slogan {
        font-size: 26rpx;
        color: #9097a3;
        margin-top: 12rpx;
      }
    }

    // 2. Tab
    .tab-container {
      height: 96rpx;
      background-color: #f1f3f7;
      border-radius: 20rpx;
      display: flex;
      padding: 8rpx;
      box-sizing: border-box;
      margin-bottom: 50rpx;

      .tab-item {
        flex: 1;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 28rpx;
        color: #7a8290;
        font-weight: 500;
        transition: all 0.2s ease;
        border-radius: 16rpx;

        .tab-icon {
          margin-right: 12rpx;
        }

        &.active {
          background-color: #ffffff;
          color: #0a1931;
          font-weight: bold;
          box-shadow: 0 4rpx 16rpx rgba(10, 25, 49, 0.05);
        }
      }
    }

    // 3. Form
    .form-container {
      .form-item-wrapper {
        margin-bottom: 36rpx;

        .form-label {
          font-size: 26rpx;
          color: #0a1931;
          font-weight: bold;
          display: block;
          margin-bottom: 16rpx;
        }

        .input-box {
          height: 104rpx;
          background-color: #f4f6fa;
          border: 2rpx solid #e1e6eb;
          border-radius: 24rpx;
          display: flex;
          align-items: center;
          padding: 0 30rpx;
          box-sizing: border-box;

          .input-icon {
            margin-right: 16rpx;
          }

          .native-input {
            flex: 1;
            height: 100%;
            font-size: 28rpx;
            color: #0a1931;
          }
        }

        // 验证码一栏的双列布局
        .code-row {
          display: flex;
          align-items: center;

          .flex-1 {
            flex: 1;
          }

          .code-btn {
            width: 220rpx;
            height: 104rpx;
            border-radius: 24rpx;
            background-color: #f9fdf2;
            border: 2rpx solid #d0ee9c;
            display: flex;
            justify-content: center;
            align-items: center;
            margin-left: 20rpx;
            font-size: 26rpx;
            color: #8fc31f;
            font-weight: bold;
            transition: all 0.2s ease;

            &:active {
              opacity: 0.8;
            }

            &.disabled {
              color: #b0b8c4;
              background-color: #f4f6fa;
              border-color: #e1e6eb;
            }
          }
        }
      }
    }

    // 4. Agreement
    .agreement-container {
      display: flex;
      align-items: flex-start;
      margin-top: 40rpx;
      margin-bottom: 50rpx;
      padding: 0 6rpx;

      .checkbox-box {
        width: 32rpx;
        height: 32rpx;
        border: 2rpx solid #c2c9d3;
        border-radius: 8rpx;
        margin-right: 16rpx;
        margin-top: 4rpx;
        display: flex;
        justify-content: center;
        align-items: center;
        transition: all 0.15s ease;
        box-sizing: border-box;

        &.checked {
          border-color: #00b074;
          background-color: #00b074;
        }
      }

      .agreement-text {
        flex: 1;
        font-size: 24rpx;
        color: #7a8290;
        line-height: 1.5;

        .link-text {
          color: #8fc31f;
          font-weight: 500;
        }
      }
    }

    // 5. Submit Button
    .submit-btn {
      height: 104rpx;
      border-radius: 24rpx;
      background: linear-gradient(135deg, #79d621 0%, #00b074 100%);
      box-shadow: 0 16rpx 40rpx rgba(0, 176, 116, 0.25);
      display: flex;
      justify-content: center;
      align-items: center;
      color: #ffffff;
      font-size: 32rpx;
      font-weight: bold;
      letter-spacing: 2rpx;
      transition: all 0.2s ease;

      &:active {
        transform: scale(0.98);
        opacity: 0.9;
      }

      .btn-arrow {
        margin-left: 10rpx;
      }
    }
  }
</style>