Login.vue 5.7 KB
<template>
  <div class="login-wrap">
    <div class="login-shell">
      <section class="login-visual">
        <div class="visual-badge">Modern Soft-Neo UI</div>
        <h1>让配送管理像清晨的云层一样轻盈。</h1>
        <p>柔和渐变、圆润卡片与轻插画氛围,把后台工作台重新整理成更亲和的中控体验。</p>
        <div class="visual-stats">
          <div>
            <span>Theme</span>
            <strong>Lavender + Warm Glow</strong>
          </div>
          <div>
            <span>Layout</span>
            <strong>Floating Three-Column</strong>
          </div>
        </div>
        <div class="illustration-wrap">
          <div class="bubble bubble-a"></div>
          <div class="bubble bubble-b"></div>
          <img :src="heroImage" alt="soft illustration" />
        </div>
      </section>

      <a-card class="login-card">
        <div class="login-card-head">
          <span class="head-chip">Welcome Back</span>
          <h2>外卖管理系统</h2>
          <p>登录到柔和重构后的运营工作台</p>
        </div>
        <a-form :model="form" @finish="onSubmit" layout="vertical">
          <a-form-item name="role" label="登录身份">
            <a-radio-group v-model:value="form.role" button-style="solid">
              <a-radio-button value="substation">分站管理员</a-radio-button>
              <a-radio-button value="admin">超级管理员</a-radio-button>
            </a-radio-group>
          </a-form-item>
          <a-form-item name="account" :rules="[{ required: true, message: '请输入账号' }]">
            <a-input v-model:value="form.account" placeholder="登录账号" size="large">
              <template #prefix><user-outlined /></template>
            </a-input>
          </a-form-item>
          <a-form-item name="pass" :rules="[{ required: true, message: '请输入密码' }]">
            <a-input-password v-model:value="form.pass" placeholder="密码" size="large">
              <template #prefix><lock-outlined /></template>
            </a-input-password>
          </a-form-item>
          <a-form-item>
            <a-button type="primary" html-type="submit" block size="large" :loading="loading">
              登录进入中台
            </a-button>
          </a-form-item>
        </a-form>
      </a-card>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { useAuthStore } from '@/stores/auth'
import request from '@/utils/request'
import heroImage from '@/assets/hero.png'

const router = useRouter()
const auth = useAuthStore()
const loading = ref(false)
const form = reactive({ account: '', pass: '', role: 'substation' })

async function onSubmit() {
  loading.value = true
  try {
    const res: any = await request.post('/api/admin/auth/login', form)
    auth.setToken(res.data.token)
    auth.setUserInfo(res.data)
    router.push('/')
  } catch {
    // 错误已由 request 拦截器处理
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.login-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.login-shell {
  width: min(1180px, 100%);
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(360px, 430px);
  gap: 26px;
}

.login-visual,
.login-card {
  border: 1px solid rgba(255, 255, 255, 0.62);
  background: rgba(255, 255, 255, 0.76);
  backdrop-filter: blur(24px);
  box-shadow: 0 20px 50px rgba(126, 110, 211, 0.15);
}

.login-visual {
  position: relative;
  overflow: hidden;
  border-radius: 36px;
  padding: 42px;
}

.visual-badge,
.head-chip {
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  padding: 8px 14px;
  background: linear-gradient(135deg, rgba(140, 124, 240, 0.14), rgba(255, 210, 232, 0.26));
  color: #6f5fe2;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.login-visual h1,
.login-card h2 {
  font-family: 'Outfit', sans-serif;
  color: #2f2946;
}

.login-visual h1 {
  font-size: 46px;
  line-height: 1.05;
  margin: 18px 0 14px;
}

.login-visual p,
.login-card-head p {
  color: #8d88a4;
  max-width: 520px;
}

.visual-stats {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  margin: 28px 0;
}

.visual-stats > div {
  min-width: 210px;
  border-radius: 22px;
  background: rgba(255, 255, 255, 0.7);
  padding: 14px 16px;
}

.visual-stats span {
  display: block;
  color: #9893ad;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 6px;
}

.visual-stats strong {
  color: #342d4f;
  font-family: 'Outfit', sans-serif;
}

.illustration-wrap {
  position: relative;
  min-height: 280px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.illustration-wrap img {
  position: relative;
  z-index: 2;
  width: min(100%, 360px);
  filter: drop-shadow(0 26px 40px rgba(137, 120, 216, 0.22));
}

.bubble {
  position: absolute;
  border-radius: 999px;
}

.bubble-a {
  width: 260px;
  height: 260px;
  background: radial-gradient(circle, rgba(198, 185, 255, 0.55) 0%, transparent 68%);
  left: 30px;
  bottom: 10px;
}

.bubble-b {
  width: 120px;
  height: 120px;
  background: radial-gradient(circle, rgba(255, 218, 152, 0.45) 0%, transparent 70%);
  right: 60px;
  top: 24px;
}

.login-card {
  border-radius: 32px;
  padding: 8px;
}

.login-card-head {
  margin-bottom: 20px;
}

.login-card-head h2 {
  margin: 16px 0 8px;
  font-size: 32px;
}

@media (max-width: 980px) {
  .login-shell {
    grid-template-columns: 1fr;
  }

  .login-visual {
    padding: 28px;
  }

  .login-visual h1 {
    font-size: 34px;
  }
}
</style>