Merge branch 'feat-scan' into test

This commit is contained in:
2026-11-09 18:24:02 +08:00
8 changed files with 445 additions and 0 deletions
+4
View File
@@ -8,6 +8,10 @@
"type": "folder", "type": "folder",
"value": "static/coin" "value": "static/coin"
}, },
{
"type": "folder",
"value": "static/scan-login"
},
{ {
"type": "file", "type": "file",
"value": "static/common/dialog-light.png" "value": "static/common/dialog-light.png"
+8
View File
@@ -264,6 +264,14 @@ export const tempBindOrgAPI = (scene) => {
}); });
}; };
// 小程序确认个人账号登录 Pad,未登录时可携带微信登录 code。
export const confirmScanLoginAPI = (scene, code) => {
return request("POST", "/index/scanLogin/confirm", {
scene,
...(code ? {code} : {}),
}, BASE_URL, [0], {showErrorToast: false});
};
export const bindDeviceAPI = (device) => { export const bindDeviceAPI = (device) => {
return request("POST", "/user/device/bindDevice", { return request("POST", "/user/device/bindDevice", {
device, device,
+4
View File
@@ -59,6 +59,10 @@
"type" : "folder", "type" : "folder",
"value" : "static/coin" "value" : "static/coin"
}, },
{
"type" : "folder",
"value" : "static/scan-login"
},
{ {
"type" : "file", "type" : "file",
"value" : "static/common/dialog-light.png" "value" : "static/common/dialog-light.png"
+8
View File
@@ -118,6 +118,14 @@
} }
}, },
"subPackages": [ "subPackages": [
{
"root": "pages/scan-login",
"pages": [
{
"path": "index"
}
]
},
{ {
"root": "pages/coin", "root": "pages/coin",
"pages": [ "pages": [
+421
View File
@@ -0,0 +1,421 @@
<script setup>
import { computed, ref } from "vue";
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import Container from "@/components/Container.vue";
import Signin from "@/components/Signin.vue";
import { confirmScanLoginAPI } from "@/apis";
import { capsuleHeight, wxLogin } from "@/util";
import useStore from "@/store";
const store = useStore();
const { user } = storeToRefs(store);
const scene = ref("");
const hasToken = ref(false);
const showSignin = ref(false);
const confirming = ref(false);
const authorized = ref(false);
const invalidScene = ref(false);
const errorMessage = ref("");
const authorizedUser = ref({});
let pageActive = true;
const isLoggedIn = computed(() => hasToken.value || authorized.value);
const currentUser = computed(() => authorized.value ? authorizedUser.value : user.value);
const avatarFailed = ref(false);
const avatar = computed(() =>
!avatarFailed.value && currentUser.value.avatar
? currentUser.value.avatar
: "/static/user-icon.png"
);
const systemInfo = uni.getSystemInfoSync();
const unitScale = 750 / systemInfo.windowWidth;
const headerHeight = (capsuleHeight + 50) * unitScale;
const contentStyle = computed(() => ({
minHeight: `${Math.max(0, systemInfo.windowHeight * unitScale - headerHeight)}rpx`,
paddingTop: `${Math.max(40, (isLoggedIn.value ? 408 : 444) - headerHeight)}rpx`,
}));
const getTokenKey = () => `${uni.getAccountInfoSync().miniProgram.envVersion}_token`;
const refreshLoginState = () => {
hasToken.value = Boolean(uni.getStorageSync(getTokenKey()));
};
const closeSignin = () => {
showSignin.value = false;
};
const goHome = () => {
uni.reLaunch({ url: "/pages/index" });
};
const goBack = () => {
if (confirming.value) {
uni.showToast({ title: "正在授权,请稍候", icon: "none" });
return;
}
if (authorized.value) {
goHome();
return;
}
if (getCurrentPages().length > 1) {
uni.navigateBack();
return;
}
goHome();
};
const confirmLogin = async () => {
if (confirming.value || authorized.value || !pageActive) return;
if (!scene.value || invalidScene.value) return;
confirming.value = true;
errorMessage.value = "";
try {
refreshLoginState();
// 仅在用户点击后确认授权,未登录时先尝试通过微信 code 识别账号。
const code = hasToken.value ? "" : (await wxLogin())?.code;
if (!hasToken.value && !code) {
throw new Error("获取微信登录凭证失败,请重试。");
}
if (!pageActive) return;
const account = { ...user.value };
const result = await confirmScanLoginAPI(scene.value, code);
if (result?.success === true) {
authorizedUser.value = account;
authorized.value = true;
// Pad 已签发新 token,清理本地账号、设备和对局等会话数据。
uni.removeStorageSync(getTokenKey());
store.clearSessionState();
hasToken.value = false;
if (pageActive) {
uni.showModal({
title: "授权成功",
content: "已授权登录 iPad",
showCancel: false,
success: (res) => {
if (res.confirm && pageActive) goHome();
},
});
}
return;
}
if (!pageActive) return;
if (result?.needLogin === true) {
uni.removeStorageSync(getTokenKey());
store.clearSessionState();
hasToken.value = false;
showSignin.value = true;
return;
}
errorMessage.value = result?.msg || "授权失败,请重新扫描 Pad 端的小程序码。";
} catch (error) {
if (!pageActive) return;
if (error?.type === "AUTH_INVALID") {
store.clearSessionState();
hasToken.value = false;
errorMessage.value = "登录已失效,请重新登录后确认授权。";
showSignin.value = true;
} else {
errorMessage.value = error?.message || "网络异常,请稍后重试。";
}
} finally {
confirming.value = false;
}
};
const handlePrimaryAction = () => {
refreshLoginState();
if (!isLoggedIn.value) {
showSignin.value = true;
return;
}
return confirmLogin();
};
const handleLoginSuccess = async () => {
refreshLoginState();
avatarFailed.value = false;
await confirmLogin();
};
onLoad((options) => {
refreshLoginState();
try {
const value = decodeURIComponent(String(options?.scene || ""));
// 只接收当前扫码协议的 scene,不接收 Pad 私有的 pollId。
if (!/^sl[a-zA-Z0-9]{1,30}$/.test(value)) {
invalidScene.value = true;
errorMessage.value = "二维码无效,请重新扫描 Pad 端的小程序码。";
return;
}
scene.value = value;
} catch (error) {
invalidScene.value = true;
errorMessage.value = "二维码无效,请重新扫描 Pad 端的小程序码。";
}
});
onShow(refreshLoginState);
onUnload(() => {
pageActive = false;
closeSignin();
});
</script>
<template>
<view class="scan-login-page">
<Container
:bgType="0"
:onBack="goBack"
:usePageScroll="true"
>
<view class="scan-login-content" :style="contentStyle">
<view v-if="isLoggedIn" class="identity-transfer">
<view class="identity">
<image
class="identity-avatar"
:src="avatar"
mode="aspectFill"
@error="avatarFailed = true"
/>
<text class="identity-name">{{ currentUser.nickName || "弓箭手" }}</text>
</view>
<image
class="transfer-arrow"
src="https://static.shelingxingqiu.com/shootmini/static/scan-login/scan-login-transfer-arrow.png"
mode="aspectFit"
/>
<view class="identity">
<image
class="app-icon"
src="https://static.shelingxingqiu.com/shootmini/static/scan-login/scan-login-app-icon.png"
mode="aspectFit"
/>
<text class="identity-name">射灵星球APP</text>
</view>
</view>
<view v-else class="required-ip">
<view class="required-ip-shadow"></view>
<image
class="required-ip-image"
src="https://static.shelingxingqiu.com/shootmini/static/scan-login/scan-login-required-ip.png"
mode="aspectFit"
/>
</view>
<view v-if="isLoggedIn" class="login-title">
<text>登录 射灵星球APP </text><text class="target-device">Pad端</text>
</view>
<view v-else class="login-description">
<text>你还没有登录射灵星球小程序,</text>
<text>无法扫码登录Pad端。</text>
</view>
<button
:class="['confirm-button', isLoggedIn ? 'confirm-button--logged-in' : '']"
hover-class="none"
:disabled="confirming || authorized || (isLoggedIn && invalidScene)"
@click="$clickSound(handlePrimaryAction)"
>
{{ authorized ? "已授权登录" : confirming ? "正在授权..." : isLoggedIn ? "确认登录" : "立即登录" }}
</button>
<text v-if="isLoggedIn" class="session-tip">
登录Pad端后,小程序端将自动退出登录。
</text>
<text v-if="isLoggedIn && errorMessage" class="error-message">{{ errorMessage }}</text>
<button
v-if="isLoggedIn"
class="cancel-button"
hover-class="none"
:disabled="confirming"
@click="$clickSound(goBack)"
>
取消登录
</button>
</view>
</Container>
<Signin
:show="showSignin"
:onClose="closeSignin"
:onSuccess="handleLoginSuccess"
/>
</view>
</template>
<style scoped lang="scss">
.scan-login-page {
min-height: 100vh;
color: #fff;
}
.scan-login-content {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
padding-right: 40rpx;
padding-left: 40rpx;
}
.identity-transfer {
display: flex;
align-items: flex-start;
justify-content: center;
width: 488rpx;
}
.identity {
display: flex;
flex-direction: column;
align-items: center;
width: 160rpx;
}
.identity-avatar,
.app-icon {
width: 136rpx;
height: 136rpx;
}
.identity-avatar {
border-radius: 68rpx;
}
.app-icon {
border-radius: 36rpx;
}
.identity-name {
width: 160rpx;
margin-top: 12rpx;
overflow: hidden;
color: rgba(255, 255, 255, 0.8);
font-size: 24rpx;
line-height: 34rpx;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.transfer-arrow {
flex-shrink: 0;
width: 82rpx;
height: 50rpx;
margin: 38rpx 41rpx 0;
}
.required-ip {
position: relative;
width: 280rpx;
height: 220rpx;
}
.required-ip-shadow {
position: absolute;
top: 182rpx;
left: 53rpx;
width: 172rpx;
height: 37rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
.required-ip-image {
position: absolute;
top: 0;
left: 52rpx;
width: 176rpx;
height: 208rpx;
}
.login-title {
margin-top: 120rpx;
font-size: 34rpx;
font-weight: 500;
line-height: 48rpx;
text-align: center;
}
.target-device {
color: #fed847;
}
.login-description {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 32rpx;
font-size: 28rpx;
line-height: 40rpx;
text-align: center;
}
.confirm-button {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 400rpx;
height: 76rpx;
margin: 102rpx 0 0;
padding: 0;
border-radius: 44rpx;
background: #fed847;
color: #000;
font-size: 28rpx;
font-weight: 500;
line-height: 40rpx;
}
.confirm-button--logged-in {
margin-top: 120rpx;
}
.confirm-button::after,
.cancel-button::after {
border: none;
}
.confirm-button[disabled] {
background: #fed847;
color: #000;
opacity: 0.6;
}
.session-tip {
margin-top: 24rpx;
color: rgba(255, 255, 255, 0.7);
font-size: 24rpx;
line-height: 34rpx;
text-align: center;
}
.error-message {
max-width: 600rpx;
margin-top: 24rpx;
color: #fed847;
font-size: 24rpx;
line-height: 36rpx;
text-align: center;
}
.cancel-button {
flex-shrink: 0;
margin: auto 0 134rpx;
padding: 28rpx 40rpx;
border-radius: 0;
background: transparent;
color: #83cdff;
font-size: 26rpx;
line-height: 36rpx;
}
.cancel-button[disabled] {
background: transparent;
color: #83cdff;
opacity: 0.6;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B