Files
teaching-design/src/components/BookListPage.vue
2026-06-16 11:16:34 -06:00

180 lines
5.6 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 app-page">
<div class="app-page-header">
<h1>教学设计生成器<span class="page-subtitle">真亦假时假亦真</span></h1>
<div class="app-page-actions">
<button v-if="user?.role === 'admin'" class="ui-button" type="button" @click="emit('admin')">
用户管理
</button>
<button class="ui-button" type="button" @click="logout">退出登录</button>
</div>
</div>
<form class="book-list-create" @submit.prevent="createBook">
<input
v-model="newBookName"
class="ui-field"
type="text"
placeholder="新整本名称"
aria-label="新整本名称"
/>
<button class="ui-button ui-button--primary" 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" class="ui-field" type="text" aria-label="整本名称" />
<button
class="ui-button"
type="button"
:data-testid="`confirm-rename-${book.id}`"
@click="confirmRename"
>
保存
</button>
<button class="ui-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 }} <template v-if="book.createdBy"> · 创建者{{ book.createdBy }}</template></span>
<button class="ui-button" type="button" :data-testid="`open-${book.id}`" @click="emit('open', book.id)">
打开
</button>
<button class="ui-button" type="button" :data-testid="`rename-${book.id}`" @click="startRename(book)">
重命名
</button>
<button
class="ui-button ui-button--danger"
type="button"
:data-testid="`delete-${book.id}`"
@click="removeBook(book)"
>
删除
</button>
</template>
</li>
</ul>
</template>
</div>
</template>