create-admin-mvp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +159 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +41 -0
- package/templates/admin-portal/ .stylelintrc.cjs +39 -0
- package/templates/admin-portal/.dockerignore +60 -0
- package/templates/admin-portal/.env +29 -0
- package/templates/admin-portal/.env.development +41 -0
- package/templates/admin-portal/.env.example +4 -0
- package/templates/admin-portal/.env.production +41 -0
- package/templates/admin-portal/.eslintrc.cjs +45 -0
- package/templates/admin-portal/.github/workflows/ci.yml +217 -0
- package/templates/admin-portal/.husky/commit-msg +11 -0
- package/templates/admin-portal/.husky/pre-commit +10 -0
- package/templates/admin-portal/.lintstagedrc.json +13 -0
- package/templates/admin-portal/.prettierrc.cjs +19 -0
- package/templates/admin-portal/.stylelintrc.cjs +35 -0
- package/templates/admin-portal/Dockerfile +35 -0
- package/templates/admin-portal/README.md +196 -0
- package/templates/admin-portal/commitlint.config.cjs +41 -0
- package/templates/admin-portal/docker-compose.yml +35 -0
- package/templates/admin-portal/index.html +13 -0
- package/templates/admin-portal/nginx.conf +45 -0
- package/templates/admin-portal/package-lock.json +10730 -0
- package/templates/admin-portal/package.json +62 -0
- package/templates/admin-portal/playwright-report/index.html +85 -0
- package/templates/admin-portal/playwright.config.ts +73 -0
- package/templates/admin-portal/pnpm-lock.yaml +1620 -0
- package/templates/admin-portal/postcss.config.cjs +56 -0
- package/templates/admin-portal/public/vite.svg +1 -0
- package/templates/admin-portal/src/App.vue +15 -0
- package/templates/admin-portal/src/assets/styles/main.css +36 -0
- package/templates/admin-portal/src/assets/vue.svg +1 -0
- package/templates/admin-portal/src/components/HelloWorld.vue +41 -0
- package/templates/admin-portal/src/layout/index.vue +23 -0
- package/templates/admin-portal/src/main.ts +12 -0
- package/templates/admin-portal/src/mock/auth.ts +26 -0
- package/templates/admin-portal/src/permission.ts +15 -0
- package/templates/admin-portal/src/router/index.ts +21 -0
- package/templates/admin-portal/src/style.css +79 -0
- package/templates/admin-portal/src/views/About.vue +15 -0
- package/templates/admin-portal/src/views/Home.vue +15 -0
- package/templates/admin-portal/src/views/login/index.vue +34 -0
- package/templates/admin-portal/test-results/.last-run.json +23 -0
- package/templates/admin-portal/test-results/results.json +882 -0
- package/templates/admin-portal/tests/e2e/example.spec.ts +52 -0
- package/templates/admin-portal/tests/unit/example.test.ts +49 -0
- package/templates/admin-portal/tsconfig.app.json +18 -0
- package/templates/admin-portal/tsconfig.json +7 -0
- package/templates/admin-portal/tsconfig.node.json +22 -0
- package/templates/admin-portal/vite.config.ts +21 -0
- package/templates/admin-portal/vitest.config.ts +49 -0
- package/templates/admin-portal/vitest.setup.ts +60 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
test.describe('示例 E2E 测试', () => {
|
|
4
|
+
test.beforeEach(async ({ page }) => {
|
|
5
|
+
await page.goto('/')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
test('应该显示首页', async ({ page }) => {
|
|
9
|
+
await expect(page).toHaveTitle(/.*Admin Portal.*/)
|
|
10
|
+
await expect(page.locator('h1')).toContainText('Home Page')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('应该导航到关于页面', async ({ page }) => {
|
|
14
|
+
await page.click('text=About')
|
|
15
|
+
await expect(page).toHaveURL(/.*\/about/)
|
|
16
|
+
await expect(page.locator('h1')).toContainText('About Page')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('应该导航回首页', async ({ page }) => {
|
|
20
|
+
await page.click('text=About')
|
|
21
|
+
await expect(page).toHaveURL(/.*\/about/)
|
|
22
|
+
await page.click('text=Home')
|
|
23
|
+
await expect(page).toHaveURL(/.*\//)
|
|
24
|
+
await expect(page.locator('h1')).toContainText('Home Page')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('应该响应窗口大小变化', async ({ page }) => {
|
|
28
|
+
await page.setViewportSize({ width: 1920, height: 1080 })
|
|
29
|
+
await expect(page.locator('.home')).toBeVisible()
|
|
30
|
+
|
|
31
|
+
await page.setViewportSize({ width: 375, height: 667 })
|
|
32
|
+
await expect(page.locator('.home')).toBeVisible()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('应该支持浏览器导航', async ({ page }) => {
|
|
36
|
+
await page.click('text=About')
|
|
37
|
+
await expect(page).toHaveURL(/.*\/about/)
|
|
38
|
+
|
|
39
|
+
await page.goBack()
|
|
40
|
+
await expect(page).toHaveURL(/.*\//)
|
|
41
|
+
|
|
42
|
+
await page.goForward()
|
|
43
|
+
await expect(page).toHaveURL(/.*\/about/)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('应该支持页面刷新', async ({ page }) => {
|
|
47
|
+
const initialText = await page.locator('h1').textContent()
|
|
48
|
+
await page.reload()
|
|
49
|
+
const reloadedText = await page.locator('h1').textContent()
|
|
50
|
+
expect(initialText).toBe(reloadedText)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
describe('示例测试', () => {
|
|
4
|
+
it('应该通过 1 + 1 = 2', () => {
|
|
5
|
+
expect(1 + 1).toBe(2)
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('应该通过字符串拼接', () => {
|
|
9
|
+
const str = 'Hello'
|
|
10
|
+
expect(str + ' World').toBe('Hello World')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('应该通过数组操作', () => {
|
|
14
|
+
const arr = [1, 2, 3]
|
|
15
|
+
expect(arr).toHaveLength(3)
|
|
16
|
+
expect(arr).toContain(2)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('应该通过对象操作', () => {
|
|
20
|
+
const obj = { name: 'John', age: 30 }
|
|
21
|
+
expect(obj).toHaveProperty('name', 'John')
|
|
22
|
+
expect(obj).toMatchObject({ age: 30 })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('应该通过异步操作', async () => {
|
|
26
|
+
const promise = Promise.resolve(42)
|
|
27
|
+
await expect(promise).resolves.toBe(42)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('应该通过布尔值判断', () => {
|
|
31
|
+
expect(true).toBe(true)
|
|
32
|
+
expect(false).toBe(false)
|
|
33
|
+
expect(1).toBeTruthy()
|
|
34
|
+
expect(0).toBeFalsy()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('应该通过数字比较', () => {
|
|
38
|
+
expect(10).toBeGreaterThan(5)
|
|
39
|
+
expect(10).toBeLessThan(20)
|
|
40
|
+
expect(10).toBeGreaterThanOrEqual(10)
|
|
41
|
+
expect(10).toBeLessThanOrEqual(10)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('应该通过字符串匹配', () => {
|
|
45
|
+
const str = 'Hello World'
|
|
46
|
+
expect(str).toMatch(/World/)
|
|
47
|
+
expect(str).toContain('Hello')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
5
|
+
"types": ["vite/client"],
|
|
6
|
+
"baseUrl": ".",
|
|
7
|
+
"paths": {
|
|
8
|
+
"@/*": ["./src/*"]
|
|
9
|
+
},
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"noUnusedParameters": true,
|
|
13
|
+
"erasableSyntaxOnly": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noUncheckedSideEffectImports": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"moduleDetection": "force",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"erasableSyntaxOnly": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"noUncheckedSideEffectImports": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["vite.config.ts"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import { viteMockServe } from 'vite-plugin-mock'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
export default defineConfig(({ command }) => {
|
|
7
|
+
return {
|
|
8
|
+
plugins: [
|
|
9
|
+
vue(),
|
|
10
|
+
viteMockServe({
|
|
11
|
+
mockPath: 'src/mock',
|
|
12
|
+
enable: command === 'serve',
|
|
13
|
+
}),
|
|
14
|
+
],
|
|
15
|
+
resolve: {
|
|
16
|
+
alias: {
|
|
17
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Vitest 配置文件
|
|
2
|
+
// 配置单元测试框架
|
|
3
|
+
|
|
4
|
+
import { defineConfig } from 'vitest/config'
|
|
5
|
+
import vue from '@vitejs/plugin-vue'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
plugins: [vue() as any],
|
|
11
|
+
|
|
12
|
+
// 2. 测试配置
|
|
13
|
+
test: {
|
|
14
|
+
// 2.1 全局 API(不需要 import 就能使用 describe, it, expect 等)
|
|
15
|
+
globals: true,
|
|
16
|
+
|
|
17
|
+
// 2.2 测试环境(模拟浏览器环境)
|
|
18
|
+
environment: 'jsdom',
|
|
19
|
+
|
|
20
|
+
// 2.3 测试设置文件
|
|
21
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
22
|
+
|
|
23
|
+
// 2.4 代码覆盖率配置
|
|
24
|
+
coverage: {
|
|
25
|
+
// 2.4.1 覆盖率提供者(v8 最快)
|
|
26
|
+
provider: 'v8',
|
|
27
|
+
|
|
28
|
+
// 2.4.2 覆盖率报告格式
|
|
29
|
+
reporter: ['text', 'json', 'html'],
|
|
30
|
+
|
|
31
|
+
// 2.4.3 排除的文件
|
|
32
|
+
exclude: [
|
|
33
|
+
'node_modules/',
|
|
34
|
+
'tests/',
|
|
35
|
+
'**/*.d.ts',
|
|
36
|
+
'**/*.config.*',
|
|
37
|
+
'**/mockData',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// 3. 路径别名配置
|
|
43
|
+
resolve: {
|
|
44
|
+
alias: {
|
|
45
|
+
// @ 指向 src 目录
|
|
46
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Vitest 测试设置文件
|
|
2
|
+
// 在运行测试前执行,用于模拟浏览器 API
|
|
3
|
+
// 导入 vitest 的模拟函数
|
|
4
|
+
// 背景:Vitest 是基于 Vite 的单元测试框架,vi 是其提供的工具集,用于创建“假”函数/对象
|
|
5
|
+
// 从 vitest 中导入 vi 工具函数,用于创建测试替身(mock)
|
|
6
|
+
import { vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
// 1. 模拟 localStorage
|
|
9
|
+
// 原因:Node.js 环境没有 localStorage,需要模拟
|
|
10
|
+
// 背景:localStorage 是浏览器提供的“持久键值存储”,测试代码里若直接读取会报错 ReferenceError
|
|
11
|
+
// 做法:在 Node 全局对象 global 上手动挂一个“假”对象,让它长得跟浏览器里的 Storage 一样
|
|
12
|
+
global.localStorage = {
|
|
13
|
+
// getItem(key) 应该返回 string | null,测试里通常不关心真实值,先统一返回 null
|
|
14
|
+
getItem: vi.fn(() => null), // 获取数据
|
|
15
|
+
// setItem(key, value) 只是占位,不真写磁盘;vi.fn() 返回一个可监听的“假”函数
|
|
16
|
+
setItem: vi.fn(), // 设置数据
|
|
17
|
+
removeItem: vi.fn(), // 删除数据
|
|
18
|
+
clear: vi.fn(), // 清空数据
|
|
19
|
+
// 用 as Storage 告诉 TypeScript“相信我,这就是 Storage”,避免类型报错
|
|
20
|
+
} as Storage
|
|
21
|
+
|
|
22
|
+
// 2. 模拟 sessionStorage
|
|
23
|
+
// 原因:同上,Node.js 没有 sessionStorage
|
|
24
|
+
// 背景:sessionStorage 与 localStorage 类似,但生命周期跟随“页面会话”,测试里同样需要“假”实现
|
|
25
|
+
global.sessionStorage = {
|
|
26
|
+
getItem: vi.fn(() => null), // 获取数据
|
|
27
|
+
setItem: vi.fn(), // 设置数据
|
|
28
|
+
removeItem: vi.fn(), // 删除数据
|
|
29
|
+
clear: vi.fn(), // 清空数据
|
|
30
|
+
} as Storage
|
|
31
|
+
|
|
32
|
+
// 3. 模拟 window.matchMedia
|
|
33
|
+
// 原因:Node.js 环境没有 matchMedia,需要模拟
|
|
34
|
+
// 背景:matchMedia 用于“CSS 媒体查询”JS 化,例如 matchMedia('(max-width: 768px)').matches
|
|
35
|
+
// 测试里若组件依赖“移动端判断”会踩空,必须提前“造假”
|
|
36
|
+
// 做法:用 Object.defineProperty 给 window 动态注入属性,确保可写且可被重新赋值
|
|
37
|
+
// 这行代码在 Node 测试环境里“伪造”了浏览器的 window.matchMedia 方法。
|
|
38
|
+
// 背景:Node 没有 DOM,也就没有 window.matchMedia;但组件里可能写:
|
|
39
|
+
// if (window.matchMedia('(max-width: 768px)').matches) { ... }
|
|
40
|
+
// 如果不 mock,测试运行时会直接报错“matchMedia is not a function”。
|
|
41
|
+
// 做法:用 Object.defineProperty 把 window.matchMedia 换成一个“假”函数。
|
|
42
|
+
// 假函数被调用时,会返回一个“假”媒体查询结果对象,里面:
|
|
43
|
+
// - matches 默认 false(可后续用 vi.mocked(..).mockReturnValue 覆盖)
|
|
44
|
+
// - media 把查询字符串原样带回去,保持接口一致
|
|
45
|
+
// - 其余 addListener / addEventListener 等方法全部用 vi.fn() 占位,
|
|
46
|
+
// 防止组件调用时报“not a function”
|
|
47
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
48
|
+
writable: true,
|
|
49
|
+
value: vi.fn().mockImplementation((query) => ({
|
|
50
|
+
matches: false, // 默认“不匹配”,测试里可再 vi.mocked(..).mockReturnValue 覆盖
|
|
51
|
+
media: query, // 把传进来的查询字符串原样挂回去,保持接口一致
|
|
52
|
+
onchange: null, // 变化回调,先留空
|
|
53
|
+
// 下面四个是历史/标准兼容方法,全用“假”函数占位,防止组件调用时报“not a function”
|
|
54
|
+
addListener: vi.fn(), // 添加监听器(旧 API)
|
|
55
|
+
removeListener: vi.fn(), // 移除监听器(旧 API)
|
|
56
|
+
addEventListener: vi.fn(), // 添加事件监听(标准 API)
|
|
57
|
+
removeEventListener: vi.fn(), // 移除事件监听(标准 API)
|
|
58
|
+
dispatchEvent: vi.fn(), // 触发事件,测试里若手动派发事件也不会报错
|
|
59
|
+
})),
|
|
60
|
+
})
|