feat(web): add study session ui and auth user/logout handling

This commit is contained in:
2026-02-27 11:31:33 +08:00
parent 9a5e8cb58f
commit 906c5c37ba
4 changed files with 102 additions and 6 deletions

View File

@@ -1,6 +1,13 @@
const TOKEN_KEY = 'memora_token' const TOKEN_KEY = 'memora_token'
const USER_KEY = 'memora_user_id'
export const getToken = () => localStorage.getItem(TOKEN_KEY) export const getToken = () => localStorage.getItem(TOKEN_KEY)
export const setToken = (token: string) => localStorage.setItem(TOKEN_KEY, token) export const setToken = (token: string) => localStorage.setItem(TOKEN_KEY, token)
export const clearToken = () => localStorage.removeItem(TOKEN_KEY) export const clearToken = () => {
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(USER_KEY)
}
export const isAuthed = () => !!getToken() export const isAuthed = () => !!getToken()
export const setUserId = (userId: number) => localStorage.setItem(USER_KEY, String(userId))
export const getUserId = () => Number(localStorage.getItem(USER_KEY) || 0)

View File

@@ -21,7 +21,7 @@ import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { http } from '../services/http' import { http } from '../services/http'
import { setToken } from '../modules/auth/auth' import { setToken, setUserId } from '../modules/auth/auth'
const router = useRouter() const router = useRouter()
const loading = ref(false) const loading = ref(false)
@@ -38,6 +38,8 @@ const onSubmit = async () => {
const token = loginRes.data?.data?.token const token = loginRes.data?.data?.token
if (token) { if (token) {
setToken(token) setToken(token)
const me = await http.get('/auth/me')
setUserId(me.data?.data?.user_id || 0)
ElMessage.success('登录成功') ElMessage.success('登录成功')
router.push('/') router.push('/')
return return
@@ -52,6 +54,8 @@ const onSubmit = async () => {
const token = loginRes.data?.data?.token const token = loginRes.data?.data?.token
if (token) { if (token) {
setToken(token) setToken(token)
const me = await http.get('/auth/me')
setUserId(me.data?.data?.user_id || 0)
ElMessage.success('注册并登录成功') ElMessage.success('注册并登录成功')
router.push('/') router.push('/')
} }

View File

@@ -1,5 +1,32 @@
<template> <template>
<div class="wrap"> <div class="wrap">
<el-card style="margin-bottom: 16px">
<template #header>
<div class="h">
<div>
<div class="title">学习会话</div>
<div class="sub">系统按最新单词生成学习队列完成后自动计入复习数据</div>
</div>
<el-button type="primary" @click="startSession" :loading="sessionLoading">开始学习</el-button>
</div>
</template>
<el-empty v-if="!sessionWord" description="点击“开始学习”获取单词" />
<div v-else>
<el-alert type="info" :title="`当前单词:${sessionWord.word}`" show-icon style="margin-bottom:12px" />
<el-input v-model="sessionAnswer" placeholder="输入答案(默认拼写)" @keyup.enter="submitSessionAnswer" />
<div style="margin-top: 12px; display:flex; gap:8px">
<el-button type="success" :loading="sessionLoading" @click="submitSessionAnswer">提交</el-button>
<el-button @click="nextSessionWord" :disabled="sessionLoading">下一个</el-button>
</div>
<el-result v-if="sessionResult" :icon="sessionResult.correct ? 'success' : 'error'" :title="sessionResult.correct ? '正确' : '不对'">
<template #sub-title>
<div>正确答案{{ sessionResult.correct_ans || sessionWord.word }}</div>
</template>
</el-result>
</div>
</el-card>
<el-card> <el-card>
<template #header> <template #header>
<div class="h"> <div class="h">
@@ -46,15 +73,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { addWord, audioUrl } from '../services/api' import { addWord, audioUrl, createStudySession, submitStudyAnswer } from '../services/api'
import type { ReviewResult, Word } from '../services/api'
const word = ref('') const word = ref('')
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
import type { Word } from '../services/api'
const saved = ref<Word | null>(null) const saved = ref<Word | null>(null)
const sessionLoading = ref(false)
const sessionWords = ref<Word[]>([])
const sessionIndex = ref(0)
const sessionWord = ref<Word | null>(null)
const sessionAnswer = ref('')
const sessionResult = ref<ReviewResult | null>(null)
async function submit() { async function submit() {
const w = word.value.trim() const w = word.value.trim()
if (!w) return if (!w) return
@@ -63,7 +96,6 @@ async function submit() {
saved.value = null saved.value = null
try { try {
const res = await addWord(w) const res = await addWord(w)
// 后端建议返回 { data: Word }
saved.value = res.data ?? res saved.value = res.data ?? res
word.value = '' word.value = ''
} catch (e: any) { } catch (e: any) {
@@ -73,6 +105,44 @@ async function submit() {
} }
} }
async function startSession() {
sessionLoading.value = true
try {
const res = await createStudySession(10)
const arr = (res as any).data ?? (res as any)
sessionWords.value = Array.isArray(arr) ? arr : []
sessionIndex.value = 0
sessionWord.value = sessionWords.value[0] || null
sessionAnswer.value = ''
sessionResult.value = null
} finally {
sessionLoading.value = false
}
}
function nextSessionWord() {
if (!sessionWords.value.length) return
sessionIndex.value += 1
if (sessionIndex.value >= sessionWords.value.length) {
sessionWord.value = null
return
}
sessionWord.value = sessionWords.value[sessionIndex.value]
sessionAnswer.value = ''
sessionResult.value = null
}
async function submitSessionAnswer() {
if (!sessionWord.value || !sessionAnswer.value.trim()) return
sessionLoading.value = true
try {
const res = await submitStudyAnswer({ wordId: sessionWord.value.id, answer: sessionAnswer.value, mode: 'spelling' })
sessionResult.value = (res as any).data ?? (res as any)
} finally {
sessionLoading.value = false
}
}
function playUK() { function playUK() {
if (!saved.value?.word) return if (!saved.value?.word) return
const audio = new Audio(audioUrl(saved.value.word, 'uk')) const audio = new Audio(audioUrl(saved.value.word, 'uk'))

View File

@@ -2,12 +2,27 @@
<el-card> <el-card>
<template #header>设置</template> <template #header>设置</template>
<el-descriptions :column="1" border> <el-descriptions :column="1" border>
<el-descriptions-item label="当前用户ID">{{ userId || '未登录' }}</el-descriptions-item>
<el-descriptions-item label="有道 API">已在后端 config.yaml 配置</el-descriptions-item> <el-descriptions-item label="有道 API">已在后端 config.yaml 配置</el-descriptions-item>
<el-descriptions-item label="音频缓存目录">memora-api/audio</el-descriptions-item> <el-descriptions-item label="音频缓存目录">memora-api/audio</el-descriptions-item>
<el-descriptions-item label="当前前端框架">Vue3 + Element Plus</el-descriptions-item> <el-descriptions-item label="当前前端框架">Vue3 + Element Plus</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div style="margin-top:16px">
<el-button type="danger" plain @click="logout">退出登录</el-button>
</div>
</el-card> </el-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router'
import { clearToken, getUserId } from '../modules/auth/auth'
const router = useRouter()
const userId = getUserId()
function logout() {
clearToken()
router.push('/login')
}
</script> </script>