wz-uniapp/src/pages/login/index.vue

455 lines
10 KiB
Vue

<template>
<view class="login-page">
<view class="bg-top"></view>
<view class="content">
<view class="brand">
<view class="brand-outer">
<view class="brand-inner">U</view>
</view>
<text class="brand-title">{{ text.brandTitle }}</text>
<text class="brand-subtitle">{{ text.brandSubtitle }}</text>
</view>
<view class="btn-group" v-if="!showAccount">
<!-- #ifdef MP-WEIXIN -->
<button
class="btn btn-primary"
:disabled="!agree"
:class="{ 'btn-disabled': !agree }"
open-type="getPhoneNumber"
@getphonenumber="onGetPhoneNumber"
@click="handlePhoneLoginClick"
>
微信手机号快速登录
</button>
<button class="btn btn-secondary" @click="toggleAccount(true)">
密码登录
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<button class="btn btn-primary" @click="toggleAccount(true)">
密码登录
</button>
<!-- #endif -->
</view>
<view v-if="showAccount" class="account-panel">
<view class="field">
<text class="field-label">手机号</text>
<input
v-model.trim="form.phone"
class="field-input"
type="number"
placeholder="请输入手机号"
maxlength="11"
/>
</view>
<view class="field">
<text class="field-label">密码</text>
<input
v-model="form.password"
class="field-input"
type="text"
password
placeholder="请输入密码"
/>
</view>
<button class="btn btn-primary" @click="handlePasswordLogin">
登录
</button>
<button class="btn btn-secondary" @click="toggleAccount(false)">
取消
</button>
</view>
<view class="agreement">
<checkbox-group style="width: 10%" @change="onAgreeChange">
<checkbox class="checkbox" value="agree" :checked="agree" />
</checkbox-group>
<view class="agreement-text">
<text>我已阅读并同意</text>
<text class="link" @click="goService">《用户服务协议》</text>
<text>、</text>
<text class="link" @click="goPrivacy">隐私保护指引</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { post } from "@/common/request";
import { STORAGE_KEYS, ENV } from "@/common/env";
import { API } from "@/common/api";
const app = getApp();
export default {
data() {
return {
text: {
srandTitle: "XXX小程序",
brandSubtitle: "连接精彩生活,发现无限可能",
},
agree: false,
showAccount: false,
form: {
phone: "",
password: "",
},
};
},
onShow() {
this.text.brandTitle = app.globalData.ENV.APP_NAME;
this.text.brandSubtitle = app.globalData.ENV.APP_SUBTITLE;
},
methods: {
onAgreeChange(e) {
const values = (e.detail && e.detail.value) || [];
this.agree = values.includes("agree");
},
ensureAgree() {
if (this.agree) return true;
uni.showModal({
title: "提示",
content: "请先阅读并同意隐私保护指引与用户服务协议",
confirmText: "去阅读",
cancelText: "暂不",
success: (res) => {
if (res.confirm) {
this.goPrivacy();
}
},
});
return false;
},
handlePhoneLoginClick() {
if (!this.agree) {
this.ensureAgree();
}
},
toggleAccount(nextState) {
this.showAccount =
typeof nextState === "boolean" ? nextState : !this.showAccount;
if (!this.showAccount) {
this.form.phone = "";
this.form.password = "";
}
},
async onGetPhoneNumber(e) {
if (!this.ensureAgree()) return;
const detail = e && e.detail ? e.detail : {};
if (detail.errMsg && detail.errMsg.indexOf("ok") === -1) {
uni.showToast({
title: "已取消授权",
icon: "none",
});
return;
}
try {
const loginRes = await uni.login({
provider: "weixin",
});
if (!loginRes || !loginRes.code) {
uni.showToast({
title: "获取登录凭证失败",
icon: "none",
});
return;
}
const payload = {
code: loginRes.code,
phoneCode: detail.code || "",
encryptedData: detail.encryptedData || "",
iv: detail.iv || "",
};
const res = await post(API.WX_MINI_LOGIN, payload, {
allowGuest: true,
});
const loginData = this.normalizeLoginData(res);
if (!loginData.token) {
uni.showToast({
title: loginData.message || "登录失败",
icon: "none",
});
return;
}
uni.setStorageSync(STORAGE_KEYS.TOKEN, loginData.token);
uni.setStorageSync(STORAGE_KEYS.USER_INFO, loginData.userInfo || {});
uni.showToast({
title: "登录成功",
icon: "success",
});
uni.switchTab({
url: "/pages/index/index",
});
} catch (err) {
uni.showToast({
title: err.message || "登录失败",
icon: "none",
});
}
},
async handlePasswordLogin() {
if (!this.ensureAgree()) return;
const phone = (this.form.phone || "").trim();
const password = this.form.password || "";
if (!phone || phone.length !== 11) {
uni.showToast({
title: "请输入正确手机号",
icon: "none",
});
return;
}
if (!password) {
uni.showToast({
title: "请输入密码",
icon: "none",
});
return;
}
try {
const res = await post(
API.USER_PASSWORD_LOGIN,
{
phone,
password,
},
{
allowGuest: true,
},
);
const data = res && res.data ? res.data : res;
const token = data && data.token ? data.token : "";
const userInfo = data && data.user ? data.user : {};
if (!token) {
uni.showToast({
title: res.message || res.msg || "登录失败",
icon: "none",
});
return;
}
uni.setStorageSync(STORAGE_KEYS.TOKEN, token);
uni.setStorageSync(STORAGE_KEYS.USER_INFO, userInfo);
uni.showToast({
title: "登录成功",
icon: "success",
});
uni.switchTab({
url: "/pages/index/index",
});
} catch (err) {
uni.showToast({
title: err.message || "登录失败",
icon: "none",
});
}
},
normalizeLoginData(res) {
if (!res)
return {
token: "",
userInfo: {},
message: "",
};
if (typeof res.code !== "undefined" && ![0, 200].includes(res.code)) {
return {
token: "",
userInfo: {},
message: res.message || res.msg || "",
};
}
const data = res.data || res.result || res;
return {
token: data.token || data.access_token || data.accessToken || "",
userInfo: data.userInfo ||
data.user || {
id: data.userId,
platformUserId: data.platformUserId,
openid: data.openid,
unionid: data.unionid,
phone: data.phone,
},
message: res.message || res.msg || "",
};
},
goService() {
uni.navigateTo({
url: "/pages/protocol/service",
});
},
goPrivacy() {
uni.navigateTo({
url: "/pages/protocol/privacy",
});
},
},
};
</script>
<style scoped>
.login-page {
min-height: 100vh;
background: #f8f6f6;
position: relative;
overflow: hidden;
}
.bg-top {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 360rpx;
background: linear-gradient(
180deg,
rgba(236, 91, 19, 0.08),
rgba(248, 246, 246, 0)
);
}
.content {
position: relative;
z-index: 1;
padding: 120rpx 48rpx 80rpx;
display: flex;
flex-direction: column;
min-height: 85vh;
}
.brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.brand-outer {
width: 160rpx;
height: 160rpx;
border-radius: 80rpx;
background: rgba(236, 91, 19, 0.12);
border: 2rpx solid rgba(236, 91, 19, 0.25);
display: flex;
align-items: center;
justify-content: center;
}
.brand-inner {
width: 112rpx;
height: 112rpx;
border-radius: 28rpx;
background: #ec5b13;
color: #fff;
font-size: 52rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 24rpx rgba(236, 91, 19, 0.35);
}
.brand-title {
font-size: 44rpx;
font-weight: 700;
color: #1f2d3d;
}
.brand-subtitle {
font-size: 26rpx;
color: #7a8699;
}
.btn-group {
margin-top: 64rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.btn {
width: 100%;
height: 96rpx;
border-radius: 24rpx;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary {
background: #07c160;
color: #fff;
}
.btn-secondary {
background: #f0f2f6;
color: #3a4556;
}
.btn-disabled {
opacity: 0.5;
}
.account-panel {
margin-top: 36rpx;
padding: 28rpx;
background: #ffffff;
border-radius: 20rpx;
box-shadow: 0 12rpx 24rpx rgba(15, 33, 66, 0.06);
display: flex;
flex-direction: column;
gap: 20rpx;
}
.field-label {
font-size: 24rpx;
color: #7a8699;
}
.field-input {
margin-top: 12rpx;
padding: 20rpx;
border-radius: 16rpx;
background: #f5f6fa;
font-size: 28rpx;
color: #1f2d3d;
}
.link {
color: #ec5b13;
font-size: 26rpx;
}
.agreement {
margin-top: auto;
display: flex;
align-items: flex-start;
gap: 20rpx;
padding-top: 40rpx;
}
.checkbox {
transform: scale(0.9);
}
.agreement-text {
font-size: 22rpx;
color: #7a8699;
line-height: 1.6;
}
.agreement-text .link {
font-size: 22rpx;
font-weight: 600;
}
</style>