Files
teaching-design/src/components/BookListPage.vue
2026-06-16 00:30:19 -06:00

172 lines
5.2 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 { onMounted, ref } from 'vue'
import * as booksApi from '../services/booksApi'
import type { BookSummary } from '../services/booksApi'
import { useAuth } from '../composables/useAuth'
type LoadStatus = 'loading' | 'loaded' | 'error'
const emit = defineEmits<{ open: [id: string]; admin: [] }>()
const { user, logout } = useAuth()
const books = ref<BookSummary[]>([])
const loadStatus = ref<LoadStatus>('loading')
const loadError = ref<string | null>(null)
const newBookName = ref('')
const actionError = ref<string | null>(null)
const renamingId = ref<string | null>(null)
const renameValue = ref('')
const cstDateTimeFormatter = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
function formatCstUpdatedAt(value: string): string {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value
const parts = Object.fromEntries(
cstDateTimeFormatter
.formatToParts(date)
.filter((part) => part.type !== 'literal')
.map((part) => [part.type, part.value]),
)
return `${parts.year}/${parts.month}/${parts.day}`
}
async function loadBooks(): Promise<void> {
loadStatus.value = 'loading'
try {
books.value = await booksApi.listBooks()
loadStatus.value = 'loaded'
} catch (error) {
loadStatus.value = 'error'
loadError.value = error instanceof Error ? error.message : '加载失败。'
}
}
onMounted(loadBooks)
async function createBook(): Promise<void> {
const name = newBookName.value.trim()
if (!name) return
try {
const created = await booksApi.createBook(name)
newBookName.value = ''
emit('open', created.id)
} catch (error) {
actionError.value = error instanceof Error ? error.message : '创建失败。'
}
}
function startRename(book: BookSummary): void {
renamingId.value = book.id
renameValue.value = book.name
}
function cancelRename(): void {
renamingId.value = null
}
async function confirmRename(): Promise<void> {
const id = renamingId.value
const name = renameValue.value.trim()
if (!id || !name) return
try {
const updated = await booksApi.renameBook(id, name)
const target = books.value.find((book) => book.id === id)
if (target) target.name = updated.name
renamingId.value = null
} catch (error) {
actionError.value = error instanceof Error ? error.message : '重命名失败。'
}
}
async function removeBook(book: BookSummary): Promise<void> {
if (!window.confirm(`确定要删除「${book.name}」吗?此操作无法撤销。`)) return
try {
await booksApi.deleteBook(book.id)
books.value = books.value.filter((entry) => entry.id !== book.id)
} catch (error) {
actionError.value = error instanceof Error ? error.message : '删除失败。'
}
}
</script>
<template>
<div class="book-list-page">
<div class="page-header">
<h1>教学设计</h1>
<div class="header-actions">
<button v-if="user?.role === 'admin'" type="button" @click="emit('admin')">用户管理</button>
<button type="button" @click="logout">退出登录</button>
</div>
</div>
<form class="book-list-create" @submit.prevent="createBook">
<input v-model="newBookName" type="text" placeholder="新整本名称" aria-label="新整本名称" />
<button type="submit">新建整本</button>
</form>
<p v-if="actionError" class="app-notice app-notice--error" role="alert">
{{ actionError }}
<button type="button" @click="actionError = null">关闭</button>
</p>
<p v-if="loadStatus === 'loading'">加载中</p>
<div v-else-if="loadStatus === 'error'" class="app-notice app-notice--error" role="alert">
<span>{{ loadError }}</span>
<button type="button" data-testid="retry" @click="loadBooks">重试</button>
</div>
<template v-else>
<p v-if="books.length === 0">还没有整本创建一个开始使用</p>
<ul v-else class="book-list">
<li v-for="book in books" :key="book.id" class="book-list-item">
<template v-if="renamingId === book.id">
<input v-model="renameValue" type="text" aria-label="整本名称" />
<button type="button" :data-testid="`confirm-rename-${book.id}`" @click="confirmRename">保存</button>
<button type="button" @click="cancelRename">取消</button>
</template>
<template v-else>
<span class="book-list-name">{{ book.name }}</span>
<span class="book-list-meta">更新于 {{ formatCstUpdatedAt(book.updatedAt) }} · {{ book.lessonCount }} </span>
<button type="button" :data-testid="`open-${book.id}`" @click="emit('open', book.id)">打开</button>
<button type="button" :data-testid="`rename-${book.id}`" @click="startRename(book)">重命名</button>
<button type="button" :data-testid="`delete-${book.id}`" @click="removeBook(book)">删除</button>
</template>
</li>
</ul>
</template>
</div>
</template>
<style scoped>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.page-header h1 {
margin: 0;
}
.header-actions {
display: flex;
gap: 0.5rem;
}
</style>