Parcourir la source

修复登录问题

liuq il y a 2 semaines
Parent
commit
16fbfeae99

+ 29 - 15
App.vue

@@ -1,7 +1,9 @@
 <script>
-	import { getToken, setToken, getCurrentUserInfo } from './utils/api'
+	import { getToken, getCurrentUserInfo, normalizeUserPayload } from './utils/api'
 	import { connectWebSocket } from './composables/useWebSocket'
 	import { setupAppNotifications } from './utils/notificationSetup'
+
+	const USER_KEY = 'current_user'
 	// #ifdef APP-PLUS
 	import { scheduleAndroidApkUpdateCheck, maybeCheckAndroidApkUpdateByInterval } from './utils/appUpgrade'
 	// #endif
@@ -13,22 +15,34 @@
 				scheduleAndroidApkUpdateCheck()
 			}
 			// #endif
-			try {
-				const token = getToken()
-				if (!token) {
-					uni.reLaunch({ url: '/pages/login/index' })
-					return
-				}
-				await getCurrentUserInfo(token)
-				// 已登录启动即建 WS;登录页 setToken 后也会 connect(reLaunch 不会再次 onLaunch)
-				connectWebSocket()
-				// #ifdef APP-PLUS
-				setupAppNotifications()
-				// #endif
-			} catch (e) {
-				setToken('')
+			const token = getToken()
+			if (!token) {
 				uni.reLaunch({ url: '/pages/login/index' })
+				return
 			}
+			uni.reLaunch({ url: '/pages/index/index' })
+			// 已登录启动即建 WS;登录页 setToken 后也会 connect(reLaunch 不会再次 onLaunch)
+			connectWebSocket()
+			// #ifdef APP-PLUS
+			setupAppNotifications()
+			// #endif
+			getCurrentUserInfo(token)
+				.then((me) => {
+					const u = normalizeUserPayload(me)
+					if (!u) return
+					const orgName = u.orgName || u.org_name || ''
+					try {
+						const prev = uni.getStorageSync(USER_KEY)
+						const base = prev && typeof prev === 'object' ? prev : {}
+						uni.setStorageSync(USER_KEY, {
+							...base,
+							...u,
+							orgName,
+							org_name: orgName
+						})
+					} catch (e) {}
+				})
+				.catch(() => {})
 		},
 		onShow: function() {
 			console.log('App Show')

+ 1 - 1
composables/useContacts.js

@@ -36,7 +36,7 @@ export async function fetchContactsList() {
 			})
 		)
 	} catch (e) {
-		chatStore.setContacts([])
+		// 网络或接口失败:不覆盖已有会话列表
 	} finally {
 		chatStore.loadingContacts = false
 	}

+ 2 - 2
manifest.json

@@ -2,8 +2,8 @@
     "name" : "韫珠IM",
     "appid" : "__UNI__6801EE3",
     "description" : "",
-    "versionName" : "1.0.0",
-    "versionCode" : "100",
+    "versionName" : "1.0.1",
+    "versionCode" : 101,
     "transformPx" : false,
     /* 5+App特有相关 */
     "app-plus" : {

+ 12 - 0
pages.json

@@ -6,6 +6,18 @@
 				"navigationBarTitleText": "登录"
 			}
 		},
