feat(web): add study session ui and auth user/logout handling
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
const TOKEN_KEY = 'memora_token'
|
||||
const USER_KEY = 'memora_user_id'
|
||||
|
||||
export const getToken = () => localStorage.getItem(TOKEN_KEY)
|
||||
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 setUserId = (userId: number) => localStorage.setItem(USER_KEY, String(userId))
|
||||
export const getUserId = () => Number(localStorage.getItem(USER_KEY) || 0)
|
||||
|
||||
@@ -21,7 +21,7 @@ import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { http } from '../services/http'
|
||||
import { setToken } from '../modules/auth/auth'
|
||||
import { setToken, setUserId } from '../modules/auth/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
@@ -38,6 +38,8 @@ const onSubmit = async () => {
|
||||
const token = loginRes.data?.data?.token
|
||||
if (token) {
|
||||
setToken(token)
|
||||
const me = await http.get('/auth/me')
|
||||
setUserId(me.data?.data?.user_id || 0)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
return
|
||||
@@ -52,6 +54,8 @@ const onSubmit = async () => {
|
||||
const token = loginRes.data?.data?.token
|
||||
if (token) {
|
||||
setToken(token)
|
||||
const me = await http.get('/auth/me')
|
||||
setUserId(me.data?.data?.user_id || 0)
|
||||
ElMessage.success('注册并登录成功')
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
<template>
|
||||
<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>
|
||||
<template #header>
|
||||
<div class="h">
|
||||
@@ -46,15 +73,21 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 loading = ref(false)
|
||||
const error = ref('')
|
||||
import type { Word } from '../services/api'
|
||||
|
||||
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() {
|
||||
const w = word.value.trim()
|
||||
if (!w) return
|
||||
@@ -63,7 +96,6 @@ async function submit() {
|
||||
saved.value = null
|
||||
try {
|
||||
const res = await addWord(w)
|
||||
// 后端建议返回 { data: Word }
|
||||
saved.value = res.data ?? res
|
||||
word.value = ''
|
||||
} 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() {
|
||||
if (!saved.value?.word) return
|
||||
const audio = new Audio(audioUrl(saved.value.word, 'uk'))
|
||||
|
||||
@@ -2,12 +2,27 @@
|
||||
<el-card>
|
||||
<template #header>设置</template>
|
||||
<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="音频缓存目录">memora-api/audio</el-descriptions-item>
|
||||
<el-descriptions-item label="当前前端框架">Vue3 + Element Plus</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div style="margin-top:16px">
|
||||
<el-button type="danger" plain @click="logout">退出登录</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user