feat:新增了绑定手机检查,抖音登录等逻辑,并且更改了页面样式以符合抖音要求

This commit is contained in:
tsui110 2026-01-02 16:03:33 +08:00
parent 61df7fca5e
commit 05056c8188
10 changed files with 286 additions and 70 deletions

View File

@ -6,9 +6,23 @@ export function wechatLogin(code, invite_code) {
}
// 抖音小程序登录
/**
* 抖音小程序登录
* @param {string} code - 抖音登录 code tt.login 获取
* @param {string} anonymous_code - 匿名登录 code可选
* @param {string} invite_code - 邀请码可选
*/
export function douyinLogin(code, anonymous_code, invite_code) {
const data = {}
if (code) data.code = code
if (anonymous_code) data.anonymous_code = anonymous_code
if (invite_code) data.invite_code = invite_code
return request({ url: '/api/app/users/douyin/login', method: 'POST', data })
}
// 保持向后兼容
export function toutiaoLogin(code, invite_code) {
const data = invite_code ? { code, invite_code } : { code }
return request({ url: '/api/app/users/toutiao/login', method: 'POST', data })
return douyinLogin(code, null, invite_code)
}
// ============================================
@ -44,6 +58,15 @@ export function bindPhone(user_id, code, extraHeader = {}) {
return authRequest({ url: `/api/app/users/${user_id}/phone/bind`, method: 'POST', data: { code }, header: extraHeader })
}
/**
* 绑定抖音手机号
* @param {number} user_id - 用户ID
* @param {string} code - 抖音手机号授权 code
*/
export function bindDouyinPhone(user_id, code) {
return authRequest({ url: `/api/app/users/${user_id}/douyin/phone/bind`, method: 'POST', data: { code } })
}
export function getUserStats(user_id) {
return authRequest({ url: `/api/app/users/${user_id}/stats`, method: 'GET' })
}

View File

