| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- <script setup>
- import { computed, ref } from 'vue'
- import { onShow } from '@dcloudio/uni-app'
- import { useUserStore } from '@/stores/user'
- import { DEFAULT_PLACEHOLDER_IMAGE_URL } from '@/utils/staticAsset'
- import { useShareToLogin } from '@/utils/shareToLogin'
- useShareToLogin()
- const userStore = useUserStore()
- const form = ref({
- username: '',
- password: ''
- })
- const isLoading = ref(false)
- const agreeProtocol = ref(false)
- const loginDisabled = computed(() => isLoading.value)
- const protocolDialogVisible = ref(false)
- const protocolDialogForWechat = ref(false)
- let protocolDialogResolver = null
- onShow(() => {
- userStore.checkLoginStatus()
- if (userStore.isLoggedIn) {
- userStore.navigateToHome()
- }
- })
- const loginPageStyle = computed(() => ({ '--theme-color': '#0a9f6d' }))
- const handleSubmit = async () => {
- if (!agreeProtocol.value) {
- protocolDialogForWechat.value = false
- const shouldContinue = await new Promise((resolve) => {
- protocolDialogResolver = resolve
- protocolDialogVisible.value = true
- })
- if (!shouldContinue) return
- agreeProtocol.value = true
- }
- if (!form.value.username) {
- uni.showToast({ title: '请输入用户名', icon: 'none' })
- return
- }
- if (!form.value.password) {
- uni.showToast({ title: '请输入密码', icon: 'none' })
- return
- }
- isLoading.value = true
- try {
- const success = await userStore.login({
- username: form.value.username,
- password: form.value.password
- })
- if (success) {
- userStore.navigateToHome()
- }
- } finally {
- isLoading.value = false
- }
- }
- const goUserAgreement = () => {
- uni.navigateTo({ url: '/pages/login/user-agreement' })
- }
- const goPrivacyPolicy = () => {
- uni.navigateTo({ url: '/pages/login/privacy-policy' })
- }
- const toggleProtocolAgreement = () => {
- agreeProtocol.value = !agreeProtocol.value
- }
- const resolveProtocolDialog = (confirmed) => {
- protocolDialogVisible.value = false
- protocolDialogForWechat.value = false
- if (protocolDialogResolver) {
- protocolDialogResolver(confirmed)
- protocolDialogResolver = null
- }
- }
- const onWechatGate = () => {
- if (isLoading.value) return
- protocolDialogForWechat.value = true
- protocolDialogVisible.value = true
- }
- const onAgreeGetPhoneNumber = (e) => {
- agreeProtocol.value = true
- protocolDialogVisible.value = false
- protocolDialogForWechat.value = false
- onGetPhoneNumber(e)
- }
- const onGetPhoneNumber = async (e) => {
- if (isLoading.value) return
- if (!agreeProtocol.value) {
- onWechatGate()
- return
- }
- const detail = (e && e.detail) || {}
- const errMsg = detail.errMsg || ''
- if (errMsg && errMsg.indexOf('ok') === -1) {
- uni.showToast({ title: '需要授权手机号,或请使用账号密码登录', icon: 'none' })
- return
- }
- const phoneCode = detail.code
- if (!phoneCode) {
- uni.showToast({ title: '需要授权手机号,或请使用账号密码登录', icon: 'none' })
- return
- }
- isLoading.value = true
- try {
- const loginRes = await new Promise((resolve, reject) => {
- uni.login({ provider: 'weixin', success: resolve, fail: reject })
- })
- const ok = await userStore.wechatLogin({
- loginCode: loginRes.code,
- phoneCode
- })
- if (ok) {
- userStore.navigateToHome()
- }
- } catch (err) {
- uni.showToast({ title: (err && err.message) || '登录失败', icon: 'none' })
- } finally {
- isLoading.value = false
- }
- }
- </script>
- <template>
- <view class="login-page" :style="loginPageStyle">
- <!-- 背景装饰 -->
- <view class="login-bg">
- <view class="bg-circle circle-1"></view>
- <view class="bg-circle circle-2"></view>
- <view class="bg-circle circle-3"></view>
- </view>
- <view class="login-content">
- <view class="logo-section">
- <image class="logo-image" :src="DEFAULT_PLACEHOLDER_IMAGE_URL" mode="widthFix" />
- <text class="app-slogan">一体化业务移动端</text>
- </view>
- <view class="login-form">
- <view class="form-item">
- <text class="form-label">用户名</text>
- <view class="form-input-box">
- <uni-icons class="form-input-ico" type="person" size="18" color="#c0c4cc" />
- <input
- v-model="form.username"
- class="form-input"
- type="text"
- placeholder="请输入用户名"
- placeholder-class="input-placeholder"
- />
- </view>
- </view>
- <view class="form-item">
- <text class="form-label">密码</text>
- <view class="form-input-box">
- <uni-icons class="form-input-ico" type="locked" size="18" color="#c0c4cc" />
- <input
- v-model="form.password"
- class="form-input"
- type="password"
- placeholder="请输入密码"
- placeholder-class="input-placeholder"
- />
- </view>
- </view>
- <view class="form-actions">
- <button class="btn-login" :disabled="loginDisabled" @click="handleSubmit">
- <text v-if="isLoading" class="btn-login-text">登录中...</text>
- <text v-else class="btn-login-text">账号密码登录</text>
- </button>
- </view>
- </view>
- <view class="login-footer">
- <view class="footer-line">
- <view class="agree-check" @tap="toggleProtocolAgreement">
- <view class="agree-check__icon" :class="{ 'agree-check__icon--checked': agreeProtocol }">
- <text class="agree-check__tick">✓</text>
- </view>
- </view>
- <text class="footer-text">登录即表示同意</text>
- <text class="link" @tap="goUserAgreement">《用户服务协议》</text>
- <text class="footer-text">和</text>
- <text class="link" @tap="goPrivacyPolicy">《隐私政策》</text>
- </view>
- </view>
- <view class="login-divider">或</view>
- <button
- v-if="agreeProtocol"
- class="login-wechat"
- :disabled="isLoading"
- open-type="getPhoneNumber"
- @getphonenumber="onGetPhoneNumber"
- >
- <text class="login-wechat__label">手机号快捷登录</text>
- </button>
- <view
- v-else
- class="login-wechat"
- :class="{ 'is-disabled': isLoading }"
- @tap="onWechatGate"
- >
- <text class="login-wechat__label">手机号快捷登录</text>
- </view>
- </view>
- <view v-if="protocolDialogVisible" class="protocol-dialog-mask" @tap="resolveProtocolDialog(false)">
- <view class="protocol-dialog" @tap.stop>
- <text class="protocol-dialog__title">协议确认</text>
- <text class="protocol-dialog__desc">登录前请先同意《用户服务协议》和《隐私政策》</text>
- <view class="protocol-dialog__actions">
- <view class="protocol-dialog__btn protocol-dialog__btn--cancel" @tap="resolveProtocolDialog(false)">否</view>
- <button
- v-if="protocolDialogForWechat"
- class="protocol-dialog__btn protocol-dialog__btn--confirm"
- open-type="getPhoneNumber"
- @getphonenumber="onAgreeGetPhoneNumber"
- >
- 同意并登录
- </button>
- <view
- v-else
- class="protocol-dialog__btn protocol-dialog__btn--confirm"
- @tap="resolveProtocolDialog(true)"
- >
- 是,继续登录
- </view>
- </view>
- </view>
- </view>
- <app-env-badge />
- </view>
- </template>
- <style lang="scss" scoped>
- .login-page {
- min-height: 100vh;
- min-height: 100dvh;
- background: linear-gradient(180deg, #f8fff8 0%, #ffffff 100%);
- position: relative;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- padding: calc(32rpx + var(--sq-safe-top, env(safe-area-inset-top, 0px))) 32rpx
- calc(32rpx + env(safe-area-inset-bottom, 0px));
- /* 与 placeholder 同色,供图标 mask 与 :deep(.input-placeholder) 共用 */
- --login-input-placeholder: #c0c4cc;
- }
- .login-bg {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- pointer-events: none;
- }
- .login-bg .bg-circle {
- position: absolute;
- border-radius: 50%;
- background: var(--theme-color);
- opacity: 0.05;
- }
- .login-bg .circle-1 {
- width: 400rpx;
- height: 400rpx;
- top: -100rpx;
- right: -100rpx;
- }
- .login-bg .circle-2 {
- width: 300rpx;
- height: 300rpx;
- top: 300rpx;
- left: -150rpx;
- }
- .login-bg .circle-3 {
- width: 200rpx;
- height: 200rpx;
- bottom: 100rpx;
- right: -50rpx;
- }
- .login-content {
- position: relative;
- z-index: 1;
- width: 100%;
- max-width: 680rpx;
- margin: 0 auto;
- box-sizing: border-box;
- padding: 0 8rpx;
- flex-shrink: 0;
- }
- /* 对齐 bxapp 登录页 brand-logo:横向自适应宽图,非圆角方标 */
- .logo-section {
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- margin-bottom: 44rpx;
- }
- .logo-image {
- display: block;
- width: 360rpx;
- max-width: 48vw;
- height: auto;
- margin: 0 auto 24rpx;
- }
- .app-slogan {
- font-size: 44rpx;
- font-weight: 700;
- letter-spacing: 0.02em;
- color: #1e293b;
- margin-top: 4rpx;
- }
- .login-form {
- background: #fff;
- border-radius: 24rpx;
- padding: 50rpx 40rpx;
- box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.08);
- }
- .form-item {
- margin-bottom: 36rpx;
- }
- .form-item:last-of-type {
- margin-bottom: 50rpx;
- }
- .form-label {
- display: block;
- font-size: 28rpx;
- color: #303133;
- margin-bottom: 16rpx;
- font-weight: 500;
- }
- .form-input-box {
- display: flex;
- align-items: center;
- height: 88rpx;
- background: #f5f7fa;
- border-radius: 16rpx;
- padding: 0 24rpx;
- box-sizing: border-box;
- }
- .form-input-ico {
- margin-right: 10rpx;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .form-input {
- flex: 1;
- min-width: 0;
- height: 100%;
- padding: 0;
- border: none;
- background: transparent;
- font-size: 28rpx;
- color: #303133;
- line-height: 1.25;
- }
- .form-actions {
- width: 100%;
- }
- .btn-login {
- width: 100%;
- height: 88rpx;
- border-radius: 44rpx;
- font-size: 30rpx;
- color: #fff;
- background: linear-gradient(135deg, var(--theme-color) 0%, #07c160 100%);
- border: none;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- line-height: 1.2;
- }
- .btn-login::after {
- border: none;
- }
- .btn-login.button-hover,
- .btn-login:active {
- background: linear-gradient(135deg, var(--theme-color) 0%, #07c160 100%);
- color: #fff;
- opacity: 1;
- }
- .btn-login-text {
- line-height: 1.2;
- font-size: 30rpx;
- }
- .login-footer {
- margin-top: 60rpx;
- padding: 0 20rpx;
- }
- .footer-line {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: center;
- column-gap: 4rpx;
- row-gap: 8rpx;
- }
- .agree-check {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 8rpx;
- margin-right: 4rpx;
- flex-shrink: 0;
- }
- .agree-check__icon {
- width: 30rpx;
- height: 30rpx;
- border-radius: 50%;
- border: 2rpx solid #c0c4cc;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #fff;
- }
- .agree-check__tick {
- font-size: 18rpx;
- font-weight: 700;
- line-height: 1;
- color: transparent;
- }
- .agree-check__icon--checked {
- border-color: var(--theme-color);
- background: var(--theme-color);
- }
- .agree-check__icon--checked .agree-check__tick {
- color: #fff;
- }
- .footer-text,
- .link {
- font-size: 22rpx;
- line-height: 1.6;
- }
- .footer-text {
- color: #909399;
- }
- .link {
- color: var(--theme-color);
- }
- .protocol-dialog-mask {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.42);
- z-index: 99;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 40rpx;
- box-sizing: border-box;
- }
- .protocol-dialog {
- width: 100%;
- max-width: 620rpx;
- background: #fff;
- border-radius: 24rpx;
- padding: 36rpx 30rpx 28rpx;
- box-sizing: border-box;
- box-shadow: 0 14rpx 48rpx rgba(0, 0, 0, 0.16);
- }
- .protocol-dialog__title {
- display: block;
- text-align: center;
- font-size: 34rpx;
- font-weight: 700;
- color: #303133;
- }
- .protocol-dialog__desc {
- display: block;
- margin-top: 16rpx;
- text-align: center;
- font-size: 26rpx;
- line-height: 1.6;
- color: #606266;
- }
- .protocol-dialog__actions {
- margin-top: 30rpx;
- display: flex;
- gap: 16rpx;
- }
- .protocol-dialog__btn {
- flex: 1;
- min-height: 78rpx;
- border-radius: 999rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 28rpx;
- font-weight: 600;
- }
- .protocol-dialog__btn--cancel {
- background: #f4f7f6;
- color: #5b6b64;
- border: 1px solid #ddebe5;
- }
- .protocol-dialog__btn--confirm {
- background: linear-gradient(135deg, var(--theme-color) 0%, #07c160 100%);
- color: #fff;
- border: none;
- margin: 0;
- padding: 0;
- line-height: normal;
- }
- button.protocol-dialog__btn--confirm::after {
- border: none;
- }
- .login-divider {
- display: flex;
- align-items: center;
- gap: 24rpx;
- margin: 44rpx 0 28rpx;
- width: 100%;
- font-size: 24rpx;
- color: #94a3b8;
- }
- .login-divider::before,
- .login-divider::after {
- content: '';
- flex: 1;
- height: 1px;
- background: #e2e8f0;
- }
- .login-wechat {
- height: 88rpx;
- border-radius: 24rpx;
- border: 1px solid #e2e8f0;
- background: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- margin: 0;
- padding: 0;
- line-height: normal;
- box-sizing: border-box;
- font-size: 28rpx;
- font-weight: 500;
- color: #1e293b;
- }
- button.login-wechat::after {
- border: none;
- }
- .login-wechat__label {
- font-size: 28rpx;
- font-weight: 500;
- color: #1e293b;
- }
- </style>
- <!-- placeholder-class 在部分真机需非 scoped 才稳定生效;颜色与 --login-input-placeholder 一致 -->
- <style lang="scss">
- .input-placeholder {
- color: #c0c4cc;
- font-size: 28rpx;
- }
- </style>
|