From 8973d46a425c3424a0d5e1738f79ee9057c9dd09 Mon Sep 17 00:00:00 2001 From: dlandy Date: Wed, 15 Apr 2026 13:47:39 +0800 Subject: [PATCH] =?UTF-8?q?update:=E5=A2=9E=E5=8A=A0=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.spec.js | 131 +++++++++- client/src/App.vue | 33 ++- client/src/admin/AdminApp.vue | 31 ++- .../admin/components/AccessModeDialog.spec.js | 69 ++++++ .../src/admin/components/AccessModeDialog.vue | 2 +- .../components/AdminOverviewSection.spec.js | 57 +++++ .../admin/components/AdminOverviewSection.vue | 20 +- .../components/AdminToolsSection.spec.js | 75 ++++++ .../admin/components/AdminToolsSection.vue | 19 +- .../admin/components/ToolFormDialog.spec.js | 85 +++++++ .../src/admin/components/ToolFormDialog.vue | 13 +- client/src/admin/components/test-stubs.js | 229 +++++++++++++++++ client/src/admin/stores/console.js | 1 + client/src/pages/ToolDetailPage.spec.js | 49 ++++ client/src/pages/ToolDetailPage.vue | 28 ++- ...-14-tool-display-version-none-mode-plan.md | 141 +++++++++++ design/session-state-20260413095000.md | 44 ++++ design/session-state-20260414181132.md | 28 +++ design/session-state.md | 88 ++++--- .../2026-04-13-download-launch-url-design.md | 56 +++++ ...4-tool-display-version-none-mode-design.md | 152 ++++++++++++ .../migration.sql | 4 + server/prisma/prisma.zip | Bin 0 -> 5558 bytes server/prisma/schema.prisma | 2 + server/prisma/seed.ts | 42 ++++ server/scripts/start-with-migrate.js | 58 ++++- .../src/modules/access/access.service.spec.ts | 128 ++++++++++ server/src/modules/access/access.service.ts | 12 +- .../admin-overview.service.spec.ts | 111 +++++++++ .../admin-overview/admin-overview.service.ts | 2 + .../admin-tools/admin-tools.service.spec.ts | 232 ++++++++++++++++++ .../admin-tools/admin-tools.service.ts | 80 +++++- .../admin-tools/dto/create-tool.dto.ts | 9 + .../admin-tools/dto/update-access-mode.dto.ts | 3 +- .../src/modules/tools/tools.service.spec.ts | 89 +++++++ server/src/modules/tools/tools.service.ts | 33 ++- server/test/tool-launch-none.e2e-spec.ts | 81 ++++++ server/test/tools-detail.e2e-spec.ts | 39 +++ 38 files changed, 2193 insertions(+), 83 deletions(-) create mode 100644 client/src/admin/components/AccessModeDialog.spec.js create mode 100644 client/src/admin/components/AdminOverviewSection.spec.js create mode 100644 client/src/admin/components/AdminToolsSection.spec.js create mode 100644 client/src/admin/components/ToolFormDialog.spec.js create mode 100644 client/src/admin/components/test-stubs.js create mode 100644 design/plans/2026-04-14-tool-display-version-none-mode-plan.md create mode 100644 design/session-state-20260413095000.md create mode 100644 design/session-state-20260414181132.md create mode 100644 design/specs/2026-04-13-download-launch-url-design.md create mode 100644 design/specs/2026-04-14-tool-display-version-none-mode-design.md create mode 100644 server/prisma/migrations/20260414192000_tool_display_version_none_mode/migration.sql create mode 100644 server/prisma/prisma.zip create mode 100644 server/src/modules/access/access.service.spec.ts create mode 100644 server/src/modules/admin-overview/admin-overview.service.spec.ts create mode 100644 server/src/modules/admin-tools/admin-tools.service.spec.ts create mode 100644 server/test/tool-launch-none.e2e-spec.ts diff --git a/client/src/App.spec.js b/client/src/App.spec.js index 1758ed3..af9a26c 100644 --- a/client/src/App.spec.js +++ b/client/src/App.spec.js @@ -1,6 +1,6 @@ import { flushPromises, mount } from '@vue/test-utils'; import { createMemoryHistory, createRouter } from 'vue-router'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import App from './App.vue'; @@ -47,6 +47,8 @@ function createRouterForTest() { } describe('App', () => { + let openSpy; + beforeEach(() => { apiMocks.fetchCategories.mockResolvedValue([]); apiMocks.fetchHotKeywords.mockResolvedValue([]); @@ -65,6 +67,7 @@ describe('App', () => { description: 'Simple tool', category: { id: 'cat_demo', name: 'Developer Tools' }, tags: ['cli'], + displayVersion: '2026.04', latestVersion: '1.0.0', updatedAt: '2026-04-11', accessMode: 'web', @@ -95,10 +98,17 @@ describe('App', () => { features: [], updatedAt: '2026-04-11', openUrl: 'https://example.com/tool', + displayVersion: '2026.04', latestVersion: null, fileSize: null, downloadReady: true, }); + + openSpy = vi.spyOn(window, 'open').mockReturnValue({}); + }); + + afterEach(() => { + vi.restoreAllMocks(); }); it('navigates detail actions to the slug route and does not render the old detail modal', async () => { @@ -143,4 +153,123 @@ describe('App', () => { expect(wrapper.findAll('.modal-backdrop.open')).toHaveLength(1); expect(wrapper.get('button.nav-btn').attributes('aria-expanded')).toBe('true'); }); + + it('renders displayVersion from the API payload', async () => { + const router = createRouterForTest(); + await router.push('/'); + await router.isReady(); + + const wrapper = mount(App, { + global: { + plugins: [router], + }, + }); + + await flushPromises(); + + expect(wrapper.text()).toContain('2026.04'); + expect(wrapper.text()).not.toContain('1.0.0'); + }); + + it('keeps the current route unchanged when launching a download tool', async () => { + apiMocks.fetchTools.mockResolvedValue({ + list: [ + { + id: 'tool_download', + slug: 'download-tool', + name: 'Download Tool', + description: 'Download tool', + category: { id: 'cat_demo', name: 'Developer Tools' }, + tags: ['installer'], + displayVersion: '2.0.0', + latestVersion: '2.0.0', + updatedAt: '2026-04-11', + accessMode: 'download', + openUrl: 'https://example.com/download', + downloadReady: true, + downloadCount: 12, + openCount: 0, + }, + ], + pagination: { + page: 1, + pageSize: 6, + total: 1, + totalPages: 1, + }, + }); + apiMocks.launchTool.mockResolvedValue({ + mode: 'download', + actionUrl: 'https://example.com/download', + openIn: 'same_tab', + }); + + const router = createRouterForTest(); + await router.push('/'); + await router.isReady(); + + const wrapper = mount(App, { + global: { + plugins: [router], + }, + }); + + await flushPromises(); + + await wrapper.findAll('button.btn-small.btn-with-icon')[0].trigger('click'); + await flushPromises(); + + expect(openSpy).toHaveBeenCalledWith('https://example.com/download', '_blank', 'noopener,noreferrer'); + expect(router.currentRoute.value.fullPath).toBe('/'); + }); + + it('uses the primary action as a detail shortcut for none mode tools', async () => { + apiMocks.fetchTools.mockResolvedValue({ + list: [ + { + id: 'tool_none', + slug: 'preview-tool', + name: 'Preview Tool', + description: 'Preview only', + category: { id: 'cat_demo', name: 'Developer Tools' }, + tags: ['preview'], + displayVersion: 'preview-1', + latestVersion: null, + updatedAt: '2026-04-11', + accessMode: 'none', + openUrl: null, + downloadReady: false, + downloadCount: 0, + openCount: 0, + }, + ], + pagination: { + page: 1, + pageSize: 6, + total: 1, + totalPages: 1, + }, + }); + + const router = createRouterForTest(); + await router.push('/'); + await router.isReady(); + + const wrapper = mount(App, { + global: { + plugins: [router], + }, + }); + + await flushPromises(); + + expect(wrapper.text()).toContain('查看详情'); + expect(wrapper.text()).toContain('preview-1'); + + await wrapper.get('button.btn-small.btn-with-icon').trigger('click'); + await flushPromises(); + + expect(apiMocks.launchTool).not.toHaveBeenCalled(); + expect(router.currentRoute.value.fullPath).toBe('/tools/preview-tool'); + }); }); diff --git a/client/src/App.vue b/client/src/App.vue index d7ac2f2..e9a864d 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -152,7 +152,7 @@ 版本 - {{ tool.latestVersion || '暂无版本' }} + {{ tool.displayVersion || '暂无版本' }}
  • @@ -175,7 +175,7 @@