Files
lexgo/learner/src/views/LibraryView.vue
T
ila 3ebec5b11b feat: 词汇库搜索、筛选与编辑 (#12)
- GET /api/v1/terms 列表:查询键白名单、page/limit 边界(默认 20、上限 100)、
  排序 updated_at DESC;搜索同时匹配身份键、显示原文与个人释义,转小写后比较(term 为
  二进制排序规则),%/_/\ 按字面值转义;status 与 kind 可组合
- PATCH /api/v1/terms/:id 编辑:复用释义/例句与状态/等级校验,身份不可编辑;
  只有状态或等级变化才重排复习时间,历史作答记录与计数保留
- 学习端新增 /vocab 路由与导航、stores/vocabulary.ts、VocabView.vue(搜索、筛选、
  分页、空态与无结果清除、编辑对话框含 1~7 等级选择器、筛选与页码写入 URL)
- 释义/例句/状态/等级抽成 TermFormFields.vue,与阅读器面板共用,避免规则漂移
- Wiki 记录 Architecture、Business-Rules、Local-Development 与需求更新
2026-09-14 23:46:42 +08:00

75 lines
3.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { ElButton } from 'element-plus'
import BookMark from '../components/BookMark.vue'
import { statusSummary, useLibraryStore } from '../stores/library'
import { useSessionStore } from '../stores/session'
const session = useSessionStore()
const library = useLibraryStore()
const router = useRouter()
const route = useRoute()
const loading = ref(true)
const error = ref('')
// A deletion returns here with the count it removed, so the learner sees what happened.
const deletedNotice = computed(() => {
const raw = Array.isArray(route.query.deleted) ? route.query.deleted[0] : route.query.deleted
if (typeof raw !== 'string' || !/^\d+$/.test(raw)) return ''
return '书籍已删除 · 已保存的生词和短语仍保留在生词本。'
})
async function load() {
loading.value = true
error.value = ''
try { await session.loadSpace() }
catch (reason) { error.value = reason instanceof Error ? reason.message : '暂时无法加载,请重试。' }
// The book list keeps its own error so one failing call still shows a retry.
if (!error.value) await library.loadBooks()
loading.value = false
}
async function logout() {
try { await session.logout() }
catch { session.notice = '已退出此设备。服务器暂时无法连接,请稍后重试。' }
finally { await router.replace('/login') }
}
onMounted(load)
</script>
<template>
<div v-if="session.user">
<header class="site-header">
<RouterLink to="/" class="brand">LexGo<span class="brand-dot">.</span></RouterLink>
<nav aria-label="学习导航"><RouterLink to="/" class="active-nav">我的书库</RouterLink> · <RouterLink to="/vocab">生词本</RouterLink> · <RouterLink to="/review">到期复习</RouterLink></nav>
<div class="account">
<span class="account-name">{{ session.user.username }}</span>
<ElButton text @click="logout">退出登录</ElButton>
</div>
</header>
<main class="library">
<div class="library-title">
<div><h1>我的书库</h1><p class="subtle">你的阅读与学习,从这里开始。</p></div>
<div class="library-actions">
<span class="language">英语</span>
<ElButton type="primary" @click="router.push('/import')">导入内容</ElButton>
</div>
</div>
<p v-if="deletedNotice" role="status" class="chapter-notice" data-testid="library-notice">{{ deletedNotice }}</p>
<p v-if="loading" role="status" class="loading">正在加载…</p>
<div v-else-if="error || library.booksError" class="notice">
<p role="alert">{{ error || library.booksError }}</p>
<ElButton @click="load">重试</ElButton>
</div>
<ul v-else-if="library.books.length" class="book-grid" aria-label="书籍列表">
<li v-for="item in library.books" :key="item.id" class="book-card">
<RouterLink :to="`/books/${item.id}`" class="book-title">{{ item.title }}</RouterLink>
<p class="subtle">{{ item.chapterCount }} 个章节</p>
<p v-if="statusSummary(item)" class="status-summary">{{ statusSummary(item) }}</p>
<p v-else class="subtle">尚未导入章节</p>
</li>
</ul>
<section v-else class="empty-library" aria-label="书库内容">
<BookMark /><h2>书库还是空的</h2><p class="subtle">粘贴一段英文,开始你的第一篇阅读。</p>
</section>
</main>
</div>
</template>