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 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)
|
||||||
|
|||||||
@@ -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('/')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user