+		{
+			"path": "pages/reset-password/index",
+			"style": {
+				"navigationBarTitleText": "重置密码"
+			}
+		},
+		{
+			"path": "pages/change-password/index",
+			"style": {
+				"navigationBarTitleText": "修改密码"
+			}
+		},
 		{
 			"path": "pages/index/index",
 			"style": {

+ 128 - 0
pages/change-password/index.vue

@@ -0,0 +1,128 @@
+<template>
+	<view class="change-page">
+		<text class="hint">修改成功后请使用新密码登录。</text>
+
+		<view class="form">
+			<input class="input" v-model="oldPassword" placeholder="当前密码" password />
+			<input class="input" v-model="newPassword" placeholder="新密码(须含字母与数字)" password />
+			<input class="input" v-model="confirmPassword" placeholder="确认新密码" password />
+
+			<view class="submit" :class="{ disabled: loading }" @click="onSubmit">
+				<text>{{ loading ? '提交中...' : '确认修改' }}</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import { getToken, setToken, changePasswordLoggedIn } from '../../utils/api'
+
+	function passwordMeetsRule(pwd) {
+		const s = String(pwd || '')
+		return /[A-Za-z]/.test(s) && /\d/.test(s)
+	}
+
+	export default {
+		data() {
+			return {
+				oldPassword: '',
+				newPassword: '',
+				confirmPassword: '',
+				loading: false
+			}
+		},
+		onShow() {
+			if (!getToken()) {
+				uni.showToast({ title: '请先登录', icon: 'none' })
+				setTimeout(() => {
+					uni.reLaunch({ url: '/pages/login/index' })
+				}, 80)
+			}
+		},
+		methods: {
+			async onSubmit() {
+				if (this.loading) return
+				const oldPwd = String(this.oldPassword || '')
+				const newPwd = String(this.newPassword || '')
+				const confirm = String(this.confirmPassword || '')
+				if (!oldPwd) {
+					uni.showToast({ title: '请输入当前密码', icon: 'none' })
+					return
+				}
+				if (!newPwd) {
+					uni.showToast({ title: '请输入新密码', icon: 'none' })
+					return
+				}
+				if (!passwordMeetsRule(newPwd)) {
+					uni.showToast({ title: '新密码须同时包含字母与数字', icon: 'none' })
+					return
+				}
+				if (newPwd !== confirm) {
+					uni.showToast({ title: '两次新密码不一致', icon: 'none' })
+					return
+				}
+				if (oldPwd === newPwd) {
+					uni.showToast({ title: '新密码不能与当前密码相同', icon: 'none' })
+					return
+				}
+				this.loading = true
+				try {
+					await changePasswordLoggedIn(oldPwd, newPwd)
+					uni.showToast({ title: '修改成功,请重新登录', icon: 'success' })
+					setToken('')
+					try {
+						uni.removeStorageSync('current_user')
+					} catch (e) {}
+					setTimeout(() => {
+						uni.reLaunch({ url: '/pages/login/index' })
+					}, 500)
+				} catch (e) {
+					uni.showToast({ title: e.message || '修改失败', icon: 'none' })
+				} finally {
+					this.loading = false
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.change-page {
+		min-height: 100vh;
+		background: #fff;
+		padding: 32rpx 48rpx 64rpx;
+		box-sizing: border-box;
+	}
+	.hint {
+		display: block;
+		font-size: 26rpx;
+		color: #888;
+		line-height: 1.5;
+		margin-bottom: 32rpx;
+	}
+	.form {
+		margin-top: 8rpx;
+	}
+	.input {
+		height: 88rpx;
+		padding: 0 24rpx;
+		background: #f7f7f7;
+		border-radius: 16rpx;
+		font-size: 28rpx;
+		margin-bottom: 20rpx;
+	}
+	.submit {
+		margin-top: 32rpx;
+		height: 92rpx;
+		border-radius: 18rpx;
+		background: #259653;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		color: #fff;
+		font-size: 30rpx;
+	}
+	.submit.disabled {
+		opacity: 0.7;
+	}
+</style>

+ 26 - 1
pages/login/index.vue

@@ -40,12 +40,16 @@
 			<view class="submit" :class="{ disabled: loading }" @click="onLogin">
 				<text>{{ loading ? '登录中...' : '登录' }}</text>
 			</view>
+
+			<view class="footer-links">
+				<text class="link" @click="goResetPassword">忘记密码</text>
+			</view>
 		</view>
 	</view>
 </template>
 
 <script>
-	import { login, loginBySms, sendSmsCode, setToken, getCurrentUserInfo, getUserIdFromToken } from '../../utils/api'
+	import { login, loginBySms, sendSmsCode, setToken, getToken, getCurrentUserInfo, getUserIdFromToken } from '../../utils/api'
 	import { connectWebSocket } from '../../composables/useWebSocket'
 
 	const USER_KEY = 'current_user'
@@ -85,6 +89,10 @@
 			}
 		},
 		onLoad() {
+			if (getToken()) {
+				uni.reLaunch({ url: '/pages/index/index' })
+				return
+			}
 			try {
 				const pref = uni.getStorageSync(REMEMBER_PREF_KEY)
 				if (pref === true || pref === 'true') this.rememberPassword = true
@@ -95,11 +103,19 @@
 				if (this.rememberPassword && list[0].password) this.password = list[0].password
 			}
 		},
+		onShow() {
+			if (getToken()) {
+				uni.reLaunch({ url: '/pages/index/index' })
+			}
+		},
 		onUnload() {
 			if (this.timer) clearInterval(this.timer)
 			this.timer = null
 		},
 		methods: {
+			goResetPassword() {
+				uni.navigateTo({ url: '/pages/reset-password/index' })
+			},
 			onMobileFocus() {
 				const list = loadSavedAccounts()
 				if (!list.length || this._historyShownForFocus) return
@@ -366,4 +382,13 @@
 	.submit.disabled {
 		opacity: 0.7;
 	}
+	.footer-links {
+		margin-top: 32rpx;
+		display: flex;
+		justify-content: flex-end;
+	}
+	.footer-links .link {
+		font-size: 26rpx;
+		color: #259653;
+	}
 </style>

+ 7 - 0
pages/profile/index.vue

@@ -46,6 +46,7 @@
 			>
 				检测更新
 			</view>
+			<view class="action-btn change-pwd-btn" @click="onChangePassword">修改密码</view>
 			<view class="logout-btn" @click="onLogout">退出登录</view>
 		</view>
 	</view>
@@ -152,6 +153,9 @@
 				manualCheckAndroidApkUpdate()
 				// #endif
 			},
+			onChangePassword() {
+				uni.navigateTo({ url: '/pages/change-password/index' })
+			},
 			onLogout() {
 				uni.showModal({
 					title: '提示',
@@ -264,6 +268,9 @@
 	.check-update-btn {
 		color: #259653;
 	}
+	.change-pwd-btn {
+		color: #333;
+	}
 	.logout-btn {
 		background: #fff;
 		border-radius: 16rpx;

+ 265 - 0
pages/reset-password/index.vue

@@ -0,0 +1,265 @@
+<template>
+	<view class="reset-page">
+		<text class="hint">通过手机号与短信验证码重置密码,无需旧密码。</text>
+
+		<view class="form">
+			<input class="input" v-model="mobile" placeholder="手机号" type="number" />
+
+			<view class="captcha-row">
+				<input
+					class="input captcha-input"
+					v-model="captchaCode"
+					placeholder="图形验证码"
+					maxlength="8"
+				/>
+				<view class="captcha-img-wrap" @click="refreshCaptcha">
+					<image v-if="captchaImageSrc" class="captcha-img" :src="captchaImageSrc" mode="aspectFit" />
+					<text v-else class="captcha-placeholder">{{ captchaLoading ? '加载中' : '点击刷新' }}</text>
+				</view>
+			</view>
+
+			<view class="sms-row">
+				<input class="input sms-input" v-model="smsCode" placeholder="短信验证码" type="number" maxlength="8" />
+				<view class="sms-btn" :class="{ disabled: sending || countdown > 0 }" @click="onSendSms">
+					<text>{{ countdown > 0 ? countdown + 's' : (sending ? '发送中' : '获取验证码') }}</text>
+				</view>
+			</view>
+
+			<input class="input" v-model="newPassword" placeholder="新密码(须含字母与数字)" password />
+			<input class="input" v-model="confirmPassword" placeholder="确认新密码" password />
+
+			<view class="submit" :class="{ disabled: loading }" @click="onSubmit">
+				<text>{{ loading ? '提交中...' : '重置密码' }}</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import { getCaptcha, sendOpenSms, resetPasswordOpen } from '../../utils/api'
+
+	function passwordMeetsRule(pwd) {
+		const s = String(pwd || '')
+		return /[A-Za-z]/.test(s) && /\d/.test(s)
+	}
+
+	function normalizeCaptchaImageSrc(image) {
+		if (!image) return ''
+		const s = String(image)
+		if (s.startsWith('data:')) return s
+		return `data:image/png;base64,${s}`
+	}
+
+	export default {
+		data() {
+			return {
+				mobile: '',
+				captchaId: '',
+				captchaCode: '',
+				captchaImageSrc: '',
+				captchaLoading: false,
+				smsCode: '',
+				newPassword: '',
+				confirmPassword: '',
+				sending: false,
+				countdown: 0,
+				timer: null,
+				loading: false
+			}
+		},
+		onLoad() {
+			this.refreshCaptcha()
+		},
+		onUnload() {
+			if (this.timer) clearInterval(this.timer)
+			this.timer = null
+		},
+		methods: {
+			async refreshCaptcha() {
+				this.captchaLoading = true
+				this.captchaCode = ''
+				try {
+					const data = await getCaptcha()
+					this.captchaId = data.captcha_id != null ? String(data.captcha_id) : ''
+					this.captchaImageSrc = normalizeCaptchaImageSrc(data.image)
+				} catch (e) {
+					this.captchaImageSrc = ''
+					uni.showToast({ title: e.message || '图形码加载失败', icon: 'none' })
+				} finally {
+					this.captchaLoading = false
+				}
+			},
+			async onSendSms() {
+				if (this.sending || this.countdown > 0) return
+				const m = String(this.mobile || '').trim()
+				if (!m) {
+					uni.showToast({ title: '请输入手机号', icon: 'none' })
+					return
+				}
+				if (!this.captchaId) {
+					uni.showToast({ title: '请等待图形验证码加载', icon: 'none' })
+					return
+				}
+				const cc = String(this.captchaCode || '').trim()
+				if (!cc) {
+					uni.showToast({ title: '请输入图形验证码', icon: 'none' })
+					return
+				}
+				this.sending = true
+				try {
+					await sendOpenSms(m, this.captchaId, cc)
+					uni.showToast({ title: '短信已发送', icon: 'none' })
+					this.countdown = 60
+					this.timer = setInterval(() => {
+						this.countdown -= 1
+						if (this.countdown <= 0) {
+							clearInterval(this.timer)
+							this.timer = null
+							this.countdown = 0
+						}
+					}, 1000)
+				} catch (e) {
+					uni.showToast({ title: e.message || '发送失败', icon: 'none' })
+					this.refreshCaptcha()
+				} finally {
+					this.sending = false
+				}
+			},
+			async onSubmit() {
+				if (this.loading) return
+				const m = String(this.mobile || '').trim()
+				if (!m) {
+					uni.showToast({ title: '请输入手机号', icon: 'none' })
+					return
+				}
+				const code = String(this.smsCode || '').trim()
+				if (!code) {
+					uni.showToast({ title: '请输入短信验证码', icon: 'none' })
+					return
+				}
+				const pwd = String(this.newPassword || '')
+				const confirm = String(this.confirmPassword || '')
+				if (!pwd) {
+					uni.showToast({ title: '请输入新密码', icon: 'none' })
+					return
+				}
+				if (!passwordMeetsRule(pwd)) {
+					uni.showToast({ title: '新密码须同时包含字母与数字', icon: 'none' })
+					return
+				}
+				if (pwd !== confirm) {
+					uni.showToast({ title: '两次密码不一致', icon: 'none' })
+					return
+				}
+				this.loading = true
+				try {
+					await resetPasswordOpen(m, code, pwd)
+					uni.showToast({ title: '密码重置成功', icon: 'success' })
+					setTimeout(() => {
+						uni.reLaunch({ url: '/pages/login/index' })
+					}, 400)
+				} catch (e) {
+					uni.showToast({ title: e.message || '重置失败', icon: 'none' })
+				} finally {
+					this.loading = false
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.reset-page {
+		min-height: 100vh;
+		background: #fff;
+		padding: 32rpx 48rpx 64rpx;
+		box-sizing: border-box;
+	}
+	.hint {
+		display: block;
+		font-size: 26rpx;
+		color: #888;
+		line-height: 1.5;
+		margin-bottom: 32rpx;
+	}
+	.form {
+		margin-top: 8rpx;
+	}
+	.input {
+		height: 88rpx;
+		padding: 0 24rpx;
+		background: #f7f7f7;
+		border-radius: 16rpx;
+		font-size: 28rpx;
+		margin-bottom: 20rpx;
+	}
+	.captcha-row {
+		display: flex;
+		align-items: center;
+		gap: 16rpx;
+		margin-bottom: 20rpx;
+	}
+	.captcha-input {
+		flex: 1;
+		margin-bottom: 0;
+	}
+	.captcha-img-wrap {
+		width: 220rpx;
+		height: 88rpx;
+		border-radius: 16rpx;
+		background: #f0f0f0;
+		overflow: hidden;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-shrink: 0;
+	}
+	.captcha-img {
+		width: 100%;
+		height: 100%;
+	}
+	.captcha-placeholder {
+		font-size: 22rpx;
+		color: #999;
+		padding: 0 8rpx;
+		text-align: center;
+	}
+	.sms-row {
+		display: flex;
+		align-items: center;
+		gap: 16rpx;
+		margin-bottom: 20rpx;
+	}
+	.sms-input {
+		flex: 1;
+		margin-bottom: 0;
+	}
+	.sms-btn {
+		height: 88rpx;
+		padding: 0 24rpx;
+		border-radius: 16rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		background: #259653;
+		color: #fff;
+		font-size: 26rpx;
+	}
+	.sms-btn.disabled {
+		opacity: 0.6;
+	}
+	.submit {
+		margin-top: 32rpx;
+		height: 92rpx;
+		border-radius: 18rpx;
+		background: #259653;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		color: #fff;
+		font-size: 30rpx;
+	}
+	.submit.disabled {
+		opacity: 0.7;
+	}
+</style>

+ 6 - 6
update_log/latest.json

@@ -1,7 +1,7 @@
 {
-    "versionCode": 100,
-    "versionName": "1.0.0",
-    "apkUrl": "https://api.hnyunzhu.com:9004/app-updates/1773848909/android/update/__UNI__6801EE3__20260326143808.apk",
-    "forceUpdate": false,
-    "changelog": "初始版本"
-  }
+  "versionCode": 101,
+  "versionName": "1.0.1",
+  "apkUrl": "https://api.hnyunzhu.com:9004/app-updates/1773848909/android/update/__UNI__6801EE3__20260328010135.apk",
+  "forceUpdate": false,
+  "changelog": "1.修复杀掉后台恢复自动登录问题。2.新增修改密码功能。"
+}

+ 7 - 0
update_log/latest_100.json

@@ -0,0 +1,7 @@
+{
+    "versionCode": 101,
+    "versionName": "1.0.1",
+    "apkUrl": "https://api.hnyunzhu.com:9004/app-updates/1773848909/android/update/__UNI__6801EE3__20260328010135.apk",
+    "forceUpdate": false,
+    "changelog": "1.修复杀掉后台恢复自动登录问题。2.新增修改密码功能。3.新增修改密码功能。4.新增修改密码功能。5.新增修改密码功能。6.新增修改密码功能。7.新增修改密码功能。8.新增修改密码功能。9.新增修改密码功能。10.新增修改密码功能。"
+  }

+ 69 - 4
utils/api.js

@@ -15,6 +15,16 @@ export function getToken() {
 	}
 }
 
+/** 401 未授权:清 token 并回登录页(网络错误勿调用) */
+function clearSessionAndRedirectToLogin(toastTitle = '请先登录') {
+	setToken('')
+	uni.showToast({ title: toastTitle, icon: 'none' })
+	// 让 Toast 有机会展示再切页
+	setTimeout(() => {
+		uni.reLaunch({ url: '/pages/login/index' })
+	}, 80)
+}
+
 export function setToken(token) {
 	const v = token || ''
 	try {
@@ -53,8 +63,7 @@ function request(options) {
 			},
 			success: (res) => {
 				if (res.statusCode === 401) {
-					setToken('')
-					uni.showToast({ title: '请先登录', icon: 'none' })
+					clearSessionAndRedirectToLogin()
 					reject(new Error('请先登录'))
 					return
 				}
@@ -91,8 +100,8 @@ export function getMessages(token, otherUserId, params = {}) {
 			},
 			success: (res) => {
 				if (res.statusCode === 401) {
-					setToken('')
-					reject(new Error('Unauthorized'))
+					clearSessionAndRedirectToLogin()
+					reject(new Error('请先登录'))
 					return
 				}
 				if (res.statusCode >= 400) {
@@ -152,6 +161,11 @@ export function uploadFile(token, filePath, fileName, onProgress) {
 				Authorization: token ? `Bearer ${token}` : ''
 			},
 			success: (res) => {
+				if (res.statusCode === 401) {
+					clearSessionAndRedirectToLogin()
+					reject(new Error('请先登录'))
+					return
+				}
 				if (res.statusCode >= 400) {
 					reject(new Error('上传失败'))
 					return
@@ -419,6 +433,57 @@ export function ssoLogin(appId, username = '', password = '') {
 	})
 }
 
+/**
+ * 图形验证码(开放接口)
+ * GET /utils/captcha → { captcha_id, image, expire_seconds }
+ */
+export function getCaptcha() {
+	return request({
+		token: '',
+		url: '/utils/captcha',
+		method: 'GET'
+	})
+}
+
+/**
+ * 开放:校验图形码后发送短信(忘记密码)
+ * POST /open/sms/send
+ */
+export function sendOpenSms(mobile, captcha_id, captcha_code) {
+	return request({
+		token: '',
+		url: '/open/sms/send',
+		method: 'POST',
+		data: { mobile, captcha_id, captcha_code }
+	})
+}
+
+/**
+ * 开放:短信校验后重置密码
+ * POST /open/pwd/reset
+ */
+export function resetPasswordOpen(mobile, sms_code, new_password) {
+	return request({
+		token: '',
+		url: '/open/pwd/reset',
+		method: 'POST',
+		data: { mobile, sms_code, new_password }
+	})
+}
+
+/**
+ * 已登录修改密码(需 Bearer)
+ * POST /simple/me/change-password
+ * 若后端字段名不同,请与此处保持一致调整
+ */
+export function changePasswordLoggedIn(old_password, new_password) {
+	return request({
+		url: '/simple/me/change-password',
+		method: 'POST',
+		data: { old_password, new_password }
+	})
+}
+
 /**
  * 与后端约定一致:TEXT / IMAGE / VIDEO / FILE / USER_NOTIFICATION
  */