Files
art-design-uniapp/pages/login/feishu-callback.vue
T
2026-02-06 14:39:37 +08:00

227 lines
4.6 KiB
Vue

<template>
<view class="auth-page">
<view class="auth-card">
<view v-if="status === 'loading'">
<view class="spinner"></view>
<view class="text-wrap">
<text class="title">{{ title }}</text>
<text class="desc">{{ desc }}<text class="dots">{{ dots }}</text></text>
</view>
<view class="progress">
<view class="bar"></view>
</view>
</view>
<view v-else class="error-wrap">
<uni-icons type="clear" size="44" color="#ff4d4f"></uni-icons>
<text class="title">登录失败</text>
<text class="desc">{{ errorMsg }}</text>
<button class="retry-btn" @click="retry">重试</button>
</view>
</view>
</view>
</template>
<script>
import { request, showToast } from '@/api/apiT.js'
export default {
data() {
return {
status: 'loading',
title: '正在完成登录',
desc: '处理中',
dots: '',
errorMsg: '请稍后重试',
_dotTimer: null,
minStayMs: 600,
_startedAt: 0,
_code: ''
}
},
onLoad(options) {
this._startedAt = Date.now()
this.startDots()
// #ifdef H5
this._code = options.code || ''
// #endif
if (!this._code) {
this.fail('缺少 code 参数')
return
}
this.logins(this._code)
},
onUnload() {
this.stopDots()
},
methods: {
startDots() {
this.stopDots()
let n = 0
this._dotTimer = setInterval(() => {
n = (n + 1) % 4
this.dots = '.'.repeat(n)
}, 350)
},
stopDots() {
if (this._dotTimer) {
clearInterval(this._dotTimer)
this._dotTimer = null
}
},
fail(msg) {
this.status = 'error'
this.errorMsg = msg || '请稍后重试'
uni.showToast({ title: msg || '登录失败', icon: 'none' })
},
async logins(code) {
try {
this.title = '正在交换登录凭证'
this.desc = '请稍候'
const res = await request({
url: 'common/auth/login',
method: 'POST',
data: { feishu_code: code,is_mob:1}
})
const token = res?.data?.data?.access_token
if (!token) {
throw new Error(res?.data?.msg || '未获取到 access_token')
}
uni.setStorageSync('token', token)
this.title = '登录成功'
this.desc = '正在进入首页'
this.goHomeSmooth()
} catch (err) {
console.error('[feishu callback] error:', err)
this.fail(err?.message || '登录失败')
}
},
goHomeSmooth() {
const elapsed = Date.now() - this._startedAt
const wait = Math.max(0, this.minStayMs - elapsed)
setTimeout(() => {
uni.reLaunch({ url: '/pages/index/index' })
}, wait)
},
retry() {
this.status = 'loading'
this.title = '正在完成登录'
this.desc = '处理中'
this.startDots()
if (this._code) this.logins(this._code)
else this.fail('缺少 code 参数')
}
}
}
</script>
<style scoped lang="scss">
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #3074F7 0%, #F4F6F8 38%, #F4F6F8 100%);
}
.auth-card {
width: 100%;
max-width: 640rpx;
background: rgba(255, 255, 255, 0.92);
border-radius: 20rpx;
padding: 44rpx 36rpx 32rpx;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
backdrop-filter: blur(10px);
}
.spinner {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
border: 8rpx solid rgba(48, 116, 247, 0.18);
border-top-color: rgba(48, 116, 247, 1);
animation: spin 0.9s linear infinite;
margin-bottom: 24rpx;
}
.text-wrap {
text-align: center;
}
.title {
display: block;
font-size: 34rpx;
color: #1f2937;
font-weight: 600;
margin-bottom: 8rpx;
}
.desc {
display: block;
font-size: 26rpx;
color: #6b7280;
}
.dots {
display: inline-block;
width: 40rpx;
text-align: left;
}
.progress {
width: 100%;
height: 10rpx;
border-radius: 999rpx;
background: rgba(48, 116, 247, 0.10);
overflow: hidden;
margin-top: 26rpx;
}
.progress .bar {
height: 100%;
width: 40%;
border-radius: 999rpx;
background: rgba(48, 116, 247, 0.55);
animation: move 1.1s ease-in-out infinite;
}
.error-wrap {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.retry-btn {
margin-top: 24rpx;
width: 240rpx;
height: 72rpx;
line-height: 72rpx;
border-radius: 999rpx;
background: #3074F7;
color: #fff;
font-size: 28rpx;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes move {
0% { transform: translateX(-60%); }
50% { transform: translateX(140%); }
100% { transform: translateX(-60%); }
}
</style>