appointment_system/pages/login/login-page.vue
2025-12-15 23:42:31 +08:00

575 lines
13 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-container">
<view class="header-row">
<view class="back-button" @click="back">
<image src="/static/ic_back.png" class="back-icon"></image>
</view>
<text class="title">{{ $t('login.title') }}</text>
<view class="back-button" @click="markAllRead">
</view>
</view>
<!-- Logo Section -->
<view class="logo-section">
<view class="logo-box">
<image v-if="appLogo" :src="appLogo" class="logo-image" mode="aspectFit"></image>
<text v-else class="logo-text">LOGO</text>
</view>
</view>
<!-- Main Content -->
<view class="main-content">
<!-- One-Click Login Button -->
<button class="login-button" @click="handleWechatLogin" :loading="isLoading">
{{ $t('login.oneClickLogin') }}
</button>
<!-- Agreement Checkbox -->
<view class="agreement-section">
<checkbox-group @change="handleAgreementChange">
<label class="agreement-label">
<checkbox value="agree" :checked="agreeToTerms" />
<text class="agreement-text">
{{ $t('login.agreeToTerms') }}
<text class="link-text" @click="showUserAgreement">{{ $t('login.userAgreement') }}</text>
{{ $t('login.and') }}
<text class="link-text" @click="showPrivacyPolicy">{{ $t('login.privacyPolicy') }}</text>
</text>
</label>
</checkbox-group>
</view>
</view>
<!-- User Agreement Modal -->
<view v-if="showAgreementModal" class="modal-overlay" @click="closeUserAgreement">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ $t('login.userAgreement') }}</text>
<text class="modal-close" @click="closeUserAgreement">×</text>
</view>
<scroll-view class="modal-body" scroll-y>
<text class="modal-text">{{ userAgreementContent }}</text>
</scroll-view>
<view class="modal-footer">
<button class="modal-button" @click="closeUserAgreement">{{ $t('login.agree') }}</button>
</view>
</view>
</view>
<!-- Privacy Policy Modal -->
<view v-if="showPrivacyModal" class="modal-overlay" @click="closePrivacyPolicy">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ $t('login.privacyPolicy') }}</text>
<text class="modal-close" @click="closePrivacyPolicy">×</text>
</view>
<scroll-view class="modal-body" scroll-y>
<text class="modal-text">{{ privacyPolicyContent }}</text>
</scroll-view>
<view class="modal-footer">
<button class="modal-button" @click="closePrivacyPolicy">{{ $t('login.agree') }}</button>
</view>
</view>
</view>
</view>
</template>
<script>
import {
AppServer
} from '../../modules/api/AppServer';
import {
saveAuthData
} from '@/utils/auth.js';
import Config from '@/modules/Config.js';
export default {
data() {
return {
isLoading: false,
agreeToTerms: false,
showAgreementModal: false,
showPrivacyModal: false,
userAgreementContent: this.getUserAgreementContent(),
privacyPolicyContent: this.getPrivacyPolicyContent(),
statusBarHeight: 0,
appLogo: ''
}
},
onLoad() {
// Get status bar height
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
// 加载配置
this.loadConfig()
},
methods: {
back() {
uni.navigateBack({
delta: 1
});
},
// Handle WeChat login
async handleWechatLogin() {
// 防止重复点击
if (this.isLoading) {
return;
}
// 检查是否勾选协议
if (!this.agreeToTerms) {
uni.showToast({
title: this.$t('login.mustAgreeToTerms'),
icon: 'none'
})
return
}
this.isLoading = true
try {
// 获取微信登录code
const loginRes = await this.getWechatLoginCode();
console.log('微信登录 code:', loginRes);
if (!loginRes || !loginRes.code) {
uni.showToast({
title: this.$t('login.wechatLoginFailed'),
icon: 'none'
})
this.isLoading = false
return
}
// 调用后端登录接口
const appserver = new AppServer();
const data = await appserver.WechatLogin(loginRes.code);
console.log('登录接口返回:', data);
// 检查API响应
if (!data) {
console.error('登录接口无响应');
uni.showToast({
title: this.$t('login.loginFailed'),
icon: 'none'
})
this.isLoading = false
return
}
// 检查返回码0表示成功
if (data.code !== 0) {
console.error('登录失败code:', data.code, 'message:', data.message);
uni.showToast({
title: data.message || this.$t('login.loginFailed'),
icon: 'none'
})
this.isLoading = false
return
}
// 检查token数据
if (!data.data || !data.data.token) {
console.error('Token 数据缺失:', data.data);
uni.showToast({
title: this.$t('login.loginFailed'),
icon: 'none'
})
this.isLoading = false
return
}
console.log('登录成功,保存认证信息');
// 保存认证信息
const token = "Bearer " + data.data.token
saveAuthData(token, data.data.refreshToken, data.data.user)
// 登录成功
uni.showToast({
title: this.$t('login.loginSuccess') || '登录成功',
icon: 'success',
duration: 1500
});
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
this.redirectToHome();
}, 500);
} catch (error) {
console.error('登录异常:', error);
console.error('错误堆栈:', error.stack);
uni.showToast({
title: this.$t('login.loginError') + ': ' + (error.message || ''),
icon: 'none',
duration: 3000
})
} finally {
this.isLoading = false
}
},
// 获取微信登录codePromise封装
getWechatLoginCode() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
resolve(res);
},
fail: (err) => {
console.error('uni.login 失败:', err);
reject(err);
}
});
});
},
// 登录成功后跳转到首页
redirectToHome() {
// 设置全局标志,标记登录成功
const app = getApp();
if (app.globalData) {
app.globalData.shouldRefresh = true;
app.globalData.loginTime = Date.now();
}
// 跳转到首页tabBar页面
uni.switchTab({
url: '/pages/index/index',
success: () => {
console.log('登录成功,已跳转到首页');
},
fail: (err) => {
console.error('跳转首页失败:', err);
// 如果 switchTab 失败,尝试使用 reLaunch
uni.reLaunch({
url: '/pages/index/index'
});
}
});
},
// Handle agreement checkbox change
handleAgreementChange(e) {
this.agreeToTerms = e.detail.value.includes('agree')
},
// Show user agreement
showUserAgreement() {
this.showAgreementModal = true
},
// Close user agreement
closeUserAgreement() {
this.showAgreementModal = false
},
// Show privacy policy
showPrivacyPolicy() {
this.showPrivacyModal = true
},
// Close privacy policy
closePrivacyPolicy() {
this.showPrivacyModal = false
},
// Get user agreement content
getUserAgreementContent() {
return this.$t('login.userAgreementContent') || `
用户协议
欢迎使用本应用。本协议规定了您使用本应用的条款和条件。
1. 服务条款
用户同意遵守本协议的所有条款和条件。
2. 用户责任
用户对其账户的安全负责,并同意不与他人共享登录凭证。
3. 禁止行为
用户不得进行任何非法或有害的活动。
4. 免责声明
本应用按"现状"提供,不提供任何明示或暗示的保证。
5. 修改权利
我们保留随时修改本协议的权利。
`
},
// Get privacy policy content
getPrivacyPolicyContent() {
return this.$t('login.privacyPolicyContent') || `
隐私政策
我们重视您的隐私。本政策说明我们如何收集、使用和保护您的信息。
1. 信息收集
我们收集您在使用本应用时提供的信息,包括账户信息和使用数据。
2. 信息使用
我们使用收集的信息来改进服务、进行分析和提供个性化体验。
3. 信息保护
我们采取适当的安全措施来保护您的个人信息。
4. 第三方共享
我们不会将您的个人信息出售给第三方。
5. 联系我们
如有隐私问题,请通过应用内的联系方式与我们联系。
`
},
// 加载配置
async loadConfig() {
try {
const config = await Config.getPublicConfig()
if (config.app_logo) {
this.appLogo = Config.getImageUrl(config.app_logo)
}
} catch (error) {
console.error('加载配置失败:', error)
}
},
},
}
</script>
<style lang="scss" scoped>
.login-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.header-row {
width: 100%;
margin-top: 100rpx;
padding-bottom: 20rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.back-button {
width: 80rpx;
height: 50rpx;
margin-left: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 48rpx;
height: 48rpx;
}
// Logo Section
.logo-section {
display: flex;
justify-content: center;
padding: 30px 20px 20px;
flex: 0 0 auto;
.logo-box {
width: 140px;
height: 140px;
border: 2px solid #17a2b8;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
box-shadow: 0 2px 8px rgba(23, 162, 184, 0.1);
overflow: hidden;
.logo-image {
width: 100%;
height: 100%;
}
.logo-text {
font-size: 36px;
color: #17a2b8;
font-weight: 600;
}
}
}
// Main Content
.main-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 0 20px 40px;
}
// Login Button
.login-button {
width: 100%;
height: 50px;
background-color: #17a2b8;
color: #fff;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
border: none;
margin-bottom: 20px;
box-shadow: 0 3px 10px rgba(23, 162, 184, 0.2);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
line-height: 50px;
padding: 0;
text-align: center;
&:active {
background-color: #138496;
box-shadow: 0 2px 6px rgba(23, 162, 184, 0.15);
}
&::after {
border: none;
}
}
// Agreement Section
.agreement-section {
display: flex;
align-items: flex-start;
checkbox-group {
width: 100%;
}
.agreement-label {
display: flex;
align-items: flex-start;
font-size: 13px;
color: #666;
line-height: 1.8;
checkbox {
margin-right: 8px;
margin-top: 3px;
flex-shrink: 0;
transform: scale(0.9);
}
.agreement-text {
flex: 1;
padding-top: 1px;
}
.link-text {
color: #17a2b8;
text-decoration: underline;
font-weight: 500;
}
}
}
// Modal Overlay
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
padding: 20px;
}
// Modal Styles
.modal-content {
width: 100%;
max-width: 480px;
max-height: 85vh;
background-color: #fff;
border-radius: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
border-bottom: none;
.modal-title {
font-size: 18px;
font-weight: 700;
color: #fff;
}
.modal-close {
font-size: 32px;
color: #fff;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.9;
}
}
.modal-body {
flex: 1;
padding: 20px;
overflow-y: auto;
.modal-text {
font-size: 14px;
color: #555;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-word;
}
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
background-color: #fafafa;
.modal-button {
padding: 10px 28px;
background-color: #17a2b8;
color: #fff;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
border: none;
transition: all 0.3s ease;
&:active {
background-color: #138496;
}
}
}
}
</style>