@ -71,7 +71,12 @@
},
"mp-toutiao": {
"usingComponents": true,
"appid": "ttf031868c6f33d91001"
"appid": "ttf031868c6f33d91001",
"privacy": {
"getPhoneNumber": {
"desc": "用于登录和账号绑定"
}
}
},
"uniStatistics": {
"enable": false

View File

@ -437,62 +437,132 @@ export default {
setupSocketListeners() {
nakamaManager.setListeners({
onmatchmakermatched: async (matched) => {
// ========== - ==========
console.log('=== 匹配成功事件 ===');
console.log('[匹配时间]', new Date().toLocaleString());
console.log('[Match ID]', matched.match_id);
console.log('[Match Token]', matched.token ? '存在' : '不存在');
console.log('[完整匹配对象]', JSON.stringify(matched, null, 2));
console.log('==================');
this.addLog('system', `📡 信号锁定!同步中...`);
this.isMatching = false;
clearInterval(this.matchInterval);
//
this.pendingMatchId = matched.match_id;
this.pendingMatchToken = matched.token;
// joinMatch
const maxRetries = 3;
for (let i = 0; i < maxRetries; i++) {
try {
console.log(`[尝试 ${i + 1}/${maxRetries}] 开始加入游戏房间...`);
const match = await nakamaManager.joinMatch(matched.match_id, matched.token);
// ========== - ==========
console.log('=== 成功加入游戏房间 ===');
console.log('[房间 ID]', match.match_id);
console.log('[我的用户 ID]', this.myUserId);
console.log('[房间对象]', JSON.stringify(match, null, 2));
console.log('======================');
this.matchId = match.match_id;
this.addLog('system', `成功接入战局`);
setTimeout(() => {
console.log('[发送状态请求] 请求初始游戏状态...');
nakamaManager.sendMatchState(match.match_id, 100, JSON.stringify({ action: 'getState' }));
}, 100);
return; // 退
} catch (e) {
// ========== - ==========
console.error(`[尝试 ${i + 1}/${maxRetries} 失败]`, e.message);
console.error('[错误详情]', e);
this.addLog('system', `⚠️ 接入尝试 ${i + 1}/${maxRetries} 失败: ${e.message}`);
if (i < maxRetries - 1) {
console.log(`等待 1 秒后重试...`);
await new Promise(r => setTimeout(r, 1000)); // 1
}
}
}
// ========== - ==========
console.error('=== 加入游戏房间最终失败 ===');
console.error('[Match ID]', matched.match_id);
console.error('[重试次数]', maxRetries);
console.error('============================');
this.addLog('system', `❌ 接入失败,请检查网络后重试`);
},
onmatchdata: (matchData) => {
const opCode = matchData.op_code;
const data = JSON.parse(new TextDecoder().decode(matchData.data));
// ========== - ==========
console.log('=== 接收游戏数据 ===');
console.log('[OpCode]', opCode, `(${this.getOpCodeName(opCode)})`);
console.log('[数据]', data);
console.log('[时间]', new Date().toLocaleString());
console.log('====================');
this.handleGameData(opCode, data);
},
ondisconnect: async () => {
// ========== - ==========
console.warn('=== 连接断开 ===');
console.warn('[断开时间]', new Date().toLocaleString());
console.warn('[Match ID]', this.matchId);
console.warn('===============');
this.addLog('system', `⚠️ 连接断开,尝试重连中...`);
this.isConnected = false;
//
try {
console.log('[重连] 开始重新连接...');
await nakamaManager.authenticateWithGameToken(this.gameToken);
this.isConnected = true;
console.log('[重连] 认证成功');
this.addLog('system', `✅ 重连成功`);
//
if (this.matchId) {
console.log('[重连] 尝试重新加入游戏房间:', this.matchId);
const match = await nakamaManager.joinMatch(this.matchId);
console.log('[重连] 成功重新加入游戏房间');
this.addLog('system', `✅ 已重新加入战局`);
nakamaManager.sendMatchState(this.matchId, 100, JSON.stringify({ action: 'getState' }));
}
} catch (e) {
console.error('[重连] 失败:', e);
this.addLog('system', `❌ 重连失败: ${e.message}`);
}
}
});
},
handleGameData(opCode, data) {
//
if (opCode === 1) {
// ========== - ==========
console.log('🎮 ========== 游戏开始 ==========');
console.log('[玩家数量]', Object.keys(data.players || {}).length);
console.log('[我的位置]', data.turnOrder?.indexOf(this.myUserId));
console.log('[网格大小]', data.gridSize);
console.log('[游戏状态]', data);
console.log('================================');
} else if (opCode === 6) {
// ========== - ==========
console.log('🏁 ========== 游戏结束 ==========');
console.log('[获胜者]', data.winnerId);
console.log('[是否胜利]', data.winnerId === this.myUserId);
console.log('[最终状态]', data);
console.log('================================');
}
if (opCode === 1 || opCode === 2) {
this.gameState = data;
if (opCode === 1) this.addLog('system', '战局开始,准备翻格!');
@ -588,7 +658,18 @@ export default {
if (content === 'empty') return '✅';
return this.getItemIcon(content);
},
getNumColor(n) { return ['','blue','green','red','purple','orange'][n] || 'black'; }
getNumColor(n) { return ['','blue','green','red','purple','orange'][n] || 'black'; },
getOpCodeName(code) {
const map = {
1: '游戏开始',
2: '状态更新',
3: '玩家点击格子',
5: '游戏事件',
6: '游戏结束',
100: '请求游戏状态'
};
return map[code] || '未知';
}
}
}
</script>

View File

@ -174,14 +174,14 @@
{
"path": "game/minesweeper/index",
"style": {
"navigationStyle": "custom",
"navigationStyle": "default",
"navigationBarTitleText": "扫雷 game"
}
},
{
"path": "game/minesweeper/play",
"style": {
"navigationStyle": "custom",
"navigationStyle": "default",
"navigationBarTitleText": "扫雷对战",
"disableScroll": true,
"app-plus": {

View File

@ -165,6 +165,7 @@ import { ref, computed } from 'vue'
import { onShow, onReachBottom, onShareAppMessage, onPullDownRefresh } from '@dcloudio/uni-app'
import { getInventory, getProductDetail, redeemInventory, requestShipping, cancelShipping, listAddresses, getShipments, createAddressShare } from '@/api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
import { checkPhoneBound } from '@/utils/checkPhone.js'
const currentTab = ref(0)
const aggregatedList = ref([])
@ -214,6 +215,9 @@ async function fetchProductMeta(productId) {
}
onShow(() => {
//
if (!checkPhoneBound()) return
// Check for external tab switch request
try {
const targetTab = uni.getStorageSync('cabinet_target_tab')
@ -224,13 +228,10 @@ onShow(() => {
} catch (e) {}
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
console.log('cabinet onShow token:', token, 'isLogin:', !!token, 'phoneBound:', phoneBound)
if (!token || !phoneBound) {
if (!token) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
content: '请先登录',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {

View File

@ -132,6 +132,7 @@
<script>
import { authRequest, request } from '../../utils/request.js'
import SplashScreen from '@/components/SplashScreen.vue'
import { checkPhoneBound } from '../../utils/checkPhone.js'
export default {
components: {
@ -179,6 +180,9 @@ export default {
}
},
onLoad() {
//
if (!checkPhoneBound()) return
// 200ms Token/Session
//
setTimeout(() => {

View File

@ -77,16 +77,15 @@
<text class="icon-emoji">🎵</text>
</view>
</view>
<text class="panel-title">一键获取手机号</text>
<text class="panel-desc">授权获取本机手机号安全快速登录</text>
<text class="panel-title">抖音快捷登录</text>
<text class="panel-desc">使用抖音账号快速登录</text>
<button
class="btn-primary btn-login"
open-type="getPhoneNumber"
:disabled="loading"
@getphonenumber="onToutiaoGetPhoneNumber"
@tap="handleDouyinLogin"
>
{{ loading ? '获取中...' : '一键获取手机号' }}
{{ loading ? '登录中...' : '抖音登录' }}
</button>
</view>
<!-- #endif -->
@ -175,7 +174,7 @@
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { request } from '../../utils/request'
import { wechatLogin, toutiaoLogin, bindPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin } from '../../api/appUser'
import { wechatLogin, douyinLogin, bindPhone, bindDouyinPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const loading = ref(false)
@ -221,14 +220,19 @@ async function ensureOpenID() {
// #endif
// #ifdef MP-TOUTIAO
uni.login({
provider: 'toutiao',
// 使 tt.login
tt.login({
success: async (loginRes) => {
try {
console.log('[DEBUG] 抖音登录成功code:', loginRes.code)
// code
uni.setStorageSync('douyin_login_code', loginRes.code)
// openid
const res = await request({
url: '/api/app/common/openid',
method: 'POST',
data: { code: loginRes.code, platform: 'toutiao' }
data: { code: loginRes.code, platform: 'douyin' }
})
if (res && res.openid) {
console.log('[DEBUG] 静默获取 openid 成功:', res.openid)
@ -237,6 +241,9 @@ async function ensureOpenID() {
} catch (err) {
console.error('[DEBUG] 静默获取 openid 失败:', err)
}
},
fail: (err) => {
console.error('[DEBUG] 抖音登录失败:', err)
}
})
// #endif
@ -421,54 +428,58 @@ function onGetPhoneNumber(e) {
})
}
//
function onToutiaoGetPhoneNumber(e) {
// -
async function handleDouyinLogin() {
console.log('[DEBUG] 抖音登录按钮点击')
if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
vibrateShort()
return
}
const phoneCode = e.detail.code
if (!phoneCode) {
uni.showToast({ title: '需要授权手机号', icon: 'none' })
return
}
loading.value = true
uni.login({
provider: 'toutiao',
success: async (res) => {
try {
const inviterCode = uni.getStorageSync('inviter_code')
const data = await toutiaoLogin(res.code, inviterCode)
try {
// 使 code
const loginCode = uni.getStorageSync('douyin_login_code')
saveUserData(data)
// ()
const isBound = data.phone || data.phone_number || data.mobile
if (!isBound) {
try {
await bindPhone(data.user_id, phoneCode)
} catch (e) {}
}
//
fetchExtraData(data.user_id)
uni.showToast({ title: '✨ 登录成功!', icon: 'none', duration: 1200 })
setTimeout(() => uni.reLaunch({ url: '/pages/mine/index' }), 600)
} catch (err) {
uni.showToast({ title: err.message || '登录失败', icon: 'none' })
} finally {
loading.value = false
}
},
fail: () => {
loading.value = false
let loginData
if (!loginCode) {
// code
console.log('[DEBUG] 未保存登录 code开始重新登录...')
const loginRes = await new Promise((resolve, reject) => {
tt.login({
success: resolve,
fail: reject
})
})
console.log('[DEBUG] 重新获取抖音登录 code:', loginRes.code)
loginData = await douyinLogin(loginRes.code, null, uni.getStorageSync('inviter_code'))
} else {
console.log('[DEBUG] 使用保存的登录 code:', loginCode)
loginData = await douyinLogin(loginCode, null, uni.getStorageSync('inviter_code'))
}
})
console.log('[DEBUG] 抖音登录成功:', loginData)
// saveUserData
saveUserData(loginData)
//
const hasPhone = loginData.mobile || loginData.phone || loginData.phone_number
if (hasPhone) {
fetchExtraData(loginData.user_id)
uni.showToast({ title: '✨ 登录成功!', icon: 'none', duration: 1200 })
setTimeout(() => uni.reLaunch({ url: '/pages/mine/index' }), 600)
}
// saveUserData tab
} catch (err) {
console.error('[DEBUG] 抖音登录失败:', err)
uni.showToast({ title: err.message || '登录失败,请重试', icon: 'none', duration: 2000 })
} finally {
loading.value = false
}
}
function saveUserData(data) {
@ -481,10 +492,18 @@ function saveUserData(data) {
if (data.nickname) uni.setStorageSync('nickname', data.nickname)
if (data.invite_code) uni.setStorageSync('invite_code', data.invite_code)
if (data.mobile) uni.setStorageSync('last_login_mobile', data.mobile)
//
uni.setStorageSync('phone_bound', true)
//
const hasPhone = data.mobile || data.phone || data.phone_number
console.log('[DEBUG] 检查手机号绑定状态:', hasPhone ? '已绑定' : '未绑定')
// phone_bound
if (hasPhone) {
uni.setStorageSync('phone_bound', true)
} else {
uni.setStorageSync('phone_bound', false)
}
// openid ( openid)
const openid = data.openid || data.open_id
if (openid) {
@ -493,6 +512,21 @@ function saveUserData(data) {
} else {
console.warn('[DEBUG] 登录接口未返回 openid, 请检查后端或联系管理员')
}
if (!hasPhone) {
// tab
console.log('[DEBUG] 未检测到手机号切换到短信登录tab')
uni.showModal({
title: '绑定手机号',
content: '登录成功!为了账号安全,请绑定手机号',
showCancel: false,
confirmText: '去绑定',
success: () => {
// tab
loginMode.value = 'sms'
}
})
}
}
function fetchExtraData(userId) {

View File

@ -470,6 +470,7 @@ import {
getUserInfo, getUserStats, getPointsBalance, getUserPoints, getUserCoupons, getItemCards,
getUserTasks, getTaskProgress, getInviteRecords, modifyUser
} from '../../api/appUser.js'
import { checkPhoneBound } from '../../utils/checkPhone.js'
export default {
data() {
@ -479,6 +480,7 @@ export default {
avatar: '',
title: '', //
inviteCode: '',
mobile: '', //
pointsBalance: 0,
stats: {
@ -535,6 +537,9 @@ export default {
}
},
onShow() {
//
if (!checkPhoneBound()) return
this.loadUserInfo()
},
onShareAppMessage() {
@ -554,6 +559,23 @@ export default {
}
},
methods: {
//
checkPhoneBound() {
if (!this.mobile) {
uni.showModal({
title: '需要绑定手机号',
content: '为了账号安全,请先绑定手机号',
showCancel: false,
confirmText: '去绑定',
success: () => {
uni.navigateTo({ url: '/pages/login/index?mode=sms' })
}
})
return false
}
return true
},
//
handleEditAvatar() {
if (!this.userId) {
@ -561,6 +583,8 @@ export default {
return
}
if (!this.checkPhoneBound()) return
uni.chooseImage({
count: 1,
sizeType: ['compressed'], //
@ -726,6 +750,7 @@ export default {
this.inviteCode = cachedUser.invite_code
this.title = cachedUser.title || ''
this.pointsBalance = this.normalizePointsBalance(cachedUser.points_balance)
this.mobile = cachedUser.mobile || cachedUser.phone || cachedUser.phone_number || ''
} else if (cachedUserId) {
this.userId = cachedUserId
}
@ -754,6 +779,7 @@ export default {
this.title = res.title || res.level_name || ''
this.inviteCode = res.invite_code
this.pointsBalance = this.normalizePointsBalance(res.points_balance)
this.mobile = res.mobile || res.phone || res.phone_number || ''
uni.setStorageSync('user_info', res)
uni.setStorageSync('user_id', res.id)
@ -779,28 +805,36 @@ export default {
uni.navigateTo({ url: '/pages/login/index' })
},
toOrders(status) {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: `/pages-user/orders/index?status=${status}` })
},
toCabinetTab(tabIndex) {
if (!this.checkPhoneBound()) return
uni.setStorageSync('cabinet_target_tab', tabIndex)
uni.switchTab({ url: '/pages/cabinet/index' })
},
toAddresses() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/address/index' })
},
toPointsPage() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/points/index' })
},
toCouponsPage() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/coupons/index' })
},
toItemCardsPage() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/item-cards/index' })
},
toInvitesPage() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/invites/index' })
},
toTasksPage() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/tasks/index' })
},
toHelp() {
@ -882,6 +916,7 @@ export default {
// --- Points Logic ---
async showPointsPopup() {
if (!this.checkPhoneBound()) return
this.pointsVisible = true
this.pointsList = []
this.pointsPage = 1
@ -918,6 +953,7 @@ export default {
// --- Coupons Logic ---
async showCouponsPopup() {
if (!this.checkPhoneBound()) return
this.couponsVisible = true
this.couponsTab = 1
this.couponsList = []
@ -976,6 +1012,7 @@ export default {
// --- Item Cards Logic ---
async showItemCardsPopup() {
if (!this.checkPhoneBound()) return
this.itemCardsVisible = true
this.itemCardsTab = 0
this.itemCardsList = []
@ -1026,6 +1063,7 @@ export default {
// --- Tasks Logic ---
async showTasksPopup() {
if (!this.checkPhoneBound()) return
this.tasksVisible = true
this.tasksLoading = true
try {
@ -1162,6 +1200,7 @@ export default {
// --- Invites Logic ---
async showInvitesPopup() {
if (!this.checkPhoneBound()) return
this.invitesVisible = true
this.invitesLoading = true
try {

View File

@ -157,6 +157,7 @@
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import { ref, watch, onUnmounted } from 'vue'
import { getStoreItems, redeemProductByPoints, redeemCouponByPoints, redeemItemCardByPoints } from '../../api/appUser'
import { checkPhoneBound } from '../../utils/checkPhone.js'
const loading = ref(false)
const keyword = ref('')
@ -413,15 +414,15 @@ async function onRedeemTap(item) {
}
onShow(() => {
//
if (!checkPhoneBound()) return
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (token && phoneBound) {
if (token) {
page.value = 1
hasMore.value = true
allItems.value = []
loadItems()
} else {
// Redirect logic if needed
}
})

28
utils/checkPhone.js Normal file
View File

@ -0,0 +1,28 @@
/**
* 检查手机号绑定状态
* 如果未绑定手机号,则跳转到登录页面进行绑定
* @returns {boolean} 是否已绑定手机号
*/
export function checkPhoneBound() {
// 获取用户信息
const userInfo = uni.getStorageSync('user_info') || {}
const mobile = userInfo.mobile || userInfo.phone || userInfo.phone_number || ''
// 如果已绑定手机号,直接返回
if (mobile) {
return true
}
// 未绑定手机号,显示提示并跳转
uni.showModal({
title: '需要绑定手机号',
content: '为了账号安全,请先绑定手机号',
showCancel: false,
confirmText: '去绑定',
success: () => {
uni.navigateTo({ url: '/pages/login/index?mode=sms' })
}
})
return false
}