feat(GOV-009): 建立 Vue 前端工程基线
This commit is contained in:
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<title>A 股市场分析工具</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@a-share-analysis/web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "vue-tsc --noEmit && tsc --noEmit -p tsconfig.node.json",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"build": "vue-tsc --noEmit && vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"naive-ui": "2.44.1",
|
||||
"pinia": "3.0.4",
|
||||
"vue": "3.5.41",
|
||||
"vue-router": "4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.62.1",
|
||||
"@types/node": "22.20.0",
|
||||
"@vitejs/plugin-vue": "5.2.4",
|
||||
"@vue/test-utils": "2.4.11",
|
||||
"dotenv": "16.6.1",
|
||||
"jsdom": "26.1.0",
|
||||
"katex": "0.18.4",
|
||||
"sass": "1.102.0",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "6.4.3",
|
||||
"vitest": "3.2.7",
|
||||
"vue-tsc": "3.3.9"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: false,
|
||||
retries: 0,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:5173',
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
webServer: {
|
||||
command: 'env -u NO_COLOR pnpm dev',
|
||||
url: 'http://127.0.0.1:5173',
|
||||
reuseExistingServer: false,
|
||||
timeout: 30_000,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'desktop-chromium',
|
||||
use: { browserName: 'chromium', viewport: { width: 1440, height: 900 } },
|
||||
},
|
||||
{
|
||||
name: 'mobile-chromium',
|
||||
use: { browserName: 'chromium', viewport: { width: 390, height: 844 } },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createPinia } from 'pinia';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createMemoryHistory } from 'vue-router';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import App from './App.vue';
|
||||
import { createAppRouter } from './router';
|
||||
|
||||
describe('应用外壳', () => {
|
||||
it('提供可访问的应用标题和主内容区域', async () => {
|
||||
const router = createAppRouter(createMemoryHistory());
|
||||
await router.push('/');
|
||||
await router.isReady();
|
||||
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
plugins: [createPinia(), router],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.get('header').attributes('aria-label')).toBe('应用导航');
|
||||
expect(wrapper.get('main').attributes('id')).toBe('main-content');
|
||||
expect(wrapper.text()).toContain('A 股市场分析工具');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { NConfigProvider, NGlobalStyle, NMessageProvider } from 'naive-ui';
|
||||
|
||||
const appTitle = import.meta.env.VITE_APP_TITLE || 'A 股市场分析工具';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NConfigProvider>
|
||||
<NGlobalStyle />
|
||||
<NMessageProvider>
|
||||
<div class="app-shell">
|
||||
<header class="app-header" aria-label="应用导航">
|
||||
<span class="app-mark" aria-hidden="true">A</span>
|
||||
<strong>{{ appTitle }}</strong>
|
||||
</header>
|
||||
<main id="main-content" class="app-main">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
</NMessageProvider>
|
||||
</NConfigProvider>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createPinia } from 'pinia';
|
||||
import type { App } from 'vue';
|
||||
|
||||
import { createAppRouter } from './router';
|
||||
|
||||
export function installAppProviders(app: App) {
|
||||
app.use(createPinia());
|
||||
app.use(createAppRouter());
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
type RouterHistory,
|
||||
type RouteRecordRaw,
|
||||
} from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: {
|
||||
template: '<section aria-label="应用就绪"><p>工程基线已就绪</p></section>',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function createAppRouter(history: RouterHistory = createWebHistory()) {
|
||||
return createRouter({ history, routes });
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_ENV?: string;
|
||||
readonly VITE_API_BASE_URL?: string;
|
||||
readonly VITE_APP_TITLE?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import App from './app/App.vue';
|
||||
import { installAppProviders } from './app/providers';
|
||||
import './styles/index.scss';
|
||||
|
||||
const app = createApp(App);
|
||||
installAppProviders(app);
|
||||
app.mount('#app');
|
||||
@@ -0,0 +1,78 @@
|
||||
:root {
|
||||
color: #171a1f;
|
||||
background: #f4f6f8;
|
||||
font-family:
|
||||
Inter,
|
||||
'PingFang SC',
|
||||
'Microsoft YaHei',
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
min-width: 320px;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
a,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid #1677ff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 52px;
|
||||
padding: 0 20px;
|
||||
color: #fff;
|
||||
background: #1d2229;
|
||||
border-bottom: 1px solid #343b44;
|
||||
}
|
||||
|
||||
.app-mark {
|
||||
display: inline-grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
place-items: center;
|
||||
color: #1d2229;
|
||||
font-weight: 700;
|
||||
background: #50c878;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
width: min(100%, 1600px);
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.app-header,
|
||||
.app-main {
|
||||
padding-right: 14px;
|
||||
padding-left: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('应用外壳在桌面和移动视口均可正常呈现', async ({ page }) => {
|
||||
const runtimeErrors: string[] = [];
|
||||
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') runtimeErrors.push(message.text());
|
||||
});
|
||||
page.on('pageerror', (error) => runtimeErrors.push(error.message));
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.locator('main')).toBeVisible();
|
||||
await expect(page.getByText('A 股市场分析工具')).toBeVisible();
|
||||
|
||||
const hasHorizontalOverflow = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
||||
);
|
||||
expect(hasHorizontalOverflow).toBe(false);
|
||||
expect(runtimeErrors).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"types": ["node", "vite/client", "vitest/globals"]
|
||||
},
|
||||
"include": ["vite.config.ts", "vitest.config.ts", "playwright.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { parse } from 'dotenv';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
const webDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const developmentEnvFile = path.resolve(webDirectory, '../deploy/env/development/web.env.local');
|
||||
|
||||
function readDevelopmentPublicEnv() {
|
||||
if (process.env.NODE_ENV === 'production') return {};
|
||||
try {
|
||||
return Object.fromEntries(
|
||||
Object.entries(parse(readFileSync(developmentEnvFile))).filter(([key]) =>
|
||||
key.startsWith('VITE_'),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return {};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const publicEnv = readDevelopmentPublicEnv();
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
define: Object.fromEntries(
|
||||
Object.entries(publicEnv).map(([key, value]) => [
|
||||
`import.meta.env.${key}`,
|
||||
JSON.stringify(value),
|
||||
]),
|
||||
),
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user