fix: FilePreview fileType case + Tailwind v4 gradient transparent bug

- FilePreview.vue: add normalizedFileType computed to handle backend
  returning uppercase HTML/MD/PPTX (fixes preview/download buttons)
- FilePreview.vue: bg-gradient-to-r from-orange-500 -> bg-orange-500
  (Tailwind v4 gradient + CSS variable = transparent)
- ReportCard.vue: bg-gradient-to-r -> bg-orange-600 for selected state
- Add .opencode/, node_modules/, dist/ to .gitignore
- Initial git setup for publish project
This commit is contained in:
2026-05-24 20:09:42 +08:00
commit b9137204a0
78 changed files with 12950 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
import { test, expect } from '@playwright/test'
/**
* Cover Image Upload E2E Tests
* Tests for the cover image upload functionality on ProjectDetail page
*/
test.describe('Cover Image Upload', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)
})
test('should navigate to project detail page', async ({ page }) => {
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
// Click the first project card by text
await page.locator('text=项目一').first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
})
test('should enter project edit mode by clicking project name', async ({ page }) => {
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
await page.locator('text=项目一').first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
// Click on project name to enter edit mode
const projectName = page.locator('h2').first()
await expect(projectName).toBeVisible({ timeout: 5000 })
await projectName.click()
// Should show edit form with file input
await expect(page.locator('input[type="file"]').first()).toBeVisible({ timeout: 5000 })
})
test('should show save and cancel buttons in edit mode', async ({ page }) => {
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
await page.locator('text=项目一').first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
// Enter edit mode
const projectName = page.locator('h2').first()
await projectName.click()
await page.waitForTimeout(300)
// Should show save button
const saveButton = page.locator('button:has-text("保存")')
await expect(saveButton).toBeVisible({ timeout: 5000 })
// Should show cancel button
const cancelButton = page.locator('button:has-text("取消")')
await expect(cancelButton).toBeVisible({ timeout: 5000 })
})
test('should have image file input accepting image types', async ({ page }) => {
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
await page.locator('text=项目一').first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
// Enter edit mode
const projectName = page.locator('h2').first()
await projectName.click()
await page.waitForTimeout(300)
// File input should accept images
const fileInput = page.locator('input[type="file"]').first()
await expect(fileInput).toBeVisible({ timeout: 5000 })
const accept = await fileInput.getAttribute('accept')
expect(accept).toBe('image/*')
})
test('should cancel edit mode and restore original state', async ({ page }) => {
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
await page.locator('text=项目一').first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
// Enter edit mode
const projectName = page.locator('h2').first()
await projectName.click()
await page.waitForTimeout(300)
// Click cancel
const cancelButton = page.locator('button:has-text("取消")')
await cancelButton.click()
await page.waitForTimeout(300)
// Edit form should be hidden, back to display mode
const fileInput = page.locator('input[type="file"]')
await expect(fileInput).toHaveCount(0, { timeout: 5000 })
})
})
/**
* Cover Image Refresh E2E Tests
* Tests that cover image changes reflect on project list after saving
*/
test.describe('Cover Image - List Refresh', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)
})
test('should refresh project list after navigating back from detail', async ({ page }) => {
// Get initial project count
await expect(page.locator('text=个项目').first()).toBeVisible({ timeout: 15000 })
// Navigate to a project
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
await page.locator('text=项目一').first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
// Navigate back to project list
await page.locator('a[href="/"]').click()
await expect(page.locator('text=选择项目').first()).toBeVisible({ timeout: 10000 })
// Should still show correct project count (list refreshed)
await expect(page.locator('text=个项目').first()).toBeVisible({ timeout: 5000 })
})
test('should display report count on stats cards', async ({ page }) => {
// Stats cards should show actual counts
await expect(page.locator('text=个项目').first()).toBeVisible({ timeout: 15000 })
await expect(page.locator('text=份报告').first()).toBeVisible({ timeout: 5000 })
await expect(page.locator('text=文件类型').first()).toBeVisible({ timeout: 5000 })
// Check that the stats numbers are visible (4xl font size)
const statsNumbers = page.locator('.text-4xl')
const count = await statsNumbers.count()
expect(count).toBeGreaterThanOrEqual(3)
})
})
+141
View File
@@ -0,0 +1,141 @@
import { test, expect } from '@playwright/test'
/**
* Project Management E2E Tests
* Tests for the UI redesigned project list and detail pages
*/
test.describe('Project Management (New UI)', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
// Wait for the page to fully load
await page.waitForLoadState('networkidle')
// Wait for content to render
await page.waitForTimeout(1000)
})
test('should display project list with mock data', async ({ page }) => {
// Verify that projects are displayed
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
await expect(page.locator('text=项目二').first()).toBeVisible({ timeout: 5000 })
await expect(page.locator('text=项目三').first()).toBeVisible({ timeout: 5000 })
})
test('should navigate to project detail page when clicking a project card', async ({ page }) => {
// Wait for project to be visible
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
// Click on the project card (large card with background image)
// The new UI uses a carousel with ProjectCard components
const projectCards = page.locator('.group.relative.h-\\[420px\\]')
await projectCards.first().click()
// Verify navigation to project detail
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
})
test('should display stats cards with correct counts', async ({ page }) => {
// Wait for projects to load
await expect(page.locator('text=个项目').first()).toBeVisible({ timeout: 15000 })
// Check stats cards are visible
await expect(page.locator('text=个项目').first()).toBeVisible({ timeout: 5000 })
await expect(page.locator('text=份报告').first()).toBeVisible({ timeout: 5000 })
await expect(page.locator('text=文件类型').first()).toBeVisible({ timeout: 5000 })
})
test('should display project carousel with navigation arrows', async ({ page }) => {
// Wait for projects to load
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
// Check carousel navigation arrows exist
const leftArrow = page.locator('button').filter({ has: page.locator('svg path[d*="M15 19l-7-7 7-7"]') })
const rightArrow = page.locator('button').filter({ has: page.locator('svg path[d*="M9 5l7 7-7 7"]') })
// At least one navigation button should be visible
await expect(page.locator('button').first()).toBeVisible({ timeout: 5000 })
})
test('should display project cards with report count badges', async ({ page }) => {
// Check that project cards show report count
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
// Report count badge should be visible in cards
await expect(page.locator('text=份报告').first()).toBeVisible({ timeout: 5000 })
})
test('should navigate to project detail and show reports', async ({ page }) => {
// Navigate to a project
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
// Click on a project card
const projectCards = page.locator('.group.relative.h-\\[420px\\]')
await projectCards.first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
// Should show the project name in detail page
await expect(page.locator('h2').first()).toBeVisible({ timeout: 5000 })
})
test('should navigate back to project list from project detail', async ({ page }) => {
// Navigate to a project
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
const projectCards = page.locator('.group.relative.h-\\[420px\\]')
await projectCards.first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
// Click back button (router-link to="/")
await page.locator('a[href="/"]').click()
// Verify we're back on the project list
await expect(page.locator('text=选择项目').first()).toBeVisible({ timeout: 10000 })
})
test('should display glass effect sidebar in project detail', async ({ page }) => {
// Navigate to a project
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
const projectCards = page.locator('.group.relative.h-\\[420px\\]')
await projectCards.first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
// Check that the sidebar has glass effect class
const sidebar = page.locator('.glass-light')
await expect(sidebar).toBeVisible({ timeout: 5000 })
})
test('should display report cards in project detail sidebar', async ({ page }) => {
// Navigate to a project
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
const projectCards = page.locator('.group.relative.h-\\[420px\\]')
await projectCards.first().click()
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
// Wait for reports to load (look for report file type badges)
await expect(page.locator('text=HTML').first()).toBeVisible({ timeout: 5000 })
})
test('should show loading state while fetching', async ({ page }) => {
// The loading spinner should appear briefly
// But we can verify the content eventually loads
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
})
test('should display correct report count from backend', async ({ page }) => {
// Wait for projects to load
await expect(page.locator('text=个项目').first()).toBeVisible({ timeout: 15000 })
// Stats cards show actual report counts from backend
await expect(page.locator('.text-4xl').first()).toBeVisible({ timeout: 5000 })
})
test('should display cover image on project cards when available', async ({ page }) => {
// Wait for project cards to load
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
// ProjectCard should render with background image style when coverImage is set
// Cards with cover image will have background-image CSS property
const projectCards = page.locator('.group.relative.h-\\[420px\\]')
const cardCount = await projectCards.count()
expect(cardCount).toBeGreaterThan(0)
})
})
+121
View File
@@ -0,0 +1,121 @@
import { test, expect } from '@playwright/test'
/**
* Report View E2E Tests (New UI)
* Tests report viewing, rendering, and download functionality
*/
test.describe('Report View (New UI)', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/project/1')
await page.waitForLoadState('networkidle')
// Wait for content to render
await page.waitForTimeout(1000)
})
test('should render HTML content in iframe preview', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on HTML report
await page.click('text=2026-05-22 日报.html')
// Wait for iframe to be visible
const iframe = page.locator('iframe[srcdoc]')
await expect(iframe).toBeVisible({ timeout: 10000 })
})
test('should render Markdown content with proper formatting', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on MD report
await page.click('text=2026-05-21 日报.md')
// Verify markdown is rendered (not raw markdown)
// The header should be rendered as h1
const header = page.locator('h1:has-text("日报标题")')
await expect(header).toBeVisible({ timeout: 10000 })
})
test('should show download button for PPTX reports', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on PPTX report
await page.click('text=2026-05-20 周报.pptx')
// Verify download button is visible (new UI has single download button for all types)
const downloadBtn = page.locator('button:has-text("下载")')
await expect(downloadBtn).toBeVisible({ timeout: 10000 })
})
test('should show report name in preview header when report is selected', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on HTML report
await page.click('text=2026-05-22 日报.html')
// Verify report name appears in preview header
await expect(page.locator('h3:has-text("2026-05-22 日报.html")').first()).toBeVisible({ timeout: 10000 })
})
test('should show report date and size in preview header', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on HTML report
await page.click('text=2026-05-22 日报.html')
// Verify date and size appear
await expect(page.locator('text=2026-05-22').first()).toBeVisible({ timeout: 5000 })
await expect(page.locator('text=15KB').first()).toBeVisible({ timeout: 5000 })
})
test('should have download button for all report types', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on HTML report
await page.click('text=2026-05-22 日报.html')
await expect(page.locator('button:has-text("下载")').first()).toBeVisible({ timeout: 5000 })
// Click on MD report
await page.click('text=2026-05-21 日报.md')
await expect(page.locator('button:has-text("下载")').first()).toBeVisible({ timeout: 5000 })
// Click on PPTX report
await page.click('text=2026-05-20 周报.pptx')
await expect(page.locator('button:has-text("下载")').first()).toBeVisible({ timeout: 5000 })
})
test('should navigate between reports smoothly', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Select first report
await page.click('text=2026-05-22 日报.html')
await expect(page.locator('h3:has-text("2026-05-22 日报.html")').first()).toBeVisible({ timeout: 5000 })
// Select second report
await page.click('text=2026-05-21 日报.md')
await expect(page.locator('h3:has-text("2026-05-21 日报.md")').first()).toBeVisible({ timeout: 5000 })
// Select third report
await page.click('text=2026-05-20 周报.pptx')
await expect(page.locator('h3:has-text("2026-05-20 周报.pptx")').first()).toBeVisible({ timeout: 5000 })
})
test('should show empty state for non-existent project', async ({ page }) => {
// Navigate to project with no reports (project ID 999 - mock data returns empty)
await page.goto('/project/999')
await page.waitForLoadState('networkidle')
// Should show the project detail page with "项目 999" header
await expect(page.locator('h2:has-text("项目 999")')).toBeVisible({ timeout: 15000 })
// With no reports, should show empty state in list
await expect(page.locator('text=暂无报告').first()).toBeVisible({ timeout: 5000 })
// Should show empty state in preview
await expect(page.locator('text=选择一份报告以预览')).toBeVisible({ timeout: 5000 })
})
})
+100
View File
@@ -0,0 +1,100 @@
import { test, expect } from '@playwright/test'
/**
* Report Upload E2E Tests (New UI)
* Tests report upload functionality in the project detail view
*/
test.describe('Report Upload (New UI)', () => {
test.beforeEach(async ({ page }) => {
// Navigate directly to a project detail page
await page.goto('/project/1')
await page.waitForLoadState('networkidle')
// Wait for content to render
await page.waitForTimeout(1000)
})
test('should display existing reports in the report list', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Verify HTML report appears in list
await expect(page.locator('text=2026-05-22 日报.html')).toBeVisible({ timeout: 5000 })
// Verify MD report appears in list
await expect(page.locator('text=2026-05-21 日报.md')).toBeVisible({ timeout: 5000 })
// Verify PPTX report appears in list
await expect(page.locator('text=2026-05-20 周报.pptx')).toBeVisible({ timeout: 5000 })
})
test('should display report type badges in sidebar', async ({ page }) => {
// Check that file type badges are visible
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// File type labels should be visible
await expect(page.locator('text=HTML').first()).toBeVisible({ timeout: 5000 })
await expect(page.locator('text=Markdown').first()).toBeVisible({ timeout: 5000 })
await expect(page.locator('text=PowerPoint').first()).toBeVisible({ timeout: 5000 })
})
test('should select and preview HTML report', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on HTML report
await page.click('text=2026-05-22 日报.html')
// Verify preview appears (HTML reports show in iframe)
await expect(page.locator('iframe[srcdoc]')).toBeVisible({ timeout: 10000 })
})
test('should select and preview MD report', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on MD report
await page.click('text=2026-05-21 日报.md')
// Verify markdown content is rendered
await expect(page.locator('text=日报标题')).toBeVisible({ timeout: 10000 })
await expect(page.locator('text=工作内容')).toBeVisible({ timeout: 5000 })
})
test('should show download button for PPTX report', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on PPTX report
await page.click('text=2026-05-20 周报.pptx')
// Verify download UI appears (new UI has unified download button)
await expect(page.locator('button:has-text("下载")').first()).toBeVisible({ timeout: 10000 })
})
test('should show empty state when no report is selected', async ({ page }) => {
// The page starts without a selected report
// Empty state should be visible (select prompt)
await expect(page.locator('text=选择一份报告以预览')).toBeVisible({ timeout: 15000 })
})
test('should highlight selected report in the list', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Click on a report
await page.click('text=2026-05-22 日报.html')
// The selected report should have different styling (we can't easily test color, but we can test it's clickable)
// Report should remain selected
await expect(page.locator('iframe[srcdoc]')).toBeVisible({ timeout: 10000 })
})
test('should show glass effect sidebar', async ({ page }) => {
// Wait for project to load
await expect(page.locator('h2').first()).toBeVisible({ timeout: 15000 })
// Check that the sidebar has glass effect class
const sidebar = page.locator('.glass-light')
await expect(sidebar).toBeVisible({ timeout: 5000 })
})
})
+119
View File
@@ -0,0 +1,119 @@
import { test, expect } from '@playwright/test'
/**
* Responsive Layout E2E Tests
* Tests responsive design for PC (1920px) and Mobile (375px) viewports
* Note: Sidebar (<aside>) exists only on ProjectDetail.vue, not ProjectList.vue
*/
test.describe('Responsive Layout', () => {
test('should display main content on PC width (1920px)', async ({ page }) => {
// Set PC viewport
await page.setViewportSize({ width: 1920, height: 1080 })
await page.goto('/')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)
// Main content should be visible
const mainContent = page.locator('main')
await expect(mainContent).toBeVisible({ timeout: 15000 })
// Page title should be visible
await expect(page.locator('text=选择项目')).toBeVisible({ timeout: 5000 })
// Projects should be displayed
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 5000 })
})
test('should display stats cards on mobile width (375px)', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 })
await page.goto('/')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)
// Main content should be visible
const mainContent = page.locator('main')
await expect(mainContent).toBeVisible({ timeout: 10000 })
// Stats cards should be visible
await expect(page.locator('text=个项目')).toBeVisible({ timeout: 5000 })
})
test('should display project cards in single column on mobile', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 })
await page.goto('/')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)
// Project cards should still be visible
await expect(page.locator('text=项目一').first()).toBeVisible({ timeout: 15000 })
})
test('should navigate to project detail on mobile', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 })
await page.goto('/')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)
// Click on a project to navigate to detail (use force to bypass overlay)
await page.locator('text=项目一').first().click({ force: true })
// Should navigate to project detail
await expect(page).toHaveURL(/\/project\/\d+/, { timeout: 10000 })
})
test('should display sidebar in project detail on PC width', async ({ page }) => {
// Set PC viewport
await page.setViewportSize({ width: 1920, height: 1080 })
// First go to home page to load mock data
await page.goto('/')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)
// Navigate to project detail via click
await page.locator('text=项目一').first().click({ force: true })
await page.waitForURL(/\/project\/\d+/, { timeout: 10000 })
// Wait for the sidebar to appear
await expect(page.locator('text=返回项目列表')).toBeVisible({ timeout: 15000 })
})
test('should display reports list in project detail on mobile width', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 })
// First go to home page to load mock data
await page.goto('/')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)
// Navigate to project detail via click
await page.locator('text=项目一').first().click({ force: true })
await page.waitForURL(/\/project\/\d+/, { timeout: 10000 })
// Reports should be visible (from mock data)
await expect(page.locator('text=2026-05-22 日报.html')).toBeVisible({ timeout: 5000 })
})
test('should close sidebar after navigation on mobile', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 })
await page.goto('/project/1')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(1000)
// Navigate back to project list
await page.locator('text=返回项目列表').click()
// Should be back on project list page
await expect(page).toHaveURL(/\/$/, { timeout: 10000 })
})
})