Files
pingxiangcard/src/pages/login/login.vue
T
jacobxu666andClaude Haiku 4.5 f10a1896e7 fix: comply with F2E Spec — filename naming, any types, JSDoc
- Rename PascalCase component files to kebab-case (AnnouncementModal →
  announcement-modal, McGlass → mc-glass)
- Replace any types with proper uni-app/WeChat callback types
  (RequestSuccessCallbackResult, LoginRes, ShowModalRes, etc.)
- Add JSDoc documentation comments to all exported/important functions

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-26 14:45:49 +08:00

367 lines
8.6 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">
<!-- Brand area -->
<view class="brand" :class="{ 'brand--visible': showContent }">
<image class="brand-logo" src="/static/logo.png" mode="aspectFit" />
<text class="brand-name">爱上萍乡</text>
<text class="brand-desc">赣湘之间,发现旅途美好</text>
</view>
<!-- Actions: 2 buttons -->
<view class="actions" :class="{ 'actions--visible': showContent }">
<!-- 未同意条款:普通按钮,点击提示 -->
<button
v-if="!agreed"
class="btn-primary"
:class="{ 'btn-primary--disabled': loading }"
:disabled="loading"
@tap="showToast('请先阅读并同意服务条款和隐私政策')"
>
<view class="btn-primary__inner">
<view class="wx-icon" />
<text class="btn-primary__text">微信一键登录</text>
</view>
</button>
<!-- 已同意条款:微信授权按钮 -->
<button
v-else
class="btn-primary"
:class="{ 'btn-primary--disabled': loading }"
open-type="getPhoneNumber"
@getphonenumber="handleLogin"
:disabled="loading"
>
<view v-if="!loading" class="btn-primary__inner">
<view class="wx-icon" />
<text class="btn-primary__text">微信一键登录</text>
</view>
<view v-else class="btn-primary__inner">
<view class="loading-spinner" />
<text class="btn-primary__text">登录中...</text>
</view>
</button>
<button class="btn-cancel" @tap="handleCancel">
<text class="btn-cancel__text">取消</text>
</button>
</view>
<!-- Agreement: checkbox + links -->
<view class="agreement" :class="{ 'agreement--visible': showContent }">
<view
class="checkbox"
:class="{ 'checkbox--checked': agreed }"
@tap="agreed = !agreed"
>
<view v-if="agreed" class="checkbox__check" />
</view>
<view class="agreement__text">
<text class="agreement__label">我已阅读并同意</text>
<text class="agreement__link" @tap="openAgreement">《服务条款》</text>
<text class="agreement__link" @tap="openPrivacy">《隐私政策》</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useUserStore } from '../../store/user'
import { showToast } from '../../utils'
const userStore = useUserStore()
const agreed = ref(false)
const loading = ref(false)
const showContent = ref(false)
onMounted(() => {
setTimeout(() => {
showContent.value = true
}, 80)
})
/** 取消登录,返回上一页或首页 */
function handleCancel() {
uni.navigateBack({ fail: () => uni.switchTab({ url: '/pages/index/index' }) })
}
/** 打开服务条款 */
function openAgreement() {
uni.navigateTo({ url: '/pages/login/agreement' })
}
/** 打开隐私政策 */
function openPrivacy() {
uni.navigateTo({ url: '/pages/login/privacy' })
}
/** 微信一键登录(获取手机号后调后端登录接口) */
async function handleLogin(e: { detail: { errMsg: string; code?: string } }) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
return
}
loading.value = true
try {
const phoneCode = e.detail.code
if (!phoneCode) {
showToast('手机号获取失败,请重试')
return
}
const loginRes = await new Promise<UniNamespace.LoginRes>((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject })
})
if (!loginRes || !loginRes.code) {
showToast('微信登录失败,请重试')
return
}
await userStore.wxLogin({
loginCode: loginRes.code,
phoneCode: phoneCode
})
showToast('登录成功', 'success')
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
} catch (err: unknown) {
showToast(err instanceof Error ? err.message : '登录失败')
} finally {
loading.value = false
}
}
</script>
<style lang="scss" scoped>
/* Lucide icons — data URI pattern (matches scenic-detail) */
$icon-size: 40rpx;
$icon-wechat: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.9 20A9 9 0 1 0 4 16.1L2 22Z'/%3E%3C/svg%3E");
$icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
.login-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background: #FFFFFF;
padding: 0 48rpx;
gap: 48rpx;
overflow-x: hidden;
box-sizing: border-box;
}
/* ========== Brand ========== */
.brand {
display: flex;
flex-direction: column;
align-items: center;
opacity: 0;
transform: translateY(20rpx);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.brand--visible {
opacity: 1;
transform: translateY(0);
}
.brand-logo {
width: 160rpx;
height: 160rpx;
margin-bottom: 24rpx;
}
.brand-name {
font-family: $font-family-display;
font-size: $font-size-title;
font-weight: $font-weight-bold;
color: $uv-main-color;
line-height: $line-height-tight;
letter-spacing: 4rpx;
margin-bottom: 24rpx;
}
.brand-desc {
font-size: $font-size-base;
color: $uv-tips-color;
font-weight: $font-weight-normal;
letter-spacing: 2rpx;
}
/* ========== Actions ========== */
.actions {
width: 100%;
opacity: 0;
transform: translateY(20rpx);
transition: opacity 0.6s ease 0.1s, transform 0.6s ease 0.1s;
}
.actions--visible {
opacity: 1;
transform: translateY(0);
}
.btn-primary {
width: 100%;
height: 92rpx;
border-radius: $radius-lg;
background: $uv-primary;
border: none;
padding: 0;
margin: 0 0 24rpx 0;
line-height: normal;
box-shadow: 0 6rpx 20rpx rgba(60, 156, 255, 0.28);
transition: all $transition-base;
&::after {
display: none;
}
}
.btn-primary--disabled {
opacity: 0.6;
}
.btn-primary:active:not(.btn-primary--disabled) {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(60, 156, 255, 0.2);
}
.btn-primary__inner {
display: flex;
align-items: center;
justify-content: center;
height: 92rpx;
}
.btn-primary__text {
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: #FFFFFF;
margin-left: $spacing-2;
letter-spacing: 2rpx;
}
.wx-icon {
width: $icon-size;
height: $icon-size;
background-image: $icon-wechat;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.loading-spinner {
width: 36rpx;
height: 36rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #FFFFFF;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.btn-cancel {
width: 100%;
height: 92rpx;
border-radius: $radius-lg;
background: #FFFFFF;
border: 2rpx solid $uv-border-color;
padding: 0;
margin: 0;
line-height: 92rpx;
text-align: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
transition: all $transition-base;
&::after {
display: none;
}
}
.btn-cancel:active {
transform: scale(0.98);
background: $uv-bg-color;
}
.btn-cancel__text {
display: block;
width: 100%;
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $uv-content-color;
text-align: center;
}
/* ========== Agreement ========== */
.agreement {
width: 100%;
display: flex;
align-items: flex-start;
opacity: 0;
transform: translateY(20rpx);
transition: opacity 0.6s ease 0.2s, transform 0.6s ease 0.2s;
}
.agreement--visible {
opacity: 1;
transform: translateY(0);
}
.checkbox {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
border: 3rpx solid $uv-border-color;
border-radius: 10rpx;
margin-right: $spacing-2;
margin-top: 2rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all $transition-base;
}
.checkbox--checked {
background: $uv-primary;
border-color: $uv-primary;
}
.checkbox__check {
width: 24rpx;
height: 24rpx;
background-image: $icon-check;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.agreement__text {
display: flex;
flex-wrap: wrap;
align-items: center;
line-height: $line-height-normal;
}
.agreement__label {
font-size: $font-size-base;
color: $uv-content-color;
}
.agreement__link {
font-size: $font-size-base;
color: $uv-primary;
&:active {
opacity: 0.7;
}
}
</style>