Sync source to CNB / Sync Git ref (push) Skipped
登录入口原先会切到主页并打开内嵌网页。改为弹出启动器自己的登录弹窗: - SkinSiteLoginModal:账号密码校验规则与皮肤站一致,登录前走验证码, 成功后保存 JWT 并刷新会话。 - SkinSiteCaptchaModal:拉取旋转图片验证码,用滑块调整角度后提交。 - AccountsCard 的登录按钮改为打开该弹窗,登录成功后重新拉取玩家列表。 - 修正 verifyCaptcha 的参数:服务端校验的是旋转角度 degree,不是文字答案。
156 lines
3.8 KiB
Vue
156 lines
3.8 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
commonMessages,
|
|
defineMessages,
|
|
injectNotificationManager,
|
|
NewModal,
|
|
useVIntl,
|
|
} from '@modrinth/ui'
|
|
import { computed, ref } from 'vue'
|
|
|
|
import { requestCaptcha, verifyCaptcha } from '@/helpers/skin-site-api'
|
|
|
|
const { formatMessage } = useVIntl()
|
|
const { handleError } = injectNotificationManager()
|
|
|
|
const messages = defineMessages({
|
|
title: { id: 'app.skin-site.captcha.title', defaultMessage: 'Let us check you are human' },
|
|
subtitle: {
|
|
id: 'app.skin-site.captcha.subtitle',
|
|
defaultMessage: 'Drag the slider to rotate the image upright.',
|
|
},
|
|
submit: { id: 'app.skin-site.captcha.submit', defaultMessage: 'Submit' },
|
|
loading: { id: 'app.skin-site.captcha.loading', defaultMessage: 'Loading captcha…' },
|
|
})
|
|
|
|
const emit = defineEmits<{ (e: 'verified', clientId: string): void; (e: 'cancel'): void }>()
|
|
|
|
const modal = ref<InstanceType<typeof NewModal>>()
|
|
const loading = ref(false)
|
|
const submitting = ref(false)
|
|
const image = ref('')
|
|
const challengeClientId = ref('')
|
|
const degree = ref(0)
|
|
|
|
/** The image is rotated by the server; the slider counter-rotates the preview. */
|
|
const previewRotation = computed(() => `${degree.value}deg`)
|
|
|
|
function resetState() {
|
|
degree.value = 0
|
|
image.value = ''
|
|
challengeClientId.value = ''
|
|
}
|
|
|
|
async function loadCaptcha() {
|
|
loading.value = true
|
|
try {
|
|
const result = await requestCaptcha()
|
|
if (!result.success) throw new Error(result.errorMessage)
|
|
image.value = result.payload.image ?? ''
|
|
challengeClientId.value = result.payload.clientId
|
|
degree.value = 0
|
|
} catch (error) {
|
|
handleError(error)
|
|
modal.value?.hide()
|
|
emit('cancel')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function show() {
|
|
resetState()
|
|
modal.value?.show()
|
|
await loadCaptcha()
|
|
}
|
|
|
|
async function submit() {
|
|
if (submitting.value || !challengeClientId.value) return
|
|
submitting.value = true
|
|
try {
|
|
const result = await verifyCaptcha({
|
|
clientId: challengeClientId.value,
|
|
degree: degree.value,
|
|
})
|
|
if (!result.success) {
|
|
handleError(new Error(result.errorMessage))
|
|
await loadCaptcha()
|
|
return
|
|
}
|
|
modal.value?.hide()
|
|
emit('verified', result.payload)
|
|
} catch (error) {
|
|
handleError(error)
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function cancel() {
|
|
modal.value?.hide()
|
|
emit('cancel')
|
|
}
|
|
|
|
defineExpose({ show })
|
|
</script>
|
|
|
|
<template>
|
|
<NewModal ref="modal" :header="formatMessage(messages.title)" @hide="resetState">
|
|
<div class="flex flex-col items-center gap-6">
|
|
<p class="m-0 text-secondary">{{ formatMessage(messages.subtitle) }}</p>
|
|
|
|
<div
|
|
class="size-44 overflow-hidden rounded-full border-4 border-solid border-bg-raised bg-button-bg"
|
|
>
|
|
<img
|
|
v-if="image"
|
|
:src="image"
|
|
alt=""
|
|
class="size-full object-cover transition-transform duration-75"
|
|
:style="{ transform: `rotate(${previewRotation})` }"
|
|
draggable="false"
|
|
/>
|
|
</div>
|
|
|
|
<div class="w-full max-w-sm">
|
|
<input
|
|
v-model.number="degree"
|
|
type="range"
|
|
min="0"
|
|
max="360"
|
|
step="1"
|
|
class="w-full accent-brand"
|
|
:disabled="loading || submitting"
|
|
/>
|
|
<div class="mt-1 flex justify-between text-xs text-secondary">
|
|
<span>0°</span>
|
|
<span>90°</span>
|
|
<span>180°</span>
|
|
<span>270°</span>
|
|
<span>360°</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template #actions>
|
|
<div class="flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
class="rounded-full px-4 py-2 text-secondary hover:bg-button-bg"
|
|
@click="cancel"
|
|
>
|
|
{{ formatMessage(commonMessages.cancelButton) }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="rounded-full bg-brand px-4 py-2 font-semibold text-contrast"
|
|
:disabled="loading || submitting || !image"
|
|
@click="submit"
|
|
>
|
|
{{ formatMessage(loading ? messages.loading : messages.submit) }}
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</NewModal>
|
|
</template>
|