feat: remove cover page

This commit is contained in:
2026-06-16 07:14:39 -06:00
parent 085f70bd64
commit 7b95324649
15 changed files with 261 additions and 171 deletions

View File

@@ -10,15 +10,22 @@ function mockGetBook(data: TeachingBook, id = 'b1'): void {
vi.mocked(booksApi.getBook).mockResolvedValue({ id, name: '示例整本', updatedAt: data.updatedAt, data })
}
function createBookWithDesign(filename = '1.md'): { data: TeachingBook; design: ReturnType<typeof createEmptyTeachingDesign> } {
const data = createEmptyBook()
const design = createEmptyTeachingDesign(filename)
data.designs.push(design)
data.selectedId = design.id
return { data, design }
}
describe('useTeachingBook', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
})
it('loads the book from the API', async () => {
const data = createEmptyBook()
data.cover.courseName = 'Web 前端开发'
it('loads the book from the API without cover state', async () => {
const { data } = createBookWithDesign()
mockGetBook(data)
const store = useTeachingBook('b1')
@@ -26,7 +33,8 @@ describe('useTeachingBook', () => {
expect(booksApi.getBook).toHaveBeenCalledWith('b1')
expect(store.loadStatus.value).toBe('loaded')
expect(store.book.value.cover.courseName).toBe('Web 前端开发')
expect(store.book.value).not.toHaveProperty('cover')
expect(store.book.value.selectedId).toBe(data.selectedId)
})
it('sets loadStatus to error when loading fails', async () => {
@@ -82,13 +90,16 @@ describe('useTeachingBook', () => {
})
it('autosaves the book via the API after the debounce delay', async () => {
mockGetBook(createEmptyBook())
const { data, design } = createBookWithDesign()
mockGetBook(data)
vi.mocked(booksApi.updateBook).mockResolvedValue({ id: 'b1', name: '示例整本', updatedAt: 'later' })
const store = useTeachingBook('b1')
await flushPromises()
store.updateCover({ courseName: '新课程名' })
store.updateDesign(design.id, (current) => {
current.title = '新课程名'
})
await vi.advanceTimersByTimeAsync(300)
expect(booksApi.updateBook).toHaveBeenCalledWith('b1', store.book.value)
@@ -96,13 +107,16 @@ describe('useTeachingBook', () => {
})
it('sets saveStatus to error when autosave fails', async () => {
mockGetBook(createEmptyBook())
const { data, design } = createBookWithDesign()
mockGetBook(data)
vi.mocked(booksApi.updateBook).mockRejectedValue(new Error('保存失败。'))
const store = useTeachingBook('b1')
await flushPromises()
store.updateCover({ courseName: '新课程名' })
store.updateDesign(design.id, (current) => {
current.title = '新课程名'
})
await vi.advanceTimersByTimeAsync(300)
expect(store.saveStatus.value).toBe('error')
@@ -139,10 +153,8 @@ describe('useTeachingBook', () => {
expect(store.book.value.designs).toHaveLength(0)
})
it('clearBook empties designs but keeps the cover', async () => {
const data = createEmptyBook()
data.cover.courseName = 'Web 前端开发'
data.designs.push(createEmptyTeachingDesign('1.md'))
it('clearBook empties designs and clears selection', async () => {
const { data } = createBookWithDesign()
mockGetBook(data)
const store = useTeachingBook('b1')
@@ -151,7 +163,22 @@ describe('useTeachingBook', () => {
store.clearBook()
expect(store.book.value.designs).toEqual([])
expect(store.book.value.cover.courseName).toBe('Web 前端开发')
expect(store.book.value.selectedId).toBe('cover')
expect(store.book.value).not.toHaveProperty('cover')
expect(store.book.value.selectedId).toBeNull()
})
it('selects null after removing the last selected lesson', async () => {
const { data, design } = createBookWithDesign()
mockGetBook(data)
const store = useTeachingBook('b1')
await flushPromises()
store.removeDesign(design.id)
await flushPromises()
expect(store.book.value.designs).toEqual([])
expect(store.book.value.selectedId).toBeNull()
expect(store.selectedDesign.value).toBeNull()
})
})

View File

@@ -1,7 +1,6 @@
import { nextTick, ref, watch, type Ref } from 'vue'
import {
createEmptyBook,
type BookCover,
type DesignId,
type TeachingBook,
type TeachingDesign,
@@ -38,10 +37,9 @@ export interface TeachingBookStore {
warningCount: Ref<number>
importFiles: (files: readonly File[], strategy: DuplicateStrategy) => Promise<ImportResult>
detectDuplicates: (files: readonly File[]) => string[]
selectPage: (id: 'cover' | DesignId) => void
selectPage: (id: DesignId) => void
moveDesign: (from: number, to: number) => void
removeDesign: (id: DesignId) => void
updateCover: (patch: Partial<BookCover>) => void
updateDesign: (id: DesignId, updater: (design: TeachingDesign) => void) => void
clearBook: () => void
generateLesson: (topic: string) => Promise<GenerateLessonResult>
@@ -67,7 +65,7 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
const current = book.value
hasDesigns.value = current.designs.length > 0
selectedDesign.value =
current.selectedId === 'cover'
current.selectedId === null
? null
: current.designs.find((design) => design.id === current.selectedId) ?? null
warningCount.value = current.designs.reduce(
@@ -176,7 +174,7 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
}
}
if (imported > 0 && book.value.selectedId === 'cover' && book.value.designs.length > 0) {
if (imported > 0 && book.value.selectedId === null && book.value.designs.length > 0) {
book.value.selectedId = book.value.designs[0]!.id
}
@@ -187,7 +185,7 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
return { imported, failed, duplicates }
}
function selectPage(id: 'cover' | DesignId): void {
function selectPage(id: DesignId): void {
book.value.selectedId = id
}
@@ -210,17 +208,12 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
designs.splice(index, 1)
if (book.value.selectedId === id) {
book.value.selectedId = designs[index]?.id ?? designs[index - 1]?.id ?? 'cover'
book.value.selectedId = designs[index]?.id ?? designs[index - 1]?.id ?? null
}
touch()
}
function updateCover(patch: Partial<BookCover>): void {
Object.assign(book.value.cover, patch)
touch()
}
function updateDesign(id: DesignId, updater: (design: TeachingDesign) => void): void {
const design = book.value.designs.find((candidate) => candidate.id === id)
if (!design) {
@@ -232,7 +225,7 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
function clearBook(): void {
book.value.designs = []
book.value.selectedId = 'cover'
book.value.selectedId = null
touch()
}
@@ -286,7 +279,6 @@ export function useTeachingBook(bookId: string): TeachingBookStore {
selectPage,
moveDesign,
removeDesign,
updateCover,
updateDesign,
clearBook,
generateLesson,