소스 검색

记住密码功能

liuq 3 주 전
부모
커밋
843fdeeada
4개의 변경된 파일263개의 추가작업 그리고 22개의 파일을 삭제
  1. 0 1
      composables/useMessages.js
  2. 68 3
      manifest.json
  3. 39 15
      pages/chat/index.vue
  4. 156 3
      pages/login/index.vue

+ 0 - 1
composables/useMessages.js

@@ -50,7 +50,6 @@ function normalizeMessageList(list, currentUserId) {
 
 export function useMessages() {
 	async function fetchMessages(contactId) {
-		if (chatStore.hasContactLoaded(contactId)) return
 		const token = getToken()
 		if (!token) return
 		const currentUserId = getUserIdFromToken(token)

+ 68 - 3
manifest.json

@@ -1,5 +1,5 @@
 {
-    "name" : "yunzhu-im-mobile",
+    "name" : "韫珠IM",
     "appid" : "__UNI__6801EE3",
     "description" : "",
     "versionName" : "1.0.0",
@@ -17,7 +17,13 @@
             "delay" : 0
         },
         /* 模块配置 */
-        "modules" : {},
+        "modules" : {
+            "Barcode" : {},
+            "Camera" : {},
+            "iBeacon" : {},
+            "SQLite" : {},
+            "Push" : {}
+        },
         /* 应用发布信息 */
         "distribute" : {
             /* android打包配置 */
@@ -46,7 +52,66 @@
                 "dSYMs" : false
             },
             /* SDK配置 */
-            "sdkConfigs" : {}
+            "sdkConfigs" : {
+                "geolocation" : {
+                    "system" : {
+                        "__platform__" : [ "ios", "android" ]
+                    }
+                },
+                "maps" : {},
+                "push" : {
+                    "unipush" : {
+                        "version" : "2",
+                        "offline" : false,
+                        "icons" : {
+                            "small" : {
+                                "ldpi" : ""
+                            }
+                        }
+                    }
+                },
+                "share" : {
+                    "weixin" : {
+                        "appid" : "",
+                        "UniversalLinks" : ""
+                    }
+                }
+            },
+            "icons" : {
+                "android" : {
+                    "hdpi" : "unpackage/res/icons/72x72.png",
+                    "xhdpi" : "unpackage/res/icons/96x96.png",
+                    "xxhdpi" : "unpackage/res/icons/144x144.png",
+                    "xxxhdpi" : "unpackage/res/icons/192x192.png"
+                },
+                "ios" : {
+                    "appstore" : "unpackage/res/icons/1024x1024.png",
+                    "ipad" : {
+                        "app" : "unpackage/res/icons/76x76.png",
+                        "app@2x" : "unpackage/res/icons/152x152.png",
+                        "notification" : "unpackage/res/icons/20x20.png",
+                        "notification@2x" : "unpackage/res/icons/40x40.png",
+                        "proapp@2x" : "unpackage/res/icons/167x167.png",
+                        "settings" : "unpackage/res/icons/29x29.png",
+                        "settings@2x" : "unpackage/res/icons/58x58.png",
+                        "spotlight" : "unpackage/res/icons/40x40.png",
+                        "spotlight@2x" : "unpackage/res/icons/80x80.png"
+                    },
+                    "iphone" : {
+                        "app@2x" : "unpackage/res/icons/120x120.png",
+                        "app@3x" : "unpackage/res/icons/180x180.png",
+                        "notification@2x" : "unpackage/res/icons/40x40.png",
+                        "notification@3x" : "unpackage/res/icons/60x60.png",
+                        "settings@2x" : "unpackage/res/icons/58x58.png",
+                        "settings@3x" : "unpackage/res/icons/87x87.png",
+                        "spotlight@2x" : "unpackage/res/icons/80x80.png",
+                        "spotlight@3x" : "unpackage/res/icons/120x120.png"
+                    }
+                }
+            },
+            "splashscreen" : {
+                "androidStyle" : "common"
+            }
         }
     },
     /* 快应用特有相关 */

+ 39 - 15
pages/chat/index.vue

@@ -99,8 +99,8 @@
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted } from 'vue'
-import { onLoad, onUnload } from '@dcloudio/uni-app'
+import { ref, computed, watch, nextTick } from 'vue'
+import { onLoad, onUnload, onShow } from '@dcloudio/uni-app'
 import PrivateMessageBubble from '../../components/chat/PrivateMessageBubble.vue'
 import NotificationBubble from '../../components/chat/NotificationBubble.vue'
 import { useMessages } from '../../composables/useMessages'
@@ -115,6 +115,8 @@ const inputValue = ref('')
 const scrollIntoView = ref('')
 /** 从消息搜索进入时滚动到指定消息 id,定位后清空 */
 const scrollToMessageId = ref('')
