rebuild: capture

This commit is contained in:
KUN1007 2023-09-23 22:09:07 +08:00
parent 7b79461e6e
commit c9fd19fb58
6 changed files with 361 additions and 143 deletions

View file

@ -38,6 +38,7 @@
"shinnku",
"signin",
"sina",
"SMEE",
"tada",
"tdesign",
"templating",

View file

@ -1,29 +1,154 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
//
import { questionsEN } from './questionsEN'
import { questionsCN } from './questionsCN'
//
import message from '@/components/alert/Message'
//
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
import { storeToRefs } from 'pinia'
// 使 store
const { showKUNGalgameLanguage } = storeToRefs(useKUNGalgameSettingsStore())
//
const questions =
showKUNGalgameLanguage.value === 'en' ? questionsEN : questionsCN
const props = defineProps<{
isShowValidate: boolean
}>()
const emits = defineEmits<{
handleVerify: [res: boolean]
handleClose: [isShowValidate: boolean]
}>()
//
const userAnswers = ref([])
//
const currentQuestionIndex = ref(0)
//
const currentQuestion = computed(() => questions[currentQuestionIndex.value])
//
const errorCounter = ref(0)
const expectedKeys = ref(['k', 'u', 'n'])
const currentIndex = ref(0)
//
const isShowHint = ref(false)
//
const isShowAnswer = ref(false)
//
const checkKeyPress = (event: KeyboardEvent) => {
const pressedKey = event.key
if (pressedKey === expectedKeys.value[currentIndex.value]) {
//
if (currentIndex.value === expectedKeys.value.length - 1) {
// "n"
isShowAnswer.value = true
} else {
//
currentIndex.value++
}
} else {
//
currentIndex.value = 0
}
}
//
const submitAnswer = () => {
const userAnswer = userAnswers.value[currentQuestionIndex.value]
const correctOption = currentQuestion.value.correctOption
if (userAnswer === correctOption) {
//
emits('handleVerify', true)
} else {
//
errorCounter.value++
message('Wrong answer!', '回答错误!', 'warn')
//
nextQuestion()
//
if (errorCounter.value >= 3) {
isShowHint.value = true
}
}
}
const nextQuestion = () => {
if (currentQuestionIndex.value < questions.length - 1) {
currentQuestionIndex.value++
} else {
message('', '已经没有问题了,杂鱼~♡', 'info')
}
}
</script>
<template>
<Teleport to="body" :disabled="props.isShowValidate">
<Transition name="capture">
<div class="mask" v-if="props.isShowValidate">
<!-- 遮罩 -->
<div
class="mask"
@keydown="checkKeyPress($event)"
tabindex="0"
v-if="props.isShowValidate"
>
<div class="validate">
<div class="text">
<div
class="character"
v-for="(char, index) in characters"
:key="index"
:style="char.style"
@click="handleCharacterClick(char)"
>
<text>{{ char.value }}</text>
</div>
<!-- 标题 -->
<div class="title">
<!-- <span>{{ `` }}</span> -->
<h2>请回答下面的问题</h2>
<!-- <span>{{ `` }}</span> -->
</div>
<div class="msg">
<div v-if="errorCount > 0" class="error">
{{ $tm('AlertInfo.capture.error') }}
<p class="question">{{ currentQuestion.text }}</p>
<!-- 选择项 -->
<div class="select">
<label
v-for="(option, index) in currentQuestion.options"
:key="index"
>
<input
type="radio"
v-model="userAnswers[currentQuestionIndex]"
:value="option"
/>
{{ option }}
</label>
</div>
<!-- 提交按钮 -->
<div class="btn">
<button @click="submitAnswer">提交</button>
<button @click="emits('handleClose', false)">关闭</button>
</div>
<!-- 提示 -->
<!-- tabindex 使得该元素可以被页面聚焦 -->
<div class="hint-container">
<div v-if="isShowHint" class="hint">
<div>真是杂鱼呢~这都答不出来~杂鱼~杂鱼~</div>
<div>
臭杂鱼试试在页面上敲击 <span>kun</span> 杂鱼~杂鱼~
</div>
</div>
<div class="hint" v-if="!isVerified">
{{ $tm('AlertInfo.capture.order') }}
{{ characters.map((char) => char.value).join(' ') }}
</div>
<div class="refresh" @click="resetCharacters">
{{ $tm('AlertInfo.capture.refresh') }}
<div v-if="isShowAnswer" class="answer">
<div>杂鱼~杂鱼~你就看吧最后害的还是你自己</div>
<a
href="http://github.com/KUN1007/kun-galgame-vue"
target="_blank"
rel="noopener noreferrer"
>
答案
</a>
</div>
</div>
</div>
@ -32,103 +157,6 @@
</Teleport>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const containerRef = ref<HTMLDivElement | null>(null)
const characters = ref(generateRandomCharacters())
const clickedCharacters = ref('')
const isVerified = ref(false)
const errorCount = ref(0)
const props = defineProps(['isShowValidate'])
const emits = defineEmits(['validate'])
interface Character {
value: string
style: {
top: string
left: string
}
}
emits('validate', false)
//
function generateRandomCharacters(): Character[] {
const chars = t('AlertInfo.capture.text')
const randomChars: Character[] = []
// Set
const charSet = new Set<string>()
while (charSet.size < 3) {
const randomIndex = Math.floor(Math.random() * chars.length)
const char = chars[randomIndex]
//
if (!charSet.has(char)) {
charSet.add(char)
const character: Character = {
value: char,
style: {
top: `${getRandomCoordinate()}px`,
left: `${getRandomCoordinate()}px`,
},
}
randomChars.push(character)
}
}
return randomChars
}
//
function getRandomCoordinate(): number {
const minCoordinate = 50
const maxCoordinate = 150
return (
Math.floor(Math.random() * (maxCoordinate - minCoordinate + 1)) +
minCoordinate
)
}
//
const handleCharacterClick = (char: Character): Promise<boolean> => {
return new Promise<boolean>((resolve, reject) => {
clickedCharacters.value += char.value
if (
clickedCharacters.value.length === 3 &&
clickedCharacters.value === characters.value.map((c) => c.value).join('')
) {
isVerified.value = true
emits('validate', true)
} else if (clickedCharacters.value.length >= 3) {
errorCount.value += 1
resetCharacters()
emits('validate', false)
}
})
}
// .
function resetCharacters() {
characters.value = generateRandomCharacters()
clickedCharacters.value = ''
isVerified.value = false
}
onMounted(() => {
const container = containerRef.value
if (container) {
container.style.width = '300px'
container.style.height = '300px'
}
})
</script>
<style lang="scss" scoped>
.mask {
position: fixed;
@ -148,39 +176,90 @@ onMounted(() => {
width: 300px;
height: 300px;
margin: auto;
padding: 20px 30px;
padding: 17px;
background-color: var(--kungalgame-trans-white-2);
border: 1px solid var(--kungalgame-blue-1);
border-radius: 2px;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.text {
cursor: pointer;
position: absolute;
font-size: 30px;
transform: translateX(-30px);
}
.character {
position: relative;
}
.refresh {
width: 64px;
.title {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 17px;
color: var(--kungalgame-blue-4);
margin-top: 20px;
cursor: pointer;
border-bottom: 2px solid var(--kungalgame-trans-white-9);
&:hover {
border-bottom: 2px solid var(--kungalgame-blue-4);
}
.question {
font-size: 17px;
margin-bottom: 20px;
font-style: oblique;
}
.select {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
margin-bottom: 20px;
}
.btn {
display: flex;
justify-content: space-around;
button {
width: 77px;
padding: 5px;
color: var(--kungalgame-blue-4);
border: 1px solid var(--kungalgame-blue-4);
border-radius: 5px;
background-color: var(--kungalgame-trans-white-9);
transition: all 0.2s;
&:hover {
color: var(--kungalgame-white);
background-color: var(--kungalgame-blue-4);
}
}
}
.msg {
position: absolute;
.hint-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: end;
justify-content: end;
font-style: oblique;
.hint {
width: 100%;
font-size: 10px;
span {
color: var(--kungalgame-pink-4);
font-weight: bold;
}
}
.answer {
width: 100%;
div {
font-size: 10px;
}
a {
color: var(--kungalgame-blue-5);
&:hover {
text-decoration: underline;
}
}
}
}
/* 激活后的样式 */
.active {
transition: all 0.2s;
border: 2px solid var(--kungalgame-pink-3);
}
.capture-enter-from {
@ -197,3 +276,4 @@ onMounted(() => {
transform: scale(1.1);
}
</style>
./questionsCN

View file

@ -0,0 +1,58 @@
import { reactive } from 'vue'
interface Question {
id: number
text: string
options: string[]
correctOption: string
}
export const questionsCN: Question[] = reactive([
{
id: 1,
text: '下列哪个不是《千恋万花》中的角色?',
options: ['丛雨', '芦花', '芳乃', '宁宁'],
correctOption: '宁宁',
},
{
id: 2,
text: '《夏日口袋》中紬文德斯的发色是?',
options: ['红色', '金色', '紫色', '白色'],
correctOption: '金色',
},
{
id: 3,
text: '`Galgame`被称为?',
options: ['Gay game', '美少女游戏', '乙女游戏', '啊这可海星'],
correctOption: '美少女游戏',
},
{
id: 4,
text: '下列哪个游戏不属于五彩斑斓系列?',
options: [
'五彩斑斓的世界',
'美少女游戏',
'五彩斑斓的曙光',
'五彩斑斓的未来',
],
correctOption: '五彩斑斓的未来',
},
{
id: 5,
text: '以下哪部作品中男主没有女装?',
options: ['近月少女的礼仪', '少女领域', '美少女万华镜1', '我们没有翅膀'],
correctOption: '美少女万华镜1',
},
{
id: 6,
text: '以下哪个作品是《SMEE》制作的?',
options: ['Friend to Lover', 'Dal Segno', 'Eden*', 'LOOPERS'],
correctOption: 'Friend to Lover',
},
{
id: 7,
text: '鲲可爱吗?',
options: ['可爱!', '很可爱!', '最可爱了!', '总之就是非常可爱!'],
correctOption: '总之就是非常可爱!',
},
])

View file

@ -0,0 +1,68 @@
import { reactive } from 'vue'
interface Question {
id: number
text: string
options: string[]
correctOption: string
}
export const questionsEN: Question[] = reactive([
{
id: 1,
text: 'Which one of the following is not a character in "Thousand Love Ten Thousand Flowers"?',
options: ['Congyu', 'Luhua', 'Fangna', 'Ningning'],
correctOption: 'Ningning',
},
{
id: 2,
text: 'What is the hair color of Tsudumeandesu in "Summer Pockets"?',
options: ['Red', 'Gold', 'Purple', 'White'],
correctOption: 'Gold',
},
{
id: 3,
text: 'What is "Galgame" known as?',
options: [
'Gay game',
'Beautiful girl game',
'Otome game',
'Ah, this is a starfish',
],
correctOption: 'Beautiful girl game',
},
{
id: 4,
text: 'Which of the following games does not belong to the "Colorful World" series?',
options: [
'Colorful World',
'Beautiful Girl Game',
'Colorful Dawn',
'Colorful Future',
],
correctOption: 'Colorful Future',
},
{
id: 5,
text: 'In which of the following works does the male protagonist not cross-dress?',
options: [
'Closer to the Moon Girl',
'Girl Territory',
'Beautiful Girl Kaleidoscope 1',
'We Have No Wings',
],
correctOption: 'Beautiful Girl Kaleidoscope 1',
},
{
id: 6,
text: 'Which of the following works was produced by "SMEE"?',
options: ['Friend to Lover', 'Dal Segno', 'Eden*', 'LOOPERS'],
correctOption: 'Friend to Lover',
},
{
id: 7,
text: 'Is Kun cute?',
options: ['Cute!', 'Very cute!', 'Cutest!', 'In short, extremely cute!'],
correctOption: 'In short, extremely cute!',
},
])

View file

@ -1,12 +1,14 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ref, reactive, defineAsyncComponent } from 'vue'
import { useKUNGalgameUserStore } from '@/store/modules/kungalgamer'
import { useRouter } from 'vue-router'
// 使
import { useKUNGalgameMessageStore } from '@/store/modules/message'
//
import Capture from '@/components/capture/Capture.vue'
const Capture = defineAsyncComponent(
() => import('@/components/capture/Capture.vue')
)
//
import { isValidEmail, isValidName, isValidPassword } from '@/utils/validate'
@ -101,7 +103,12 @@ const handleLogin = () => {
<template>
<!-- 登陆 -->
<div class="login">
<Capture @validate="handleVerify" :isShowValidate="isShowValidate" />
<!-- 人机验证 -->
<Capture
@handleVerify="handleVerify"
@handleClose="isShowValidate = false"
:isShowValidate="isShowValidate"
/>
<div class="form">
<h2 class="title">{{ $tm('login.loginTitle') }}</h2>
<input

View file

@ -137,7 +137,11 @@ const handleRegister = () => {
<!-- 注册 -->
<div class="register">
<!-- 人机验证 -->
<Capture @validate="handleVerify" :isShowValidate="isShowValidate" />
<Capture
@handleVerify="handleVerify"
@handleClose="isShowValidate = false"
:isShowValidate="isShowValidate"
/>
<!-- 注册表单 -->
<div class="form">