From be53ff00c0c3b01c4d41528b080ce249d77f2d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?mac=C2=B7ufutx?= Date: Mon, 9 Jun 2025 10:23:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 5 + .eslintignore | 7 + .eslintrc.cjs | 60 ++++++++ .gitignore | 24 ++++ .husky/pre-commit | 5 + .prettierignore | 5 + .prettierrc.cjs | 13 ++ .vscode/extensions.json | 3 + README.md | 250 ++++++++++++++++++++++++++++++++++ eslint.config.js | 0 index.html | 13 ++ package.json | 48 +++++++ public/vite.svg | 1 + src/App.vue | 30 ++++ src/assets/vue.svg | 1 + src/components/HelloWorld.vue | 48 +++++++ src/components/Test.vue | 6 + src/main.ts | 5 + src/style.css | 79 +++++++++++ src/utils/request.ts | 138 +++++++++++++++++++ src/vite-env.d.ts | 1 + tsconfig.app.json | 19 +++ tsconfig.json | 7 + tsconfig.node.json | 25 ++++ vite.config.ts | 12 ++ 25 files changed, 805 insertions(+) create mode 100644 .env.development create mode 100644 .eslintignore create mode 100644 .eslintrc.cjs create mode 100644 .gitignore create mode 100755 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc.cjs create mode 100644 .vscode/extensions.json create mode 100644 README.md create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package.json create mode 100644 public/vite.svg create mode 100644 src/App.vue create mode 100644 src/assets/vue.svg create mode 100644 src/components/HelloWorld.vue create mode 100644 src/components/Test.vue create mode 100644 src/main.ts create mode 100644 src/style.css create mode 100644 src/utils/request.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..abffc39 --- /dev/null +++ b/.env.development @@ -0,0 +1,5 @@ +# .env.development (开发环境) +VITE_API_BASE_URL = 'http://localhost:3000/api' + +# .env.production (生产环境) +VITE_API_BASE_URL = 'https://your-api-domain.com/api' \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..c7b44f5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +# .eslintignore +node_modules +dist +public +*.d.ts +.vscode +.husky \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..20c755f --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,60 @@ +// .eslintrc.cjs +module.exports = { + root: true, + env: { + browser: true, + node: true, + es6: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:vue/vue3-recommended', // Vue 3 推荐规则 + '@vue/eslint-config-typescript', // TypeScript 支持 + 'prettier', // 关闭与 Prettier 冲突的规则(需先安装 prettier) + 'plugin:prettier/recommended', // 将 Prettier 规则作为 ESLint 规则 + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + rules: { + // 自定义规则(根据团队需求调整) + 'vue/multi-word-component-names': 'off', + 'prettier/prettier': 'error', // 将 Prettier 错误视为 ESLint 错误 + // 强制使用 === 而非 == + 'eqeqeq': 'error', + + // 组件 props 必须有类型和默认值 + 'vue/require-default-prop': 'error', + 'vue/require-prop-types': 'error', + + // 限制每行最大长度 + 'max-len': ['warn', {code: 120}], + + // 禁止使用未定义的组件 + 'vue/no-unregistered-components': 'error', + + // 组件名必须为 PascalCase + 'vue/component-name-in-template-casing': ['error', 'PascalCase'], + + // 禁止使用 v-html(防止 XSS) + 'vue/no-v-html': 'warn', + // 自定义规则(根据团队需求调整) + 'vue/multi-word-component-names': 'off', // 允许单字组件名 + 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + '@typescript-eslint/no-unused-vars': ['warn', {argsIgnorePattern: '^_'}], + }, + overrides: [ + // 针对测试文件的规则 + { + files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], + env: { + jest: true, + }, + }, + ], +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..e1c12eb --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm test +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5a1e050 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +# .prettierignore +node_modules +dist +public +*.d.ts \ No newline at end of file diff --git a/.prettierrc.cjs b/.prettierrc.cjs new file mode 100644 index 0000000..8751066 --- /dev/null +++ b/.prettierrc.cjs @@ -0,0 +1,13 @@ +// .prettierrc.cjs +module.exports = { + printWidth: 120, // 每行最大字符数 + tabWidth: 2, // 缩进空格数 + useTabs: false, // 不使用制表符缩进 + semi: false, // 句末不加分号 + singleQuote: true, // 使用单引号 + quoteProps: 'as-needed', // 对象属性仅在必要时加引号 + trailingComma: 'none', // 不使用尾随逗号 + bracketSpacing: true, // 对象括号内加空格 + bracketSameLine: false, // HTML 标签的 > 不另起一行 + arrowParens: 'avoid', // 箭头函数参数仅在必要时加括号 +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..aaaf337 --- /dev/null +++ b/README.md @@ -0,0 +1,250 @@ +# Vue 3 + TypeScript + Vite + Axios 基础框架 + +这是一个基于 **Vue 3**、**TypeScript** 和 **Vite** 的现代化前端基础框架,集成了 **Axios** 请求封装、**ESLint** 代码检查、**Prettier** 代码格式化等工具,帮助你快速搭建高质量的前端项目。 + + +## 项目特性 + +- **核心框架**:Vue 3、Vue Router、Pinia(状态管理) +- **构建工具**:Vite 4(极速构建) +- **类型支持**:TypeScript 5(严格类型检查) +- **HTTP 请求**:Axios 封装(支持拦截器、错误处理、文件上传/下载) +- **代码规范**:ESLint + Prettier(统一代码风格) +- **样式方案**:Less(CSS 预处理) +- **提交规范**:Git 钩子(提交前自动检查代码) + + +## 快速开始 + +### 环境准备 + +- **Node.js**:v18+(推荐使用 [nvm](https://github.com/nvm-sh/nvm) 管理) +- **包管理器**:npm 或 pnpm + + +### 初始化项目 + +```bash +# 克隆仓库 +git clone https://github.com/your-repo/vue3-ts-template.git +cd vue3-ts-template + +# 安装依赖 +npm install + +# 启动开发服务器 +npm run dev +``` + + +### 主要命令 + +| 命令 | 描述 | +|-----------------|---------------------------------| +| `npm run dev` | 启动开发服务器 | +| `npm run build` | 构建生产环境包 | +| `npm run preview` | 预览生产环境包 | +| `npm run lint` | 代码检查并自动修复 | +| `npm run format` | 使用 Prettier 格式化代码 | + + +## 目录结构 + +``` +src/ +├── api/ # API 请求封装 +├── assets/ # 静态资源 +├── components/ # 通用组件 +├── router/ # 路由配置 +├── store/ # 状态管理 (Pinia) +├── utils/ # 工具函数 +│ └── request.ts # Axios 封装 +├── views/ # 页面组件 +├── App.vue # 根组件 +└── main.ts # 入口文件 +``` + + +## HTTP 请求封装 + +框架已封装完整的 Axios 请求工具,支持 **GET**、**POST**、**PUT**、**DELETE**、**文件上传** 和 **文件下载**,内置错误处理。 + +### 使用示例 + +```typescript +import request from '@/utils/request' + +// GET 请求 +const fetchUsers = async () => { + try { + const res = await request.get('/api/users', { page: 1, limit: 10 }) + console.log('用户列表:', res) + } catch (error) { + console.error('请求失败:', error) + } +} + +// POST 请求 +const createUser = async (data: any) => { + try { + const res = await request.post('/api/users', data) + console.log('创建成功:', res) + } catch (error) { + console.error('创建失败:', error) + } +} + +// 文件上传 +const uploadFile = async (file: File) => { + try { + const res = await request.upload('/api/upload', file) + console.log('上传成功:', res) + } catch (error) { + console.error('上传失败:', error) + } +} + +// 文件下载 +const downloadFile = async () => { + try { + await request.download('/api/download/report', null, '报告.pdf') + console.log('下载完成') + } catch (error) { + console.error('下载失败:', error) + } +} +``` + + +## 代码规范 + +### ESLint + Prettier + +框架使用 ESLint 和 Prettier 统一代码风格,配置文件: + +- `.eslintrc.cjs`:ESLint 规则 +- `.prettierrc.cjs`:Prettier 规则 + +### 自动格式化 + +推荐在 VSCode 中安装以下插件,实现保存时自动格式化: + +- ESLint +- Prettier +- Volar + + +## 状态管理 + +使用 **Pinia** 作为状态管理工具,示例: + +```typescript +// store/userStore.ts +import { defineStore } from 'pinia' +import request from '@/utils/request' + +export const useUserStore = defineStore('user', { + state: () => ({ + userList: [], + loading: false, + error: null + }), + actions: { + async fetchUsers() { + this.loading = true + try { + const res = await request.get('/api/users') + this.userList = res + } catch (error) { + this.error = error + } finally { + this.loading = false + } + } + } +}) +``` + + +## 部署 + +### 构建生产环境包 + +```bash +npm run build +``` + +构建后的文件会输出到 `dist` 目录。 + + +### 部署到 GitHub Pages + +```bash +npm run deploy +``` + +需先配置 `package.json` 中的 `homepage` 字段。 + + +## 常见问题 + +### 1. Axios 请求拦截器类型错误 + +如果遇到 `InternalAxiosRequestConfig` 类型错误,请确保: + +1. 导入正确的类型: + ```typescript + import { type InternalAxiosRequestConfig } from 'axios' + ``` + +2. 请求拦截器使用正确的类型: + ```typescript + service.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + // ... + return config + } + ) + ``` + + +### 2. 跨域问题 + +在开发环境中,可通过 `vite.config.ts` 配置代理: + +```typescript +// vite.config.ts +import { defineConfig } from 'vite' + +export default defineConfig({ + server: { + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + } + } + } +}) +``` +`` +目前的基础框架默认不包含 PC 端和移动端适配 的配置,需要根据项目需求添加相应方案。以下是几种常见的适配方法及其实现步骤: +一、现有框架适配状态 +你的项目当前是 基础框架,未集成任何适配方案,默认行为是: +PC 端:按设计稿尺寸直接渲染,大屏幕可能显得紧凑。 +移动端:内容可能过大,需要手动缩放或滚动查看 +`` + +## 贡献指南 + +1. Fork 仓库 +2. 创建特性分支:`git checkout -b feature/new-feature` +3. 提交代码:`git commit -m 'Add new feature'` +4. 推送分支:`git push origin feature/new-feature` +5. 提交 Pull Request + + +## 许可证 + +本项目采用 [MIT 许可证](LICENSE)。 \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..dde16aa --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Vue + TS + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..d63a614 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "my-website", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.ts,.tsx --fix", + "lint:check": "eslint . --ext .vue,.js,.jsx,.ts,.tsx", + "prepare": "husky install" + }, + "lint-staged": { + "*.{vue,js,ts,jsx,tsx}": [ + "eslint --fix", + "prettier --write" + ] + }, + "dependencies": { + "@vueuse/core": "^13.3.0", + "axios": "^1.9.0", + "pinia": "^2.1.7", + "vue": "^3.5.13", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@types/axios": "^0.9.36", + "@typescript-eslint/eslint-plugin": "^8.33.1", + "@typescript-eslint/parser": "^8.33.1", + "@vitejs/plugin-vue": "^4.2.3", + "@vue/eslint-config-typescript": "^14.5.0", + "@vue/tsconfig": "^0.7.0", + "autoprefixer": "^10.4.14", + "eslint": "^9.28.0", + "eslint-plugin-vue": "^10.2.0", + "husky": "^8.0.0", + "less": "^4.3.0", + "less-loader": "^12.3.0", + "lint-staged": "^16.1.0", + "postcss": "^8.4.24", + "prettier": "^3.5.3", + "typescript": "~5.8.3", + "vite": "^4.4.9", + "vite-plugin-image-optimizer": "^1.1.8", + "vue-tsc": "^2.2.8" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..58b0f21 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/assets/vue.svg b/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue new file mode 100644 index 0000000..63199d1 --- /dev/null +++ b/src/components/HelloWorld.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/components/Test.vue b/src/components/Test.vue new file mode 100644 index 0000000..e2ab41e --- /dev/null +++ b/src/components/Test.vue @@ -0,0 +1,6 @@ + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..2425c0f --- /dev/null +++ b/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..f691315 --- /dev/null +++ b/src/style.css @@ -0,0 +1,79 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..3967c6f --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,138 @@ +// src/utils/request.ts +import axios, { + type AxiosInstance, + type AxiosRequestConfig, + type AxiosResponse, + type AxiosError, + type InternalAxiosRequestConfig +} from 'axios' + +// 定义 API 响应结构 +interface ApiResponse { + code: number + message: string + data: T +} + +// 创建 axios 实例 +const service: AxiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '', + timeout: 10000, + headers: { + 'Content-Type': 'application/json;charset=utf-8' + } +}) + +// 请求拦截器 +service.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error: AxiosError) => { + console.error('请求错误:', error) + showError('请求发送失败') + return Promise.reject(error) + } +) + +// 响应拦截器 +service.interceptors.response.use( + (response: AxiosResponse) => { + const res = response.data + if (res.code !== 200) { + showError(res.message || '请求失败') + return Promise.reject(new Error(res.message || '请求失败')) + } + return res.data + }, + (error: AxiosError) => { + console.error('响应错误:', error) + const status = error.response?.status + const message = error.response?.data?.message || '网络错误,请稍后重试' + + switch (status) { + case 401: + showError('未登录或登录已过期') + window.location.href = '/login' + break + case 403: + showError('权限不足,无法访问') + break + case 404: + showError('请求资源不存在') + break + case 500: + showError('服务器内部错误') + break + default: + showError(message) + } + + return Promise.reject(error) + } +) + +// 原生错误提示函数 +function showError(message: string) { + alert(message) + + // 或使用 DOM 创建自定义提示(可选) + // const errorDiv = document.createElement('div') + // errorDiv.textContent = message + // errorDiv.style.cssText = 'position:fixed;top:20px;right:20px;background-color:#f56c6c;color:white;padding:10px;border-radius:4px;z-index:1000;' + // document.body.appendChild(errorDiv) + // setTimeout(() => { + // document.body.removeChild(errorDiv) + // }, 3000) +} + +// 定义请求方法 +const request = { + get(url: string, params?: any, config?: AxiosRequestConfig): Promise { + return service.get(url, { params, ...config }) + }, + + post(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return service.post(url, data, config) + }, + + put(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return service.put(url, data, config) + }, + + delete(url: string, params?: any, config?: AxiosRequestConfig): Promise { + return service.delete(url, { params, ...config }) + }, + + upload(url: string, file: File, config?: AxiosRequestConfig): Promise { + const formData = new FormData() + formData.append('file', file) + return service.post(url, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + ...config + }) + }, + + download(url: string, params?: any, filename?: string): Promise { + return service + .get(url, { + params, + responseType: 'blob' + }) + .then(response => { + const url = window.URL.createObjectURL(new Blob([response.data])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', filename || '文件下载') + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + }) + } +} + +export default request diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..fefd94d --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,19 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] // 添加路径别名 + }, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..9728af2 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..267c242 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' // 引入 path 模块 + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src') // 添加路径别名 + } + } +})