+/** 用于区分「底部最后一条是否变化」:加载更多只 prepend 时不变,避免误滚到底 */
+const lastBottomMsgKey = ref('')
 const showPlusPanel = ref(false)
 /** 正在对哪条 USER_NOTIFICATION 执行「再次提醒」(用于按钮 loading,并防并发连点) */
 const remindingNotificationId = ref('')
@@ -139,7 +141,24 @@ const messageList = computed(() => {
 
 const loadingMoreForContact = computed(() => !!loadingMore[String(otherUserId.value)])
 
+/** 滚到列表底部;先清空 scroll-into-view 再设锚点,避免同 id 时不滚动 */
+function scrollToBottom() {
+	if (scrollToMessageId.value) return
+	const list = messageList.value
+	if (!list.length) return
+	const last = list[list.length - 1]
+	const anchor = 'msg-' + (last.id || last.tempId)
+	scrollIntoView.value = ''
+	nextTick(() => {
+		scrollIntoView.value = anchor
+		setTimeout(() => {
+			scrollIntoView.value = anchor
+		}, 50)
+	})
+}
+
 onLoad((options) => {
+	lastBottomMsgKey.value = ''
 	otherUserId.value = String(
 		(options && (options.otherUserId || options.userId || options.contactId)) || '0'
 	)
@@ -181,21 +200,18 @@ onLoad((options) => {
 		// contacts 已有数据时也做一次兜底(例如 otherUserId 刚好没命中但后续会更新)
 		syncContactTitle()
 	}
-	fetchMessages(otherUserId.value)
 })
 
-onUnload(() => {
-	chatStore.setActiveContact('')
+/** 每次显示页面都拉最新一页(含从子页返回),并在数据就绪后滚到底 */
+onShow(async () => {
+	const id = otherUserId.value
+	if (!id || id === '0') return
+	await fetchMessages(id)
+	scrollToBottom()
 })
 
-onMounted(() => {
-	// 滚动到底部(若需定位到某条消息则交给 watch)
-	setTimeout(() => {
-		const list = messageList.value
-		if (!list.length) return
-		if (scrollToMessageId.value) return
-		scrollIntoView.value = 'msg-' + (list[list.length - 1].id || list[list.length - 1].tempId)
-	}, 300)
+onUnload(() => {
+	chatStore.setActiveContact('')
 })
 
 watch(messageList, (list) => {
@@ -204,13 +220,21 @@ watch(messageList, (list) => {
 	if (target) {
 		const hit = list.find((m) => String(m.id) === String(target) || String(m.tempId) === String(target))
 		if (hit) {
-			scrollIntoView.value = 'msg-' + (hit.id || hit.tempId)
+			const anchor = 'msg-' + (hit.id || hit.tempId)
+			scrollIntoView.value = ''
+			nextTick(() => {
+				scrollIntoView.value = anchor
+			})
 			scrollToMessageId.value = ''
 			return
 		}
 		return
 	}
-	scrollIntoView.value = 'msg-' + (list[list.length - 1].id || list[list.length - 1].tempId)
+	const last = list[list.length - 1]
+	const key = String(last.id || last.tempId)
+	if (lastBottomMsgKey.value === key) return
+	lastBottomMsgKey.value = key
+	scrollToBottom()
 }, { deep: true })
 
 // contacts 更新后同步聊天标题(避免从联系人详情进入仍显示“会话”)

+ 156 - 3
pages/login/index.vue

@@ -8,10 +8,28 @@
 		</view>
 
 		<view class="form">
-			<input class="input" v-model="mobile" placeholder="手机号" type="number" />
+			<view class="input-row">
+				<input
+					class="input input-grow"
+					v-model="mobile"
+					placeholder="手机号"
+					type="number"
+					@focus="onMobileFocus"
+				/>
+				<view class="history-btn" @click="showAccountHistory">
+					<text>历史</text>
+				</view>
+			</view>
 
 			<input v-if="loginType === 'password'" class="input" v-model="password" placeholder="密码" password />
 
+			<view v-if="loginType === 'password'" class="remember-row" @click="rememberPassword = !rememberPassword">
+				<view class="remember-box" :class="{ on: rememberPassword }">
+					<text v-if="rememberPassword" class="remember-tick">✓</text>
+				</view>
+				<text class="remember-label">记住密码</text>
+			</view>
+
 			<view v-else class="sms-row">
 				<input class="input sms-input" v-model="code" placeholder="验证码" type="number" />
 				<view class="sms-btn" :class="{ disabled: sending || countdown > 0 }" @click="onSendCode">
@@ -30,6 +48,25 @@
 	import { login, loginBySms, sendSmsCode, setToken, getCurrentUserInfo, getUserIdFromToken } from '../../utils/api'
 
 	const USER_KEY = 'current_user'
+	const LOGIN_ACCOUNTS_KEY = 'login_saved_accounts'
+	const REMEMBER_PREF_KEY = 'login_remember_password_pref'
+
+	function loadSavedAccounts() {
+		try {
+			const raw = uni.getStorageSync(LOGIN_ACCOUNTS_KEY)
+			if (raw == null || raw === '') return []
+			const arr = typeof raw === 'string' ? JSON.parse(raw) : raw
+			return Array.isArray(arr) ? arr : []
+		} catch (e) {
+			return []
+		}
+	}
+
+	function saveAccounts(list) {
+		try {
+			uni.setStorageSync(LOGIN_ACCOUNTS_KEY, JSON.stringify(list))
+		} catch (e) {}
+	}
 
 	export default {
 		data() {
@@ -38,10 +75,23 @@
 				mobile: '',
 				password: '',
 				code: '',
+				rememberPassword: false,
 				loading: false,
 				sending: false,
 				countdown: 0,
-				timer: null
+				timer: null,
+				_historyShownForFocus: false
+			}
+		},
+		onLoad() {
+			try {
+				const pref = uni.getStorageSync(REMEMBER_PREF_KEY)
+				if (pref === true || pref === 'true') this.rememberPassword = true
+			} catch (e) {}
+			const list = loadSavedAccounts()
+			if (list.length && list[0].mobile) {
+				this.mobile = list[0].mobile
+				if (this.rememberPassword && list[0].password) this.password = list[0].password
 			}
 		},
 		onUnload() {
@@ -49,6 +99,56 @@
 			this.timer = null
 		},
 		methods: {
+			onMobileFocus() {
+				const list = loadSavedAccounts()
+				if (!list.length || this._historyShownForFocus) return
+				this._historyShownForFocus = true
+				this.showAccountHistory()
+			},
+
+			showAccountHistory() {
+				const accounts = loadSavedAccounts()
+				if (!accounts.length) {
+					uni.showToast({ title: '暂无历史账号', icon: 'none' })
+					return
+				}
+				const itemList = accounts.map((a) => String(a.mobile || ''))
+				uni.showActionSheet({
+					itemList,
+					success: (res) => {
+						const acc = accounts[res.tapIndex]
+						if (!acc || !acc.mobile) return
+						this.mobile = String(acc.mobile)
+						if (this.loginType === 'password' && acc.password) {
+							this.password = acc.password
+							this.$nextTick(() => this.onLogin())
+						} else {
+							this.password = ''
+						}
+					},
+					fail: () => {}
+				})
+			},
+
+			recordSuccessfulLogin() {
+				const m = String(this.mobile || '').trim()
+				if (!m) return
+				const prevList = loadSavedAccounts()
+				const prev = prevList.find((a) => a.mobile === m)
+				const rest = prevList.filter((a) => a.mobile !== m)
+				const item = { mobile: m }
+				if (this.loginType === 'password') {
+					if (this.rememberPassword) item.password = this.password
+				} else if (prev && prev.password) {
+					item.password = prev.password
+				}
+				try {
+					uni.setStorageSync(REMEMBER_PREF_KEY, !!this.rememberPassword)
+				} catch (e) {}
+				rest.unshift(item)
+				saveAccounts(rest.slice(0, 30))
+			},
+
 			async onSendCode() {
 				if (this.sending || this.countdown > 0) return
 				if (!this.mobile) {
@@ -122,6 +222,8 @@
 						uni.setStorageSync(USER_KEY, user || {})
 					} catch (e) {}
 
+					this.recordSuccessfulLogin()
+
 					uni.reLaunch({ url: '/pages/index/index' })
 				} catch (e) {
 					uni.showToast({ title: e.message || '登录失败', icon: 'none' })
@@ -165,6 +267,28 @@
 	.form {
 		margin-top: 24rpx;
 	}
+	.input-row {
+		display: flex;
+		align-items: center;
+		gap: 16rpx;
+		margin-bottom: 20rpx;
+	}
+	.input-grow {
+		flex: 1;
+		margin-bottom: 0;
+	}
+	.history-btn {
+		height: 88rpx;
+		padding: 0 28rpx;
+		border-radius: 16rpx;
+		background: #f0f0f0;
+		color: #259653;
+		font-size: 26rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-shrink: 0;
+	}
 	.input {
 		height: 88rpx;
 		padding: 0 24rpx;
@@ -173,6 +297,36 @@
 		font-size: 28rpx;
 		margin-bottom: 20rpx;
 	}
+	.remember-row {
+		display: flex;
+		align-items: center;
+		gap: 16rpx;
+		margin-bottom: 20rpx;
+		margin-top: -8rpx;
+	}
+	.remember-box {
+		width: 36rpx;
+		height: 36rpx;
+		border-radius: 8rpx;
+		border: 2rpx solid #ccc;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		box-sizing: border-box;
+	}
+	.remember-box.on {
+		background: #259653;
+		border-color: #259653;
+	}
+	.remember-tick {
+		color: #fff;
+		font-size: 22rpx;
+		line-height: 1;
+	}
+	.remember-label {
+		font-size: 26rpx;
+		color: #666;
+	}
 	.sms-row {
 		display: flex;
 		align-items: center;
@@ -211,4 +365,3 @@
 		opacity: 0.7;
 	}
 </style>
-