Skip to content

Commit 5a0b72c

Browse files
committed
feat(client): implementation main flow
1 parent 2a98fdc commit 5a0b72c

21 files changed

+13929
-7588
lines changed

apps/api/src/main.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { ValidationPipe } from '@nestjs/common';
55

66
async function bootstrap() {
77
const app = await NestFactory.create(AppModule);
8+
app.enableCors({
9+
origin: [/^http:\/\/localhost(:\d+)?$/],
10+
});
811

912
app.useGlobalPipes(new ValidationPipe());
1013
const config = new DocumentBuilder()

apps/client/app.vue

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<template>
2-
<div class="text-3xl font-bold underline text-wrap tex">
3-
Hello world!
2+
<div>
3+
<NuxtPage />
44
</div>
5-
<button class="btn w-64 rounded-full">Button</button>
65
</template>
6+
7+
<script setup lang="ts">
8+
import "vfonts/Lato.css";
9+
</script>

apps/client/assets/css/globals.css

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
html,
6+
body {
7+
height: 100%;
8+
}
9+
10+
@layer base {
11+
:root {
12+
--background: 0 0% 100%;
13+
--foreground: 0 0% 3.9%;
14+
15+
--card: 0 0% 100%;
16+
--card-foreground: 0 0% 3.9%;
17+
18+
--popover: 0 0% 100%;
19+
--popover-foreground: 0 0% 3.9%;
20+
21+
--primary: 0 0% 9%;
22+
--primary-foreground: 0 0% 98%;
23+
24+
--secondary: 0 0% 96.1%;
25+
--secondary-foreground: 0 0% 9%;
26+
27+
--muted: 0 0% 96.1%;
28+
--muted-foreground: 0 0% 45.1%;
29+
30+
--accent: 0 0% 96.1%;
31+
--accent-foreground: 0 0% 9%;
32+
33+
--destructive: 0 84.2% 60.2%;
34+
--destructive-foreground: 0 0% 98%;
35+
36+
--border: 0 0% 89.8%;
37+
--input: 0 0% 89.8%;
38+
--ring: 0 0% 3.9%;
39+
40+
--radius: 0.5rem;
41+
}
42+
43+
.dark {
44+
--background: 0 0% 3.9%;
45+
--foreground: 0 0% 98%;
46+
47+
--card: 0 0% 3.9%;
48+
--card-foreground: 0 0% 98%;
49+
50+
--popover: 0 0% 3.9%;
51+
--popover-foreground: 0 0% 98%;
52+
53+
--primary: 0 0% 98%;
54+
--primary-foreground: 0 0% 9%;
55+
56+
--secondary: 0 0% 14.9%;
57+
--secondary-foreground: 0 0% 98%;
58+
59+
--muted: 0 0% 14.9%;
60+
--muted-foreground: 0 0% 63.9%;
61+
62+
--accent: 0 0% 14.9%;
63+
--accent-foreground: 0 0% 98%;
64+
65+
--destructive: 0 62.8% 30.6%;
66+
--destructive-foreground: 0 0% 98%;
67+
68+
--border: 0 0% 14.9%;
69+
--input: 0 0% 14.9%;
70+
--ring: 0 0% 83.1%;
71+
}
72+
}
73+
74+
/* @layer base {
75+
* {
76+
@apply border-border;
77+
}
78+
79+
body {
80+
@apply bg-background text-foreground;
81+
}
82+
} */
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<template>
2+
<div class="text-center mb-20 mt-10">
3+
<div class="text-5xl mb-3 text-fuchsia-500 dark:text-gray-50">
4+
{{ word }}
5+
<svg
6+
class="w-7 h-7 inline-block ml-1 cursor-pointer"
7+
viewBox="0 0 1024 1024"
8+
version="1.1"
9+
xmlns="http://www.w3.org/2000/svg"
10+
@click="handlePlaySound"
11+
>
12+
<path
13+
d="M342.4 384H128v256h214.4L576 826.8V197.2L342.4 384zM64 320h256L640 64v896L320 704H64V320z m640 256h256v-64H704v64z m16.8 159.5l181 181 45.3-45.3-181-181-45.3 45.3z m33.9-343.9l181-181-45.3-45.3-181 181 45.3 45.3z"
14+
fill="#666666"
15+
></path>
16+
</svg>
17+
</div>
18+
<div class="text-2xl text-slate-600">{{ soundmark }}</div>
19+
<button
20+
class="border-solid border-2 border-slate-400 bg-slate-100 dark:bg-fuchsia-500 rounded-lg mt-8 mb-11 indent-1 h-10 text-2xl pl-10 pr-10 hover:bg-slate-200"
21+
@click="handleToNextStatement"
22+
>
23+
next
24+
</button>
25+
</div>
26+
</template>
27+
28+
<script setup lang="ts">
29+
import { useCoursesStore } from "~/store/courses";
30+
import { useEnglishSound } from "~/composables/useEnglishSound";
31+
32+
const emit = defineEmits(["nextQuestion"]);
33+
34+
const { currentStatement, toNextStatement } = useCoursesStore();
35+
36+
const soundmark = ref("");
37+
const word = ref("");
38+
39+
onMounted(() => {
40+
play();
41+
});
42+
43+
const { play } = useEnglishSound(word);
44+
45+
watchEffect(() => {
46+
soundmark.value = currentStatement?.soundmark!;
47+
word.value = currentStatement?.english!;
48+
});
49+
50+
async function handleToNextStatement() {
51+
toNextStatement();
52+
emit("nextQuestion");
53+
}
54+
55+
function handlePlaySound() {
56+
play();
57+
}
58+
59+
// TODO 后续处理
60+
// useKeyboardShortcut()
61+
function useKeyboardShortcut() {
62+
const handleKeyDown = (event: KeyboardEvent) => {
63+
if (event.key === "Enter") {
64+
console.log("enter");
65+
// handleToNextStatement();
66+
}
67+
};
68+
69+
onMounted(() => {
70+
document.addEventListener("keydown", handleKeyDown);
71+
});
72+
73+
onUnmounted(() => {
74+
document.removeEventListener("keydown", handleKeyDown);
75+
});
76+
}
77+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<div>进度:{{ currentSchedule }}/{{ max }}</div>
3+
</template>
4+
5+
<script setup lang="ts">
6+
import { useCoursesStore } from "~/store/courses";
7+
8+
const coursesStore = useCoursesStore();
9+
10+
const currentSchedule = computed(() => {
11+
return coursesStore.statementIndex + 1;
12+
});
13+
14+
const max = computed(() => {
15+
return coursesStore.currentCourse?.statements.length || 0;
16+
});
17+
18+
</script>
19+
20+
<style scoped></style>

apps/client/components/main/Game.vue

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<template>
2+
<div class="h-full flex justify-center items-center relative">
3+
<div>
4+
<template v-if="mode === 'question'">
5+
<Question @bingo="handleBingo"></Question>
6+
</template>
7+
<template v-else-if="mode === 'answer'">
8+
<Answer @next-question="handleNextQuestion"></Answer>
9+
</template>
10+
</div>
11+
<div class="absolute bottom-10 mb-10 w-full flex flex-col items-center">
12+
<CourseProgress></CourseProgress>
13+
<Tips onShowAnswer="{handleShowAnswer}"></Tips>
14+
</div>
15+
</div>
16+
</template>
17+
18+
<script setup lang="ts">
19+
import Question from "./Question.vue";
20+
import Answer from "./Answer.vue";
21+
import CourseProgress from "./CourseProgress.vue";
22+
import Tips from './Tips.vue';
23+
24+
const mode = ref<"question" | "answer">("question");
25+
26+
function handleBingo() {
27+
mode.value = "answer";
28+
}
29+
30+
function handleNextQuestion() {
31+
mode.value = "question";
32+
}
33+
</script>
34+
35+
<style scoped></style>
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<template>
2+
<header class="py-4 px-4">
3+
<div class="flex justify-between items-center md:max-w-5xl m-auto">
4+
<div class="flex items-center">
5+
<div class="mr-4">
6+
<NuxtImg alt="logo" src="/logo.png" width="48" height="48"></NuxtImg>
7+
</div>
8+
<h1 class="text-2xl font-bold text-fuchsia-500">EarthWorm</h1>
9+
</div>
10+
<div class="flex gap-4 items-center"></div>
11+
<NuxtLink href="/courses">更多课程</NuxtLink>
12+
<!-- <div class="flex gap-4 items-center">
13+
{session.isLogin && (
14+
<div>
15+
{session.username}{" "}
16+
<a class="text-red-400 cursor-pointer" onClick={handleLogout}>
17+
退出
18+
</a>
19+
</div>
20+
)} -->
21+
<!-- {session.isLogin && courseId && courseId !== "1" && (
22+
<Dialog
23+
action={
24+
<div class="text-red-400 cursor-pointer hover:text-red-800">
25+
重置进度
26+
</div>
27+
}
28+
title="重置进度"
29+
description="重置进度后,将会清空所有课程的学习进度,确定要重置吗?"
30+
confirm={handleResetProgress}
31+
/>
32+
)} -->
33+
<!-- <DarkModeBtn /> -->
34+
<!-- </div> -->
35+
</div>
36+
</header>
37+
</template>
38+
39+
<script setup lang="ts"></script>
40+
41+
<style scoped></style>
+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<template>
2+
<div class="text-5xl text-center mb-20 mt-10">
3+
<div class="text-fuchsia-500 dark:text-gray-50">
4+
{{ currentStatement?.chinese }}
5+
</div>
6+
<div class="code-box">
7+
<template v-for="i in lineNum" :key="i">
8+
<div
9+
:class="[
10+
'code-item',
11+
'border-b-2',
12+
'border-b-solid',
13+
'border-b-gray-300 dark:border-b-gray-500',
14+
i - 1 === activeInputIndex && focusing ? 'active' : '',
15+
'dark:text-indigo-500 text-[rgba(32,32,32,0.6)]',
16+
]"
17+
>
18+
{{ words[i - 1] }}
19+
</div>
20+
</template>
21+
<input
22+
class="code-input"
23+
type="text"
24+
v-model="inputValue"
25+
@keydown="handleKeyDown"
26+
@focus="handleInputFocus"
27+
@blur="handleBlur"
28+
autoFocus
29+
/>
30+
</div>
31+
</div>
32+
</template>
33+
34+
<script setup lang="ts">
35+
import { useCoursesStore } from "~/store/courses";
36+
37+
const emit = defineEmits(["bingo"]);
38+
39+
const { currentStatement, checkCorrect } = useCoursesStore();
40+
41+
const lineNum = ref(1);
42+
const focusing = ref(true);
43+
const inputValue = ref("");
44+
const words = ref<string[]>([]);
45+
46+
const activeInputIndex = computed(() => {
47+
return Math.min(words.value.length - 1, lineNum.value - 1);
48+
});
49+
50+
watchEffect(() => {
51+
lineNum.value = currentStatement?.english.split(" ").length || 1;
52+
});
53+
54+
watchEffect(() => {
55+
words.value = inputValue.value.trimStart().split(" ");
56+
});
57+
58+
function handleKeyDown(e: KeyboardEvent) {
59+
if (e.code === "Enter") {
60+
if (checkCorrect(inputValue.value.trim())) {
61+
emit("bingo");
62+
}
63+
inputValue.value = "";
64+
}
65+
}
66+
67+
function handleInputFocus() {
68+
focusing.value = true;
69+
}
70+
71+
function handleBlur() {
72+
focusing.value = false;
73+
}
74+
</script>
75+
76+
<style scoped>
77+
.code-box {
78+
height: 10vh;
79+
display: flex;
80+
flex-wrap: wrap;
81+
justify-content: center;
82+
max-width: 80vw;
83+
position: relative;
84+
margin-top: 8px;
85+
}
86+
87+
.code-box .code-item {
88+
min-width: 10vw;
89+
min-height: 6vh;
90+
text-align: center;
91+
font-size: 4vw;
92+
transition: border 0.3s;
93+
box-sizing: border-box;
94+
margin-right: 8px;
95+
display: flex;
96+
justify-content: center;
97+
align-items: flex-end;
98+
}
99+
100+
.code-box .code-input {
101+
position: absolute;
102+
width: 100%;
103+
104+
height: 100%;
105+
opacity: 0;
106+
}
107+
108+
.active {
109+
border-bottom: 3px solid #1e80ff !important;
110+
}
111+
</style>

0 commit comments

Comments
 (0)