From 7b95324649786e42daea95d8bd7a981f918035da Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 16 Jun 2026 07:14:39 -0600 Subject: [PATCH] feat: remove cover page --- server/db.test.ts | 85 ++++++++++++++++++++++++- server/db.ts | 66 ++++++++++++++++++- server/routes/books.test.ts | 12 ++-- src/components/A4Workspace.test.ts | 27 ++++++++ src/components/A4Workspace.vue | 16 +---- src/components/CoverPage.vue | 40 ------------ src/components/LessonSidebar.test.ts | 12 +++- src/components/LessonSidebar.vue | 13 +--- src/components/WorkspaceView.test.ts | 13 ++++ src/components/WorkspaceView.vue | 4 -- src/composables/useTeachingBook.test.ts | 55 ++++++++++++---- src/composables/useTeachingBook.ts | 20 ++---- src/domain/teachingDesign.test.ts | 12 ++-- src/domain/teachingDesign.ts | 11 +--- src/style.css | 46 ------------- 15 files changed, 261 insertions(+), 171 deletions(-) create mode 100644 src/components/A4Workspace.test.ts delete mode 100644 src/components/CoverPage.vue diff --git a/server/db.test.ts b/server/db.test.ts index af7a381..eef7fed 100644 --- a/server/db.test.ts +++ b/server/db.test.ts @@ -1,3 +1,6 @@ +import { existsSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { afterEach, describe, expect, it, setSystemTime } from 'bun:test' import { createEmptyBook, createEmptyTeachingDesign } from '../src/domain/teachingDesign' import { @@ -10,6 +13,12 @@ afterEach(() => { setSystemTime() }) +function tempDbPath(name: string): string { + const path = join(tmpdir(), `fake-teaching-design-${name}-${crypto.randomUUID()}.db`) + if (existsSync(path)) rmSync(path) + return path +} + describe('db', () => { it('creates a book with empty data', () => { const db = openDb(':memory:') @@ -55,13 +64,85 @@ describe('db', () => { const db = openDb(':memory:') const created = createBook(db, '示例整本') const data = createEmptyBook() - data.cover.courseName = 'Web 前端开发' + data.designs.push(createEmptyTeachingDesign('1.md')) setSystemTime(new Date('2026-02-01T00:00:00.000Z')) const result = saveBookData(db, created.id, data) expect(result).toEqual({ id: created.id, name: '示例整本', updatedAt: '2026-02-01T00:00:00.000Z' }) - expect(getBook(db, created.id)?.data.cover.courseName).toBe('Web 前端开发') + expect(getBook(db, created.id)?.data).not.toHaveProperty('cover') + }) + + it('migrates legacy cover data and cover selection on open', () => { + const path = tempDbPath('cover-migration') + const db = openDb(path) + const design = createEmptyTeachingDesign('1.md') + const legacy = { + schemaVersion: 1, + cover: { courseName: '旧课程', teacherName: '旧教师' }, + designs: [design], + selectedId: 'cover', + updatedAt: '2026-01-01T00:00:00.000Z', + } + db.run('INSERT INTO books (id, name, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [ + 'legacy-1', + '旧整本', + JSON.stringify(legacy), + '2026-01-01T00:00:00.000Z', + '2026-01-01T00:00:00.000Z', + ]) + db.close() + + const reopened = openDb(path) + const migrated = getBook(reopened, 'legacy-1')!.data + const raw = reopened.query<{ data: string }, [string]>('SELECT data FROM books WHERE id = ?').get('legacy-1')!.data + reopened.close() + rmSync(path) + + expect(migrated).not.toHaveProperty('cover') + expect(migrated.selectedId).toBe(design.id) + expect(JSON.parse(raw)).not.toHaveProperty('cover') + }) + + it('migrates legacy cover selection to null when no lessons exist', () => { + const path = tempDbPath('empty-cover-migration') + const db = openDb(path) + db.run('INSERT INTO books (id, name, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [ + 'legacy-empty', + '空整本', + JSON.stringify({ + schemaVersion: 1, + cover: { courseName: '旧课程', teacherName: '旧教师' }, + designs: [], + selectedId: 'cover', + updatedAt: '2026-01-01T00:00:00.000Z', + }), + '2026-01-01T00:00:00.000Z', + '2026-01-01T00:00:00.000Z', + ]) + db.close() + + const reopened = openDb(path) + const migrated = getBook(reopened, 'legacy-empty')!.data + reopened.close() + rmSync(path) + + expect(migrated).not.toHaveProperty('cover') + expect(migrated.selectedId).toBeNull() + }) + + it('normalizes invalid selected ids to the first lesson', () => { + const db = openDb(':memory:') + const created = createBook(db, '示例整本') + const data = createEmptyBook() + const design = createEmptyTeachingDesign('1.md') + data.designs.push(design) + db.run('UPDATE books SET data = ? WHERE id = ?', [ + JSON.stringify({ ...data, selectedId: 'missing-id' }), + created.id, + ]) + + expect(getBook(db, created.id)?.data.selectedId).toBe(design.id) }) it('returns null when saving data for a missing book', () => { diff --git a/server/db.ts b/server/db.ts index 60c75d1..eb302b0 100644 --- a/server/db.ts +++ b/server/db.ts @@ -28,6 +28,16 @@ interface BookRow { updated_at: string } +type StoredTeachingBook = Omit & { + cover?: unknown + selectedId?: string | null +} + +interface NormalizedBookData { + data: TeachingBook + changed: boolean +} + export interface UserRecord { id: string username: string @@ -77,10 +87,59 @@ const SCHEMA = ` ) ` +function normalizeBookData(raw: StoredTeachingBook): NormalizedBookData { + const data = { ...raw, designs: Array.isArray(raw.designs) ? raw.designs : [] } + let changed = false + + if ('cover' in data) { + delete data.cover + changed = true + } + + const selectedId = data.selectedId ?? null + const firstDesignId = data.designs[0]?.id ?? null + const selectedExists = + selectedId !== null && data.designs.some((design) => design.id === selectedId) + + let normalizedSelectedId: TeachingBook['selectedId'] + if (selectedId === 'cover' || (selectedId !== null && !selectedExists)) { + normalizedSelectedId = firstDesignId + changed = selectedId !== normalizedSelectedId || changed + } else { + normalizedSelectedId = selectedId as TeachingBook['selectedId'] + } + + return { + data: { + schemaVersion: data.schemaVersion, + designs: data.designs, + selectedId: normalizedSelectedId, + updatedAt: data.updatedAt, + }, + changed, + } +} + +function parseBookData(data: string): TeachingBook { + return normalizeBookData(JSON.parse(data) as StoredTeachingBook).data +} + +function migrateStoredBooks(db: Database): void { + const rows = db.query<{ id: string; data: string }, []>('SELECT id, data FROM books').all() + + for (const row of rows) { + const normalized = normalizeBookData(JSON.parse(row.data) as StoredTeachingBook) + if (normalized.changed) { + db.run('UPDATE books SET data = ? WHERE id = ?', [JSON.stringify(normalized.data), row.id]) + } + } +} + export function openDb(path: string): Database { const db = new Database(path) db.run('PRAGMA foreign_keys = ON') db.run(SCHEMA) + migrateStoredBooks(db) return db } @@ -93,7 +152,7 @@ export function listBooks(db: Database): BookSummary[] { id: row.id, name: row.name, updatedAt: row.updated_at, - lessonCount: (JSON.parse(row.data) as TeachingBook).designs.length, + lessonCount: parseBookData(row.data).designs.length, })) } @@ -124,7 +183,7 @@ export function getBook(db: Database, id: string): BookRecord | null { id: row.id, name: row.name, updatedAt: row.updated_at, - data: JSON.parse(row.data) as TeachingBook, + data: parseBookData(row.data), } } @@ -135,7 +194,8 @@ export function saveBookData(db: Database, id: string, data: TeachingBook): Book if (!existing) return null const now = new Date().toISOString() - db.run('UPDATE books SET data = ?, updated_at = ? WHERE id = ?', [JSON.stringify(data), now, id]) + const normalized = normalizeBookData(data as unknown as StoredTeachingBook).data + db.run('UPDATE books SET data = ?, updated_at = ? WHERE id = ?', [JSON.stringify(normalized), now, id]) return { id, name: existing.name, updatedAt: now } } diff --git a/server/routes/books.test.ts b/server/routes/books.test.ts index 69bfcb3..f340108 100644 --- a/server/routes/books.test.ts +++ b/server/routes/books.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it } from 'bun:test' import type { Database } from 'bun:sqlite' import { Hono } from 'hono' -import { createEmptyBook } from '../../src/domain/teachingDesign' +import { createEmptyBook, createEmptyTeachingDesign } from '../../src/domain/teachingDesign' import { openDb } from '../db' import { createBooksRouter } from './books' @@ -64,22 +64,22 @@ describe('books routes', () => { expect(res.status).toBe(404) }) - it('saves book data', async () => { + it('saves book data without cover state', async () => { const created = await createViaApi('示例整本') const data = createEmptyBook() - data.cover.courseName = 'Web 前端开发' + data.designs.push(createEmptyTeachingDesign('1.md')) const res = await app.request(`/api/books/${created.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ data }), + body: JSON.stringify({ data: { ...data, cover: { courseName: '旧课程', teacherName: '旧教师' } } }), }) expect(res.status).toBe(200) const fetched = await app.request(`/api/books/${created.id}`) - const body = (await fetched.json()) as { data: { cover: { courseName: string } } } - expect(body.data.cover.courseName).toBe('Web 前端开发') + const body = (await fetched.json()) as { data: Record } + expect(body.data).not.toHaveProperty('cover') }) it('returns 404 when saving data for a missing book', async () => { diff --git a/src/components/A4Workspace.test.ts b/src/components/A4Workspace.test.ts new file mode 100644 index 0000000..53c0f0b --- /dev/null +++ b/src/components/A4Workspace.test.ts @@ -0,0 +1,27 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import { createEmptyTeachingDesign } from '../domain/teachingDesign' +import A4Workspace from './A4Workspace.vue' + +describe('A4Workspace', () => { + it('renders a selected lesson without cover state', () => { + const design = createEmptyTeachingDesign('1.md') + design.topic = 'CSS 弹性布局' + + const wrapper = mount(A4Workspace, { + props: { selectedDesign: design }, + }) + + expect(Object.keys(wrapper.props()).sort()).toEqual(['selectedDesign']) + expect(wrapper.find('.cover-page').exists()).toBe(false) + expect(wrapper.find('.teaching-design-page').exists()).toBe(true) + }) + + it('renders no page when no lesson is selected', () => { + const wrapper = mount(A4Workspace, { + props: { selectedDesign: null }, + }) + + expect(wrapper.find('.page').exists()).toBe(false) + }) +}) diff --git a/src/components/A4Workspace.vue b/src/components/A4Workspace.vue index 08114cb..328172b 100644 --- a/src/components/A4Workspace.vue +++ b/src/components/A4Workspace.vue @@ -1,16 +1,12 @@ @@ -18,16 +14,8 @@ const emit = defineEmits<{