视频连线

This commit is contained in:
duoaohui
2026-05-26 22:41:53 +08:00
parent 331941f9ce
commit 24e3a37c96
19 changed files with 860 additions and 4 deletions

View File

@@ -37,6 +37,10 @@ const login = (data) => {
return post('/api/v1/auth/login', data, { needAuth: false })
}
const getOAuthAuthorizeUrl = (provider) => {
return get(`/api/v1/auth/oauth/${provider}/authorize`, {}, { needAuth: false })
}
/**
* 退出登录
* POST /api/v1/auth/logout
@@ -107,6 +111,7 @@ const changePassword = (data) => {
export default {
register,
login,
getOAuthAuthorizeUrl,
logout,
refreshToken,
getProfile,

View File

@@ -31,6 +31,12 @@
"navigationBarTitleText": "登录中"
}
},
{
"path": "pages/auth/oauth-callback",
"style": {
"navigationBarTitleText": "登录中"
}
},
{
"path": "pages/chat/index",
"style": {

View File

@@ -73,6 +73,18 @@
<text class="btn-label">{{ loading ? '登录中…' : '登录' }}</text>
</button>
<view class="oauth-section">
<view class="oauth-divider">
<view class="oauth-line"></view>
<text class="oauth-text">其他登录方式</text>
<view class="oauth-line"></view>
</view>
<view class="oauth-buttons">
<button class="oauth-btn oauth-wechat" :disabled="oauthLoading" @tap="oauthLogin('wechat')">微信登录</button>
<button class="oauth-btn oauth-qq" :disabled="oauthLoading" @tap="oauthLogin('qq')">QQ登录</button>
</view>
</view>
<!-- 底部链接 -->
<view class="link-row">
<text class="link-hint">还没有账号</text>
@@ -94,6 +106,7 @@ import { useWebSocketStore } from '@/store/websocket'
import { useChatStore } from '@/store/chat'
import { useContactStore } from '@/store/contact'
import { useNotifyStore } from '@/store/notify'
import authApi from '@/api/auth'
function readSsoQueryFromLocation() {
try {
@@ -125,6 +138,7 @@ export default {
form: { account: '', password: '' },
passwordVisible: false,
loading: false,
oauthLoading: false,
focusState: { account: false, password: false },
errors: { account: '', password: '' }
}
@@ -200,6 +214,29 @@ export default {
goRegister() {
uni.navigateTo({ url: '/pages/auth/register' })
},
async oauthLogin(provider) {
if (this.oauthLoading) return
this.oauthLoading = true
try {
const res = await authApi.getOAuthAuthorizeUrl(provider)
const url = res && res.data && res.data.url
if (!url) {
uni.showToast({ title: '第三方登录未配置', icon: 'none' })
return
}
// #ifdef H5
window.location.href = url
// #endif
// #ifndef H5
uni.showToast({ title: '当前端暂不支持第三方登录', icon: 'none' })
// #endif
} catch (e) {
console.error('第三方登录失败:', e)
} finally {
this.oauthLoading = false
}
}
}
}
@@ -362,6 +399,54 @@ export default {
letter-spacing: 2rpx;
}
.oauth-section {
margin-top: 40rpx;
}
.oauth-divider {
display: flex;
align-items: center;
gap: 20rpx;
}
.oauth-line {
flex: 1;
height: 2rpx;
background-color: #E2E8F0;
}
.oauth-text {
font-size: 24rpx;
color: #94A3B8;
}
.oauth-buttons {
display: flex;
gap: 24rpx;
margin-top: 28rpx;
}
.oauth-btn {
flex: 1;
height: 80rpx;
border-radius: 16rpx;
font-size: 28rpx;
color: #FFFFFF;
border: none;
}
.oauth-btn::after {
border: none;
}
.oauth-wechat {
background-color: #07C160;
}
.oauth-qq {
background-color: #12B7F5;
}
/* ---- 底部链接 ---- */
.link-row {
display: flex;

View File

@@ -0,0 +1,131 @@
<template>
<view class="callback-page">
<view class="callback-card">
<view class="loading-dot"></view>
<text class="callback-title">正在完成登录</text>
<text class="callback-desc">请稍候正在同步账号信息</text>
</view>
</view>
</template>
<script>
import { useUserStore } from '@/store/user'
import { useWebSocketStore } from '@/store/websocket'
import { useChatStore } from '@/store/chat'
import { useContactStore } from '@/store/contact'
import { useNotifyStore } from '@/store/notify'
function decodePayload(payload) {
const normalized = String(payload || '').replace(/-/g, '+').replace(/_/g, '/')
const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4)
const binary = atob(padded)
return decodeURIComponent(Array.prototype.map.call(binary, c => {
return `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`
}).join(''))
}
function readRawQueryParams() {
const result = {}
try {
if (typeof window === 'undefined') return result
const search = String(window.location.search || '').replace(/^\?/, '')
const hash = String(window.location.hash || '')
const hashQuery = hash.indexOf('?') >= 0 ? hash.slice(hash.indexOf('?') + 1) : ''
;[search, hashQuery].forEach(raw => {
if (!raw) return
const params = new URLSearchParams(raw)
params.forEach((value, key) => {
if (result[key] === undefined) {
result[key] = value
}
})
})
} catch (e) {}
return result
}
export default {
name: 'OAuthCallbackPage',
onLoad(query) {
this.handleCallback({ ...readRawQueryParams(), ...(query || {}) })
},
methods: {
async handleCallback(query) {
try {
if (query.status !== 'success') {
const message = query.payload ? decodePayload(query.payload) : '第三方登录失败'
uni.showToast({ title: message || '第三方登录失败', icon: 'none' })
setTimeout(() => uni.redirectTo({ url: '/pages/auth/login' }), 1200)
return
}
const loginData = JSON.parse(decodePayload(query.payload))
const store = useUserStore()
store.applyOAuthLogin(loginData)
const wsStore = useWebSocketStore()
wsStore.connect()
useChatStore().initWsListeners()
useContactStore().initWsListeners()
const notifyStore = useNotifyStore()
notifyStore.initWsListeners()
notifyStore.fetchUnreadCount().catch(() => {})
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => uni.reLaunch({ url: '/pages/index/index' }), 600)
} catch (e) {
console.error('OAuth 回调处理失败:', e)
uni.showToast({ title: '登录失败', icon: 'none' })
setTimeout(() => uni.redirectTo({ url: '/pages/auth/login' }), 1200)
}
}
}
}
</script>
<style scoped>
.callback-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #F8FAFC;
padding: 48rpx;
}
.callback-card {
width: 100%;
max-width: 640rpx;
padding: 64rpx 40rpx;
border-radius: 24rpx;
background-color: #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
align-items: center;
}
.loading-dot {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
border: 6rpx solid #DBEAFE;
border-top-color: #2563EB;
animation: spin 800ms linear infinite;
}
.callback-title {
margin-top: 32rpx;
font-size: 34rpx;
font-weight: 600;
color: #1E293B;
}
.callback-desc {
margin-top: 12rpx;
font-size: 26rpx;
color: #64748B;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>

View File

@@ -106,6 +106,11 @@ export const useUserStore = defineStore('user', () => {
return res.data
}
const applyOAuthLogin = (data) => {
_handleAuthSuccess(data)
return data
}
/**
* 退出登录
* 1. 调用后端 API 从 Redis 删除 Token使 Token 立即失效)
@@ -207,6 +212,7 @@ export const useUserStore = defineStore('user', () => {
// actions
login,
register,
applyOAuthLogin,
logout,
getProfile,
updateProfile,