Merge branch 'feat-scan' into test
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -118,6 +118,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"subPackages": [
|
"subPackages": [
|
||||||
|
{
|
||||||
|
"root": "pages/scan-login",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "index"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/coin",
|
"root": "pages/coin",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
@@ -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 |
Reference in New Issue
Block a user