| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- <template>
- <view class="login-page">
- <view class="title">登录</view>
- <view class="tabs">
- <view class="tab" :class="{ active: loginType === 'password' }" @click="loginType = 'password'">密码登录</view>
- <view class="tab" :class="{ active: loginType === 'sms' }" @click="loginType = 'sms'">验证码登录</view>
- </view>
- <view class="form">
- <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">
- <text>{{ countdown > 0 ? countdown + 's' : (sending ? '发送中' : '获取验证码') }}</text>
- </view>
- </view>
- <view class="submit" :class="{ disabled: loading }" @click="onLogin">
- <text>{{ loading ? '登录中...' : '登录' }}</text>
- </view>
- </view>
- </view>
- </template>
- <script>
- 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() {
- return {
- loginType: 'password',
- mobile: '',
- password: '',
- code: '',
- rememberPassword: false,
- loading: false,
- sending: false,
- countdown: 0,
- 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() {
- if (this.timer) clearInterval(this.timer)
- 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) {
- uni.showToast({ title: '请输入手机号', icon: 'none' })
- return
- }
- this.sending = true
- try {
- // 与桌面端保持一致,后端期望 platform: 'pc'
- await sendSmsCode(this.mobile)
- 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' })
- } finally {
- this.sending = false
- }
- },
- async onLogin() {
- if (this.loading) return
- if (!this.mobile) {
- uni.showToast({ title: '请输入手机号', icon: 'none' })
- return
- }
- if (this.loginType === 'password' && !this.password) {
- uni.showToast({ title: '请输入密码', icon: 'none' })
- return
- }
- if (this.loginType === 'sms' && !this.code) {
- uni.showToast({ title: '请输入验证码', icon: 'none' })
- return
- }
- this.loading = true
- try {
- const data =
- this.loginType === 'password'
- ? await login(this.mobile, this.password)
- : await loginBySms(this.mobile, this.code)
- const accessToken = data.access_token
- if (!accessToken) throw new Error('登录失败:未返回 access_token')
- setToken(accessToken)
- // 尽量补全 user 信息:优先用 login 返回的 user,否则拉 /users/me
- let user = data.user || null
- if (!user || !user.id) {
- const id = getUserIdFromToken(accessToken)
- user = user || {}
- if (id) user.id = id
- }
- if (!user || !user.id || !user.name) {
- try {
- const me = await getCurrentUserInfo(accessToken)
- // 兼容不同后端包裹
- user = me.user || me.data || me
- } catch (e) {}
- }
- try {
- uni.setStorageSync(USER_KEY, user || {})
- } catch (e) {}
- this.recordSuccessfulLogin()
- uni.reLaunch({ url: '/pages/index/index' })
- } catch (e) {
- uni.showToast({ title: e.message || '登录失败', icon: 'none' })
- } finally {
- this.loading = false
- }
- }
- }
- }
- </script>
- <style scoped>
- .login-page {
- min-height: 100vh;
- background: #fff;
- padding: 64rpx 48rpx;
- box-sizing: border-box;
- }
- .title {
- font-size: 44rpx;
- font-weight: 700;
- color: #222;
- margin-bottom: 48rpx;
- }
- .tabs {
- display: flex;
- gap: 24rpx;
- margin-bottom: 32rpx;
- }
- .tab {
- padding: 16rpx 24rpx;
- border-radius: 999rpx;
- background: #f0f0f0;
- color: #666;
- font-size: 26rpx;
- }
- .tab.active {
- background: rgba(37, 150, 83, 0.12);
- color: #259653;
- }
- .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;
- background: #f7f7f7;
- border-radius: 16rpx;
- 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;
- gap: 16rpx;
- }
- .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>
|