[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-config":3,"footer-socials":22,"home":23},{"site_title":4,"site_subtitle":5,"home_intro":6,"avatar":7,"seo_keywords":8,"seo_description":9,"site_subtitle_highlight":10,"home_description":11,"about_name":12,"about_intro":13,"code_comment_2":14,"code_log":15,"code_skills":16,"code_goal":17,"code_comment_1":18,"meteor_density":19,"meteor_max_count":19,"meteor_enabled":20,"meteor_speed":21},"ShineGoldYao","架构代码，","全栈开发者 \u002F 开源爱好者 \u002F 技术探索者","https:\u002F\u002Fforuda.gitee.com\u002Favatar\u002F1762402862010015318\u002F16382196_yaoxingjin_1762402861.png!avatar200","ShiGoldYao,技术博客,全栈开发","专注于前沿技术分享与开源项目展示的个人技术博客","书写未来。","大家好，我是 ShiGoldYao。一名全栈学习与技术爱好者。在这里，我分享关于现代 Web 开发、技术框架学习记录以及极客生活的深度思考。","ShiGoldYao","我是一名全栈技术学习者，对构建高性能、可扩展的现代 Web 应用充满热情。过去一年里，我从初识互联网前后端开发，到逐步沉淀技术体系，始终保持着对前端、后端与工程化的持续探索。\n\n我坚信 “代码如诗”。除了日常学习与项目实践，我也会花大量时间关注开源社区，尝试摸索 WebAssembly、Rust 等前沿技术在浏览器端的更多可能，不断挑战性能与体验的边界。\n\n生活里的我并不只有代码：闲暇时会打打永劫无间，享受博弈与操作的快感；也喜欢打乒乓球，在运动中放松自己；当然，最幸福的时光，还是和女朋友一起慢慢生活、认真恋爱。","\u002F\u002F 🚀 开启学习之旅","","Vue , TypeScript , Nest , Mysql","成为优秀的前端全栈工程师","\u002F\u002F 欢迎来到我的技术世界","3","true","5",[],{"banners":24,"skillCategories":51,"articles":52},[25,35,43],{"id":26,"title":27,"description":28,"imageUrl":15,"bgColor":29,"bgColorDark":15,"linkUrl":30,"sort":31,"isEnable":32,"createTime":33,"updateTime":34,"deleteTime":31},2,"重构未来：2026 前端架构趋势","探索微前端，Edge Computing 与 AI 辅助编程如何重塑我们的开发工作流，","linear-gradient(135deg, #e0e7ff 0%, #f3e8ff 50%, #fce7f3 100%)","#",0,1,"2026-04-02T07:40:51.580Z","2026-04-14T15:11:30.445Z",{"id":36,"title":37,"description":38,"imageUrl":15,"bgColor":39,"bgColorDark":40,"linkUrl":30,"sort":31,"isEnable":32,"createTime":41,"updateTime":42,"deleteTime":31},3,"全栈 Serverless：拥抱边缘计算","告别繁琐的运维工作，将应用部署在全球边缘节点，实现真正的毫秒级响应。","linear-gradient(135deg, #fef3c7 0%, #fce7f3 50%, #ede9fe 100%)","linear-gradient(135deg, #0c4a6e 0%, #1e3a5f 50%, #1e1b4b 100%)","2026-04-02T07:46:41.239Z","2026-04-02T07:56:26.565Z",{"id":44,"title":45,"description":46,"imageUrl":15,"bgColor":47,"bgColorDark":48,"linkUrl":30,"sort":31,"isEnable":32,"createTime":49,"updateTime":50,"deleteTime":31},4,"Rust & WebAssembly 性能优化实战","突破浏览器性能极限，将核心计算模块下沉至系统级语言，实现 400% 性能提升。","linear-gradient(135deg, #d1fae5 0%, #cffafe 50%, #e0f2fe 100%)","linear-gradient(135deg, #4c0519 0%, #4c1d95 50%, #1e1b4b 100%)","2026-04-02T07:47:36.067Z","2026-04-02T07:56:29.901Z",[],[53,78,95,109,129,140],{"id":54,"title":55,"slug":56,"coverUrl":15,"summary":15,"content":57,"htmlContent":58,"categoryId":59,"viewCount":60,"likeCount":32,"isTop":31,"isPublish":32,"seoKeywords":15,"seoDescription":15,"publishTime":61,"createTime":62,"updateTime":63,"deleteTime":64,"category":65,"tags":71},"18","NestJS 学习笔记","nestjs学习笔记","# NestJS 学习笔记\n\n## 目录\n\n1. [NestJS 简介](#1-nestjs-简介)\n2. [环境搭建](#2-环境搭建)\n3. [项目结构](#3-项目结构)\n4. [控制器 Controller](#4-控制器-controller)\n5. [服务 Service（Provider）](#5-服务-serviceprovider)\n6. [模块 Module](#6-模块-module)\n7. [中间件 Middleware](#7-中间件-middleware)\n8. [异常过滤器 Exception Filter](#8-异常过滤器-exception-filter)\n9. [管道 Pipe](#9-管道-pipe)\n10. [守卫 Guard](#10-守卫-guard)\n11. [拦截器 Interceptor](#11-拦截器-interceptor)\n12. [自定义装饰器](#12-自定义装饰器)\n13. [数据库集成（TypeORM）](#13-数据库集成typeorm)\n14. [数据验证与转换（DTO）](#14-数据验证与转换dto)\n15. [身份认证（JWT）](#15-身份认证jwt)\n16. [文件上传](#16-文件上传)\n17. [Swagger 接口文档](#17-swagger-接口文档)\n18. [配置管理](#18-配置管理)\n19. [部署与优化](#19-部署与优化)\n\n***\n\n## 1. NestJS 简介\n\n### 1.1 什么是 NestJS\n\n​\tNestJS 是一个用于构建高效、可扩展的 Node.js 服务端应用程序的框架。它使用 TypeScript 构建，底层默认使用 Express（也可以切换为 Fastify），借鉴了 Angular 的架构思想（模块化、依赖注入、装饰器）。\n\n**核心特性：**\n\n- 完全支持 TypeScript\n- 模块化架构（Module）\n- 依赖注入（DI \u002F IoC）\n- 装饰器驱动开发\n- 丰富的生态系统（微服务、WebSocket、GraphQL 等）\n- 内置对测试的支持\n\n### 1.2 与 Express 的对比\n\n| 特性       | Express          | NestJS                    |\n| ---------- | ---------------- | ------------------------- |\n| 语言       | JavaScript       | TypeScript（原生支持）    |\n| 架构       | 无固定架构       | 模块化 + 依赖注入         |\n| 学习曲线   | 低               | 中等                      |\n| 适用场景   | 小型\u002F中型项目    | 中大型企业级项目          |\n| 代码组织   | 自由组织         | 强约束，分层清晰          |\n| 底层框架   | 自身             | Express \u002F Fastify         |\n| 装饰器     | 不支持           | 核心特性                  |\n| 依赖注入   | 无               | 内置 IoC 容器             |\n\n### 1.3 核心概念一览\n\nNestJS 的请求生命周期（按顺序执行）：\n\n```\n客户端请求 → 中间件(Middleware) → 守卫(Guard) → 拦截器(Interceptor-前) \n→ 管道(Pipe) → 控制器(Controller) → 服务(Service) \n→ 拦截器(Interceptor-后) → 异常过滤器(Exception Filter) → 客户端响应\n```\n\n***\n\n## 2. 环境搭建\n\n### 2.1 系统要求\n\n- Node.js >= 16.0.0\n- 包管理器：npm \u002F yarn \u002F pnpm\n- TypeScript 基础知识\n\n### 2.2 安装 NestJS CLI\n\n```bash\n# 全局安装 NestJS 脚手架\nnpm i -g @nestjs\u002Fcli\n\n# 验证安装\nnest --version\n```\n\n### 2.3 创建项目\n\n```bash\n# 创建新项目\nnest new my-nest-app\n\n# 选择包管理器 npm\u002Fyarn\u002Fpnpm\n\n# 进入项目目录\ncd my-nest-app\n\n# 启动开发服务器（热重载）\nnpm run start:dev\n```\n\n​\t启动后访问 http:\u002F\u002Flocalhost:3000，看到 `Hello World!` 即成功。\n\n### 2.4 常用 CLI 命令\n\n```bash\n# 生成模块\nnest g module users\n\n# 生成控制器\nnest g controller users\n\n# 生成服务\nnest g service users\n\n# 一键生成完整 CRUD 资源（推荐）\nnest g resource users\n\n# 生成中间件\nnest g middleware logger\n\n# 生成守卫\nnest g guard auth\n\n# 生成拦截器\nnest g interceptor transform\n\n# 生成管道\nnest g pipe validation\n\n# 生成过滤器\nnest g filter http-exception\n```\n\n***\n\n## 3. 项目结构\n\n### 3.1 目录结构说明\n\n```\nmy-nest-app\u002F\n├── dist\u002F                    # 编译输出目录\n├── node_modules\u002F\n├── src\u002F\n│   ├── app.controller.ts    # 根控制器\n│   ├── app.controller.spec.ts # 控制器测试\n│   ├── app.module.ts        # 根模块\n│   ├── app.service.ts       # 根服务\n│   └── main.ts              # 应用入口文件\n├── test\u002F                    # 端到端测试\n│   ├── app.e2e-spec.ts\n│   └── jest-e2e.json\n├── nest-cli.json            # NestJS CLI 配置\n├── tsconfig.json            # TypeScript 配置\n├── tsconfig.build.json\n└── package.json\n```\n\n### 3.2 入口文件 main.ts\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { NestFactory } from '@nestjs\u002Fcore';\nimport { AppModule } from '.\u002Fapp.module';\n\nasync function bootstrap() {\n  \u002F\u002F 创建 Nest 应用实例\n  const app = await NestFactory.create(AppModule);\n  \n  \u002F\u002F 设置全局路由前缀\n  app.setGlobalPrefix('api');\n  \n  \u002F\u002F 启用 CORS 跨域\n  app.enableCors();\n  \n  \u002F\u002F 监听端口\n  await app.listen(3000);\n  console.log('服务器运行在 http:\u002F\u002Flocalhost:3000');\n}\nbootstrap();\n```\n\n### 3.3 推荐的项目结构（模块化）\n\n```\nsrc\u002F\n├── main.ts\n├── app.module.ts\n├── common\u002F                  # 公共模块\n│   ├── decorators\u002F          # 自定义装饰器\n│   ├── filters\u002F             # 异常过滤器\n│   ├── guards\u002F              # 守卫\n│   ├── interceptors\u002F        # 拦截器\n│   ├── middleware\u002F           # 中间件\n│   └── pipes\u002F               # 管道\n├── config\u002F                  # 配置模块\n│   └── config.module.ts\n├── users\u002F                   # 用户模块\n│   ├── dto\u002F                 # 数据传输对象\n│   │   ├── create-user.dto.ts\n│   │   └── update-user.dto.ts\n│   ├── entities\u002F            # 数据库实体\n│   │   └── user.entity.ts\n│   ├── users.controller.ts\n│   ├── users.service.ts\n│   ├── users.module.ts\n│   └── users.controller.spec.ts\n└── auth\u002F                    # 认证模块\n    ├── auth.controller.ts\n    ├── auth.service.ts\n    ├── auth.module.ts\n    └── strategies\u002F\n        └── jwt.strategy.ts\n```\n\n***\n\n## 4. 控制器 Controller\n\n### 4.1 控制器的作用\n\n​\t控制器负责处理传入的 HTTP 请求，返回响应给客户端。使用 `@Controller()` 装饰器标记类。\n\n### 4.2 基本用法\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.controller.ts\nimport {\n  Controller,\n  Get,\n  Post,\n  Put,\n  Delete,\n  Param,\n  Body,\n  Query,\n  HttpCode,\n  HttpStatus,\n} from '@nestjs\u002Fcommon';\n\n@Controller('users') \u002F\u002F 路由前缀 \u002Fusers\nexport class UsersController {\n\n  \u002F\u002F GET \u002Fusers\n  @Get()\n  findAll() {\n    return { code: 200, msg: '查询全部用户成功', data: [] };\n  }\n\n  \u002F\u002F GET \u002Fusers\u002F:id\n  @Get(':id')\n  findOne(@Param('id') id: string) {\n    return { code: 200, msg: '查询成功', data: { id } };\n  }\n\n  \u002F\u002F POST \u002Fusers\n  @Post()\n  @HttpCode(HttpStatus.CREATED) \u002F\u002F 设置状态码 201\n  create(@Body() createUserDto: any) {\n    return { code: 201, msg: '创建成功', data: createUserDto };\n  }\n\n  \u002F\u002F PUT \u002Fusers\u002F:id\n  @Put(':id')\n  update(@Param('id') id: string, @Body() updateUserDto: any) {\n    return { code: 200, msg: '修改成功', data: { id, ...updateUserDto } };\n  }\n\n  \u002F\u002F DELETE \u002Fusers\u002F:id\n  @Delete(':id')\n  remove(@Param('id') id: string) {\n    return { code: 200, msg: '删除成功', data: { id } };\n  }\n}\n```\n\n### 4.3 请求参数获取\n\n```typescript\n@Controller('users')\nexport class UsersController {\n\n  \u002F\u002F Query 参数: GET \u002Fusers?page=1&limit=10\n  @Get()\n  findAll(@Query('page') page: number, @Query('limit') limit: number) {\n    return { page, limit };\n  }\n\n  \u002F\u002F 动态路由参数: GET \u002Fusers\u002F123\n  @Get(':id')\n  findOne(@Param('id') id: string) {\n    return { id };\n  }\n\n  \u002F\u002F 请求体: POST \u002Fusers  body: { name: 'zhangsan', age: 20 }\n  @Post()\n  create(@Body() body: any) {\n    return body;\n  }\n\n  \u002F\u002F 获取请求头\n  @Get('info')\n  getInfo(@Headers('authorization') token: string) {\n    return { token };\n  }\n\n  \u002F\u002F 获取原始 Request 和 Response 对象\n  @Get('raw')\n  getRaw(@Req() req: Request, @Res() res: Response) {\n    res.status(200).json({ url: req.url });\n  }\n}\n```\n\n### 4.4 路由通配符与版本控制\n\n```typescript\n\u002F\u002F 通配符路由\n@Get('ab*cd')\nfindWildcard() {\n  return '匹配 abcd, ab_cd, abecd 等';\n}\n\n\u002F\u002F 路由版本控制（需在 main.ts 中启用）\n\u002F\u002F main.ts: app.enableVersioning({ type: VersioningType.URI });\n@Controller({ path: 'users', version: '1' })\nexport class UsersV1Controller {\n  @Get()\n  findAll() {\n    return 'v1 版本';\n  }\n}\n```\n\n***\n\n## 5. 服务 Service（Provider）\n\n### 5.1 服务的作用\n\n​\t服务负责处理业务逻辑，是 NestJS 中 **Provider** 的主要形式。通过 **依赖注入（DI）** 的方式注入到控制器中使用。\n\n### 5.2 创建服务\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.service.ts\nimport { Injectable, NotFoundException } from '@nestjs\u002Fcommon';\n\n\u002F\u002F @Injectable() 标记这个类可以被 IoC 容器管理\n@Injectable()\nexport class UsersService {\n  \u002F\u002F 模拟数据库\n  private users = [\n    { id: 1, name: '张三', age: 20 },\n    { id: 2, name: '李四', age: 25 },\n  ];\n\n  \u002F\u002F 查询全部\n  findAll() {\n    return this.users;\n  }\n\n  \u002F\u002F 根据 ID 查询\n  findOne(id: number) {\n    const user = this.users.find((u) => u.id === id);\n    if (!user) {\n      throw new NotFoundException(`用户 ID ${id} 不存在`);\n    }\n    return user;\n  }\n\n  \u002F\u002F 新增\n  create(createUserDto: { name: string; age: number }) {\n    const newUser = {\n      id: this.users.length + 1,\n      ...createUserDto,\n    };\n    this.users.push(newUser);\n    return newUser;\n  }\n\n  \u002F\u002F 修改\n  update(id: number, updateUserDto: { name?: string; age?: number }) {\n    const user = this.findOne(id);\n    Object.assign(user, updateUserDto);\n    return user;\n  }\n\n  \u002F\u002F 删除\n  remove(id: number) {\n    const index = this.users.findIndex((u) => u.id === id);\n    if (index === -1) {\n      throw new NotFoundException(`用户 ID ${id} 不存在`);\n    }\n    return this.users.splice(index, 1)[0];\n  }\n}\n```\n\n### 5.3 在控制器中注入服务\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.controller.ts\nimport { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs\u002Fcommon';\nimport { UsersService } from '.\u002Fusers.service';\n\n@Controller('users')\nexport class UsersController {\n  \u002F\u002F 构造函数注入（推荐方式）\n  constructor(private readonly usersService: UsersService) {}\n\n  @Get()\n  findAll() {\n    return {\n      code: 200,\n      msg: '查询成功',\n      data: this.usersService.findAll(),\n    };\n  }\n\n  @Get(':id')\n  findOne(@Param('id') id: string) {\n    return {\n      code: 200,\n      msg: '查询成功',\n      data: this.usersService.findOne(+id), \u002F\u002F +id 将字符串转为数字\n    };\n  }\n\n  @Post()\n  create(@Body() createUserDto: { name: string; age: number }) {\n    return {\n      code: 201,\n      msg: '创建成功',\n      data: this.usersService.create(createUserDto),\n    };\n  }\n\n  @Put(':id')\n  update(@Param('id') id: string, @Body() updateUserDto: any) {\n    return {\n      code: 200,\n      msg: '修改成功',\n      data: this.usersService.update(+id, updateUserDto),\n    };\n  }\n\n  @Delete(':id')\n  remove(@Param('id') id: string) {\n    return {\n      code: 200,\n      msg: '删除成功',\n      data: this.usersService.remove(+id),\n    };\n  }\n}\n```\n\n### 5.4 依赖注入原理\n\n```\n1. @Injectable() 标记 UsersService 为可注入的 Provider\n2. 在 UsersModule 的 providers 数组中注册 UsersService\n3. 控制器通过构造函数声明依赖\n4. NestJS 的 IoC 容器自动创建实例并注入\n\n好处：解耦、易测试、易替换\n```\n\n***\n\n## 6. 模块 Module\n\n### 6.1 模块的作用\n\n​\t模块是 NestJS 组织代码的基本单元。每个应用至少有一个根模块 `AppModule`，通过模块将相关的控制器和服务组织在一起。\n\n### 6.2 基本模块定义\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { UsersController } from '.\u002Fusers.controller';\nimport { UsersService } from '.\u002Fusers.service';\n\n@Module({\n  controllers: [UsersController],   \u002F\u002F 注册控制器\n  providers: [UsersService],        \u002F\u002F 注册服务（Provider）\n  exports: [UsersService],          \u002F\u002F 导出服务，供其他模块使用\n})\nexport class UsersModule {}\n```\n\n### 6.3 根模块注册\n\n```typescript\n\u002F\u002F src\u002Fapp.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { UsersModule } from '.\u002Fusers\u002Fusers.module';\nimport { AuthModule } from '.\u002Fauth\u002Fauth.module';\n\n@Module({\n  imports: [UsersModule, AuthModule], \u002F\u002F 导入子模块\n  controllers: [],\n  providers: [],\n})\nexport class AppModule {}\n```\n\n### 6.4 模块分类\n\n```typescript\n\u002F\u002F 1. 功能模块 - 组织特定业务功能\n@Module({\n  controllers: [UsersController],\n  providers: [UsersService],\n})\nexport class UsersModule {}\n\n\u002F\u002F 2. 共享模块 - 被多个模块复用\n@Module({\n  providers: [HelperService],\n  exports: [HelperService], \u002F\u002F 必须导出才能被其他模块使用\n})\nexport class SharedModule {}\n\n\u002F\u002F 3. 全局模块 - 注册一次，全局可用\n@Global() \u002F\u002F 添加 @Global() 装饰器\n@Module({\n  providers: [ConfigService],\n  exports: [ConfigService],\n})\nexport class ConfigModule {}\n\n\u002F\u002F 4. 动态模块 - 可接受配置参数\n@Module({})\nexport class DatabaseModule {\n  static forRoot(options: DatabaseOptions): DynamicModule {\n    return {\n      module: DatabaseModule,\n      providers: [\n        {\n          provide: 'DATABASE_OPTIONS',\n          useValue: options,\n        },\n        DatabaseService,\n      ],\n      exports: [DatabaseService],\n    };\n  }\n}\n\u002F\u002F 使用: imports: [DatabaseModule.forRoot({ host: 'localhost', port: 3306 })]\n```\n\n***\n\n## 7. 中间件 Middleware\n\n### 7.1 中间件的作用\n\n​\t在路由处理程序之前执行的函数，可以访问请求和响应对象，常用于日志记录、身份验证前置处理等。\n\n### 7.2 创建中间件\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fmiddleware\u002Flogger.middleware.ts\nimport { Injectable, NestMiddleware } from '@nestjs\u002Fcommon';\nimport { Request, Response, NextFunction } from 'express';\n\n@Injectable()\nexport class LoggerMiddleware implements NestMiddleware {\n  use(req: Request, res: Response, next: NextFunction) {\n    const { method, originalUrl } = req;\n    const start = Date.now();\n\n    \u002F\u002F 响应完成时记录日志\n    res.on('finish', () => {\n      const duration = Date.now() - start;\n      console.log(`[${method}] ${originalUrl} - ${res.statusCode} ${duration}ms`);\n    });\n\n    next(); \u002F\u002F 必须调用 next()，否则请求会挂起\n  }\n}\n```\n\n### 7.3 注册中间件\n\n```typescript\n\u002F\u002F src\u002Fapp.module.ts\nimport { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs\u002Fcommon';\nimport { LoggerMiddleware } from '.\u002Fcommon\u002Fmiddleware\u002Flogger.middleware';\nimport { UsersModule } from '.\u002Fusers\u002Fusers.module';\n\n@Module({\n  imports: [UsersModule],\n})\nexport class AppModule implements NestModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer\n      .apply(LoggerMiddleware)\n      \u002F\u002F 指定路由\n      .forRoutes('users');\n    \n    \u002F\u002F 也可以指定请求方法\n    \u002F\u002F .forRoutes({ path: 'users', method: RequestMethod.GET });\n    \n    \u002F\u002F 排除某些路由\n    \u002F\u002F .exclude({ path: 'users', method: RequestMethod.POST })\n    \u002F\u002F .forRoutes('users');\n  }\n}\n```\n\n### 7.4 函数式中间件（简单场景）\n\n```typescript\n\u002F\u002F 简单中间件可以用函数代替\nexport function logger(req: Request, res: Response, next: NextFunction) {\n  console.log(`[请求] ${req.method} ${req.url}`);\n  next();\n}\n\n\u002F\u002F 注册\nconsumer.apply(logger).forRoutes('*');\n```\n\n### 7.5 全局中间件\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  \n  \u002F\u002F 全局中间件\n  app.use(logger);\n  \n  await app.listen(3000);\n}\n```\n\n***\n\n## 8. 异常过滤器 Exception Filter\n\n### 8.1 内置异常类\n\nNestJS 提供了多种内置 HTTP 异常：\n\n```typescript\nimport {\n  BadRequestException,      \u002F\u002F 400\n  UnauthorizedException,     \u002F\u002F 401\n  ForbiddenException,        \u002F\u002F 403\n  NotFoundException,         \u002F\u002F 404\n  ConflictException,         \u002F\u002F 409\n  InternalServerErrorException, \u002F\u002F 500\n} from '@nestjs\u002Fcommon';\n\n\u002F\u002F 使用方式：直接在 Service 或 Controller 中抛出\nthrow new NotFoundException('用户不存在');\nthrow new BadRequestException('参数错误');\nthrow new UnauthorizedException('请先登录');\n```\n\n### 8.2 自定义异常过滤器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Ffilters\u002Fhttp-exception.filter.ts\nimport {\n  ExceptionFilter,\n  Catch,\n  ArgumentsHost,\n  HttpException,\n  HttpStatus,\n} from '@nestjs\u002Fcommon';\nimport { Request, Response } from 'express';\n\n@Catch(HttpException)\nexport class HttpExceptionFilter implements ExceptionFilter {\n  catch(exception: HttpException, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse\u003CResponse>();\n    const request = ctx.getRequest\u003CRequest>();\n    const status = exception.getStatus();\n    const exceptionResponse = exception.getResponse();\n\n    \u002F\u002F 统一异常响应格式\n    response.status(status).json({\n      code: status,\n      msg: typeof exceptionResponse === 'string'\n        ? exceptionResponse\n        : (exceptionResponse as any).message,\n      data: null,\n      timestamp: new Date().toISOString(),\n      path: request.url,\n    });\n  }\n}\n```\n\n### 8.3 捕获所有异常\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Ffilters\u002Fall-exception.filter.ts\nimport {\n  ExceptionFilter,\n  Catch,\n  ArgumentsHost,\n  HttpException,\n  HttpStatus,\n} from '@nestjs\u002Fcommon';\n\n@Catch() \u002F\u002F 不传参数，捕获所有异常\nexport class AllExceptionFilter implements ExceptionFilter {\n  catch(exception: unknown, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse();\n    const request = ctx.getRequest();\n\n    const status =\n      exception instanceof HttpException\n        ? exception.getStatus()\n        : HttpStatus.INTERNAL_SERVER_ERROR;\n\n    const message =\n      exception instanceof HttpException\n        ? exception.message\n        : '服务器内部错误';\n\n    response.status(status).json({\n      code: status,\n      msg: message,\n      data: null,\n      timestamp: new Date().toISOString(),\n      path: request.url,\n    });\n  }\n}\n```\n\n### 8.4 注册过滤器\n\n```typescript\n\u002F\u002F 方式一：全局注册（推荐）在 main.ts 中\napp.useGlobalFilters(new AllExceptionFilter());\n\n\u002F\u002F 方式二：控制器级别\n@Controller('users')\n@UseFilters(new HttpExceptionFilter())\nexport class UsersController {}\n\n\u002F\u002F 方式三：方法级别\n@Get(':id')\n@UseFilters(new HttpExceptionFilter())\nfindOne(@Param('id') id: string) {}\n```\n\n***\n\n## 9. 管道 Pipe\n\n### 9.1 管道的作用\n\n​\t管道有两个典型用途：**数据转换**（将输入数据转为目标类型）和 **数据验证**（验证输入数据是否合法）。\n\n### 9.2 内置管道\n\n```typescript\nimport {\n  ParseIntPipe,       \u002F\u002F 转换为整数\n  ParseFloatPipe,     \u002F\u002F 转换为浮点数\n  ParseBoolPipe,      \u002F\u002F 转换为布尔值\n  ParseUUIDPipe,      \u002F\u002F 验证 UUID 格式\n  ParseArrayPipe,     \u002F\u002F 转换为数组\n  DefaultValuePipe,   \u002F\u002F 设置默认值\n  ValidationPipe,     \u002F\u002F 类验证管道\n} from '@nestjs\u002Fcommon';\n\n\u002F\u002F 使用示例\n@Get(':id')\nfindOne(@Param('id', ParseIntPipe) id: number) {\n  \u002F\u002F id 已经被自动转换为 number 类型\n  \u002F\u002F 如果传入 'abc' 会自动返回 400 错误\n  return this.usersService.findOne(id);\n}\n\n@Get()\nfindAll(\n  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,\n  @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,\n) {\n  return { page, limit };\n}\n```\n\n### 9.3 全局验证管道（最常用）\n\n```bash\n# 安装验证相关依赖\nnpm i class-validator class-transformer\n```\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { ValidationPipe } from '@nestjs\u002Fcommon';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  \n  \u002F\u002F 全局启用验证管道\n  app.useGlobalPipes(\n    new ValidationPipe({\n      whitelist: true,            \u002F\u002F 自动剥离 DTO 中未定义的属性\n      forbidNonWhitelisted: true, \u002F\u002F 传入未定义属性时直接报错\n      transform: true,            \u002F\u002F 自动类型转换\n    }),\n  );\n  \n  await app.listen(3000);\n}\n```\n\n### 9.4 自定义管道\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fpipes\u002Fparse-int.pipe.ts\nimport {\n  PipeTransform,\n  Injectable,\n  ArgumentMetadata,\n  BadRequestException,\n} from '@nestjs\u002Fcommon';\n\n@Injectable()\nexport class MyParseIntPipe implements PipeTransform\u003Cstring, number> {\n  transform(value: string, metadata: ArgumentMetadata): number {\n    const val = parseInt(value, 10);\n    if (isNaN(val)) {\n      throw new BadRequestException(`\"${value}\" 不是一个有效的数字`);\n    }\n    return val;\n  }\n}\n```\n\n***\n\n## 10. 守卫 Guard\n\n### 10.1 守卫的作用\n\n​\t守卫决定请求是否应该被路由处理程序处理，通常用于**身份认证和权限控制**。返回 `true` 放行，返回 `false` 拒绝。\n\n### 10.2 创建守卫\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fguards\u002Fauth.guard.ts\nimport {\n  CanActivate,\n  ExecutionContext,\n  Injectable,\n  UnauthorizedException,\n} from '@nestjs\u002Fcommon';\nimport { Request } from 'express';\n\n@Injectable()\nexport class AuthGuard implements CanActivate {\n  canActivate(context: ExecutionContext): boolean {\n    const request = context.switchToHttp().getRequest\u003CRequest>();\n    const token = request.headers['authorization'];\n\n    if (!token) {\n      throw new UnauthorizedException('缺少认证 Token');\n    }\n\n    \u002F\u002F 验证 token 的逻辑...\n    return true;\n  }\n}\n```\n\n### 10.3 角色守卫（基于权限）\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fguards\u002Froles.guard.ts\nimport { Injectable, CanActivate, ExecutionContext } from '@nestjs\u002Fcommon';\nimport { Reflector } from '@nestjs\u002Fcore';\n\n@Injectable()\nexport class RolesGuard implements CanActivate {\n  constructor(private reflector: Reflector) {}\n\n  canActivate(context: ExecutionContext): boolean {\n    \u002F\u002F 获取路由上通过 @Roles() 装饰器设置的角色\n    const requiredRoles = this.reflector.get\u003Cstring[]>(\n      'roles',\n      context.getHandler(),\n    );\n    if (!requiredRoles) {\n      return true; \u002F\u002F 没设置角色要求，直接放行\n    }\n\n    const request = context.switchToHttp().getRequest();\n    const user = request.user; \u002F\u002F 通常由认证中间件设置\n    return requiredRoles.some((role) => user?.roles?.includes(role));\n  }\n}\n```\n\n### 10.4 注册守卫\n\n```typescript\n\u002F\u002F 方式一：全局注册\napp.useGlobalGuards(new AuthGuard());\n\n\u002F\u002F 方式二：控制器级别\n@Controller('users')\n@UseGuards(AuthGuard)\nexport class UsersController {}\n\n\u002F\u002F 方式三：方法级别\n@Get('profile')\n@UseGuards(AuthGuard, RolesGuard)\ngetProfile() {}\n```\n\n***\n\n## 11. 拦截器 Interceptor\n\n### 11.1 拦截器的作用\n\n​\t拦截器可以在方法执行之前\u002F之后绑定额外的逻辑，常用于：统一响应格式、日志记录、缓存、超时处理等。\n\n### 11.2 统一响应格式拦截器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Finterceptors\u002Ftransform.interceptor.ts\nimport {\n  Injectable,\n  NestInterceptor,\n  ExecutionContext,\n  CallHandler,\n} from '@nestjs\u002Fcommon';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs\u002Foperators';\n\n\u002F\u002F 统一响应结构\ninterface Response\u003CT> {\n  code: number;\n  msg: string;\n  data: T;\n}\n\n@Injectable()\nexport class TransformInterceptor\u003CT>\n  implements NestInterceptor\u003CT, Response\u003CT>>\n{\n  intercept(\n    context: ExecutionContext,\n    next: CallHandler,\n  ): Observable\u003CResponse\u003CT>> {\n    return next.handle().pipe(\n      map((data) => ({\n        code: 200,\n        msg: '请求成功',\n        data,\n      })),\n    );\n  }\n}\n```\n\n### 11.3 日志拦截器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Finterceptors\u002Flogging.interceptor.ts\nimport {\n  Injectable,\n  NestInterceptor,\n  ExecutionContext,\n  CallHandler,\n} from '@nestjs\u002Fcommon';\nimport { Observable } from 'rxjs';\nimport { tap } from 'rxjs\u002Foperators';\n\n@Injectable()\nexport class LoggingInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable\u003Cany> {\n    const request = context.switchToHttp().getRequest();\n    const { method, url } = request;\n    const now = Date.now();\n\n    console.log(`[请求开始] ${method} ${url}`);\n\n    return next.handle().pipe(\n      tap(() => {\n        console.log(`[请求结束] ${method} ${url} - ${Date.now() - now}ms`);\n      }),\n    );\n  }\n}\n```\n\n### 11.4 超时拦截器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Finterceptors\u002Ftimeout.interceptor.ts\nimport {\n  Injectable,\n  NestInterceptor,\n  ExecutionContext,\n  CallHandler,\n  RequestTimeoutException,\n} from '@nestjs\u002Fcommon';\nimport { Observable, throwError, TimeoutError } from 'rxjs';\nimport { catchError, timeout } from 'rxjs\u002Foperators';\n\n@Injectable()\nexport class TimeoutInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable\u003Cany> {\n    return next.handle().pipe(\n      timeout(5000), \u002F\u002F 5秒超时\n      catchError((err) => {\n        if (err instanceof TimeoutError) {\n          return throwError(() => new RequestTimeoutException('请求超时'));\n        }\n        return throwError(() => err);\n      }),\n    );\n  }\n}\n```\n\n### 11.5 注册拦截器\n\n```typescript\n\u002F\u002F 全局注册（推荐）\napp.useGlobalInterceptors(new TransformInterceptor());\n\n\u002F\u002F 控制器级别\n@Controller('users')\n@UseInterceptors(LoggingInterceptor)\nexport class UsersController {}\n```\n\n***\n\n## 12. 自定义装饰器\n\n### 12.1 参数装饰器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fdecorators\u002Fcurrent-user.decorator.ts\nimport { createParamDecorator, ExecutionContext } from '@nestjs\u002Fcommon';\n\n\u002F\u002F 获取当前登录用户信息\nexport const CurrentUser = createParamDecorator(\n  (data: string, ctx: ExecutionContext) => {\n    const request = ctx.switchToHttp().getRequest();\n    const user = request.user;\n    \u002F\u002F 如果传了属性名，返回指定属性；否则返回整个 user 对象\n    return data ? user?.[data] : user;\n  },\n);\n\n\u002F\u002F 使用\n@Get('profile')\ngetProfile(@CurrentUser() user: any) {\n  return user;\n}\n\n@Get('name')\ngetName(@CurrentUser('name') name: string) {\n  return { name };\n}\n```\n\n### 12.2 方法\u002F类装饰器\n\n```typescript\n\u002F\u002F src\u002Fcommon\u002Fdecorators\u002Froles.decorator.ts\nimport { SetMetadata } from '@nestjs\u002Fcommon';\n\n\u002F\u002F 角色装饰器\nexport const Roles = (...roles: string[]) => SetMetadata('roles', roles);\n\n\u002F\u002F 使用：配合 RolesGuard\n@Post()\n@Roles('admin')\ncreate(@Body() dto: CreateUserDto) {}\n```\n\n### 12.3 组合装饰器\n\n```typescript\n\u002F\u002F 将多个装饰器组合为一个\nimport { applyDecorators, UseGuards } from '@nestjs\u002Fcommon';\n\nexport function Auth(...roles: string[]) {\n  return applyDecorators(\n    Roles(...roles),\n    UseGuards(AuthGuard, RolesGuard),\n  );\n}\n\n\u002F\u002F 使用：一个装饰器搞定认证 + 权限\n@Post()\n@Auth('admin')\ncreate(@Body() dto: CreateUserDto) {}\n```\n\n***\n\n## 13. 数据库集成（TypeORM）\n\n### 13.1 安装依赖\n\n```bash\n# TypeORM + MySQL\nnpm i @nestjs\u002Ftypeorm typeorm mysql2\n\n# 如果使用 PostgreSQL\n# npm i @nestjs\u002Ftypeorm typeorm pg\n```\n\n### 13.2 配置数据库连接\n\n```typescript\n\u002F\u002F src\u002Fapp.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { TypeOrmModule } from '@nestjs\u002Ftypeorm';\n\n@Module({\n  imports: [\n    TypeOrmModule.forRoot({\n      type: 'mysql',\n      host: 'localhost',\n      port: 3306,\n      username: 'root',\n      password: '123456',\n      database: 'nest_db',\n      entities: [__dirname + '\u002F**\u002F*.entity{.ts,.js}'], \u002F\u002F 自动加载实体\n      synchronize: true, \u002F\u002F 开发环境自动同步表结构（生产环境务必关闭！）\n    }),\n    UsersModule,\n  ],\n})\nexport class AppModule {}\n```\n\n### 13.3 定义实体\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fentities\u002Fuser.entity.ts\nimport {\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n} from 'typeorm';\n\n@Entity('users') \u002F\u002F 表名\nexport class User {\n  @PrimaryGeneratedColumn() \u002F\u002F 自增主键\n  id: number;\n\n  @Column({ length: 50, comment: '用户名' })\n  username: string;\n\n  @Column({ comment: '密码' })\n  password: string;\n\n  @Column({ nullable: true, comment: '邮箱' })\n  email: string;\n\n  @Column({ default: 1, comment: '状态 1正常 0禁用' })\n  status: number;\n\n  @Column({ type: 'enum', enum: ['admin', 'user'], default: 'user' })\n  role: string;\n\n  @CreateDateColumn({ comment: '创建时间' })\n  createTime: Date;\n\n  @UpdateDateColumn({ comment: '更新时间' })\n  updateTime: Date;\n}\n```\n\n### 13.4 在模块中注册实体\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { TypeOrmModule } from '@nestjs\u002Ftypeorm';\nimport { User } from '.\u002Fentities\u002Fuser.entity';\nimport { UsersController } from '.\u002Fusers.controller';\nimport { UsersService } from '.\u002Fusers.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([User])], \u002F\u002F 注册实体\n  controllers: [UsersController],\n  providers: [UsersService],\n  exports: [UsersService],\n})\nexport class UsersModule {}\n```\n\n### 13.5 Service 中使用 Repository\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fusers.service.ts\nimport { Injectable, NotFoundException } from '@nestjs\u002Fcommon';\nimport { InjectRepository } from '@nestjs\u002Ftypeorm';\nimport { Repository, Like } from 'typeorm';\nimport { User } from '.\u002Fentities\u002Fuser.entity';\n\n@Injectable()\nexport class UsersService {\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository\u003CUser>,\n  ) {}\n\n  \u002F\u002F 查询全部\n  async findAll(): Promise\u003CUser[]> {\n    return await this.userRepository.find();\n  }\n\n  \u002F\u002F 分页查询\n  async findPage(page: number, limit: number) {\n    const [data, total] = await this.userRepository.findAndCount({\n      skip: (page - 1) * limit,\n      take: limit,\n      order: { createTime: 'DESC' },\n    });\n    return { data, total, page, limit };\n  }\n\n  \u002F\u002F 条件查询\n  async findByUsername(username: string): Promise\u003CUser[]> {\n    return await this.userRepository.find({\n      where: { username: Like(`%${username}%`) },\n    });\n  }\n\n  \u002F\u002F 根据 ID 查询\n  async findOne(id: number): Promise\u003CUser> {\n    const user = await this.userRepository.findOneBy({ id });\n    if (!user) {\n      throw new NotFoundException(`用户 ID ${id} 不存在`);\n    }\n    return user;\n  }\n\n  \u002F\u002F 新增\n  async create(createUserDto: Partial\u003CUser>): Promise\u003CUser> {\n    const user = this.userRepository.create(createUserDto);\n    return await this.userRepository.save(user);\n  }\n\n  \u002F\u002F 修改\n  async update(id: number, updateUserDto: Partial\u003CUser>): Promise\u003CUser> {\n    await this.userRepository.update(id, updateUserDto);\n    return this.findOne(id);\n  }\n\n  \u002F\u002F 删除\n  async remove(id: number): Promise\u003Cvoid> {\n    const result = await this.userRepository.delete(id);\n    if (result.affected === 0) {\n      throw new NotFoundException(`用户 ID ${id} 不存在`);\n    }\n  }\n}\n```\n\n### 13.6 实体关系\n\n```typescript\n\u002F\u002F 一对多关系示例\n\u002F\u002F 一个用户有多篇文章\n\n\u002F\u002F user.entity.ts\n@Entity()\nexport class User {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  username: string;\n\n  @OneToMany(() => Article, (article) => article.author)\n  articles: Article[];\n}\n\n\u002F\u002F article.entity.ts\n@Entity()\nexport class Article {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  title: string;\n\n  @Column('text')\n  content: string;\n\n  @ManyToOne(() => User, (user) => user.articles)\n  @JoinColumn({ name: 'author_id' })\n  author: User;\n}\n\n\u002F\u002F 查询时关联加载\nconst users = await this.userRepository.find({\n  relations: ['articles'],\n});\n```\n\n***\n\n## 14. 数据验证与转换（DTO）\n\n### 14.1 什么是 DTO\n\n​\tDTO（Data Transfer Object）数据传输对象，用于定义接口接收数据的结构和验证规则。配合 `class-validator` 和 `class-transformer` 使用。\n\n### 14.2 安装依赖\n\n```bash\nnpm i class-validator class-transformer\n```\n\n### 14.3 定义 DTO\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fdto\u002Fcreate-user.dto.ts\nimport {\n  IsString,\n  IsNotEmpty,\n  IsEmail,\n  IsOptional,\n  MinLength,\n  MaxLength,\n  IsEnum,\n} from 'class-validator';\n\nexport class CreateUserDto {\n  @IsString({ message: '用户名必须是字符串' })\n  @IsNotEmpty({ message: '用户名不能为空' })\n  @MinLength(2, { message: '用户名最少2个字符' })\n  @MaxLength(20, { message: '用户名最多20个字符' })\n  username: string;\n\n  @IsString()\n  @IsNotEmpty({ message: '密码不能为空' })\n  @MinLength(6, { message: '密码最少6个字符' })\n  password: string;\n\n  @IsEmail({}, { message: '邮箱格式不正确' })\n  @IsOptional() \u002F\u002F 可选字段\n  email?: string;\n\n  @IsEnum(['admin', 'user'], { message: '角色只能是 admin 或 user' })\n  @IsOptional()\n  role?: string;\n}\n```\n\n```typescript\n\u002F\u002F src\u002Fusers\u002Fdto\u002Fupdate-user.dto.ts\nimport { PartialType } from '@nestjs\u002Fmapped-types';\nimport { CreateUserDto } from '.\u002Fcreate-user.dto';\n\n\u002F\u002F PartialType 将所有字段变为可选\nexport class UpdateUserDto extends PartialType(CreateUserDto) {}\n```\n\n### 14.4 在控制器中使用 DTO\n\n```typescript\n@Controller('users')\nexport class UsersController {\n  constructor(private readonly usersService: UsersService) {}\n\n  @Post()\n  create(@Body() createUserDto: CreateUserDto) {\n    \u002F\u002F 到达这里时，数据已经过验证\n    return this.usersService.create(createUserDto);\n  }\n\n  @Put(':id')\n  update(\n    @Param('id', ParseIntPipe) id: number,\n    @Body() updateUserDto: UpdateUserDto,\n  ) {\n    return this.usersService.update(id, updateUserDto);\n  }\n}\n```\n\n### 14.5 常用验证装饰器\n\n| 装饰器                | 说明           |\n| --------------------- | -------------- |\n| `@IsString()`         | 必须是字符串   |\n| `@IsNumber()`         | 必须是数字     |\n| `@IsInt()`            | 必须是整数     |\n| `@IsBoolean()`        | 必须是布尔值   |\n| `@IsEmail()`          | 必须是邮箱     |\n| `@IsNotEmpty()`       | 不能为空       |\n| `@IsOptional()`       | 可选字段       |\n| `@MinLength(n)`       | 最小长度       |\n| `@MaxLength(n)`       | 最大长度       |\n| `@Min(n)`             | 最小值         |\n| `@Max(n)`             | 最大值         |\n| `@IsEnum(entity)`     | 枚举值验证     |\n| `@IsArray()`          | 必须是数组     |\n| `@IsDate()`           | 必须是日期     |\n| `@Matches(regex)`     | 正则匹配       |\n| `@IsUrl()`            | 必须是URL      |\n| `@ValidateNested()`   | 嵌套对象验证   |\n\n***\n\n## 15. 身份认证（JWT）\n\n### 15.1 安装依赖\n\n```bash\nnpm i @nestjs\u002Fjwt @nestjs\u002Fpassport passport passport-jwt\nnpm i -D @types\u002Fpassport-jwt\n```\n\n### 15.2 Auth 模块\n\n```typescript\n\u002F\u002F src\u002Fauth\u002Fauth.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { JwtModule } from '@nestjs\u002Fjwt';\nimport { PassportModule } from '@nestjs\u002Fpassport';\nimport { AuthController } from '.\u002Fauth.controller';\nimport { AuthService } from '.\u002Fauth.service';\nimport { JwtStrategy } from '.\u002Fstrategies\u002Fjwt.strategy';\nimport { UsersModule } from '..\u002Fusers\u002Fusers.module';\n\n@Module({\n  imports: [\n    UsersModule,\n    PassportModule,\n    JwtModule.register({\n      secret: 'your-secret-key', \u002F\u002F 实际项目中应放到环境变量\n      signOptions: { expiresIn: '7d' }, \u002F\u002F token 有效期 7 天\n    }),\n  ],\n  controllers: [AuthController],\n  providers: [AuthService, JwtStrategy],\n  exports: [AuthService],\n})\nexport class AuthModule {}\n```\n\n### 15.3 Auth Service\n\n```typescript\n\u002F\u002F src\u002Fauth\u002Fauth.service.ts\nimport { Injectable, UnauthorizedException } from '@nestjs\u002Fcommon';\nimport { JwtService } from '@nestjs\u002Fjwt';\nimport { UsersService } from '..\u002Fusers\u002Fusers.service';\nimport * as bcrypt from 'bcryptjs'; \u002F\u002F npm i bcryptjs @types\u002Fbcryptjs\n\n@Injectable()\nexport class AuthService {\n  constructor(\n    private readonly usersService: UsersService,\n    private readonly jwtService: JwtService,\n  ) {}\n\n  \u002F\u002F 登录\n  async login(username: string, password: string) {\n    const user = await this.usersService.findByUsername(username);\n    if (!user) {\n      throw new UnauthorizedException('用户名或密码错误');\n    }\n\n    const isPasswordValid = await bcrypt.compare(password, user.password);\n    if (!isPasswordValid) {\n      throw new UnauthorizedException('用户名或密码错误');\n    }\n\n    \u002F\u002F 生成 JWT Token\n    const payload = { sub: user.id, username: user.username, role: user.role };\n    return {\n      access_token: this.jwtService.sign(payload),\n      user: { id: user.id, username: user.username, role: user.role },\n    };\n  }\n\n  \u002F\u002F 注册\n  async register(username: string, password: string) {\n    \u002F\u002F 密码加密\n    const hashedPassword = await bcrypt.hash(password, 10);\n    return this.usersService.create({\n      username,\n      password: hashedPassword,\n    });\n  }\n}\n```\n\n### 15.4 JWT 策略\n\n```typescript\n\u002F\u002F src\u002Fauth\u002Fstrategies\u002Fjwt.strategy.ts\nimport { Injectable, UnauthorizedException } from '@nestjs\u002Fcommon';\nimport { PassportStrategy } from '@nestjs\u002Fpassport';\nimport { ExtractJwt, Strategy } from 'passport-jwt';\n\n@Injectable()\nexport class JwtStrategy extends PassportStrategy(Strategy) {\n  constructor() {\n    super({\n      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n      ignoreExpiration: false,\n      secretOrKey: 'your-secret-key',\n    });\n  }\n\n  \u002F\u002F 验证通过后，返回值会被挂载到 request.user\n  async validate(payload: any) {\n    return {\n      id: payload.sub,\n      username: payload.username,\n      role: payload.role,\n    };\n  }\n}\n```\n\n### 15.5 Auth Controller\n\n```typescript\n\u002F\u002F src\u002Fauth\u002Fauth.controller.ts\nimport { Controller, Post, Body, UseGuards, Get } from '@nestjs\u002Fcommon';\nimport { AuthGuard } from '@nestjs\u002Fpassport';\nimport { AuthService } from '.\u002Fauth.service';\nimport { CurrentUser } from '..\u002Fcommon\u002Fdecorators\u002Fcurrent-user.decorator';\n\n@Controller('auth')\nexport class AuthController {\n  constructor(private readonly authService: AuthService) {}\n\n  @Post('login')\n  login(@Body() body: { username: string; password: string }) {\n    return this.authService.login(body.username, body.password);\n  }\n\n  @Post('register')\n  register(@Body() body: { username: string; password: string }) {\n    return this.authService.register(body.username, body.password);\n  }\n\n  \u002F\u002F 需要登录才能访问\n  @Get('profile')\n  @UseGuards(AuthGuard('jwt'))\n  getProfile(@CurrentUser() user: any) {\n    return user;\n  }\n}\n```\n\n***\n\n## 16. 文件上传\n\n### 16.1 安装依赖\n\n```bash\nnpm i @nestjs\u002Fplatform-express multer\nnpm i -D @types\u002Fmulter\n```\n\n### 16.2 文件上传接口\n\n```typescript\n\u002F\u002F src\u002Fupload\u002Fupload.controller.ts\nimport {\n  Controller,\n  Post,\n  UseInterceptors,\n  UploadedFile,\n  UploadedFiles,\n  ParseFilePipe,\n  MaxFileSizeValidator,\n  FileTypeValidator,\n} from '@nestjs\u002Fcommon';\nimport { FileInterceptor, FilesInterceptor } from '@nestjs\u002Fplatform-express';\nimport { diskStorage } from 'multer';\nimport { extname } from 'path';\n\n@Controller('upload')\nexport class UploadController {\n\n  \u002F\u002F 单文件上传\n  @Post('single')\n  @UseInterceptors(\n    FileInterceptor('file', {\n      storage: diskStorage({\n        destination: '.\u002Fuploads',\n        filename: (req, file, callback) => {\n          \u002F\u002F 自定义文件名：时间戳 + 随机数 + 原始扩展名\n          const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;\n          callback(null, `${uniqueName}${extname(file.originalname)}`);\n        },\n      }),\n    }),\n  )\n  uploadSingle(\n    @UploadedFile(\n      new ParseFilePipe({\n        validators: [\n          new MaxFileSizeValidator({ maxSize: 5 * 1024 * 1024 }), \u002F\u002F 5MB\n          new FileTypeValidator({ fileType: \u002Fimage\\\u002F(png|jpeg|jpg|gif)\u002F }),\n        ],\n      }),\n    )\n    file: Express.Multer.File,\n  ) {\n    return {\n      originalname: file.originalname,\n      filename: file.filename,\n      path: file.path,\n      size: file.size,\n    };\n  }\n\n  \u002F\u002F 多文件上传\n  @Post('multiple')\n  @UseInterceptors(FilesInterceptor('files', 10)) \u002F\u002F 最多10个文件\n  uploadMultiple(@UploadedFiles() files: Express.Multer.File[]) {\n    return files.map((file) => ({\n      originalname: file.originalname,\n      filename: file.filename,\n      size: file.size,\n    }));\n  }\n}\n```\n\n### 16.3 静态文件托管\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { NestFactory } from '@nestjs\u002Fcore';\nimport { NestExpressApplication } from '@nestjs\u002Fplatform-express';\nimport { join } from 'path';\n\nasync function bootstrap() {\n  const app = await NestFactory.create\u003CNestExpressApplication>(AppModule);\n  \n  \u002F\u002F 静态文件托管，访问 http:\u002F\u002Flocalhost:3000\u002Fuploads\u002Fxxx.jpg\n  app.useStaticAssets(join(__dirname, '..', 'uploads'), {\n    prefix: '\u002Fuploads',\n  });\n  \n  await app.listen(3000);\n}\n```\n\n***\n\n## 17. Swagger 接口文档\n\n### 17.1 安装依赖\n\n```bash\nnpm i @nestjs\u002Fswagger\n```\n\n### 17.2 配置 Swagger\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { SwaggerModule, DocumentBuilder } from '@nestjs\u002Fswagger';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  \u002F\u002F Swagger 配置\n  const config = new DocumentBuilder()\n    .setTitle('我的API文档')\n    .setDescription('NestJS 项目接口文档')\n    .setVersion('1.0')\n    .addBearerAuth() \u002F\u002F 添加 JWT 认证\n    .build();\n\n  const document = SwaggerModule.createDocument(app, config);\n  SwaggerModule.setup('api-docs', app, document); \u002F\u002F 访问 \u002Fapi-docs\n\n  await app.listen(3000);\n  console.log('Swagger 文档: http:\u002F\u002Flocalhost:3000\u002Fapi-docs');\n}\n```\n\n### 17.3 使用装饰器标注接口\n\n```typescript\nimport { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody } from '@nestjs\u002Fswagger';\n\n@ApiTags('用户管理') \u002F\u002F 分组标签\n@Controller('users')\nexport class UsersController {\n\n  @Get()\n  @ApiOperation({ summary: '获取所有用户', description: '返回用户列表' })\n  @ApiResponse({ status: 200, description: '查询成功' })\n  findAll() {\n    return this.usersService.findAll();\n  }\n\n  @Post()\n  @ApiBearerAuth() \u002F\u002F 标记需要 Token\n  @ApiOperation({ summary: '创建用户' })\n  @ApiBody({ type: CreateUserDto })\n  @ApiResponse({ status: 201, description: '创建成功' })\n  @ApiResponse({ status: 400, description: '参数错误' })\n  create(@Body() dto: CreateUserDto) {\n    return this.usersService.create(dto);\n  }\n}\n```\n\n### 17.4 DTO 中添加 Swagger 描述\n\n```typescript\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs\u002Fswagger';\n\nexport class CreateUserDto {\n  @ApiProperty({ description: '用户名', example: 'zhangsan', minLength: 2 })\n  @IsString()\n  @IsNotEmpty()\n  username: string;\n\n  @ApiProperty({ description: '密码', example: '123456', minLength: 6 })\n  @IsString()\n  @MinLength(6)\n  password: string;\n\n  @ApiPropertyOptional({ description: '邮箱', example: 'zhangsan@example.com' })\n  @IsEmail()\n  @IsOptional()\n  email?: string;\n}\n```\n\n​\t启动后访问 http:\u002F\u002Flocalhost:3000\u002Fapi-docs 即可看到自动生成的接口文档，支持在线测试。\n\n***\n\n## 18. 配置管理\n\n### 18.1 安装依赖\n\n```bash\nnpm i @nestjs\u002Fconfig\n```\n\n### 18.2 环境变量配置\n\n```env\n# .env\nDB_HOST=localhost\nDB_PORT=3306\nDB_USERNAME=root\nDB_PASSWORD=123456\nDB_DATABASE=nest_db\nJWT_SECRET=my-jwt-secret-key\nJWT_EXPIRES_IN=7d\nPORT=3000\n```\n\n### 18.3 配置模块注册\n\n```typescript\n\u002F\u002F src\u002Fapp.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { ConfigModule, ConfigService } from '@nestjs\u002Fconfig';\nimport { TypeOrmModule } from '@nestjs\u002Ftypeorm';\n\n@Module({\n  imports: [\n    \u002F\u002F 全局配置模块\n    ConfigModule.forRoot({\n      isGlobal: true,      \u002F\u002F 全局可用，无需在每个模块中导入\n      envFilePath: '.env',  \u002F\u002F 指定 .env 文件路径\n    }),\n\n    \u002F\u002F 使用 ConfigService 动态配置数据库\n    TypeOrmModule.forRootAsync({\n      imports: [ConfigModule],\n      inject: [ConfigService],\n      useFactory: (configService: ConfigService) => ({\n        type: 'mysql',\n        host: configService.get('DB_HOST'),\n        port: configService.get\u003Cnumber>('DB_PORT'),\n        username: configService.get('DB_USERNAME'),\n        password: configService.get('DB_PASSWORD'),\n        database: configService.get('DB_DATABASE'),\n        autoLoadEntities: true,\n        synchronize: true,\n      }),\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### 18.4 在 Service 中使用配置\n\n```typescript\nimport { Injectable } from '@nestjs\u002Fcommon';\nimport { ConfigService } from '@nestjs\u002Fconfig';\n\n@Injectable()\nexport class AuthService {\n  constructor(private configService: ConfigService) {}\n\n  getJwtSecret(): string {\n    return this.configService.get\u003Cstring>('JWT_SECRET');\n  }\n}\n```\n\n***\n\n## 19. 部署与优化\n\n### 19.1 构建命令\n\n```bash\n# 开发模式（热重载）\nnpm run start:dev\n\n# 调试模式\nnpm run start:debug\n\n# 生产构建\nnpm run build\n\n# 运行生产版本\nnpm run start:prod\n```\n\n### 19.2 生产环境 main.ts 完整配置\n\n```typescript\n\u002F\u002F src\u002Fmain.ts\nimport { NestFactory } from '@nestjs\u002Fcore';\nimport { ValidationPipe } from '@nestjs\u002Fcommon';\nimport { SwaggerModule, DocumentBuilder } from '@nestjs\u002Fswagger';\nimport { AppModule } from '.\u002Fapp.module';\nimport { HttpExceptionFilter } from '.\u002Fcommon\u002Ffilters\u002Fhttp-exception.filter';\nimport { TransformInterceptor } from '.\u002Fcommon\u002Finterceptors\u002Ftransform.interceptor';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  \u002F\u002F 全局路由前缀\n  app.setGlobalPrefix('api');\n\n  \u002F\u002F 跨域\n  app.enableCors();\n\n  \u002F\u002F 全局验证管道\n  app.useGlobalPipes(\n    new ValidationPipe({\n      whitelist: true,\n      forbidNonWhitelisted: true,\n      transform: true,\n    }),\n  );\n\n  \u002F\u002F 全局异常过滤器\n  app.useGlobalFilters(new HttpExceptionFilter());\n\n  \u002F\u002F 全局拦截器（统一响应格式）\n  app.useGlobalInterceptors(new TransformInterceptor());\n\n  \u002F\u002F Swagger 文档（仅开发环境）\n  if (process.env.NODE_ENV !== 'production') {\n    const config = new DocumentBuilder()\n      .setTitle('API 文档')\n      .setVersion('1.0')\n      .addBearerAuth()\n      .build();\n    const document = SwaggerModule.createDocument(app, config);\n    SwaggerModule.setup('api-docs', app, document);\n  }\n\n  const port = process.env.PORT || 3000;\n  await app.listen(port);\n  console.log(`服务器运行在 http:\u002F\u002Flocalhost:${port}`);\n}\nbootstrap();\n```\n\n### 19.3 Docker 部署\n\n```dockerfile\n# Dockerfile\nFROM node:18-alpine\n\nWORKDIR \u002Fapp\n\nCOPY package*.json .\u002F\nRUN npm ci --only=production\n\nCOPY . .\nRUN npm run build\n\nEXPOSE 3000\n\nCMD [\"node\", \"dist\u002Fmain.js\"]\n```\n\n```yaml\n# docker-compose.yml\nversion: '3.8'\nservices:\n  app:\n    build: .\n    ports:\n      - '3000:3000'\n    env_file:\n      - .env\n    depends_on:\n      - mysql\n\n  mysql:\n    image: mysql:8.0\n    environment:\n      MYSQL_ROOT_PASSWORD: 123456\n      MYSQL_DATABASE: nest_db\n    ports:\n      - '3306:3306'\n    volumes:\n      - mysql_data:\u002Fvar\u002Flib\u002Fmysql\n\nvolumes:\n  mysql_data:\n```\n\n### 19.4 PM2 部署\n\n```javascript\n\u002F\u002F ecosystem.config.js\nmodule.exports = {\n  apps: [\n    {\n      name: 'nest-app',\n      script: 'dist\u002Fmain.js',\n      instances: 'max',          \u002F\u002F 多进程\n      exec_mode: 'cluster',      \u002F\u002F 集群模式\n      env_production: {\n        NODE_ENV: 'production',\n        PORT: 3000,\n      },\n    },\n  ],\n};\n```\n\n```bash\n# 启动\npm2 start ecosystem.config.js --env production\n\n# 查看日志\npm2 logs nest-app\n\n# 重启\npm2 restart nest-app\n```\n\n***\n\n## 最佳实践总结\n\n### 项目开发流程\n\n```\n1. nest g resource xxx      → 生成 CRUD 资源\n2. 定义 Entity 实体          → 数据库表结构\n3. 定义 DTO                  → 请求参数验证\n4. 编写 Service 业务逻辑     → 操作数据库\n5. 编写 Controller           → 处理请求路由\n6. 注册到 Module             → 模块化管理\n7. 添加 Swagger 注解         → 自动生成文档\n```\n\n### 常用装饰器速查表\n\n| 装饰器               | 作用                           |\n| -------------------- | ------------------------------ |\n| `@Controller()`      | 标记控制器类                   |\n| `@Injectable()`      | 标记可注入的 Provider          |\n| `@Module()`          | 标记模块                       |\n| `@Get() @Post()` 等  | HTTP 请求方法                  |\n| `@Param()`           | 获取路由参数                   |\n| `@Query()`           | 获取查询参数                   |\n| `@Body()`            | 获取请求体                     |\n| `@Headers()`         | 获取请求头                     |\n| `@UseGuards()`       | 使用守卫                       |\n| `@UseInterceptors()` | 使用拦截器                     |\n| `@UseFilters()`      | 使用异常过滤器                 |\n| `@UsePipes()`        | 使用管道                       |\n| `@SetMetadata()`     | 设置元数据                     |\n\n***\n\n## 学习资源\n\n- [NestJS 官方文档](https:\u002F\u002Fdocs.nestjs.com)\n- [NestJS 中文文档](https:\u002F\u002Fdocs.nestjs.cn)\n- [TypeORM 文档](https:\u002F\u002Ftypeorm.io)\n- [class-validator 文档](https:\u002F\u002Fgithub.com\u002Ftypestack\u002Fclass-validator)\n- [NestJS GitHub](https:\u002F\u002Fgithub.com\u002Fnestjs\u002Fnest)\n\n***\n\n## 学习路线建议\n\n1. **入门阶段**\n   - 掌握 TypeScript 基础和装饰器语法\n   - 理解 Controller \u002F Service \u002F Module 三件套\n   - 学会使用 CLI 快速生成代码\n   - 完成简单的 CRUD 接口\n\n2. **进阶阶段**\n   - 掌握 DTO 验证与管道\n   - 学会中间件、守卫、拦截器、异常过滤器\n   - 集成 TypeORM 数据库操作\n   - 实现 JWT 身份认证\n\n3. **高级阶段**\n   - Swagger 自动文档生成\n   - 配置管理与环境变量\n   - 文件上传与静态资源\n   - 自定义装饰器与高级 DI\n\n4. **实战阶段**\n   - 完整项目开发（用户系统 + 权限管理）\n   - Docker 部署与 PM2 运维\n   - 微服务架构\n   - WebSocket 实时通信\n",null,"2",132,"2026-04-07T16:59:45.709Z","2026-04-07T16:59:45.712Z","2026-06-10T13:42:37.588Z","0",{"id":59,"categoryName":66,"slug":67,"description":68,"sort":31,"isEnable":32,"createTime":69,"updateTime":70,"deleteTime":31},"后端开发","backend-engineering","222","2026-04-03T02:36:33.016Z","2026-04-07T16:38:50.279Z",[72],{"articleId":54,"tagId":73,"createTime":62,"tag":74},"4",{"id":73,"tagName":75,"slug":75,"themeColor":76,"description":75,"createTime":77,"updateTime":77,"deleteTime":64},"Nest","#ff00c3","2026-04-07T16:39:43.674Z",{"id":79,"title":80,"slug":81,"coverUrl":15,"summary":15,"content":82,"htmlContent":58,"categoryId":83,"viewCount":84,"likeCount":32,"isTop":31,"isPublish":32,"seoKeywords":15,"seoDescription":15,"publishTime":85,"createTime":86,"updateTime":87,"deleteTime":64,"category":88,"tags":94},"17","Nuxt.js 学习笔记","nuxt学习笔记","# Nuxt.js 学习笔记\n\n## 目录\n\n1. [Nuxt.js 简介](#1-nuxtjs-简介)\n2. [环境搭建](#2-环境搭建)\n3. [项目结构](#3-项目结构)\n4. [路由系统](#4-路由系统)\n5. [视图与布局](#5-视图与布局)\n6. [数据获取](#6-数据获取)\n7. [状态管理](#7-状态管理)\n8. [中间件](#8-中间件)\n9. [插件系统](#9-插件系统)\n10. [模块系统](#10-模块系统)\n11. [部署与优化](#11-部署与优化)\n\n***\n\n## 1. Nuxt.js 简介\n\n### 1.1 什么是 Nuxt.js\n\nNuxt.js 是基于 Vue.js 的通用应用框架，主要用于构建服务端渲染（SSR）应用。\n\n**核心特性：**\n\n- 服务端渲染（SSR）\n- 静态站点生成（SSG）\n- 自动路由生成\n- 代码分割\n- 热模块替换\n- 强大的模块生态系统\n\n### 1.2 渲染模式对比\n\n| 模式     | 说明         | 适用场景      |\n| ------ | ---------- | --------- |\n| SSR    | 服务端渲染，首屏直出 | SEO要求高的应用 |\n| SSG    | 静态站点生成     | 博客、文档站    |\n| CSR    | 客户端渲染      | 后台管理系统    |\n| Hybrid | 混合渲染       | 复杂业务场景    |\n\n***\n\n## 2. 环境搭建\n\n### 2.1 系统要求\n\n- Node.js >= 18.0.0\n- 包管理器：npm \u002F yarn \u002F pnpm\n\n### 2.2 创建项目\n\n```bash\n# 使用 npx\nnpx nuxi@latest init my-nuxt-app\n\n# 或使用 pnpm\npnpm dlx nuxi@latest init my-nuxt-app\n\n# 进入项目目录\ncd my-nuxt-app\n\n# 安装依赖\nnpm install\n\n# 启动开发服务器\nnpm run dev\n```\n\n### 2.3 配置文件\n\n`nuxt.config.ts` 核心配置：\n\n```typescript\nexport default defineNuxtConfig({\n  devtools: { enabled: true },\n  \n  app: {\n    head: {\n      title: 'My Nuxt App',\n      meta: [\n        { name: 'description', content: 'My amazing Nuxt application' }\n      ]\n    }\n  },\n  \n  modules: [\n    '@nuxtjs\u002Ftailwindcss'\n  ],\n  \n  runtimeConfig: {\n    public: {\n      apiBase: process.env.NUXT_PUBLIC_API_BASE\n    }\n  }\n})\n```\n\n***\n\n## 3. 项目结构\n\n### 3.1 目录结构说明\n\n```\nmy-nuxt-app\u002F\n├── .nuxt\u002F              # 构建生成目录（自动生成）\n├── .output\u002F            # 构建输出目录\n├── app.vue             # 根组件\n├── nuxt.config.ts      # Nuxt 配置文件\n├── package.json\n├── pages\u002F              # 页面目录（自动路由）\n├── components\u002F         # 组件目录（自动导入）\n├── composables\u002F        # 组合式函数（自动导入）\n├── layouts\u002F            # 布局组件\n├── plugins\u002F            # 插件目录\n├── middleware\u002F         # 中间件目录\n├── public\u002F             # 静态资源\n├── assets\u002F             # 需要构建的资源\n├── server\u002F             # 服务端代码\n│   ├── api\u002F           # API 路由\n│   ├── routes\u002F        # 服务端路由\n│   └── middleware\u002F    # 服务端中间件\n└── utils\u002F              # 工具函数\n```\n\n### 3.2 自动导入机制\n\nNuxt 自动导入以下目录中的模块：\n\n- `components\u002F` - 组件\n- `composables\u002F` - 组合式函数\n- `utils\u002F` - 工具函数\n- Vue 3 API（ref, computed, watch 等）\n\n***\n\n## 4. 路由系统\n\n### 4.1 文件路由\n\nNuxt 基于文件系统自动生成路由：\n\n```\npages\u002F\n├── index.vue           → \u002F\n├── about.vue           → \u002Fabout\n├── users\u002F\n│   ├── index.vue       → \u002Fusers\n│   └── [id].vue        → \u002Fusers\u002F:id\n└── blog\u002F\n    └── [slug].vue      → \u002Fblog\u002F:slug\n```\n\n### 4.2 动态路由\n\n```vue\n\u003C!-- pages\u002Fusers\u002F[id].vue -->\n\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst userId = route.params.id\n\nconst { data: user } = await useFetch(`\u002Fapi\u002Fusers\u002F${userId}`)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>User: {{ user?.name }}\u003C\u002Fh1>\n    \u003Cp>ID: {{ userId }}\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 4.3 路由导航\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst router = useRouter()\nconst route = useRoute()\n\nfunction navigateToAbout() {\n  router.push('\u002Fabout')\n}\n\nfunction goBack() {\n  router.back()\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cnav>\n    \u003CNuxtLink to=\"\u002F\">首页\u003C\u002FNuxtLink>\n    \u003CNuxtLink to=\"\u002Fabout\">关于\u003C\u002FNuxtLink>\n    \u003CNuxtLink :to=\"`\u002Fusers\u002F${userId}`\">用户详情\u003C\u002FNuxtLink>\n    \n    \u003Cbutton @click=\"navigateToAbout\">跳转关于页\u003C\u002Fbutton>\n    \u003Cbutton @click=\"goBack\">返回\u003C\u002Fbutton>\n  \u003C\u002Fnav>\n\u003C\u002Ftemplate>\n```\n\n### 4.4 嵌套路由\n\n```\npages\u002F\n├── parent.vue\n└── parent\u002F\n    ├── index.vue\n    └── child.vue\n```\n\n```vue\n\u003C!-- pages\u002Fparent.vue -->\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>Parent Page\u003C\u002Fh1>\n    \u003CNuxtPage \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 4.5 路由中间件\n\n```typescript\n\u002F\u002F middleware\u002Fauth.ts\nexport default defineNuxtRouteMiddleware((to, from) => {\n  const isAuthenticated = useState('auth')\n  \n  if (!isAuthenticated.value) {\n    return navigateTo('\u002Flogin')\n  }\n})\n```\n\n```vue\n\u003C!-- pages\u002Fdashboard.vue -->\n\u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n  middleware: 'auth'\n})\n\u003C\u002Fscript>\n```\n\n***\n\n## 5. 视图与布局\n\n### 5.1 布局系统\n\n```vue\n\u003C!-- layouts\u002Fdefault.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"layout\">\n    \u003Cheader>\n      \u003Cnav>\n        \u003CNuxtLink to=\"\u002F\">首页\u003C\u002FNuxtLink>\n        \u003CNuxtLink to=\"\u002Fabout\">关于\u003C\u002FNuxtLink>\n      \u003C\u002Fnav>\n    \u003C\u002Fheader>\n    \n    \u003Cmain>\n      \u003Cslot \u002F>\n    \u003C\u002Fmain>\n    \n    \u003Cfooter>\n      \u003Cp>&copy; 2024 My App\u003C\u002Fp>\n    \u003C\u002Ffooter>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n```vue\n\u003C!-- layouts\u002Fadmin.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"admin-layout\">\n    \u003Caside>侧边栏\u003C\u002Faside>\n    \u003Cmain>\n      \u003Cslot \u002F>\n    \u003C\u002Fmain>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 5.2 页面使用布局\n\n```vue\n\u003C!-- pages\u002Fadmin\u002Fdashboard.vue -->\n\u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n  layout: 'admin'\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>Admin Dashboard\u003C\u002Fh1>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 5.3 页面元信息\n\n```vue\n\u003Cscript setup lang=\"ts\">\nuseHead({\n  title: '页面标题',\n  meta: [\n    { name: 'description', content: '页面描述' },\n    { property: 'og:title', content: '分享标题' }\n  ]\n})\n\u003C\u002Fscript>\n```\n\n***\n\n## 6. 数据获取\n\n### 6.1 useFetch\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data, pending, error, refresh } = await useFetch('\u002Fapi\u002Fusers')\n\nif (error.value) {\n  console.error('获取数据失败:', error.value)\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">加载中...\u003C\u002Fdiv>\n  \u003Cdiv v-else-if=\"error\">加载失败\u003C\u002Fdiv>\n  \u003Cdiv v-else>\n    \u003Cul>\n      \u003Cli v-for=\"user in data\" :key=\"user.id\">\n        {{ user.name }}\n      \u003C\u002Fli>\n    \u003C\u002Ful>\n    \u003Cbutton @click=\"refresh\">刷新\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 6.2 useAsyncData\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data } = await useAsyncData('users', () => \n  $fetch('\u002Fapi\u002Fusers')\n)\n\u003C\u002Fscript>\n```\n\n### 6.3 useLazyFetch\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data, pending } = await useLazyFetch('\u002Fapi\u002Farticles')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">\n    \u003CSkeletonLoader \u002F>\n  \u003C\u002Fdiv>\n  \u003CArticleList v-else :articles=\"data\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n### 6.4 服务端 API\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Fusers.ts\nexport default defineEventHandler(async (event) => {\n  const method = getMethod(event)\n  \n  if (method === 'GET') {\n    const users = await db.users.findMany()\n    return users\n  }\n  \n  if (method === 'POST') {\n    const body = await readBody(event)\n    const user = await db.users.create({\n      data: body\n    })\n    return user\n  }\n})\n```\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Fusers\u002F[id].ts\nexport default defineEventHandler(async (event) => {\n  const id = getRouterParam(event, 'id')\n  const method = getMethod(event)\n  \n  if (method === 'GET') {\n    return await db.users.findUnique({ where: { id } })\n  }\n  \n  if (method === 'DELETE') {\n    return await db.users.delete({ where: { id } })\n  }\n})\n```\n\n***\n\n## 7. 状态管理\n\n### 7.1 useState\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst counter = useState('counter', () => 0)\n\nfunction increment() {\n  counter.value++\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cp>Count: {{ counter }}\u003C\u002Fp>\n    \u003Cbutton @click=\"increment\">+1\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n### 7.2 全局状态管理\n\n```typescript\n\u002F\u002F composables\u002FuseAuth.ts\nexport const useAuth = () => {\n  const user = useState\u003CUser | null>('user', () => null)\n  const isAuthenticated = computed(() => !!user.value)\n  \n  async function login(credentials: LoginCredentials) {\n    const response = await $fetch('\u002Fapi\u002Fauth\u002Flogin', {\n      method: 'POST',\n      body: credentials\n    })\n    user.value = response.user\n  }\n  \n  async function logout() {\n    await $fetch('\u002Fapi\u002Fauth\u002Flogout', { method: 'POST' })\n    user.value = null\n  }\n  \n  return {\n    user,\n    isAuthenticated,\n    login,\n    logout\n  }\n}\n```\n\n### 7.3 Pinia 集成\n\n```bash\nnpm install @pinia\u002Fnuxt pinia\n```\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@pinia\u002Fnuxt']\n})\n```\n\n```typescript\n\u002F\u002F stores\u002Fuser.ts\nexport const useUserStore = defineStore('user', {\n  state: () => ({\n    user: null as User | null,\n    token: null as string | null\n  }),\n  \n  getters: {\n    isAuthenticated: (state) => !!state.token,\n    userName: (state) => state.user?.name ?? 'Guest'\n  },\n  \n  actions: {\n    async login(credentials: LoginCredentials) {\n      const response = await $fetch('\u002Fapi\u002Fauth\u002Flogin', {\n        method: 'POST',\n        body: credentials\n      })\n      this.user = response.user\n      this.token = response.token\n    },\n    \n    logout() {\n      this.user = null\n      this.token = null\n    }\n  }\n})\n```\n\n***\n\n## 8. 中间件\n\n### 8.1 路由中间件\n\n```typescript\n\u002F\u002F middleware\u002Fauth.ts\nexport default defineNuxtRouteMiddleware((to, from) => {\n  const { isAuthenticated } = useAuth()\n  \n  if (!isAuthenticated.value) {\n    return navigateTo('\u002Flogin')\n  }\n})\n```\n\n```typescript\n\u002F\u002F middleware\u002Fadmin.ts\nexport default defineNuxtRouteMiddleware((to, from) => {\n  const { user } = useAuth()\n  \n  if (user.value?.role !== 'admin') {\n    return navigateTo('\u002Funauthorized')\n  }\n})\n```\n\n### 8.2 全局中间件\n\n```typescript\n\u002F\u002F middleware\u002Fauth.global.ts\nexport default defineNuxtRouteMiddleware((to, from) => {\n  console.log(`导航: ${from.path} → ${to.path}`)\n})\n```\n\n### 8.3 服务端中间件\n\n```typescript\n\u002F\u002F server\u002Fmiddleware\u002Fauth.ts\nexport default defineEventHandler(async (event) => {\n  const url = getRequestURL(event)\n  \n  if (url.pathname.startsWith('\u002Fapi\u002Fadmin')) {\n    const token = getHeader(event, 'authorization')\n    \n    if (!token) {\n      throw createError({\n        statusCode: 401,\n        message: 'Unauthorized'\n      })\n    }\n    \n    event.context.user = await verifyToken(token)\n  }\n})\n```\n\n***\n\n## 9. 插件系统\n\n### 9.1 创建插件\n\n```typescript\n\u002F\u002F plugins\u002Fmy-plugin.ts\nexport default defineNuxtPlugin((nuxtApp) => {\n  return {\n    provide: {\n      myHelper: () => 'Hello from plugin'\n    }\n  }\n})\n```\n\n### 9.2 使用插件\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { $myHelper } = useNuxtApp()\nconsole.log($myHelper())\n\u003C\u002Fscript>\n```\n\n### 9.3 第三方库插件\n\n```typescript\n\u002F\u002F plugins\u002Faxios.ts\nimport axios from 'axios'\n\nexport default defineNuxtPlugin((nuxtApp) => {\n  const instance = axios.create({\n    baseURL: 'https:\u002F\u002Fapi.example.com'\n  })\n  \n  instance.interceptors.request.use((config) => {\n    const token = useAuth().token.value\n    if (token) {\n      config.headers.Authorization = `Bearer ${token}`\n    }\n    return config\n  })\n  \n  return {\n    provide: {\n      axios: instance\n    }\n  }\n})\n```\n\n### 9.4 仅客户端\u002F服务端插件\n\n```typescript\n\u002F\u002F plugins\u002Fclient-only.client.ts\nexport default defineNuxtPlugin(() => {\n  console.log('仅在客户端执行')\n})\n```\n\n```typescript\n\u002F\u002F plugins\u002Fserver-only.server.ts\nexport default defineNuxtPlugin(() => {\n  console.log('仅在服务端执行')\n})\n```\n\n***\n\n## 10. 模块系统\n\n### 10.1 常用模块\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: [\n    '@nuxtjs\u002Ftailwindcss',    \u002F\u002F Tailwind CSS\n    '@nuxtjs\u002Fcolor-mode',      \u002F\u002F 暗色模式\n    '@nuxt\u002Fimage',             \u002F\u002F 图片优化\n    '@nuxtjs\u002Fgoogle-fonts',    \u002F\u002F Google Fonts\n    '@pinia\u002Fnuxt',             \u002F\u002F Pinia\n    '@nuxt\u002Feslint',            \u002F\u002F ESLint\n  ]\n})\n```\n\n### 10.2 自定义模块\n\n```typescript\n\u002F\u002F modules\u002Fmy-module.ts\nimport { defineNuxtModule, addComponent } from '@nuxt\u002Fkit'\n\nexport default defineNuxtModule({\n  meta: {\n    name: 'my-module',\n    configKey: 'myModule'\n  },\n  \n  defaults: {\n    enabled: true\n  },\n  \n  setup(options, nuxt) {\n    if (!options.enabled) return\n    \n    addComponent({\n      name: 'MyComponent',\n      filePath: resolve('.\u002Fruntime\u002Fcomponents\u002FMyComponent.vue')\n    })\n  }\n})\n```\n\n***\n\n## 11. 部署与优化\n\n### 11.1 构建命令\n\n```bash\n# 开发模式\nnpm run dev\n\n# 构建生产版本\nnpm run build\n\n# 预览生产版本\nnpm run preview\n\n# 生成静态站点\nnpm run generate\n```\n\n### 11.2 环境变量\n\n```env\n# .env\nNUXT_PUBLIC_API_BASE=https:\u002F\u002Fapi.example.com\nNUXT_SECRET_KEY=your-secret-key\n```\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  runtimeConfig: {\n    secretKey: process.env.NUXT_SECRET_KEY,\n    public: {\n      apiBase: process.env.NUXT_PUBLIC_API_BASE\n    }\n  }\n})\n```\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst config = useRuntimeConfig()\nconst apiBase = config.public.apiBase\n\u003C\u002Fscript>\n```\n\n### 11.3 性能优化\n\n```typescript\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  app: {\n    head: {\n      link: [\n        { rel: 'preconnect', href: 'https:\u002F\u002Ffonts.googleapis.com' }\n      ]\n    }\n  },\n  \n  nitro: {\n    compressPublicAssets: true\n  },\n  \n  vite: {\n    build: {\n      chunkSizeWarningLimit: 1000\n    }\n  }\n})\n```\n\n### 11.4 部署配置\n\n**Docker 部署：**\n\n```dockerfile\nFROM node:18-alpine\n\nWORKDIR \u002Fapp\n\nCOPY package*.json .\u002F\nRUN npm ci\n\nCOPY . .\nRUN npm run build\n\nEXPOSE 3000\n\nCMD [\"node\", \".output\u002Fserver\u002Findex.mjs\"]\n```\n\n**PM2 部署：**\n\n```javascript\n\u002F\u002F ecosystem.config.js\nmodule.exports = {\n  apps: [{\n    name: 'nuxt-app',\n    script: '.output\u002Fserver\u002Findex.mjs',\n    instances: 'max',\n    exec_mode: 'cluster'\n  }]\n}\n```\n\n***\n\n## 最佳实践\n\n### 代码组织\n\n```\ncomposables\u002F\n├── useAuth.ts\n├── useApi.ts\n└── useUtils.ts\n\nutils\u002F\n├── format.ts\n├── validate.ts\n└── constants.ts\n\ntypes\u002F\n├── user.ts\n├── api.ts\n└── index.ts\n```\n\n### TypeScript 支持\n\n```typescript\n\u002F\u002F types\u002Fuser.ts\nexport interface User {\n  id: string\n  name: string\n  email: string\n  role: 'admin' | 'user'\n}\n\nexport interface LoginCredentials {\n  email: string\n  password: string\n}\n```\n\n### 错误处理\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data, error } = await useFetch('\u002Fapi\u002Fusers')\n\nif (error.value) {\n  throw createError({\n    statusCode: 404,\n    message: '用户不存在'\n  })\n}\n\u003C\u002Fscript>\n```\n\n***\n\n## 学习资源\n\n- [Nuxt 官方文档](https:\u002F\u002Fnuxt.com)\n- [Vue 3 文档](https:\u002F\u002Fvuejs.org)\n- [Nuxt Modules](https:\u002F\u002Fnuxt.com\u002Fmodules)\n- [Nuxt GitHub](https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fnuxt)\n\n***\n\n## 学习路线建议\n\n1. **入门阶段**\n   - 掌握 Vue 3 基础\n   - 理解 Nuxt 项目结构\n   - 熟悉自动导入机制\n   - 掌握路由系统\n2. **进阶阶段**\n   - 数据获取与状态管理\n   - 中间件与插件开发\n   - 服务端 API 开发\n   - 布局系统\n3. **高级阶段**\n   - 模块开发\n   - 性能优化\n   - 部署与运维\n   - TypeScript 深度集成\n4. **实战阶段**\n   - 完整项目开发\n   - SEO 优化\n   - 安全加固\n   - CI\u002FCD 集成\n\n","1",111,"2026-04-07T16:59:02.887Z","2026-04-07T16:59:02.889Z","2026-05-25T22:05:43.795Z",{"id":83,"categoryName":89,"slug":90,"description":91,"sort":31,"isEnable":32,"createTime":92,"updateTime":93,"deleteTime":31},"前端开发","frontend-engineering","2222","2026-04-03T02:36:11.945Z","2026-04-07T16:38:46.496Z",[],{"id":96,"title":97,"slug":98,"coverUrl":15,"summary":15,"content":99,"htmlContent":58,"categoryId":100,"viewCount":101,"likeCount":32,"isTop":31,"isPublish":32,"seoKeywords":15,"seoDescription":15,"publishTime":102,"createTime":103,"updateTime":104,"deleteTime":64,"category":105,"tags":108},"16","Git提交到Gitee学习笔记","git提交到gitee学习笔记","# Git提交到Gitee学习笔记\n\n## 一、准备工作\n\n### 1. 安装Git\n- 下载地址：https:\u002F\u002Fgit-scm.com\u002Fdownloads\n- 安装后验证：`git --version`\n\n### 2. 配置Git用户信息\n```bash\ngit config --global user.name \"你的用户名\"\ngit config --global user.email \"你的邮箱\"\n```\n\n### 3. 查看配置信息\n```bash\ngit config --global --list\n```\n\n## 二、Gitee仓库创建\n\n### 1. 登录Gitee\n- 访问：https:\u002F\u002Fgitee.com\n- 注册\u002F登录账号\n\n### 2. 创建新仓库\n- 点击右上角 \"+\" 按钮\n- 选择 \"新建仓库\"\n- 填写仓库名称（如：my-project）\n- 选择仓库类型（公开\u002F私有）\n- 初始化仓库（可选：添加README、.gitignore、开源许可证）\n- 点击 \"创建\"\n\n### 3. 获取仓库地址\n- 进入仓库页面\n- 点击 \"克隆\u002F下载\"\n- 复制HTTPS或SSH地址（推荐HTTPS）\n\n## 三、本地仓库操作\n\n### 1. 初始化本地仓库\n```bash\n# 进入项目目录\ncd your-project-folder\n\n# 初始化Git仓库\ngit init\n```\n\n### 2. 添加文件到暂存区\n```bash\n# 添加所有文件\ngit add .\n\n# 添加指定文件\ngit add filename.txt\n\n# 添加多个文件\ngit add file1.txt file2.txt\n```\n\n### 3. 提交到本地仓库\n```bash\n# 提交并添加说明\ngit commit -m \"第一次提交\"\n\n# 提交并添加详细说明\ngit commit -m \"feat: 添加新功能\"\n```\n\n## 四、连接Gitee远程仓库\n\n### 1. 添加远程仓库\n```bash\n# 添加远程仓库（origin是远程仓库的别名）\ngit remote add origin https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n\n# 例如：\ngit remote add origin https:\u002F\u002Fgitee.com\u002Fzhangsan\u002Fmy-project.git\n```\n\n### 2. 查看远程仓库\n```bash\n# 查看远程仓库列表\ngit remote -v\n```\n\n### 3. 修改远程仓库地址\n```bash\n# 修改远程仓库地址\ngit remote set-url origin https:\u002F\u002Fgitee.com\u002F用户名\u002F新仓库名.git\n```\n\n### 4. 删除远程仓库\n```bash\n# 删除远程仓库\ngit remote remove origin\n```\n\n## 五、推送到Gitee\n\n### 1. 首次推送（建立关联）\n```bash\n# 推送到main分支并建立关联\ngit push -u origin main\n\n# 如果是master分支\ngit push -u origin master\n```\n\n### 2. 后续推送\n```bash\n# 推送到当前分支\ngit push\n\n# 推送到指定分支\ngit push origin main\n\n# 强制推送（慎用）\ngit push -f origin main\n```\n\n## 六、从Gitee拉取更新\n\n### 1. 拉取最新代码\n```bash\n# 拉取并合并\ngit pull\n\n# 拉取指定分支\ngit pull origin main\n\n# 查看远程更新但不合并\ngit fetch\n```\n\n### 2. 克隆远程仓库\n```bash\n# 克隆整个仓库\ngit clone https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n\n# 克隆到指定目录\ngit clone https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git my-folder\n\n# 克隆指定分支\ngit clone -b 分支名 https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n```\n\n## 七、常用Git命令\n\n### 1. 查看状态\n```bash\n# 查看当前状态\ngit status\n\n# 查看提交历史\ngit log\n\n# 查看简洁的提交历史\ngit log --oneline\n\n# 查看分支\ngit branch\n\n# 查看所有分支（包括远程）\ngit branch -a\n```\n\n### 2. 分支操作\n```bash\n# 创建新分支\ngit branch feature-branch\n\n# 切换分支\ngit checkout feature-branch\n\n# 创建并切换分支\ngit checkout -b feature-branch\n\n# 合并分支\ngit merge feature-branch\n\n# 删除分支\ngit branch -d feature-branch\n\n# 删除远程分支\ngit push origin --delete feature-branch\n```\n\n### 3. 撤销操作\n```bash\n# 撤销工作区修改\ngit restore filename.txt\n\n# 撤销暂存区修改\ngit restore --staged filename.txt\n\n# 回退到上一个版本\ngit reset --hard HEAD~1\n\n# 回退到指定版本\ngit reset --hard commit-id\n```\n\n### 4. 查看差异\n```bash\n# 查看工作区与暂存区的差异\ngit diff\n\n# 查看暂存区与上一次提交的差异\ngit diff --cached\n\n# 查看两个版本的差异\ngit diff commit1 commit2\n```\n\n## 八、Gitee SSH配置（可选）\n\n### 1. 生成SSH密钥\n```bash\n# 生成SSH密钥\nssh-keygen -t rsa -C \"你的邮箱\"\n\n# 一路回车，使用默认设置\n```\n\n### 2. 查看公钥\n```bash\n# Windows\ncat ~\u002F.ssh\u002Fid_rsa.pub\n\n# 或直接打开文件\ntype %USERPROFILE%\\.ssh\\id_rsa.pub\n```\n\n### 3. 添加公钥到Gitee\n- 登录Gitee\n- 点击右上角头像 -> 设置\n- 选择 \"SSH公钥\"\n- 点击 \"添加公钥\"\n- 粘贴公钥内容\n- 点击 \"确定\"\n\n### 4. 测试SSH连接\n```bash\nssh -T git@gitee.com\n```\n\n### 5. 使用SSH地址\n```bash\n# 添加远程仓库（SSH方式）\ngit remote add origin git@gitee.com:用户名\u002F仓库名.git\n```\n\n## 九、常见问题解决\n\n### 1. 推送失败：远程仓库有更新\n```bash\n# 先拉取远程更新\ngit pull --rebase origin main\n\n# 再推送\ngit push origin main\n```\n\n### 2. 冲突解决\n```bash\n# 拉取时出现冲突\ngit pull origin main\n\n# 手动编辑冲突文件\n# 解决冲突后\n\ngit add .\ngit commit -m \"解决冲突\"\ngit push origin main\n```\n\n### 3. 忽略文件\n创建 `.gitignore` 文件：\n```\n# Node modules 依赖包\nnode_modules\u002F\n\n# 编译输出目录\ndist\u002F\nbuild\u002F\n\n# 本地环境配置文件\n.env\n.env.local\n.env.*.local\n\n# 日志文件\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# IDE 和编辑器相关文件\n.vscode\u002F\n.idea\u002F\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n\n# 操作系统文件\n.DS_Store\nThumbs.db\n\n# 测试覆盖率报告\ncoverage\u002F\n```\n\n### 4. 查看远程仓库信息\n```bash\n# 查看远程仓库详细信息\ngit remote show origin\n```\n\n## 十、常见使用场景详解\n\n### 场景一：将新文件用Git管理并提交到Gitee\n\n**适用情况**：你有一个本地项目或新文件，想要用Git管理并提交到Gitee远程仓库\n\n#### 步骤详解：\n\n```bash\n# 1. 进入你的项目目录\ncd your-project-folder\n\n# 2. 初始化Git仓库（如果还没有初始化）\ngit init\n\n# 3. 查看当前状态，确认哪些文件未被追踪\ngit status\n\n# 4. 添加文件到暂存区\n# 方式1：添加所有文件\ngit add .\n\n# 方式2：添加指定文件\ngit add filename.txt\n\n# 方式3：添加多个文件\ngit add file1.txt file2.txt file3.txt\n\n# 5. 再次查看状态，确认文件已添加到暂存区\ngit status\n\n# 6. 提交到本地仓库\ngit commit -m \"feat: 初始化项目，添加新文件\"\n\n# 7. 连接Gitee远程仓库（如果还没有连接）\ngit remote add origin https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n\n# 8. 查看远程仓库连接\ngit remote -v\n\n# 9. 首次推送到Gitee（-u参数建立本地分支与远程分支的关联）\ngit push -u origin main\n\n# 如果遇到错误，可能是远程仓库已有内容，可以尝试：\n# git pull --rebase origin main\n# git push -u origin main\n```\n\n#### 完整示例（从零开始）：\n\n```bash\n# 假设你有一个新项目 my-project\ncd C:\\Users\\YourName\\Desktop\\my-project\n\n# 初始化Git\ngit init\n\n# 创建一些文件（示例）\necho \"console.log('Hello World')\" > app.js\necho \"# My Project\" > README.md\n\n# 添加所有文件\ngit add .\n\n# 提交\ngit commit -m \"feat: 初始化项目\"\n\n# 连接Gitee远程仓库\ngit remote add origin https:\u002F\u002Fgitee.com\u002Fzhangsan\u002Fmy-project.git\n\n# 推送到Gitee\ngit push -u origin main\n```\n\n#### 注意事项：\n\n- **首次推送时**：如果Gitee仓库已经存在（比如创建了README），需要先 `git pull` 再 `git push`\n- **分支名称**：新版本Git默认使用 `main` 分支，旧版本可能使用 `master`，用 `git branch` 查看\n- **忽略文件**：如果有不需要提交的文件，先创建 `.gitignore` 文件\n\n---\n\n### 场景二：已提交到Gitee，后续更新文件并同步\n\n**适用情况**：项目已经用Git管理并提交到Gitee，现在修改了文件需要同步更新\n\n#### 步骤详解：\n\n```bash\n# 1. 查看当前状态，了解哪些文件被修改\ngit status\n\n# 2. 查看具体修改内容\ngit diff\n\n# 3. 添加修改的文件到暂存区\n# 方式1：添加所有修改的文件\ngit add .\n\n# 方式2：添加指定修改的文件\ngit add modified-file.js\n\n# 方式3：交互式添加（可以选择性地添加某些修改）\ngit add -i\n\n# 4. 查看状态，确认文件已添加到暂存区\ngit status\n\n# 5. 提交修改到本地仓库\ngit commit -m \"feat: 更新功能描述\"\n\n# 或者使用更详细的提交信息\ngit commit -m \"fix: 修复登录页面的bug\n- 修复了用户名验证逻辑\n- 优化了错误提示信息\"\n\n# 6. 推送到Gitee远程仓库\ngit push\n\n# 或者指定分支推送\ngit push origin main\n```\n\n#### 完整示例（日常更新流程）：\n\n```bash\n# 1. 进入项目目录\ncd C:\\Users\\YourName\\Desktop\\my-project\n\n# 2. 先拉取远程最新代码（多人协作时很重要）\ngit pull origin main\n\n# 3. 修改文件（使用编辑器修改代码）\n# ... 编辑文件 ...\n\n# 4. 查看修改内容\ngit status\ngit diff\n\n# 5. 添加修改的文件\ngit add .\n\n# 6. 提交修改\ngit commit -m \"feat: 添加用户注册功能\"\n\n# 7. 推送到Gitee\ngit push origin main\n```\n\n#### 常见更新场景：\n\n##### 场景A：修改单个文件\n```bash\n# 修改了 app.js 文件\ngit add app.js\ngit commit -m \"fix: 修复app.js中的bug\"\ngit push\n```\n\n##### 场景B：修改多个文件\n```bash\n# 修改了多个文件\ngit add .\ngit commit -m \"feat: 添加新功能模块\"\ngit push\n```\n\n##### 场景C：删除文件\n```bash\n# 删除了 old-file.js\ngit add old-file.js\ngit commit -m \"chore: 删除废弃文件\"\ngit push\n\n# 或者使用 git rm 命令\ngit rm old-file.js\ngit commit -m \"chore: 删除废弃文件\"\ngit push\n```\n\n##### 场景D：重命名或移动文件\n```bash\n# 重命名文件\ngit mv old-name.js new-name.js\ngit commit -m \"refactor: 重命名文件\"\ngit push\n```\n\n#### 注意事项：\n\n- **多人协作**：推送前先 `git pull` 拉取最新代码，避免冲突\n- **冲突处理**：如果 `git pull` 时出现冲突，手动解决冲突后再提交\n- **查看历史**：使用 `git log --oneline` 查看提交历史\n- **撤销修改**：如果修改错了，可以用 `git restore filename.txt` 撤销\n\n#### 快捷操作（一步完成添加和提交）：\n\n```bash\n# 添加所有修改并提交（不包括未追踪的新文件）\ngit commit -am \"feat: 快速提交修改\"\n\n# 注意：-a 参数只对已追踪的文件有效，新文件仍需先 git add\n```\n\n---\n\n## 十一、完整工作流程示例\n\n### 首次提交项目到Gitee\n```bash\n# 1. 初始化本地仓库\ngit init\n\n# 2. 添加所有文件\ngit add .\n\n# 3. 提交到本地仓库\ngit commit -m \"初始化项目\"\n\n# 4. 添加远程仓库\ngit remote add origin https:\u002F\u002Fgitee.com\u002F用户名\u002F仓库名.git\n\n# 5. 推送到Gitee\ngit push -u origin main\n```\n\n### 日常开发流程\n```bash\n# 1. 拉取最新代码\ngit pull origin main\n\n# 2. 创建新分支\ngit checkout -b feature\u002Fnew-feature\n\n# 3. 修改代码\n# ... 编辑文件 ...\n\n# 4. 查看状态\ngit status\n\n# 5. 添加修改的文件\ngit add .\n\n# 6. 提交修改\ngit commit -m \"feat: 添加新功能\"\n\n# 7. 切换回主分支\ngit checkout main\n\n# 8. 合并分支\ngit merge feature\u002Fnew-feature\n\n# 9. 推送到远程\ngit push origin main\n\n# 10. 删除本地分支\ngit branch -d feature\u002Fnew-feature\n```\n\n## 十二、提交信息规范\n\n### 提交信息格式\n```\n\u003Ctype>(\u003Cscope>): \u003Csubject>\n\n\u003Cbody>\n\n\u003Cfooter>\n```\n\n### 常用type类型\n- `feat`: 新功能\n- `fix`: 修复bug\n- `docs`: 文档更新\n- `style`: 代码格式调整\n- `refactor`: 重构代码\n- `test`: 测试相关\n- `chore`: 构建过程或辅助工具的变动\n\n### 示例\n```bash\ngit commit -m \"feat: 添加用户登录功能\"\ngit commit -m \"fix: 修复首页显示bug\"\ngit commit -m \"docs: 更新README文档\"\n```\n\n## 十三、学习资源\n\n- Gitee官方文档：https:\u002F\u002Fgitee.com\u002Fhelp\n- Git官方文档：https:\u002F\u002Fgit-scm.com\u002Fdoc\n- Git教程：https:\u002F\u002Fwww.liaoxuefeng.com\u002Fwiki\u002F896043488029600\n\n---\n\n**提示**：建议多练习这些命令，熟练掌握Git的基本操作对日常开发非常重要！\n","7",112,"2026-04-07T16:50:22.022Z","2026-04-07T16:50:22.025Z","2026-05-25T22:05:44.056Z",{"id":100,"categoryName":106,"slug":106,"description":106,"sort":31,"isEnable":32,"createTime":107,"updateTime":107,"deleteTime":31},"工程化技术","2026-04-07T16:50:31.279Z",[],{"id":110,"title":111,"slug":112,"coverUrl":15,"summary":15,"content":113,"htmlContent":58,"categoryId":21,"viewCount":114,"likeCount":32,"isTop":31,"isPublish":32,"seoKeywords":15,"seoDescription":15,"publishTime":115,"createTime":116,"updateTime":117,"deleteTime":64,"category":118,"tags":121},"15","HBuilderX、微信开发者工具和uniapp学习笔记","hbuilderx和uniapp学习笔记","# HBuilderX、微信开发者工具和uniapp学习笔记\n\n## 目录\n1. [开发环境搭建](#开发环境搭建)\n2. [HBuilderX详解](#hbuilderx详解)\n3. [微信开发者工具详解](#微信开发者工具详解)\n4. [uniapp基础](#uniapp基础)\n5. [uniapp项目开发](#uniapp项目开发)\n6. [uniapp与Vue差异](#uniapp与vue差异)\n7. [跨平台开发实战](#跨平台开发实战)\n8. [性能优化与调试](#性能优化与调试)\n\n---\n\n## 开发环境搭建\n\n### 1. HBuilderX安装\n\n#### 下载安装\n- 官网下载：https:\u002F\u002Fwww.dcloud.io\u002Fhbuilderx.html\n- 选择标准版或App开发版（推荐App开发版）\n- 支持Windows、macOS、Linux\n\n#### 常用插件安装\n1. **uni-app编译插件**（必装）\n   - 运行到浏览器\n   - 运行到小程序模拟器\n   - 运行到手机或模拟器\n\n2. **其他推荐插件**\n   - SCSS\u002FSASS编译\n   - less编译\n   - git插件\n   - 内置浏览器\n\n#### HBuilderX配置\n```json\n\u002F\u002F settings.json\n{\n  \"editor.fontSize\": 14,\n  \"editor.tabSize\": 2,\n  \"editor.wordWrap\": \"on\",\n  \"editor.formatOnSave\": true,\n  \"editor.minimap.enabled\": true\n}\n```\n\n### 2. 微信开发者工具安装\n\n#### 下载安装\n- 官网下载：https:\u002F\u002Fdevelopers.weixin.qq.com\u002Fminiprogram\u002Fdev\u002Fdevtools\u002Fdownload.html\n- 支持Windows、macOS\n\n#### 基本配置\n```json\n\u002F\u002F project.config.json\n{\n  \"miniprogramRoot\": \".\u002Fdist\u002Fdev\u002Fmp-weixin\u002F\",\n  \"cloudfunctionRoot\": \"\",\n  \"setting\": {\n    \"urlCheck\": false,\n    \"es6\": true,\n    \"postcss\": true,\n    \"minified\": true\n  },\n  \"appid\": \"your-appid\",\n  \"projectname\": \"uniapp-project\"\n}\n```\n\n#### 开启服务端口\n- 设置 -> 安全设置 -> 安全 -> 开启服务端口\n- 用于HBuilderX自动打开微信开发者工具\n\n### 3. 环境配置检查\n\n```bash\n# 检查Node.js版本（建议12+）\nnode -v\n\n# 检查npm版本\nnpm -v\n\n# 配置淘宝镜像（可选）\nnpm config set registry https:\u002F\u002Fregistry.npmmirror.com\n```\n\n---\n\n## HBuilderX详解\n\n### 1. 界面介绍\n\n#### 主要区域\n- **左侧项目管理器**：文件树结构\n- **中间编辑区**：代码编辑\n- **右侧工具栏**：运行、发行等\n- **底部控制台**：日志输出\n\n#### 常用快捷键\n```\nCtrl+N      新建文件\nCtrl+S      保存文件\nCtrl+D      选中单词\nCtrl+F      查找\nCtrl+H      替换\nCtrl+G      跳转行\nCtrl+\u002F      注释\nCtrl+Shift+R 格式化代码\nAlt+Shift+F 格式化选中代码\n```\n\n### 2. 项目创建\n\n#### 创建uni-app项目\n1. 文件 -> 新建 -> 项目\n2. 选择uni-app\n3. 选择模板（默认模板、Hello uni-app等）\n4. 填写项目名称和路径\n\n#### 项目结构\n```\nmy-uniapp\u002F\n├── pages\u002F              # 页面文件\n│   ├── index\u002F\n│   │   └── index.vue\n│   └── login\u002F\n│       └── login.vue\n├── static\u002F             # 静态资源\n│   ├── images\u002F\n│   └── fonts\u002F\n├── components\u002F         # 组件\n│   └── my-component\u002F\n│       └── my-component.vue\n├── uni_modules\u002F        # uni-app插件\n├── App.vue             # 应用配置\n├── main.js             # 入口文件\n├── manifest.json       # 应用配置\n├── pages.json          # 页面路由配置\n└── package.json        # 依赖配置\n```\n\n### 3. 运行和调试\n\n#### 运行到浏览器\n- 点击工具栏 -> 运行 -> 运行到浏览器 -> Chrome\n- 支持实时预览\n\n#### 运行到微信开发者工具\n- 点击工具栏 -> 运行 -> 运行到小程序模拟器 -> 微信开发者工具\n- 自动打开微信开发者工具\n\n#### 运行到手机\n- 连接手机，开启USB调试\n- 点击工具栏 -> 运行 -> 运行到手机或模拟器\n\n### 4. 代码提示和智能感知\n\n#### 内置代码块\n- 输入 `uview` 生成视图模板\n- 输入 `uinput` 生成输入框\n- 输入 `uimage` 生成图片\n- 输入 `uapi` 生成API调用模板\n\n#### 自定义代码块\n```json\n\u002F\u002F settings.json -> snippets\n{\n  \"Vue Template\": {\n    \"prefix\": \"vtemp\",\n    \"body\": [\n      \"\u003Ctemplate>\",\n      \"\\t\u003Cview class=\\\"container\\\">\",\n      \"\\t\\t\",\n      \"\\t\u003C\u002Fview>\",\n      \"\u003C\u002Ftemplate>\",\n      \"\",\n      \"\u003Cscript>\",\n      \"export default {\",\n      \"\\tdata() {\",\n      \"\\t\\treturn {\",\n      \"\\t\\t\\t\",\n      \"\\t\\t}\",\n      \"\\t},\",\n      \"\\tonLoad() {\",\n      \"\\t\\t\",\n      \"\\t}\",\n      \"}\",\n      \"\u003C\u002Fscript>\",\n      \"\",\n      \"\u003Cstyle>\",\n      \".container {\",\n      \"\\t\",\n      \"}\",\n      \"\u003C\u002Fstyle>\"\n    ]\n  }\n}\n```\n\n---\n\n## 微信开发者工具详解\n\n### 1. 界面介绍\n\n#### 主要区域\n- **模拟器**：小程序预览\n- **编辑器**：代码编辑（可选）\n- **调试器**：调试工具\n\n#### 调试器功能\n- **Console**：控制台输出\n- **Sources**：源代码调试\n- **Network**：网络请求\n- **Storage**：本地存储\n- **AppData**：页面数据\n- **Wxml**：元素结构\n- **Sensor**：传感器模拟\n\n### 2. 常用功能\n\n#### 真机调试\n- 点击工具栏 -> 真机调试\n- 扫码连接手机\n\n#### 性能监控\n- 工具栏 -> 性能监控\n- 查看FPS、内存使用等\n\n#### 代码上传\n- 工具栏 -> 上传\n- 填写版本号和备注\n\n#### 预览\n- 工具栏 -> 预览\n- 扫码在手机上预览\n\n### 3. 调试技巧\n\n#### Console调试\n```javascript\nconsole.log('普通日志')\nconsole.warn('警告信息')\nconsole.error('错误信息')\nconsole.table(data)  \u002F\u002F 表格形式输出\nconsole.time('timer')\nconsole.timeEnd('timer')\n```\n\n#### 断点调试\n- 在编辑器中点击行号设置断点\n- 使用debugger语句\n```javascript\ndebugger  \u002F\u002F 程序会在此处暂停\n```\n\n#### 网络请求调试\n- Network面板查看所有请求\n- 查看请求头、响应体、时间等\n\n### 4. 常见问题解决\n\n#### 端口占用\n```\n错误：服务端口未开启\n解决：设置 -> 安全设置 -> 开启服务端口\n```\n\n#### 域名白名单\n```\n错误：不在以下request合法域名列表中\n解决：开发阶段可关闭域名校验（详情 -> 本地设置 -> 不校验合法域名）\n```\n\n#### 缓存问题\n```\n解决：工具 -> 清缓存 -> 清除全部缓存\n```\n\n---\n\n## uniapp基础\n\n### 1. 什么是uni-app\n\nuni-app是一个使用Vue.js开发所有前端应用的框架，开发者编写一套代码，可发布到iOS、Android、Web（响应式）、以及各种小程序（微信\u002F支付宝\u002F百度\u002F头条\u002FQQ\u002F钉钉\u002F淘宝）、快应用等多个平台。\n\n### 2. 核心特性\n\n- **跨平台**：一套代码，多端运行\n- **Vue生态**：基于Vue.js，学习成本低\n- **原生体验**：调用原生能力，性能接近原生\n- **丰富组件**：内置大量组件和API\n- **插件市场**：丰富的插件生态\n\n### 3. 支持平台\n\n| 平台 | 支持情况 |\n|------|----------|\n| 微信小程序 | ✓ |\n| 支付宝小程序 | ✓ |\n| 百度小程序 | ✓ |\n| 字节跳动小程序 | ✓ |\n| QQ小程序 | ✓ |\n| H5 | ✓ |\n| Android App | ✓ |\n| iOS App | ✓ |\n\n### 4. 项目配置\n\n#### manifest.json\n```json\n{\n  \"name\": \"uni-app\",\n  \"appid\": \"__UNI__XXXXXX\",\n  \"versionName\": \"1.0.0\",\n  \"versionCode\": \"100\",\n  \"mp-weixin\": {\n    \"appid\": \"wx1234567890abcdef\",\n    \"setting\": {\n      \"urlCheck\": false\n    },\n    \"usingComponents\": true\n  },\n  \"h5\": {\n    \"title\": \"uni-app H5\",\n    \"domain\": \"\"\n  }\n}\n```\n\n#### pages.json\n```json\n{\n  \"pages\": [\n    {\n      \"path\": \"pages\u002Findex\u002Findex\",\n      \"style\": {\n        \"navigationBarTitleText\": \"首页\",\n        \"navigationBarBackgroundColor\": \"#007AFF\",\n        \"navigationBarTextStyle\": \"white\"\n      }\n    },\n    {\n      \"path\": \"pages\u002Flogin\u002Flogin\",\n      \"style\": {\n        \"navigationBarTitleText\": \"登录\"\n      }\n    }\n  ],\n  \"globalStyle\": {\n    \"navigationBarTextStyle\": \"black\",\n    \"navigationBarTitleText\": \"uni-app\",\n    \"navigationBarBackgroundColor\": \"#F8F8F8\",\n    \"backgroundColor\": \"#F8F8F8\"\n  },\n  \"tabBar\": {\n    \"color\": \"#7A7E83\",\n    \"selectedColor\": \"#007AFF\",\n    \"borderStyle\": \"black\",\n    \"backgroundColor\": \"#F8F8F8\",\n    \"list\": [\n      {\n        \"pagePath\": \"pages\u002Findex\u002Findex\",\n        \"iconPath\": \"static\u002Ftab-home.png\",\n        \"selectedIconPath\": \"static\u002Ftab-home-active.png\",\n        \"text\": \"首页\"\n      },\n      {\n        \"pagePath\": \"pages\u002Fuser\u002Fuser\",\n        \"iconPath\": \"static\u002Ftab-user.png\",\n        \"selectedIconPath\": \"static\u002Ftab-user-active.png\",\n        \"text\": \"我的\"\n      }\n    ]\n  }\n}\n```\n\n---\n\n## uniapp项目开发\n\n### 1. 页面开发\n\n#### 基本页面结构\n```vue\n\u003Ctemplate>\n  \u003Cview class=\"container\">\n    \u003Cview class=\"header\">{{ title }}\u003C\u002Fview>\n    \u003Cview class=\"content\">\n      \u003Ctext>{{ message }}\u003C\u002Ftext>\n    \u003C\u002Fview>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nexport default {\n  data() {\n    return {\n      title: 'uni-app',\n      message: 'Hello uni-app!'\n    }\n  },\n  onLoad(options) {\n    console.log('页面加载', options)\n  },\n  onShow() {\n    console.log('页面显示')\n  },\n  onReady() {\n    console.log('页面渲染完成')\n  },\n  onHide() {\n    console.log('页面隐藏')\n  },\n  onUnload() {\n    console.log('页面卸载')\n  }\n}\n\u003C\u002Fscript>\n\n\u003Cstyle>\n.container {\n  padding: 20rpx;\n}\n.header {\n  font-size: 36rpx;\n  font-weight: bold;\n}\n\u003C\u002Fstyle>\n```\n\n### 2. 常用组件\n\n#### 视图容器\n```vue\n\u003C!-- view -->\n\u003Cview class=\"box\">容器\u003C\u002Fview>\n\n\u003C!-- scroll-view -->\n\u003Cscroll-view scroll-y style=\"height: 300rpx;\">\n  \u003Cview v-for=\"item in 10\" :key=\"item\">内容 {{ item }}\u003C\u002Fview>\n\u003C\u002Fscroll-view>\n\n\u003C!-- swiper -->\n\u003Cswiper indicator-dots autoplay circular>\n  \u003Cswiper-item v-for=\"(item, index) in bannerList\" :key=\"index\">\n    \u003Cimage :src=\"item\" mode=\"aspectFill\">\u003C\u002Fimage>\n  \u003C\u002Fswiper-item>\n\u003C\u002Fswiper-view>\n\n\u003C!-- rich-text -->\n\u003Crich-text :nodes=\"htmlContent\">\u003C\u002Frich-text>\n```\n\n#### 基础内容\n```vue\n\u003C!-- text -->\n\u003Ctext selectable>可选中的文本\u003C\u002Ftext>\n\n\u003C!-- image -->\n\u003Cimage src=\"\u002Fstatic\u002Flogo.png\" mode=\"aspectFit\">\u003C\u002Fimage>\n\n\u003C!-- icon -->\n\u003Cicon type=\"success\" size=\"20\">\u003C\u002Ficon>\n\n\u003C!-- progress -->\n\u003Cprogress percent=\"20\" show-info stroke-width=\"10\">\u003C\u002Fprogress>\n```\n\n#### 表单组件\n```vue\n\u003C!-- button -->\n\u003Cbutton type=\"primary\" @click=\"handleClick\">主要按钮\u003C\u002Fbutton>\n\n\u003C!-- input -->\n\u003Cinput v-model=\"inputValue\" placeholder=\"请输入内容\" \u002F>\n\n\u003C!-- textarea -->\n\u003Ctextarea v-model=\"textValue\" placeholder=\"多行文本\" \u002F>\n\n\u003C!-- checkbox -->\n\u003Ccheckbox-group @change=\"checkboxChange\">\n  \u003Clabel v-for=\"item in items\" :key=\"item.value\">\n    \u003Ccheckbox :value=\"item.value\" :checked=\"item.checked\" \u002F>\n    {{ item.name }}\n  \u003C\u002Flabel>\n\u003C\u002Fcheckbox-group>\n\n\u003C!-- radio -->\n\u003Cradio-group @change=\"radioChange\">\n  \u003Clabel v-for=\"item in items\" :key=\"item.value\">\n    \u003Cradio :value=\"item.value\" :checked=\"item.checked\" \u002F>\n    {{ item.name }}\n  \u003C\u002Flabel>\n\u003C\u002Fradio-group>\n\n\u003C!-- switch -->\n\u003Cswitch :checked=\"switchValue\" @change=\"switchChange\" \u002F>\n\n\u003C!-- picker -->\n\u003Cpicker mode=\"selector\" :range=\"array\" @change=\"pickerChange\">\n  \u003Cview>当前选择：{{ array[pickerIndex] }}\u003C\u002Fview>\n\u003C\u002Fpicker>\n\n\u003C!-- slider -->\n\u003Cslider :value=\"sliderValue\" @change=\"sliderChange\" \u002F>\n```\n\n#### 导航组件\n```vue\n\u003C!-- navigator -->\n\u003Cnavigator url=\"\u002Fpages\u002Fdetail\u002Fdetail?id=123\" open-type=\"navigate\">\n  跳转到详情页\n\u003C\u002Fnavigator>\n\n\u003Cnavigator url=\"\u002Fpages\u002Fdetail\u002Fdetail\" open-type=\"switchTab\">\n  跳转到TabBar页面\n\u003C\u002Fnavigator>\n```\n\n### 3. 常用API\n\n#### 网络请求\n```javascript\nuni.request({\n  url: 'https:\u002F\u002Fapi.example.com\u002Fdata',\n  method: 'GET',\n  data: {\n    id: 1\n  },\n  header: {\n    'content-type': 'application\u002Fjson'\n  },\n  success: (res) => {\n    console.log(res.data)\n  },\n  fail: (err) => {\n    console.error(err)\n  }\n})\n\n\u002F\u002F Promise风格\nuni.request({\n  url: 'https:\u002F\u002Fapi.example.com\u002Fdata'\n}).then(res => {\n  console.log(res.data)\n}).catch(err => {\n  console.error(err)\n})\n\n\u002F\u002F async\u002Fawait\nasync function fetchData() {\n  try {\n    const res = await uni.request({\n      url: 'https:\u002F\u002Fapi.example.com\u002Fdata'\n    })\n    console.log(res.data)\n  } catch (err) {\n    console.error(err)\n  }\n}\n```\n\n#### 页面跳转\n```javascript\n\u002F\u002F 保留当前页面，跳转到应用内的某个页面\nuni.navigateTo({\n  url: '\u002Fpages\u002Fdetail\u002Fdetail?id=123'\n})\n\n\u002F\u002F 关闭当前页面，跳转到应用内的某个页面\nuni.redirectTo({\n  url: '\u002Fpages\u002Fdetail\u002Fdetail'\n})\n\n\u002F\u002F 跳转到tabBar页面，并关闭其他所有非tabBar页面\nuni.switchTab({\n  url: '\u002Fpages\u002Findex\u002Findex'\n})\n\n\u002F\u002F 关闭所有页面，打开到应用内的某个页面\nuni.reLaunch({\n  url: '\u002Fpages\u002Findex\u002Findex'\n})\n\n\u002F\u002F 返回上一页面\nuni.navigateBack({\n  delta: 1\n})\n```\n\n#### 数据存储\n```javascript\n\u002F\u002F 同步存储\nuni.setStorageSync('key', 'value')\nconst value = uni.getStorageSync('key')\nuni.removeStorageSync('key')\nuni.clearStorageSync()\n\n\u002F\u002F 异步存储\nuni.setStorage({\n  key: 'key',\n  data: 'value',\n  success: () => {\n    console.log('存储成功')\n  }\n})\n\nuni.getStorage({\n  key: 'key',\n  success: (res) => {\n    console.log(res.data)\n  }\n})\n```\n\n#### 提示框\n```javascript\n\u002F\u002F 消息提示\nuni.showToast({\n  title: '操作成功',\n  icon: 'success',\n  duration: 2000\n})\n\n\u002F\u002F 加载提示\nuni.showLoading({\n  title: '加载中...'\n})\n\n\u002F\u002F 隐藏加载\nuni.hideLoading()\n\n\u002F\u002F 模态框\nuni.showModal({\n  title: '提示',\n  content: '确定要删除吗？',\n  success: (res) => {\n    if (res.confirm) {\n      console.log('用户点击确定')\n    } else if (res.cancel) {\n      console.log('用户点击取消')\n    }\n  }\n})\n\n\u002F\u002F 操作菜单\nuni.showActionSheet({\n  itemList: ['选项1', '选项2', '选项3'],\n  success: (res) => {\n    console.log('选中了第' + (res.tapIndex + 1) + '个按钮')\n  }\n})\n```\n\n#### 系统信息\n```javascript\n\u002F\u002F 获取系统信息\nconst systemInfo = uni.getSystemInfoSync()\nconsole.log(systemInfo.platform)      \u002F\u002F 平台\nconsole.log(systemInfo.system)        \u002F\u002F 操作系统\nconsole.log(systemInfo.model)         \u002F\u002F 设备型号\nconsole.log(systemInfo.pixelRatio)    \u002F\u002F 设备像素比\nconsole.log(systemInfo.screenWidth)   \u002F\u002F 屏幕宽度\nconsole.log(systemInfo.screenHeight)  \u002F\u002F 屏幕高度\n\n\u002F\u002F 获取网络状态\nuni.getNetworkType({\n  success: (res) => {\n    console.log(res.networkType)  \u002F\u002F wifi、4g、none等\n  }\n})\n\n\u002F\u002F 监听网络状态变化\nuni.onNetworkStatusChange((res) => {\n  console.log(res.isConnected)\n  console.log(res.networkType)\n})\n```\n\n#### 媒体API\n```javascript\n\u002F\u002F 选择图片\nuni.chooseImage({\n  count: 9,\n  sizeType: ['original', 'compressed'],\n  sourceType: ['album', 'camera'],\n  success: (res) => {\n    console.log(res.tempFilePaths)\n  }\n})\n\n\u002F\u002F 预览图片\nuni.previewImage({\n  urls: ['https:\u002F\u002Fexample.com\u002Fimage1.jpg', 'https:\u002F\u002Fexample.com\u002Fimage2.jpg'],\n  current: 0\n})\n\n\u002F\u002F 上传文件\nuni.uploadFile({\n  url: 'https:\u002F\u002Fapi.example.com\u002Fupload',\n  filePath: tempFilePath,\n  name: 'file',\n  success: (res) => {\n    console.log(res.data)\n  }\n})\n\n\u002F\u002F 下载文件\nuni.downloadFile({\n  url: 'https:\u002F\u002Fexample.com\u002Ffile.pdf',\n  success: (res) => {\n    console.log(res.tempFilePath)\n  }\n})\n```\n\n### 4. 条件编译\n\n#### 平台判断\n```javascript\n\u002F\u002F #ifdef H5\nconsole.log('只在H5平台执行')\n\u002F\u002F #endif\n\n\u002F\u002F #ifndef H5\nconsole.log('不在H5平台执行')\n\u002F\u002F #endif\n\n\u002F\u002F #ifdef MP-WEIXIN\nconsole.log('只在微信小程序执行')\n\u002F\u002F #endif\n\n\u002F\u002F #ifdef APP-PLUS\nconsole.log('只在App执行')\n\u002F\u002F #endif\n```\n\n#### 模板条件编译\n```vue\n\u003Ctemplate>\n  \u003Cview>\n    \u003C!-- #ifdef H5 -->\n    \u003Cview>H5平台显示\u003C\u002Fview>\n    \u003C!-- #endif -->\n    \n    \u003C!-- #ifdef MP-WEIXIN -->\n    \u003Cview>微信小程序显示\u003C\u002Fview>\n    \u003C!-- #endif -->\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n```\n\n#### 样式条件编译\n```css\n\u002F* #ifdef H5 *\u002F\n.container {\n  background-color: #f0f0f0;\n}\n\u002F* #endif *\u002F\n\n\u002F* #ifdef MP-WEIXIN *\u002F\n.container {\n  background-color: #ffffff;\n}\n\u002F* #endif *\u002F\n```\n\n---\n\n## uniapp与Vue差异\n\n### 1. 标签差异\n\n### Vue标签\n| Vue标签 | uni-app标签 | 说明 |\n|---------|-------------|------|\n| div | view | 容器 |\n| span | text | 文本 |\n| img | image | 图片 |\n| a | navigator | 导航 |\n| input | input | 输入框 |\n| textarea | textarea | 多行文本 |\n| button | button | 按钮 |\n\n### 不支持的Vue标签\n- header, footer, section, article, aside, nav\n- iframe, canvas, video, audio\n- select, option, optgroup\n\n### 2. 样式差异\n\n#### 单位\n```css\n\u002F* 推荐使用rpx单位 *\u002F\n.container {\n  width: 750rpx;  \u002F* 屏幕宽度 *\u002F\n  height: 100rpx;\n}\n\n\u002F* px也会被转换 *\u002F\n.box {\n  font-size: 16px;  \u002F* 在小程序中会转换为rpx *\u002F\n}\n```\n\n#### 选择器限制\n```css\n\u002F* 不支持通配符 * *\u002F\n\u002F* * { margin: 0; }  不支持 *\u002F\n\n\u002F* 不支持属性选择器 *\u002F\n\u002F* input[type=\"text\"] { }  不支持 *\u002F\n\n\u002F* 支持的选择器 *\u002F\n.class { }\n#id { }\nview { }\nview > text { }\nview + view { }\nview text { }\n```\n\n#### 样式隔离\n```vue\n\u003Cstyle scoped>\n\u002F* scoped样式只作用于当前组件 *\u002F\n.container {\n  padding: 20rpx;\n}\n\u003C\u002Fstyle>\n```\n\n### 3. 生命周期差异\n\n#### 应用生命周期\n```javascript\nexport default {\n  onLaunch(options) {\n    \u002F\u002F 应用启动\n  },\n  onShow(options) {\n    \u002F\u002F 应用显示\n  },\n  onHide() {\n    \u002F\u002F 应用隐藏\n  },\n  onError(msg) {\n    \u002F\u002F 应用错误\n  }\n}\n```\n\n#### 页面生命周期\n```javascript\nexport default {\n  onLoad(options) {\n    \u002F\u002F 页面加载\n  },\n  onShow() {\n    \u002F\u002F 页面显示\n  },\n  onReady() {\n    \u002F\u002F 页面渲染完成\n  },\n  onHide() {\n    \u002F\u002F 页面隐藏\n  },\n  onUnload() {\n    \u002F\u002F 页面卸载\n  },\n  onPullDownRefresh() {\n    \u002F\u002F 下拉刷新\n  },\n  onReachBottom() {\n    \u002F\u002F 触底加载更多\n  },\n  onShareAppMessage() {\n    \u002F\u002F 分享\n  },\n  onPageScroll(e) {\n    \u002F\u002F 页面滚动\n  },\n  onResize(size) {\n    \u002F\u002F 页面尺寸变化\n  }\n}\n```\n\n### 4. 指令差异\n\n#### 不支持的指令\n- v-show（小程序不支持）\n- v-html（使用rich-text组件代替）\n\n#### 自定义指令\n```javascript\n\u002F\u002F uni-app不支持自定义指令\n\u002F\u002F Vue自定义指令不适用\n```\n\n### 5. 事件差异\n\n#### 事件绑定\n```vue\n\u003C!-- 阻止冒泡 -->\n\u003Cview @click.stop=\"handleClick\">点击\u003C\u002Fview>\n\n\u003C!-- 阻止默认行为 -->\n\u003Cview @click.prevent=\"handleClick\">点击\u003C\u002Fview>\n\n\u003C!-- 事件捕获 -->\n\u003Cview @click.capture=\"handleClick\">点击\u003C\u002Fview>\n\n\u003C!-- 只触发一次 -->\n\u003Cview @click.once=\"handleClick\">点击\u003C\u002Fview>\n```\n\n#### 触摸事件\n```vue\n\u003Cview \n  @touchstart=\"touchStart\"\n  @touchmove=\"touchMove\"\n  @touchend=\"touchEnd\"\n  @touchcancel=\"touchCancel\"\n>\n  触摸区域\n\u003C\u002Fview>\n```\n\n---\n\n## 跨平台开发实战\n\n### 1. 项目初始化\n\n#### 创建项目\n1. HBuilderX -> 文件 -> 新建 -> 项目\n2. 选择uni-app\n3. 选择默认模板\n4. 填写项目名称：my-uniapp\n\n#### 安装依赖\n```bash\n# 如果使用npm管理依赖\nnpm install\n```\n\n### 2. 目录结构规划\n\n```\nmy-uniapp\u002F\n├── api\u002F                # API接口\n│   ├── user.js\n│   └── product.js\n├── common\u002F             # 公共资源\n│   ├── constants.js    # 常量\n│   ├── utils.js        # 工具函数\n│   └── mixins.js       # 混入\n├── components\u002F         # 组件\n│   ├── nav-bar\u002F        # 导航栏\n│   └── tab-bar\u002F        # 底部导航\n├── pages\u002F              # 页面\n│   ├── index\u002F\n│   ├── user\u002F\n│   └── product\u002F\n├── static\u002F             # 静态资源\n│   ├── images\u002F\n│   ├── icons\u002F\n│   └── fonts\u002F\n├── store\u002F              # 状态管理\n│   └── index.js\n├── styles\u002F             # 全局样式\n│   └── common.scss\n├── uni_modules\u002F        # uni-app插件\n├── App.vue\n├── main.js\n├── manifest.json\n├── pages.json\n└── package.json\n```\n\n### 3. 网络请求封装\n\n#### request.js\n```javascript\nconst BASE_URL = 'https:\u002F\u002Fapi.example.com'\n\nfunction request(options) {\n  return new Promise((resolve, reject) => {\n    uni.request({\n      url: BASE_URL + options.url,\n      method: options.method || 'GET',\n      data: options.data || {},\n      header: {\n        'content-type': 'application\u002Fjson',\n        'Authorization': uni.getStorageSync('token') || ''\n      },\n      success: (res) => {\n        if (res.statusCode === 200) {\n          resolve(res.data)\n        } else {\n          reject(res)\n        }\n      },\n      fail: (err) => {\n        reject(err)\n      }\n    })\n  })\n}\n\nexport default {\n  get(url, data) {\n    return request({ url, method: 'GET', data })\n  },\n  post(url, data) {\n    return request({ url, method: 'POST', data })\n  },\n  put(url, data) {\n    return request({ url, method: 'PUT', data })\n  },\n  delete(url, data) {\n    return request({ url, method: 'DELETE', data })\n  }\n}\n```\n\n#### 使用示例\n```javascript\nimport http from '@\u002Fcommon\u002Frequest.js'\n\nexport default {\n  data() {\n    return {\n      userList: []\n    }\n  },\n  onLoad() {\n    this.fetchUsers()\n  },\n  methods: {\n    async fetchUsers() {\n      try {\n        const res = await http.get('\u002Fusers')\n        this.userList = res.data\n      } catch (err) {\n        console.error(err)\n        uni.showToast({\n          title: '获取数据失败',\n          icon: 'none'\n        })\n      }\n    }\n  }\n}\n```\n\n### 4. 状态管理\n\n#### 简单状态管理\n```javascript\n\u002F\u002F store\u002Findex.js\nconst store = {\n  state: {\n    userInfo: null,\n    token: ''\n  },\n  mutations: {\n    setUserInfo(state, userInfo) {\n      state.userInfo = userInfo\n      uni.setStorageSync('userInfo', userInfo)\n    },\n    setToken(state, token) {\n      state.token = token\n      uni.setStorageSync('token', token)\n    }\n  },\n  actions: {\n    login({ commit }, credentials) {\n      return http.post('\u002Flogin', credentials).then(res => {\n        commit('setToken', res.token)\n        commit('setUserInfo', res.userInfo)\n      })\n    },\n    logout({ commit }) {\n      commit('setToken', '')\n      commit('setUserInfo', null)\n    }\n  }\n}\n\nexport default store\n```\n\n#### 使用Vuex（推荐）\n```javascript\n\u002F\u002F store\u002Findex.js\nimport Vue from 'vue'\nimport Vuex from 'vuex'\n\nVue.use(Vuex)\n\nexport default new Vuex.Store({\n  state: {\n    userInfo: null,\n    token: ''\n  },\n  mutations: {\n    SET_USER_INFO(state, userInfo) {\n      state.userInfo = userInfo\n    },\n    SET_TOKEN(state, token) {\n      state.token = token\n    }\n  },\n  actions: {\n    async login({ commit }, credentials) {\n      const res = await http.post('\u002Flogin', credentials)\n      commit('SET_TOKEN', res.token)\n      commit('SET_USER_INFO', res.userInfo)\n      return res\n    }\n  },\n  getters: {\n    isLoggedIn: state => !!state.token\n  }\n})\n```\n\n### 5. 组件开发\n\n#### 组件定义\n```vue\n\u003C!-- components\u002Fmy-card\u002Fmy-card.vue -->\n\u003Ctemplate>\n  \u003Cview class=\"my-card\" @click=\"handleClick\">\n    \u003Cimage class=\"card-image\" :src=\"image\" mode=\"aspectFill\">\u003C\u002Fimage>\n    \u003Cview class=\"card-content\">\n      \u003Ctext class=\"card-title\">{{ title }}\u003C\u002Ftext>\n      \u003Ctext class=\"card-desc\">{{ description }}\u003C\u002Ftext>\n    \u003C\u002Fview>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nexport default {\n  name: 'MyCard',\n  props: {\n    title: {\n      type: String,\n      default: ''\n    },\n    description: {\n      type: String,\n      default: ''\n    },\n    image: {\n      type: String,\n      default: ''\n    }\n  },\n  methods: {\n    handleClick() {\n      this.$emit('click', this.title)\n    }\n  }\n}\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\n.my-card {\n  background-color: #fff;\n  border-radius: 12rpx;\n  overflow: hidden;\n  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);\n}\n.card-image {\n  width: 100%;\n  height: 300rpx;\n}\n.card-content {\n  padding: 20rpx;\n}\n.card-title {\n  font-size: 32rpx;\n  font-weight: bold;\n  display:block;\n  margin-bottom: 10rpx;\n}\n.card-desc {\n  font-size: 28rpx;\n  color: #666;\n}\n\u003C\u002Fstyle>\n```\n\n#### 组件使用\n```vue\n\u003Ctemplate>\n  \u003Cview>\n    \u003Cmy-card \n      v-for=\"item in list\" \n      :key=\"item.id\"\n      :title=\"item.title\"\n      :description=\"item.description\"\n      :image=\"item.image\"\n      @click=\"handleCardClick\"\n    \u002F>\n  \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport MyCard from '@\u002Fcomponents\u002Fmy-card\u002Fmy-card.vue'\n\nexport default {\n  components: {\n    MyCard\n  },\n  data() {\n    return {\n      list: []\n    }\n  },\n  methods: {\n    handleCardClick(title) {\n      console.log('点击了卡片：', title)\n    }\n  }\n}\n\u003C\u002Fscript>\n```\n\n### 6. 混入使用\n\n#### 定义混入\n```javascript\n\u002F\u002F common\u002Fmixins.js\nexport const pageMixin = {\n  data() {\n    return {\n      loading: false,\n      page: 1,\n      pageSize: 10,\n      hasMore: true\n    }\n  },\n  methods: {\n    async loadData() {\n      if (this.loading || !this.hasMore) return\n      \n      this.loading = true\n      try {\n        const res = await this.fetchData()\n        this.handleData(res)\n      } catch (err) {\n        console.error(err)\n      } finally {\n        this.loading = false\n      }\n    },\n    onReachBottom() {\n      this.page++\n      this.loadData()\n    }\n  }\n}\n```\n\n#### 使用混入\n```javascript\nimport { pageMixin } from '@\u002Fcommon\u002Fmixins.js'\n\nexport default {\n  mixins: [pageMixin],\n  data() {\n    return {\n      list: []\n    }\n  },\n  methods: {\n    async fetchData() {\n      return http.get('\u002Flist', {\n        page: this.page,\n        pageSize: this.pageSize\n      })\n    },\n    handleData(res) {\n      if (res.data.length \u003C this.pageSize) {\n        this.hasMore = false\n      }\n      this.list = this.list.concat(res.data)\n    }\n  }\n}\n```\n\n---\n\n## 性能优化与调试\n\n### 1. 性能优化\n\n#### 图片优化\n```vue\n\u003C!-- 使用合适的mode -->\n\u003Cimage src=\"logo.png\" mode=\"aspectFit\">\u003C\u002Fimage>\n\n\u003C!-- 懒加载 -->\n\u003Cimage :src=\"item.image\" lazy-load>\u003C\u002Fimage>\n\n\u003C!-- 使用webp格式（支持的平台） -->\n\u003Cimage src=\"image.webp\">\u003C\u002Fimage>\n```\n\n#### 列表优化\n```vue\n\u003C!-- 使用key -->\n\u003Cview v-for=\"item in list\" :key=\"item.id\">\n  {{ item.name }}\n\u003C\u002Fview>\n\n\u003C!-- 长列表使用虚拟滚动 -->\n\u003Crecycle-list :list=\"longList\" :item-height=\"100\">\n  \u003Ctemplate v-slot=\"{ item }\">\n    \u003Cview>{{ item.name }}\u003C\u002Fview>\n  \u003C\u002Ftemplate>\n\u003C\u002Frecycle-list>\n```\n\n#### 数据优化\n```javascript\n\u002F\u002F 避免大对象响应式\n\u002F\u002F 使用Object.freeze冻结不需要响应式的数据\nconst staticData = Object.freeze(largeArray)\n\n\u002F\u002F 合理使用computed\ncomputed: {\n  filteredList() {\n    return this.list.filter(item => item.active)\n  }\n}\n```\n\n#### 代码分包\n```json\n\u002F\u002F pages.json\n{\n  \"pages\": [\n    {\n      \"path\": \"pages\u002Findex\u002Findex\"\n    }\n  ],\n  \"subPackages\": [\n    {\n      \"root\": \"pages\u002Fuser\",\n      \"pages\": [\n        {\n          \"path\": \"profile\u002Fprofile\"\n        },\n        {\n          \"path\": \"settings\u002Fsettings\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n### 2. 调试技巧\n\n#### Console调试\n```javascript\nconsole.log('调试信息')\nconsole.table(data)  \u002F\u002F 表格输出\nconsole.time('timer')\nconsole.timeEnd('timer')\n```\n\n#### 微信开发者工具调试\n- 使用Sources面板打断点\n- 使用AppData查看页面数据\n- 使用Network查看网络请求\n- 使用Performance分析性能\n\n#### vconsole调试\n```javascript\n\u002F\u002F 安HBuilderX插件：vconsole\n\u002F\u002F 在小程序中自动显示调试面板\n```\n\n### 3. 常见问题解决\n\n#### 样式不生效\n```css\n\u002F* 检查选择器是否支持 *\u002F\n\u002F* 检查是否使用了不支持的样式属性 *\u002F\n\u002F* 检查scoped样式是否正确 *\u002F\n```\n\n#### 图片不显示\n```vue\n\u003C!-- 检查图片路径 -->\n\u003C!-- 本地图片放在static目录 -->\n\u003Cimage src=\"\u002Fstatic\u002Flogo.png\">\u003C\u002Fimage>\n\n\u003C!-- 网络图片需要配置域名白名单 -->\n```\n\n#### 跨域问题\n```javascript\n\u002F\u002F H5开发时配置代理\n\u002F\u002F manifest.json -> h5 -> devServer\n{\n  \"h5\": {\n    \"devServer\": {\n      \"proxy\": {\n        \"\u002Fapi\": {\n          \"target\": \"https:\u002F\u002Fapi.example.com\",\n          \"changeOrigin\": true,\n          \"pathRewrite\": {\n            \"^\u002Fapi\": \"\"\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n#### 小程序审核失败\n```\n常见原因：\n1. 域名未配置白名单\n2. 使用了未授权的API\n3. 内容违规\n4. 功能不完整\n\n解决方案：\n1. 登录微信公众平台配置服务器域名\n2. 检查API使用权限\n3. 审核内容合规性\n4. 完善功能流程\n```\n\n---\n\n## 学习资源\n\n### 官方文档\n- uni-app官方文档：https:\u002F\u002Funiapp.dcloud.net.cn\u002F\n- HBuilderX使用教程：https:\u002F\u002Fhx.dcloud.net.cn\u002F\n- 微信小程序文档：https:\u002F\u002Fdevelopers.weixin.qq.com\u002Fminiprogram\u002Fdev\u002Fframework\u002F\n\n### 插件市场\n- uni-app插件市场：https:\u002F\u002Fext.dcloud.net.cn\u002F\n- 常用插件：uView UI、uni-ui、colorUI等\n\n### 社区资源\n- DCloud社区：https:\u002F\u002Fask.dcloud.net.cn\u002F\n- GitHub：https:\u002F\u002Fgithub.com\u002Fdcloudio\u002Funi-app\n\n---\n\n## 最佳实践\n\n### 1. 开发规范\n- 统一使用rpx单位\n- 组件命名使用kebab-case\n- 页面文件放在pages目录\n- 静态资源放在static目录\n- API接口统一封装\n\n### 2. 代码规范\n- 使用ESLint进行代码检查\n- 统一代码风格\n- 添加必要的注释\n- 合理拆分组件\n\n### 3. 版本管理\n- 使用Git进行版本控制\n- 提交前进行代码review\n- 使用语义化版本号\n- 保留重要版本备份\n\n### 4. 测试策略\n- 多平台测试\n- 边界情况测试\n- 性能测试\n- 兼容性测试\n\n---\n\n## 总结\n\nuni-app是一个强大的跨平台开发框架，结合HBuilderX和微信开发者工具，可以高效地开发小程序、H5和App应用。掌握这些工具的使用方法和uni-app的核心概念，将大大提升开发效率。\n\n**学习路径建议：**\n1. 熟悉HBuilderX基本操作\n2. 掌握微信开发者工具调试\n3. 学习uni-app基础语法\n4. 理解uni-app与Vue的差异\n5. 实战项目开发\n6. 性能优化和问题解决\n",104,"2026-04-07T16:38:10.473Z","2026-04-07T16:38:10.477Z","2026-05-25T22:05:44.322Z",{"id":21,"categoryName":119,"slug":119,"description":119,"sort":31,"isEnable":32,"createTime":120,"updateTime":120,"deleteTime":31},"跨端开发","2026-04-07T16:38:39.509Z",[122],{"articleId":110,"tagId":123,"createTime":124,"tag":125},"6","2026-04-07T16:40:32.144Z",{"id":123,"tagName":126,"slug":126,"themeColor":127,"description":126,"createTime":128,"updateTime":128,"deleteTime":64},"Uniapp","#00ff33","2026-04-07T16:40:20.885Z",{"id":130,"title":131,"slug":132,"coverUrl":15,"summary":15,"content":133,"htmlContent":58,"categoryId":83,"viewCount":134,"likeCount":31,"isTop":31,"isPublish":32,"seoKeywords":15,"seoDescription":15,"publishTime":135,"createTime":136,"updateTime":137,"deleteTime":64,"category":138,"tags":139},"14","SASS\u002FSCSS 学习笔记","sass学习笔记","# SASS\u002FSCSS 学习笔记\n\n## 一、SASS 简介\n\n### 1.1 什么是 SASS\n- SASS (Syntactically Awesome Style Sheets) 是一种 CSS 预处理器\n- SCSS (Sassy CSS) 是 SASS 3 引入的新语法，完全兼容 CSS3\n- SASS 提供了变量、嵌套、混合、继承等功能，使 CSS 更易维护\n\n### 1.2 SASS vs SCSS\n```scss\n\u002F\u002F SCSS 语法（推荐，完全兼容 CSS）\n$color: red;\n.container {\n  color: $color;\n}\n\n\u002F\u002F SASS 语法（缩进式，不兼容 CSS）\n$color: red\n.container\n  color: $color\n```\n\n### 1.3 安装\n```bash\n# 使用 npm 安装\nnpm install -g sass\n\n# 使用 Node.js 编译\nsass input.scss output.css\n\n# 监听文件变化\nsass --watch input.scss output.css\n\n# 监听整个目录\nsass --watch scss:css\n```\n\n---\n\n## 二、变量\n\n### 2.1 定义变量\n```scss\n\u002F\u002F 使用 $ 符号定义变量\n$primary-color: #3498db;\n$font-size: 16px;\n$border-radius: 4px;\n```\n\n### 2.2 使用变量\n```scss\n.button {\n  background-color: $primary-color;\n  font-size: $font-size;\n  border-radius: $border-radius;\n}\n```\n\n### 2.3 变量作用域\n```scss\n$global-color: blue;\n\n.container {\n  $local-color: red;\n  color: $local-color;  \u002F\u002F 可以使用局部变量\n  color: $global-color; \u002F\u002F 可以使用全局变量\n}\n\n\u002F\u002F $local-color 在这里不可用\n```\n\n### 2.4 默认值\n```scss\n$color: red !default; \u002F\u002F 如果变量未定义，则使用默认值\n```\n\n---\n\n## 三、嵌套\n\n### 3.1 选择器嵌套\n```scss\n.container {\n  background: white;\n  \n  .header {\n    padding: 20px;\n  }\n  \n  .content {\n    margin: 10px;\n  }\n}\n\n\u002F\u002F 编译后：\n\u002F\u002F .container { background: white; }\n\u002F\u002F .container .header { padding: 20px; }\n\u002F\u002F .container .content { margin: 10px; }\n```\n\n### 3.2 属性嵌套\n```scss\n.button {\n  border: {\n    width: 1px;\n    style: solid;\n    color: #ccc;\n  }\n}\n\n\u002F\u002F 编译后：\n\u002F\u002F .button { border-width: 1px; border-style: solid; border-color: #ccc; }\n```\n\n### 3.3 父选择器引用 (&)\n```scss\n.button {\n  &:hover {\n    background: darken($color, 10%);\n  }\n  \n  &.active {\n    background: $primary-color;\n  }\n}\n\n\u002F\u002F 编译后：\n\u002F\u002F .button:hover { background: ...; }\n\u002F\u002F .button.active { background: ...; }\n```\n\n---\n\n## 四、混合\n\n### 4.1 基本混合\n```scss\n@mixin border-radius($radius) {\n  -webkit-border-radius: $radius;\n  -moz-border-radius: $radius;\n  border-radius: $radius;\n}\n\n.box {\n  @include border-radius(10px);\n}\n```\n\n### 4.2 默认参数\n```scss\n@mixin box-shadow($x: 0, $y: 0, $blur: 5px, $color: rgba(0,0,0,0.3)) {\n  box-shadow: $x $y $blur $color;\n}\n\n.card {\n  @include box-shadow();\n}\n\n.button {\n  @include box-shadow(2px, 2px, 10px, red);\n}\n```\n\n### 4.3 可变参数\n```scss\n@mixin box-shadow($shadows...) {\n  box-shadow: $shadows;\n}\n\n.element {\n  @include box-shadow(0 0 5px #000, 0 0 10px #333);\n}\n```\n\n### 4.4 内容块\n```scss\n@mixin mobile {\n  @media (max-width: 768px) {\n    @content;\n  }\n}\n\n.container {\n  width: 100%;\n  \n  @include mobile {\n    width: 50%;\n  }\n}\n```\n\n---\n\n## 五、继承\n\n### 5.1 基本继承\n```scss\n.message {\n  padding: 10px;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n}\n\n.success {\n  @extend .message;\n  background-color: #dff0d8;\n  border-color: #d6e9c6;\n}\n\n.error {\n  @extend .message;\n  background-color: #f2dede;\n  border-color: #ebccd1;\n}\n```\n\n### 5.2 占位符选择器\n```scss\n\u002F\u002F 使用 % 定义占位符，不会生成独立的 CSS\n%message-base {\n  padding: 10px;\n  border: 1px solid #ccc;\n}\n\n.success {\n  @extend %message-base;\n  background: green;\n}\n\n.error {\n  @extend %message-base;\n  background: red;\n}\n```\n\n---\n\n## 六、运算\n\n### 6.1 数字运算\n```scss\n$width: 100px;\n$padding: 10px;\n\n.container {\n  width: $width + $padding * 2; \u002F\u002F 120px\n}\n```\n\n### 6.2 颜色运算\n```scss\n$color: #369;\n\n.button {\n  background: $color + #111; \u002F\u002F 变亮\n  border: $color - #222;    \u002F\u002F 变暗\n}\n```\n\n### 6.3 字符串运算\n```scss\n$name: \"icon\";\n\n.icon-#{$name} {\n  background: url(\"images\u002F#{$name}.png\");\n}\n```\n\n### 6.4 单位运算\n```scss\n$width: 100px;\n\n.container {\n  width: $width \u002F 2;   \u002F\u002F 50px\n  height: 100% \u002F 2;    \u002F\u002F 50%\n}\n```\n\n---\n\n## 七、函数\n\n### 7.1 内置颜色函数\n```scss\n$color: #3498db;\n\n.button {\n  background: lighten($color, 10%);  \u002F\u002F 变亮\n  border: darken($color, 10%);       \u002F\u002F 变暗\n  color: invert($color);             \u002F\u002F 反色\n  opacity: opacity($color);          \u002F\u002F 透明度\n}\n```\n\n### 7.2 其他内置函数\n```scss\n$number: 10px;\n$string: \"hello\";\n$list: 1, 2, 3, 4;\n\n.container {\n  width: abs(-10px);           \u002F\u002F 绝对值\n  height: round(10.5px);       \u002F\u002F 四舍五入\n  margin: ceil(10.2px);        \u002F\u002F 向上取整\n  padding: floor(10.8px);      \u002F\u002F 向下取整\n  content: length($list);      \u002F\u002F 列表长度\n  content: nth($list, 2);      \u002F\u002F 获取列表第2项\n}\n```\n\n### 7.3 自定义函数\n```scss\n@function double($n) {\n  @return $n * 2;\n}\n\n.container {\n  width: double(100px); \u002F\u002F 200px\n}\n```\n\n---\n\n## 八、条件语句\n\n### 8.1 if 语句\n```scss\n@mixin theme($theme) {\n  @if $theme == 'dark' {\n    background: #333;\n    color: #fff;\n  } @else if $theme == 'light' {\n    background: #fff;\n    color: #333;\n  } @else {\n    background: #ccc;\n    color: #000;\n  }\n}\n\n.container {\n  @include theme('dark');\n}\n```\n\n### 8.2 三元运算符\n```scss\n$width: 100px;\n$max-width: 200px;\n\n.container {\n  width: if($width > $max-width, $max-width, $width);\n}\n```\n\n---\n\n## 九、循环\n\n### 9.1 for 循环\n```scss\n@for $i from 1 through 5 {\n  .col-#{$i} {\n    width: 100% \u002F 5 * $i;\n  }\n}\n\n\u002F\u002F 编译后生成 .col-1, .col-2, .col-3, .col-4, .col-5\n```\n\n### 9.2 each 循环\n```scss\n$colors: (red: #f00, green: #0f0, blue: #00f);\n\n@each $name, $color in $colors {\n  .text-#{$name} {\n    color: $color;\n  }\n}\n\n\u002F\u002F 编译后生成 .text-red, .text-green, .text-blue\n```\n\n### 9.3 while 循环\n```scss\n$i: 6;\n\n@while $i > 0 {\n  .item-#{$i} {\n    width: $i * 10px;\n  }\n  $i: $i - 1;\n}\n```\n\n---\n\n## 十、列表和映射\n\n### 10.1 列表\n```scss\n$fonts: \"Helvetica\", \"Arial\", sans-serif;\n\nbody {\n  font-family: $fonts;\n}\n\n\u002F\u002F 列表函数\n$numbers: 1, 2, 3, 4, 5;\n$first: nth($numbers, 1);      \u002F\u002F 1\n$length: length($numbers);    \u002F\u002F 5\n$last: nth($numbers, -1);     \u002F\u002F 5\n$joined: join($numbers, 6);  \u002F\u002F 1, 2, 3, 4, 5, 6\n```\n\n### 10.2 映射\n```scss\n$colors: (\n  primary: #3498db,\n  secondary: #2ecc71,\n  danger: #e74c3c\n);\n\n.button {\n  background: map-get($colors, primary);\n}\n\n\u002F\u002F 映射函数\n$keys: map-keys($colors);       \u002F\u002F (primary, secondary, danger)\n$values: map-values($colors);   \u002F\u002F (#3498db, #2ecc71, #e74c3c)\n$has-key: map-has-key($colors, primary); \u002F\u002F true\n```\n\n---\n\n## 十一、模块化\n\n### 11.1 使用 @import\n```scss\n\u002F\u002F _variables.scss\n$primary-color: #3498db;\n$font-size: 16px;\n\n\u002F\u002F _mixins.scss\n@mixin button {\n  padding: 10px 20px;\n  border-radius: 4px;\n}\n\n\u002F\u002F main.scss\n@import 'variables';\n@import 'mixins';\n\n.button {\n  @include button;\n  background: $primary-color;\n}\n```\n\n### 11.2 使用 @use（推荐）\n```scss\n\u002F\u002F _variables.scss\n$primary-color: #3498db !default;\n@forward 'mixins';\n\n\u002F\u002F main.scss\n@use 'variables' as *;\n\n.button {\n  background: $primary-color;\n}\n```\n\n### 11.3 使用 @forward\n```scss\n\u002F\u002F _mixins.scss\n@mixin button { ... }\n\n\u002F\u002F _utilities.scss\n@forward 'mixins';\n@forward 'functions';\n\n\u002F\u002F main.scss\n@use 'utilities';\n\n.button {\n  @include utilities.button;\n}\n```\n\n---\n\n## 十二、注释\n\n### 12.1 单行注释\n```scss\n\u002F\u002F 这个注释不会出现在编译后的 CSS 中\n$color: red;\n```\n\n### 12.2 多行注释\n```scss\n\u002F* 这个注释会出现在编译后的 CSS 中 *\u002F\n$color: blue;\n```\n\n### 12.3 强制注释\n```scss\n\u002F*! 这个注释会出现在压缩后的 CSS 中 *\u002F\n$color: green;\n```\n\n---\n\n## 十三、调试\n\n### 13.1 @debug\n```scss\n$width: 100px;\n\n@debug \"Width is: #{$width}\";\n\u002F\u002F 输出：Width is: 100px\n```\n\n### 13.2 @warn\n```scss\n@warn \"This feature is deprecated\";\n\u002F\u002F 输出警告信息\n```\n\n### 13.3 @error\n```scss\n@if $width \u003C 0 {\n  @error \"WidthWidth cannot be negative\";\n}\n\u002F\u002F 抛出错误，停止编译\n```\n\n---\n\n## 十四、最佳实践\n\n### 14.1 文件组织\n```\nstyles\u002F\n├── main.scss          # 主文件\n├── abstracts\u002F         # 抽象层\n│   ├── _variables.scss\n│   ├── _functions.scss\n│   └── _mixins.scss\n├── base\u002F              # 基础样式\n│   ├── _reset.scss\n│   └── _typography.scss\n├── components\u002F        # 组件\n│   ├── _button.scss\n│   └── _card.scss\n├── layout\u002F            # 布局\n│   ├── _header.scss\n│   └── _footer.scss\n└── pages\u002F             # 页面\n    └── _home.scss\n```\n\n### 14.2 命名规范\n```scss\n\u002F\u002F 使用 BEM 命名规范\n.block {\n  &__element {\n    &--modifier {\n      \n    }\n  }\n}\n\n\u002F\u002F 变量使用有意义的名称\n$primary-color: #3498db;\n$spacing-unit: 8px;\n$border-radius: 4px;\n```\n\n### 14.3 性能优化\n```scss\n\u002F\u002F 避免过度嵌套（最多3层）\n\u002F\u002F 不好\n.container {\n  .header {\n    .title {\n      .text {\n        color: red;\n      }\n    }\n  }\n}\n\n\u002F\u002F 好\n.container {\n  .header {\n    .title-text {\n      color: red;\n    }\n  }\n}\n\n\u002F\u002F 使用占位符选择器而不是类继承\n%base-button {\n  padding: 10px;\n}\n\n.primary-button {\n  @extend %base-button;\n  background: blue;\n}\n```\n\n---\n\n## 十五、常用代码片段\n\n### 15.1 响应式断点\n```scss\n$breakpoints: (\n  mobile: 480px,\n  tablet: 768px,\n  desktop: 1024px\n);\n\n@mixin respond-to($breakpoint) {\n  @if map-has-key($breakpoints, $breakpoint) {\n    @media (min-width: map-get($breakpoints, $breakpoint)) {\n      @content;\n    }\n  }\n}\n\n.container {\n  width: 100%;\n  \n  @include respond-to(tablet) {\n    width: 50%;\n  }\n}\n```\n\n### 15.2 三角形\n```scss\n@mixin triangle($size, $color, $direction) {\n  width: 0;\n  height: 0;\n  border-style: solid;\n  \n  @if $direction == 'up' {\n    border-width: 0 $size \u002F 2 $size $size \u002F 2;\n    border-color: transparent transparent $color transparent;\n  } @else if $direction == 'down' {\n    border-width: $size $size \u002F 2 0 $size \u002F 2;\n    border-color: $color transparent transparent transparent;\n  }\n}\n\n.arrow-up {\n  @include triangle(10px, red, up);\n}\n```\n\n### 15.3 文本截断\n```scss\n@mixin text-truncate {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.truncate {\n  @include text-truncate;\n}\n```\n\n### 15.4 居中\n```scss\n@mixin center {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n@mixin absolute-center {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n```\n\n---\n\n## 十六、SASS 与现代 CSS\n\n### 16.1 CSS 变量 vs SASS 变量\n```scss\n\u002F\u002F SASS 变量（编译时）\n$sass-color: red;\n\n\u002F\u002F CSS 变量（运行时）\n:root {\n  --css-color: red;\n}\n\n.element {\n  color: $sass-color;    \u002F\u002F 编译为 color: red;\n  color: var(--css-color); \u002F\u002F 保持为 var(--css-color);\n}\n```\n\n### 16.2 何时使用 SASS\n- 需要复杂的计算和逻辑\n- 需要模块化和代码复用\n- 需要兼容旧浏览器\n- 团队需要统一的样式系统\n\n### 16.3 何时使用原生 CSS\n- 简单项目\n- 不需要预处理器功能\n- 需要运行时动态修改变量\n- 追求更快的编译速度\n\n---\n\n## 总结\n\nSASS\u002FSCSS 提供了强大的功能来增强 CSS：\n- **变量**：便于维护和统一管理\n- **嵌套**：代码结构更清晰\n- **混合**：代码复用\n- **继承**：减少重复代码\n- **函数**：强大的计算能力\n- **模块化**：代码组织更合理\n\n合理使用这些功能，可以大大提高 CSS 的开发效率和可维护性。\n",100,"2026-04-07T16:23:17.106Z","2026-04-07T16:23:17.108Z","2026-05-25T22:05:44.026Z",{"id":83,"categoryName":89,"slug":90,"description":91,"sort":31,"isEnable":32,"createTime":92,"updateTime":93,"deleteTime":31},[],{"id":141,"title":142,"slug":143,"coverUrl":15,"summary":15,"content":144,"htmlContent":58,"categoryId":83,"viewCount":145,"likeCount":31,"isTop":31,"isPublish":32,"seoKeywords":15,"seoDescription":15,"publishTime":146,"createTime":147,"updateTime":148,"deleteTime":64,"category":149,"tags":150},"13","一，项目收获及技术","硅谷甄选项目笔记","\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# 一，项目收获及技术\n\n## 1.1 收获\n\n* 企业级的编码规范\n\n  ![image-20260129230723775](images\u002Fimage-20260129230723775.png)\n\n* 从零开始，封装一个后台管理系统\n\n  ![image-20260129230803256](images\u002Fimage-20260129230803256.png)\n\n* 菜单权限与按钮权限\n\n  ![image-20260129230823689](images\u002Fimage-20260129230823689.png)\n\n* 数据可视化大屏\n\n  ![image-20260129230843202](images\u002Fimage-20260129230843202.png)\n\n* svg 矢量图在项目中的应用\n\n  ![image-20260129230907605](images\u002Fimage-20260129230907605.png)\n\n* 主题颜色切换与暗黑模式的切换\n\n  ![image-20260129230933375](images\u002Fimage-20260129230933375.png)\n\n\n\n## 1.2 技术选选型\n\nVue3 + 组合式API + Vite构建工具 + element-plus + Axios + Echarts + pinia + TypeScript + vue-router\n\n\n# 二，Vue3 组件通信的艺术：构建灵活可复用的UI\n\n在任何复杂的单页应用中，组件之的通信都是一个核心挑战。高效的组件通信能够显著提高代码的复用性，可维护性以及团队的协作效率\n\n## 2.1 props: 父子组件通信的基石\n\n`props（道具）`是Vue中最直接，最常用的父子组件通信方式。在Vue3中，通过 defineProps 宏，我们可以轻松地接受父组件传递的数据，并且无需显示引入，可以在\u003Cscript setup> 中使用，极大的简化了开发流程\n\n**父组件向子组件传递数据案例** ：\n\n子组件可以通过两种方式接受 `props`\n\n父组件传数据的代码\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"box\">\n  \u003Ch1>父组件\u003C\u002Fh1>\n  \u003CChild info=\"我爱祖国\" :money=\"money\">\u003C\u002FChild>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport {ref} from \"vue\"\nimport Child from \".\u002FChild.vue\";\n\nlet money = ref(1000)\n\u003C\u002Fscript>\n\n```\n\n**方式一：带类型和默认值的声明**\n\n这种方式提供了更强的类型检查和默认值设置，增加了代码的健壮性。\n\n```typescript\n\nlet props = defineProps({\n  info:{\n    type:String,  \u002F\u002F 接受的数据类型\n    defalut:'默认参数' \u002F\u002F 接受默认数据\n  },\n  money:{\n    type:Number,\n    default:0\n\n  }\n})\n\u002F\u002F 在模板中可以直接使用 props.info,props.money   省略直接 info,money 也可以\n```\n\n**方式二：数组形式的简洁声明**\n\n适用于简单的 `props` 声明，但缺乏类型检查\n\n```typescript\nlet props = defineProps(['info','money'])\n```\n\n**重要提示：** `props` 是单向数据流，子组件只能读取 `props` 数据，不能直接修改它。\n\n\n\n## 2.2 自定义事件：子组件向父组件传递数据\n\n自定义事件是实现子组件向父组件传递数据的关键机制。与原生 DOM 事件不同，自定义事件是 Vue 组件特有的通信方式。\n\n**父组件绑定自定义事件：**\n\n父组件在子组件标签上通过 `@` 符号绑定自定义事件。\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"father\">\n  \u003Ch1>父组件\u003C\u002Fh1>\n  \u003CEvent1 @xxx=\"handler2\">\u003C\u002FEvent1>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport Event1 from '.\u002FEvent1.vue'; \n\nconst handler2 = (param1:string,param2:string) =>{\n  console.log('从子组件接收到数据',param1,param2);\n  \n}\n\n\u003C\u002Fscript>\n\n```\n\n**子组件触发自定义事件：**\n\n在子组件内部，使用 `defineEmits` 宏声明需要触发的自定义事件，然后通过 `$emit` 方法触发事件并传递数据。\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"son\">\n  \u003Ch1>我是子组件1\u003C\u002Fh1>\n  \u003Cbutton @click=\"handler\">点我触发事件xxx自定义事件\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\nlet $emit = defineEmits(['xxx']) \u002F\u002F 声明自定义事件 'xxx',\nconst handler =() =>{\n  $emit('xxx','法拉利','茅台')  \u002F\u002F 第一个参数，自定义事件的名称，父组件通过这个来监听这个事件，剩下的参数是子组件传给父组件的数据\n}\n\u003C\u002Fscript>\n\n```\n\n**Vue3 中原生 DOM 事件的特殊性：**\n\n在 Vue3 中，像 `click`、`dbclick`、`change` 这类原生 DOM 事件，无论是在普通 HTML 标签上还是在自定义组件标签上，默认都视为原生 DOM 事件。这与 Vue2 中需要 `native` 修饰符才能将组件上的事件变为原生 DOM 事件有所不同。但如果子组件内部通过 `defineEmits` 定义了同名的事件，那么它将优先被视为自定义事件。\n\n## 2.3 全局事件总线 (mitt)：实现任意组件通信\n\n在 Vue2 中，我们常利用 `Vue.prototype.$bus` 实现全局事件总线。然而，Vue3 没有 Vue 构造函数，且组合式 API 中没有 `this` 上下文，因此传统的全局事件总线方式不再适用。在 Vue3 中，我们可以借助轻量级的第三方库 `mitt` 来实现全局事件总线功能，从而让任意组件之间进行通信。\n\n**mitt 官网：** https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmitt\n\n使用：\n\n* 下载安装mitt `$ npm install --save mitt`\n* 在 src 中创建文件夹 bus，index.ts\n* 在 index.ts 中引入 mitt\n\n```vue\n\u002F\u002F引入mitt插件:mitt一个方法,方法执行会返回bus对象\nimport mitt from 'mitt';\nconst $bus = mitt();\nexport default $bus;\n\n```\n\n* 送东西的兄弟\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"child2\">\n     \u003Ch2>我是子组件2:曹丕\u003C\u002Fh2>\n     \u003Cbutton @click=\"handler\">点击我给兄弟送一台法拉利\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n    \n\u002F\u002F引入$bus对象\nimport $bus from '..\u002F..\u002Fbus';\n\n\u002F\u002F点击按钮回调\nconst handler = ()=>{\n  $bus.emit('car',{car:\"法拉利\"});  \u002F\u002F 第一个参数事件的名称（类型），告诉另一个兄弟是哪个事件，类似于一个表示这个事件的id\n}\n\u003C\u002Fscript>\n\n```\n\n* 收到东西的兄弟\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"child1\">\n    \u003Ch3>我是子组件1:曹植\u003C\u002Fh3>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n    \nimport $bus from \"..\u002F..\u002Fbus\";\n\u002F\u002F组合式API函数\nimport { onMounted } from \"vue\";\n\u002F\u002F组件挂载完毕的时候,当前组件绑定一个事件,接受将来兄弟组件传递的数据\n\nonMounted(() => {\n  \u002F\u002F第一个参数:即为事件类型  第二个参数:即为事件回调\n  $bus.on(\"car\", (car) => {\n    console.log(car);\n  });\n});\n\u003C\u002Fscript>\n```\n\n\n\n## 2.4 `v-model`：实现父子组件数据的双向绑定(同步数据)\n\n`v-model` 指令不仅用于收集表单数据实现双向绑定，它也是实现父子组件数据同步的强大工具。在底层，`v-model` 实际上是 `props` (`modelValue`) 和自定义事件 (`update:modelValue`) 的语法糖。\n\n`v-model` 实现表单数据收集，数据双向绑定：\n\n```vue\n\u003Ctemplate>\n\u003Cdiv>\n  \u003Ch1>v-model\u003C\u002Fh1>\n  \u003Cinput type=\"text\" v-model=\"info\">\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F v-model指令，收集表单数据，实现双向绑定\n\nimport {ref} from 'vue'\n\nlet info = ref('')\n\n\u003C\u002Fscript>\n\n```\n\n`v-model` 实现父子组件数据的同步：\n\n父组件：\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"fa\">\n  \u003Ch1>v-model 钱数：{{ money }}\u003C\u002Fh1>\n  \u003Cinput type=\"text\" v-model=\"info\">\n  \u003Chr>\n  \u003C!-- props：父亲给儿子数据 -->\n   \u003C!-- \u003CChild :modelValue=\"money\" @updata:model-value=\"handler\">\u003C\u002FChild> -->\n\n   \u003C!-- v-model组件身上的作用\n    第一：相当于给子组件传递props[modelValue] = 1000\n    第二：相当于给子组件绑定自定义事件 update:modelValue   事件名不能错\n    \n   -->\n   \u003CChild v-model=\"money\">\u003C\u002FChild>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F v-model指令，收集表单数据，实现双向绑定\nimport {ref} from 'vue'\n\nlet info = ref('')\n\n\u002F\u002F v-model 实现组件之间的通信，实现父子组件的数据同步\n\u002F\u002F 父亲给子组件数据 Props\n\u002F\u002F 子组件给父组件数据 自定义事件\n\n\u002F\u002F 引入子组件\nimport Child from '.\u002FChild.vue';\n\n\u002F\u002F 父组件的数据钱数\nlet money = ref(10000)\n\n\u002F\u002F 自定义事件的回调\nconst handler = (num) =>{\n  \u002F\u002F 将来接受子组件传递过来的数据\n  \u002F\u002F console.log(num)\n  money.value = num\n}\n\u003C\u002Fscript>\n```\n\n子组件：\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"child\">\n  \u003Ch3>钱数：{{ modelValue }}\u003C\u002Fh3>\n  \u003Cbutton @click=\"handler\">父子组件数据绑定\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 接受 props\nlet props = defineProps(['modelValue'])\nlet $emit = defineEmits(['update:modelValue'])\n\n\u002F\u002F 子组件内部按钮的点击回调\nconst handler = () =>{\n  \u002F\u002F 触发自定义事件\n  $emit('update:modelValue',props.modelValue+1000)\n}\n\u003C\u002Fscript>\n\n```\n\n**多 v-model 绑定：**\n\nVue3 允许一个组件使用多个 `v-model`，从而实现父子组件多个数据的同步。\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>分页示例\u003C\u002Fh1>\n    \u003C!-- 使用 v-model 绑定多个属性 -->\n    \u003CChild1 v-model:pageNo=\"pageNo\" v-model:pageSize=\"pageSize\" \u002F>\n    \u003Cp>当前页码：{{ pageNo }}\u003C\u002Fp>\n    \u003Cp>每页显示条数：{{ pageSize }}\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { ref } from 'vue';\nimport Child1 from '.\u002FChild1.vue';  \u002F\u002F 引入子组件\n\n\u002F\u002F 定义响应式数据\nlet pageNo = ref(1);   \u002F\u002F 当前页码\nlet pageSize = ref(3); \u002F\u002F 每页显示条数\n\u003C\u002Fscript>\n\n```\n\n子组件\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"son2\">\n    \u003Ch1>同时绑定多个 v-model\u003C\u002Fh1>\n    \u003Cbutton @click=\"handler\">pageNo {{ pageNo }}\u003C\u002Fbutton>\n    \u003Cbutton @click=\"updatePageSize\">pageSize {{ pageSize }}\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps({\n  pageNo: Number,  \u002F\u002F 接收父组件传递的 pageNo\n  pageSize: Number \u002F\u002F 接收父组件传递的 pageSize\n});\n\nconst emit = defineEmits(['update:pageNo', 'update:pageSize']); \u002F\u002F 声明触发的事件\n\n\u002F\u002F 更新 pageNo\nconst handler = () => {\n  emit('update:pageNo', props.pageNo + 1);  \u002F\u002F 每次点击增加页码\n};\n\n\u002F\u002F 更新 pageSize\nconst updatePageSize = () => {\n  emit('update:pageSize', props.pageSize + 2);  \u002F\u002F 每次点击增加每页显示的条数\n};\n\u003C\u002Fscript>\n```\n\n\n\n## 2.5 `useAttrs`：获取组件的非 `props` 属性和事件\n\n`useAttrs` 是 Vue3 提供的一个 Composition API，用于获取组件实例上未被 `defineProps` 声明的属性和事件（包括原生 DOM 事件和自定义事件）。这类似于 Vue2 中的 `$attrs` 和 `$listeners`。\n\n父组件\n\n```vue\n\u003Ctemplate>\n\u003Cdiv>\n  \u003Ch1>useAttrs\u003C\u002Fh1>\n  \u003Cel-button type=\"primary\" size=\"small\" :icon=\"Edit\">\u003C\u002Fel-button>\n  \u003C!-- 自定义组件 加上文字显示 -->\n   \u003CHintButton type=\"primary\" size=\"small\" :icon=\"Edit\" title=\"编辑按钮\" @click=\"handler\">\u003C\u002FHintButton>\n   \n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { Edit } from '@element-plus\u002Ficons-vue';\nimport HintButton from '.\u002FHintButton.vue';\n\n\u002F\u002F 按钮点击的回调\nconst handler = () =>{\n  alert('12306')\n}\n\u003C\u002Fscript>\n\n```\n\n子组件\n\n```vue\n\n\u003Ctemplate>\n\u003Cdiv :title=\"$attrs.title\">\n  \u003Cel-button :=\"$attrs\">\u003C\u002Fel-button>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入useAttrs方法：获取组件标签上属性与事件\nimport {useAttrs} from \"vue\"\n\u002F\u002F 此方法会执行返回一个对象\nlet $attrs = useAttrs()\nconsole.log($attrs)\n\n\u003C\u002Fscript>\n```\n\n需要注意的是，如果 `defineProps` 已经接收了某个属性，那么 `useAttrs` 返回的对象中将不再包含该属性。\n\n\n\n## 2.6 `ref` 与 `$parent`：直接访问子\u002F父组件实例\n\n`ref` 允许我们在父组件中获取子组件的实例（VC），从而直接访问子组件的方法和响应式数据。而 `$parent` 则允许子组件获取其父组件的实例。\n\n**父组件通过 `ref` 访问子组件：**\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"fa\">\n\u003Ch1>我是父亲，我有{{ money }}钱\u003C\u002Fh1>\n\u003Cbutton @click=\"handler\">点我向儿子借10块钱\u003C\u002Fbutton>\n\u003Cbr>\n\u003CSon ref=\"son\">\u003C\u002FSon>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入子组件\nimport Son from '.\u002FSon.vue';\nimport {ref} from 'vue'\n\n\u002F\u002F 父组件钱数\nlet money = ref(1000)\n\u002F\u002F 获取子组件钱数\nlet son = ref()  \u002F\u002F 必须同名\n\u002F\u002F 父组件内部按钮点击回调\nconst handler = () =>{\n  money.value += 10\n  \u002F\u002F 让儿子的钱数-10\n  son.value.money -= 10\n  \u002F\u002F 调用到儿子的方法\n  son.value.fly()\n  \n  console.log(son.value)\n}\n\u003C\u002Fscript>\n```\n\n**子组件通过 `defineExpose` 暴露数据和方法：**\n\n在 Vue3 中，组件内部的数据和方法默认不对外暴露。如果希望父组件通过 `ref` 访问，子组件必须使用 `defineExpose` 明确地暴露它们。\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"son\">\n  \u003Ch3>我是儿子，我有{{ money }}钱\u003C\u002Fh3>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\nimport {ref} from 'vue'\n\n\u002F\u002F 儿子钱数\nlet money = ref(100)\n\nconst fly = () =>{\n  console.log('我可以飞');\n}\n\n\u002F\u002F 组件内部数据对外关闭，别人不能访问\n\u002F\u002F 访问需要通过defineExpose方法对外暴露\ndefineExpose({\n  money,\n  fly\n})\n\u003C\u002Fscript>\n```\n\n**子组件通过 `$parent` 访问父组件：同样父亲的数据也得对外暴露**\n\n```vue\n\u003Ctemplate>\n\u003Cdiv class=\"dau\">\n    \u003Ch3>我是闺女,我有{{ money }}钱\u003C\u002Fh3>\n    \u003Cbutton @click=\"handler($parent)\">点击父亲给我20块钱\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport {ref} from 'vue'\n\u002F\u002F 闺女的钱\nlet money = ref(5000)\n\n\u002F\u002F 闺女内部按钮触发的回调\nconst handler = (parent:any) =>{\n    money.value += 20\n    parent.money -= 20   \n}\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\n.dau{\n    width: 300px;\n    height: 300px;\n    background-color: purple;\n}\n\u003C\u002Fstyle>\n```\n\n\n\n## 2.7 Pinia：新一代集中式状态管理容器\n\nPinia 是 Vue3 推荐的集中式状态管理库，它类似于 Vuex，但更加轻量、直观，并且移除了 `mutations` 和 `modules` 等概念，使得状态管理更加扁平化和易于理解。\n\n使用：\n\n* 安装pinia `npm install pinia`\n\n* 操作新建文件夹 `src\u002Fmain.ts `     \n\n* ```typescript\n  \u002F\u002F 操作main.ts\n  \u002F\u002F创建大仓库\n  import { createPinia } from 'pinia';\n  \u002F\u002FcreatePinia方法可以用于创建大仓库\n  let store = createPinia();\n  \u002F\u002F对外暴露,安装仓库\n  export default store;\n  ```\n\n* ```typescript\n  \u002F\u002F 主文件 main.ts 引入仓库\n  \u002F\u002F引入仓库\n  import store from '.\u002Fstore'\n  app.use(store)\n  ```\n\n* 创建小仓库  `scr\u002Fmodules\u002Finfo.ts`      **(选择式api用法)**\n\n* ```typescript\n  \u002F\u002F定义info小仓库\n  import { defineStore } from \"pinia\";\n  \n  \u002F\u002F第一个仓库:小仓库名字  第二个参数:小仓库配置对象\n  \u002F\u002FdefineStore方法执行会返回一个函数,函数作用就是让组件可以获取到仓库数据\n  let useInfoStore = defineStore(\"info\", {\n      \u002F\u002F存储数据:state\n      state: () => {\n          return {\n              count: 99,\n              arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n          }\n      },\n      \n      \u002F\u002F 业务逻辑: actions\n      actions: {\n          \u002F\u002F注意:函数没有context上下文对象\n          \u002F\u002F没有commit、没有mutations去修改数据\n          updateNum(a: number, b: number) {\n              this.count += a;\n          }\n      },\n      \n      \u002F\u002F 计算属性:getters\n      getters: {\n          total() {\n              \u002F\u002F 计算和\n              let result:any = this.arr.reduce((prev: number, next: number) => {\n                  return prev + next;\n              }, 0);\n              return result;\n          }\n      }\n  });\n  \u002F\u002F对外暴露方法\n  export default useInfoStore;\n  ```\n\n* 使用仓库数据，方法\n\n* ```vue\n  \u003Ctemplate>\n    \u003Cdiv class=\"child\">\n      \u003Ch1>{{ infoStore.count }}---{{infoStore.total}}\u003C\u002Fh1>\n      \u003Cbutton @click=\"updateCount\">点击我修改仓库数据\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  \u003C\u002Ftemplate>\n  \n  \u003Cscript setup lang=\"ts\">\n      \n  import useInfoStore from \"..\u002F..\u002Fstore\u002Fmodules\u002Finfo\";\n  \u002F\u002F获取小仓库对象\n  let infoStore = useInfoStore();\n  console.log(infoStore);\n      \n  \u002F\u002F修改数据方法\n  const updateCount = () => {\n    \u002F\u002F仓库调用自身的方法去修改仓库的数据\n    infoStore.updateNum(66,77);\n  };\n  \u003C\u002Fscript>\n  \n  ```\n\n* 组合式api\n\n* ```typescript\n  \u002F\u002F定义组合式API仓库\n  import { defineStore } from \"pinia\";\n  import { ref, computed,watch} from 'vue';\n  \u002F\u002F创建小仓库\n  let useTodoStore = defineStore('todo', () => {\n      let todos = ref([{ id: 1, title: '吃饭' }, { id: 2, title: '睡觉' }, { id: 3, title: '打豆豆' }]);\n      let arr = ref([1,2,3,4,5]);\n  \n      const total = computed(() => {\n          return arr.value.reduce((prev, next) => {\n              return prev + next;\n          }, 0)\n      })\n      \u002F\u002F务必要返回一个对象:属性与方法可以提供给组件使用\n      return {\n          todos,\n          arr,\n          total,\n          updateTodo() {\n              todos.value.push({ id: 4, title: '组合式API方法' });\n          }\n      }\n  });\n  \n  export default useTodoStore;\n  ```\n\n\n\n## 2.8 插槽 (`slot`)：灵活的内容分发\n\n插槽是 Vue 组件提供内容分发能力的机制，它允许父组件向子组件的指定位置注入内容，从而实现更灵活的组件组合。插槽分为默认插槽、具名插槽和作用域插槽。\n\n**默认插槽：**\n\n子组件定义一个 `\u003Cslot>` 标签，父组件在使用子组件时，在双标签内部书写的内容将填充到这个插槽中。\n\n```vue\n子组件 Test.vue\n\u003Ctemplate>\n  \u003Cdiv class=\"box\">\n    \u003Ch1>我是子组件默认插槽\u003C\u002Fh1>\n    \u003C!-- 默认插槽 -->\n    \u003Cslot>\u003C\u002Fslot>\n    \u003Ch1>我是子组件默认插槽\u003C\u002Fh1>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n父组件\n    \u003CTest>\n      \u003Cdiv>\n        \u003Cpre>大江东去浪淘尽,千古分流人物\u003C\u002Fpre>\n      \u003C\u002Fdiv>\n    \u003C\u002FTest>\n\n\u003Cscript setup lang=\"ts\">\nimport Test from \".\u002FTest.vue\";\n\u003C\u002Fscript>\n```\n\n**具名插槽：**\n\n子组件定义多个带有 `name` 属性的插槽，父组件通过 `v-slot:` 或 `#` 指令指定内容填充到哪个具名插槽。\n\n```vue\n子组件 Test.vue\n\u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n \u003Cslot name=\"a\">\u003C\u002Fslot>\n \u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n\n \u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n \u003Cslot name=\"b\">\u003C\u002Fslot>\n \u003Ch1>具名插槽填充数据\u003C\u002Fh1>\n\n父组件\n\u003C!-- 具名插槽填充a -->\n      \u003Ctemplate v-slot:a>\n        \u003Cdiv>我是填充具名插槽a位置结构\u003C\u002Fdiv>\n      \u003C\u002Ftemplate>\n      \u003C!-- 具名插槽填充b v-slot指令可以简化为# -->\n      \u003Ctemplate #b>\n        \u003Cdiv>我是填充具名插槽b位置结构\u003C\u002Fdiv>\n      \u003C\u002Ftemplate>\n```\n\n![image-20260129222630502](images\u002Fimage-20260129222630502.png)\n\n\n\n**作用域插槽：**\n\n作用域插槽允许子组件在渲染插槽内容时向父组件传递数据，父组件可以根据这些数据决定插槽内容的结构和样式。\n\n```vue\n父组件\n  \u003CTest1 :todos=\"todos\">    \u003C!-- 将数据传给子组件 -->\n      \u003Ctemplate v-slot=\"{ $row, $index }\">\n \t\t\u003C!-- 父组件决定子组件的结构与外观 -->\n        \u003Cp :style=\"{ color: $row.done ? 'green' : 'red' }\">\n          {{ $row.title }}--{{ $index }}\n        \u003C\u002Fp>\n      \u003C\u002Ftemplate>\n      \n     \u003C!-- 解构\n\t\t\u003Ctemplate v-slot=\"slotProps\">\n \t \t\u003Cp>\n    \t\t {{ slotProps.$row.title }}\n   \t\t\t {{ slotProps.$index }}\n  \t\t\u003C\u002Fp>\n\t\t\u003C\u002Ftemplate>\n\t\t\t\t-->\n    \u003C\u002FTest1>\n\n\u003Cscript setup lang=\"ts\">\n\nimport Test1 from \".\u002FTest1.vue\";\nimport { ref } from \"vue\";\n\u002F\u002F 父组件内部数据\nlet todos = ref([\n  { id: 1, title: \"吃饭\", done: true },\n  { id: 2, title: \"睡觉\", done: false },\n  { id: 3, title: \"打豆豆\", done: true },\n  { id: 4, title: \"打游戏\", done: false },\n]);\n\u003C\u002Fscript>\n\n\n子组件 Test1.vue\n\u003Ctemplate>\n  \u003Cdiv class=\"box\">\n    \u003Ch1>作用域插槽\u003C\u002Fh1>\n    \u003Cul>\n         \u003C!-- 组件内部遍历数组 -->\n      \u003Cli v-for=\"(item, index) in todos\" :key=\"item.id\">\n        \u003C!--作用域插槽:可以将数据回传给父组件-->\n        \u003Cslot :$row=\"item\" :$index=\"index\">\u003C\u002Fslot>\n      \u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F通过props接受父组件传递数据\ndefineProps([\"todos\"]);\n\u003C\u002Fscript>\n\n```\n\n\n\n# 二、项目初始化与规范化：构建高质量代码的基石\n\n一个高质量的项目离不开严格的代码规范和统一的开发流程。“硅谷甄选运营平台”项目在初始化阶段就集成了多项工具，确保了代码的质量和团队协作的顺畅\n\n## 2.1 环境准备与项目初始化\n\n项目采用 **Vue3** 和 **Vite** 进行构建，并强制使用 **pnpm** 作为包管理器，以确保依赖安装的一致性和高性能。\n\n**环境要求：**\n\n- Node.js v16.14.2+\n\n- pnpm 8.0.0+\n\n- **pnpm 安装：**\n\n  ```css\n  npm i -g pnpm\n  ```\n\n  ![image-20260129233201061](images\u002Fimage-20260129233201061.png)\n\n  **项目初始化命令：**\n\n  ```lua\n  pnpm create vite\n  ```\n\n  * 项目名字\n  * 项目框架\n  * 语言选择\n\n  ![image-20260129233658337](images\u002Fimage-20260129233658337.png)\n\n  * 如果没有自动安装依赖\n\n    ​\t手动安装依赖\n\n    ​\t进入项目根目录后，运行 `pnpm install` 安装所有依赖，然后 `pnpm run dev` 即可启动项目。\n\n  * 项目目录结构\n\n    ![image-20260129234148057](images\u002Fimage-20260129234148057.png)\n\n  ## 注意：\n\n  ### 1️⃣ `node_modules`（**依赖仓库**）\n\n  ```\n  node_modules\u002F\n  ```\n\n  - 所有第三方包都在这\n  - 比如：vue、vite、typescript……\n  - **不要动、不要删、不要提交到 git**\n\n  ### 2️⃣ `src`（你 90% 的代码都在这）\n\n  ```\n  src\u002F\n  ```\n\n  这是**主战场**，之后你会在里面：\n\n  - 写页面\n  - 写组件\n  - 配路由\n  - 写状态管理（pinia）\n\n  👉 admin 项目，`src` 就是“总部”\n\n  ### 3️⃣ `public`（静态资源）\n\n  ```\n  public\u002F\n  ```\n\n  - logo\n  - favicon\n  - 不需要打包处理的资源\n\n  使用方式：\n\n  ```\n  \u003Cimg src=\"\u002Flogo.png\" \u002F>\n  ```\n\n  ### 4️⃣ `index.html`（入口 HTML，非常重要）\n\n  ```\n  index.html\n  ```\n\n  ⚠️ Vite 的一个**大特点**：\n\n  - 这是项目的 **真正入口**\n  - 不是 build 后生成的\n  - **开发阶段就直接用**\n\n  里面会看到：\n\n  ```\n  \u003Cdiv id=\"app\">\u003C\u002Fdiv>\n  \u003Cscript type=\"module\" src=\"\u002Fsrc\u002Fmain.ts\">\u003C\u002Fscript>\n  ```\n\n  ### 5️⃣ `package.json`（项目说明书）\n\n  ```\n  {\n    \"scripts\": {\n      \"dev\": \"vite\",\n      \"build\": \"vite build\"\n    }\n  }\n  ```\n\n  它决定了：\n\n  - 项目叫什么\n  - 用了哪些依赖\n  - `pnpm run dev` 是干嘛的\n\n  ### 6️⃣ `pnpm-lock.yaml`（依赖锁）\n\n  - 锁定依赖版本\n  - **不能随便删**\n  - 团队协作非常重要\n\n  ### 7️⃣ `vite.config.ts`（Vite 配置文件）\n\n  ```\n  export default defineConfig({\n    plugins: [vue()]\n  })\n  ```\n\n  后面你会在这配置：\n\n  - 路径别名 `@`\n  - 代理（解决跨域）\n  - 打包配置\n\n  ### 8️⃣ `tsconfig*.json`（TypeScript 配置）\n\n  ```\n  tsconfig.json\n  tsconfig.app.json\n  tsconfig.node.json\n  ```\n\n  - 给 TS 编译器用的\n  - 初期基本不用碰\n  - 后面你学 TS 深了才会改\n\n  ### 9️⃣ `.vscode`（编辑器配置）\n\n  - VS Code 专用\n  - 代码格式、插件配置\n  - 不影响项目运行\n\n- scr\u002Fstyle.css 删掉\n\n  ![image-20260129234946814](images\u002Fimage-20260129234946814.png)\n\n- APP.vue 代码删除\n\n  ![image-20260129235030953](images\u002Fimage-20260129235030953.png)\n\n- index.html 改标题\n\n  ![image-20260129235601820](images\u002Fimage-20260129235601820.png)\n\n\n\n## 2.2 项目配置：代码质量与开发效率的保障\n\n### 2.2.1 项目自动启动\n\n package.json 中 \"dev\"项目启动 加上 --open 当项目启动的时候自动打开浏览器运行项目\n\n![image-20260130000152141](images\u002Fimage-20260130000152141.png)\n\n### 2.2.2  ESLint 配置：JavaScript\u002FTypeScript 代码质量检测\n\nESLint 是一个可插拔的 JavaScript\u002FTypeScript 代码检测工具，用于发现和报告代码中的问题。\n\n**安装：**\n\n`pnpm create @eslint\u002Fconfig@latest`\n\n![image-20260130134425119](images\u002Fimage-20260130134425119.png)\n\n![image-20260130134442157](images\u002Fimage-20260130134442157.png)\n\n![image-20260130122120694](images\u002Fimage-20260130122120694.png)\n\n\n\n**vue3校验插件安装**\n\n```javascript\npnpm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel\u002Feslint-parser\n```\n\n重新配置文件\n\n```javascript\nimport js from \"@eslint\u002Fjs\"\nimport globals from \"globals\"\nimport tseslint from \"typescript-eslint\"\nimport pluginVue from \"eslint-plugin-vue\"\nimport importPlugin from \"eslint-plugin-import\"\nimport nPlugin from \"eslint-plugin-n\"\nimport prettierPlugin from \"eslint-plugin-prettier\"\nimport prettierConfig from \"eslint-config-prettier\"\nimport { defineConfig } from \"eslint\u002Fconfig\"\n\nexport default defineConfig([\n   \u002F\u002F 配置需要忽略的文件或目录，例如 dist 和 node_modules\n    {\n    ignores: [\"node_modules\u002F**\", \"dist\u002F**\", \"build\u002F**\"],\n  },\n    \n  \u002F\u002F 基础 JavaScript 规则\n  {\n    files: [\"**\u002F*.{js,mjs,cjs,ts,mts,cts,vue}\"],\n    plugins: {\n      js,\n      import: importPlugin,\n      n: nPlugin,\n      prettier: prettierPlugin,\n    },\n    extends: [\n      \"js\u002Frecommended\",\n      prettierConfig, \u002F\u002F 关闭和 Prettier 冲突的规则\n    ],\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        ...globals.node,\n      },\n      parserOptions: {\n        ecmaVersion: \"latest\",\n        sourceType: \"module\",\n      },\n    },\n    rules: {\n      \"prettier\u002Fprettier\": \"warn\",\n      \"import\u002Forder\": [\n        \"warn\",\n        {\n          groups: [\"builtin\", \"external\", \"internal\", \"parent\", \"sibling\", \"index\"],\n          \"newlines-between\": \"always\",\n        },\n      ],\n      \"n\u002Fno-process-exit\": \"off\",\n    },\n  },\n\n  \u002F\u002F TypeScript 规则\n  tseslint.configs.recommended,\n\n  \u002F\u002F Vue 规则\n  pluginVue.configs[\"flat\u002Fessential\"],\n\n  \u002F\u002F Vue 文件中 TS 解析\n  {\n    files: [\"**\u002F*.vue\"],\n    languageOptions: {\n      parserOptions: {\n        parser: tseslint.parser,\n      },\n    },\n  },\n])\n\n```\n\n注意：\n\n`在cmd命令行中运行\npnpm remove eslint-plugin-node   移除旧版\npnpm add -D eslint-plugin-n      安装新版`\n\n\n\n在 `package.json` 中添加运行脚本，方便进行代码检查和修复：\n\n```javascript\n\"scripts\": {\n    \"lint\": \"eslint src\",\n    \"fix\": \"eslint src --fix\"\n}\n```\n\n配置其他相关\n\n```javascript\n rules: {\n      \u002F\u002F JavaScript 基础规则\n      'no-var': 'error', \u002F\u002F 禁止使用 var，强制使用 let\u002Fconst\n      'no-multiple-empty-lines': ['warn', { max: 1 }], \u002F\u002F 警告：最多允许一行空行\n      'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', \u002F\u002F 生产环境禁止使用 console\n      'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', \u002F\u002F 生产环境禁止使用 debugger\n      'no-unexpected-multiline': 'error', \u002F\u002F 防止由于自动分号插入引发的 bug\n      'no-useless-escape': 'off', \u002F\u002F 允许多余的转义字符（某些正则场景需要）\n\n      \u002F\u002F TypeScript 相关规则\n      '@typescript-eslint\u002Fno-unused-vars': 'error', \u002F\u002F 禁止未使用的变量\n      '@typescript-eslint\u002Fprefer-ts-expect-error': 'error', \u002F\u002F 更推荐使用 ts-expect-error 替代 ts-ignore\n      '@typescript-eslint\u002Fno-explicit-any': 'off', \u002F\u002F 允许使用 any 类型\n      '@typescript-eslint\u002Fno-non-null-assertion': 'off', \u002F\u002F 允许使用非空断言（!）\n      '@typescript-eslint\u002Fno-namespace': 'off', \u002F\u002F 允许使用命名空间（namespace）\n      '@typescript-eslint\u002Fsemi': 'off', \u002F\u002F 关闭对分号的强制检查\n\n      \u002F\u002F Vue 相关规则\n      'vue\u002Fmulti-word-component-names': 'off', \u002F\u002F 允许使用单词组件名（适用于页面组件等）\n      \u002F\u002F 'vue\u002Fscript-setup-uses-vars': 'error', \u002F\u002F 新版插件可能移除了此规则，先注释避免错误\n      'vue\u002Fno-mutating-props': 'off', \u002F\u002F 允许修改 props（某些业务场景可能需要）\n      'vue\u002Fattribute-hyphenation': 'off', \u002F\u002F 关闭模板中 attribute 必须使用连字符的限制\n    },\n  },\n)\n```\n\n安装加载ts格式的配置文件\n\n`pnpm add -D jiti`\n\n检查代码规范\n\n`pnpm run lint`\n\n修复代码规范\n\n`pnpm run fix`\n\n### 2.2.2 Prettier 配置：代码格式化工具\n\nPrettier 是一个强大的代码格式化工具，它专注于代码的美观和一致性，支持多种语言。ESLint 保证代码质量，而 Prettier 保证代码美观。两者结合，相得益彰。\n\n**安装 Prettier 相关依赖：**\n\n```mipsasm\npnpm install -D eslint-plugin-prettier prettier eslint-config-prettier\n```\n\n**新建.prettierrc 文件**\n\n![image-20260130152740128](images\u002Fimage-20260130152740128.png)\n\n\n\n\n\n``semi`: 代码末尾加分号\n\n`singleQuote`: 用单引号替代双引号\n\n`printWidth`: 最大行长度\n\n`tabWidth`: tab缩进宽度\n\n`trailingComma`: 多行时尾逗号风格\n\n`arrowParens`: 箭头函数参数是否加括号\n\n`endOfLine`: 换行符，防止跨平台差异`\n\n\n\n\n\n**新建.prettierignore 忽略文件**\n\n![image-20260130152833257](images\u002Fimage-20260130152833257.png)\n\n**vscode新建settings.json文件**\n\n保存文件时自动格式化代码\n\n统一使用 Prettier 插件格式化 `.vue`、`.ts`、`.js` 文件\n\n```javascript\n{\n  \"editor.formatOnSave\": true,\n  \"[vue]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  }\n}\n\n```\n\n\n\n### 2.2.3 Stylelint 配置：CSS\u002FSCSS 代码规范\n\nStylelint 是 CSS 的 Lint 工具，用于格式化 CSS 代码、检查语法错误和不合理的写法，并指定 CSS 书写顺序等。项目中使用 SCSS 作为预处理器。\n\n**安装 Stylelint 及其相关依赖：**\n\n`pnpm add sass sass-loader stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order stylelint-config-standard-scss -D`\n\n**创建.stylelintrc.cjs文件，配置**\n\n```javascript\nmodule.exports = {\n  ignoreFiles: ['**\u002F*.min.css', '**\u002Fnode_modules\u002F**', '**\u002Fdist\u002F**', '**\u002Funpackage\u002F**'],\n\n  extends: [\n    'stylelint-config-standard',\n    'stylelint-config-recess-order',\n    'stylelint-config-prettier',\n  ],\n\n  plugins: ['stylelint-scss'],\n\n  overrides: [\n    {\n      files: ['**\u002F*.vue', '**\u002F*.html'],\n      customSyntax: 'postcss-html',\n    },\n    {\n      files: ['**\u002F*.scss'],\n      customSyntax: 'postcss-scss',\n    },\n  ],\n\n  rules: {\n    \u002F* ===== 新手期先别炸 ===== *\u002F\n    'no-empty-source': null,\n    'block-no-empty': null,\n    'declaration-empty-line-before': null,\n\n    \u002F* ===== 基础风格 ===== *\u002F\n    indentation: 2,\n    'string-quotes': 'single',\n    'color-hex-case': 'lower',\n    'number-leading-zero': 'always',\n\n    \u002F* ===== 命名规则放行 ===== *\u002F\n    'selector-class-pattern': null,\n    'custom-property-pattern': null,\n    'keyframes-name-pattern': null,\n\n    \u002F* ===== uniapp \u002F vue 深度选择器 ===== *\u002F\n    'selector-pseudo-element-no-unknown': [\n      true,\n      {\n        ignorePseudoElements: ['v-deep', 'deep'],\n      },\n    ],\n\n    \u002F* ===== scss \u002F uniapp at-rule 全放行 ===== *\u002F\n    'at-rule-no-unknown': null,\n\n    \u002F* ===== :global ===== *\u002F\n    'selector-pseudo-class-no-unknown': [\n      true,\n      {\n        ignorePseudoClasses: ['global'],\n      },\n    ],\n  },\n};\n\n```\n\n**配置package.json文件**\n\n```json\n{\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n   \t\"format\": \"prettier --write \\\".\u002F**\u002F*.{html,vue,ts,js,json,md}\\\"\", \u002F\u002F 添加这个\n  }\n}\n```\n\n**运行**\n\n`pnpm run format`\n\n\n\n### 2.2.4 Husky 配置：Git Hook 自动化\n\n为了强制开发人员遵循代码规范，我们引入了 Husky。Husky 可以在 Git 提交（commit）前触发 Git 钩子，自动执行代码格式化和检查。\n\n**安装 Husky：**\n\n```mipsasm\npnpm install -D husky\n```\n\n**先初始化git 仓库**\n\n`git init`\n\n**初始化 Husky：**\n\n```csharp\nnpx husky-init\n```\n\n这会在项目根目录下生成 `.husky` 目录，其中包含 `pre-commit` 文件。\n\n**修改 `.husky\u002Fpre-commit` 文件：**\n\n```perl\npnpm run format # 在提交前自动执行格式化\n```\n\n这样，每次执行 `git commit` 时，代码都会自动格式化。\n\n\n\n### 2.2.5 Commitlint 配置：统一 Git 提交信息规范\n\n为了保持 Git 提交信息的清晰和一致性，我们使用 Commitlint 来强制执行提交规范。\n\n**安装 Commitlint：**\n\n```sql\npnpm add @commitlint\u002Fconfig-conventional @commitlint\u002Fcli -D\n```\n\n**创建 `commitlint.config.cjs` 配置文件：**\n\n```java\n\u002F\u002F commitlint.config.cjs\nmodule.exports = {\n  \u002F\u002F 继承社区推荐的 'config-conventional' 规范\n  extends: ['@commitlint\u002Fconfig-conventional'],\n\n  \u002F\u002F 自定义规则 (0=禁用, 1=警告, 2=报错)\n  rules: {\n    \u002F\u002F type 类型必须是以下之一\n    'type-enum': [\n      2,\n      'always',\n      [\n        'feat',     \u002F\u002F 新功能\n        'fix',      \u002F\u002F 修复bug\n        'docs',     \u002F\u002F 文档\n        'style',    \u002F\u002F 代码格式\n        'refactor', \u002F\u002F 重构\n        'perf',     \u002F\u002F 性能优化\n        'test',     \u002F\u002F 测试\n        'chore',    \u002F\u002F 构建或辅助工具变动\n        'revert',   \u002F\u002F 回退\n        'build',    \u002F\u002F 打包\n      ],\n    ],\n\n    \u002F\u002F type 大小写: 不作限制\n    'type-case': [0],\n\n    \u002F\u002F type 是否为空: 不作限制\n    'type-empty': [0],\n\n    \u002F\u002F scope 是否为空: 不作限制\n    'scope-empty': [0],\n\n    \u002F\u002F scope 大小写: 不作限制\n    'scope-case': [0],\n\n    \u002F\u002F subject 结尾标点: 不作限制\n    'subject-full-stop': [0, 'never'],\n\n    \u002F\u002F subject 大小写: 不作限制\n    'subject-case': [0, 'never'],\n\n    \u002F\u002F header 最大长度: 不作限制\n    'header-max-length': [0, 'always', 72],\n  },\n};\n```\n\n在 `package.json` 中添加 Commitlint 脚本：\n\n```json\n\"scripts\": {\n    \"commitlint\": \"commitlint --config commitlint.config.cjs -e -V\"\n}\n```\n\n配置 Husky，在 `commit-msg` 钩子中执行 Commitlint 检查：(cmd命令行运行)\n\n```sql\nnpx husky add .husky\u002Fcommit-msg\n```\n\n在生成的 `.husky\u002Fcommit-msg` 文件中添加：(删除里面的undefined),添加下面这个\n\n```undefined\npnpm commitlint\n```\n\n**git提交规范**\n\n`git commit -m \"feat: 增加用户登录功能\"\ngit commit -m \"fix: 修复登录接口错误\"`\n\n\n\n# 三、项目集成：提升开发效率与用户体验\n\n在项目规范化之后，我们将核心的第三方库和工具集成到项目中，进一步提升开发效率和用户体验。\n\n## 3.1 Element Plus 集成：美观高效的 UI 组件库\n\n硅谷甄选运营平台采用 Element Plus 作为 UI 组件库，它提供了丰富的组件和友好的开发体验。\n\n**安装 Element Plus：**\n\n```bash\npnpm install element-plus @element-plus\u002Ficons-vue\n```\n\n```typescript\nimport { createApp } from 'vue';\nimport App from '.\u002FApp.vue';\n\n\u002F\u002F 引入element-plus插件与样式\nimport ElementPlus from 'element-plus';\nimport 'element-plus\u002Fdist\u002Findex.css';\n\u002F\u002F 配置element-plus国际化\nimport { zhCn } from 'element-plus\u002Fes\u002Flocales.mjs';\n\n\u002F\u002F 获取应用实例对象\nconst app = createApp(App);\n\u002F\u002F 安装element-plus插件\napp.use(ElementPlus,{\n    locale:zhCn \u002F\u002F element-plus国际化\n});\n\u002F\u002F 将应用挂在到挂载点上\napp.mount('#app');\n\n```\n\n## 3.2 `src` 别名配置：简化模块导入\n\n为了简化文件路径，提高代码可读性，我们为 `src` 目录配置了 `@` 别名。\n\n**`vite.config.ts` 配置：**\n\n```typescript\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs\u002Fplugin-vue';\nimport path from 'path';\n\nexport default defineConfig({\n  plugins: [vue()],\n  resolve: {\n    alias: {\n      '@': path path.resolve(__dirname, '.\u002Fsrc'),\n    },\n  },\n});\n```\n\n**`tsconfig.json` TypeScript 编译配置：**\n\n```typescript\n{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@\u002F*\": [\"src\u002F*\"]\n    }\n  }\n}\n```\n\n`eslint.config.ts` **ESLint 导入解析器配置**：\n\n```typescript\nsettings: {\n  'import\u002Fresolver': {\n    typescript: {\n      alwaysTryTypes: true,\n      project: '.\u002Ftsconfig.app.json',\n    },\n  },\n}\n```\n\n**创建**`.eslint-import-resolver-typescript.json `**ESLint 导入解析器配置**\n\n```json\n{\n  \"alwaysTryTypes\": true,\n  \"project\": \".\u002Ftsconfig.app.json\"\n}\n```\n\n**安装依赖**\n\n```bash\npnpm add -D @types\u002Fnode\npnpm add -D eslint-import-resolver-typescript\n```\n\n\n\n## 3.3 环境变量配置：灵活适应多环境部署\n\n项目开发通常会经历开发、测试和生产等不同环境，每个环境的接口地址等配置可能不同。通过环境变量配置，我们可以轻松地在不同环境之间切换。\n\n**项目根目录创建环境变量文件：**\n\n- `.env.development` (开发环境)\n- `.env.production` (生产环境)\n- `.env.test` (测试环境)\n\n**`.env.development` 示例：**\n\n```ini\n✅ Node 环境变量\n❌ 前端代码里不能直接读（Vite 不会暴露它）\n✔ 常用于：\n后端 Node\n构建工具判断环境\nNODE_ENV = 'development' \n\n✅ 可以在前端读取\n用途：\n页面标题\n系统名称\nlogo 旁边文字\nVITE_APP_TITLE = '硅谷甄选运营平台'\n\n✅ 前端可读\n✅ 常配合 代理 使用\nVITE_APP_BASE_API = '\u002Fdev-api' # 变量必须以 VITE_ 为前缀才能暴露给外部读取\n```\n\n**`.env.test` 示例：**\n\n```ini\nNODE_ENV = 'test'\nVITE_APP_TITLE = '硅谷甄选运营平台'\nVITE_APP_BASE_API = '\u002Ftest-api' # 变量必须以 VITE_ 为前缀才能暴露给外部读取\n\n```\n\n**`.env.production` 示例：**\n\n```ini\nNODE_ENV = 'production'\nVITE_APP_TITLE = '硅谷甄选运营平台'\nVITE_APP_BASE_API = '\u002Fprod-api' # 变量必须以 VITE_ 为前缀才能暴露给外部读取\nVITE_SERVE=\"HTTP:\u002F\u002Fyyy.com\"\n```\n\n\n\n**`package.json` 配置运行命令：**\n\n```json\n\"scripts\": {\n    \"dev\": \"vite --open\",\n    \"build:test\": \"vue-tsc && vite build --mode test\",\n    \"build:pro\": \"vue-tsc && vite build --mode production\",\n    \"preview\": \"vite preview\"\n}\n```\n\n在代码中通过 `import.meta.env` 对象获取环境变量。\n\n![image-20260131131902927](images\u002Fimage-20260131131902927.png)\n\n## 3.4 SVG 图标配置与封装：轻量级矢量图方案\n\nSVG 矢量图相比传统图片资源具有体积小、不失真、性能优越等优点。项目中通过 `vite-plugin-svg-icons` 插件集成 SVG 图标。\n\n**安装插件：**\n\n```mipsasm\npnpm install vite-plugin-svg-icons -D\n```\n\n**`vite.config.ts` 配置插件：**\n\n```javascript\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs\u002Fplugin-vue';\nimport { createSvgIconsPlugin } from 'vite-plugin-svg-icons';\nimport path from 'path';\n\nexport default defineConfig(({ command }) => {\n  return {\n    plugins: [\n      vue(),\n      createSvgIconsPlugin({\n        \u002F\u002F 指定需要缓存的图标文件夹\n        iconDirs: [path.resolve(process.cwd(), 'src\u002Fassets\u002Ficons')],\n        \u002F\u002F 指定 symbolId 的格式\n        symbolId: 'icon-[dir]-[name]',\n      }),\n    ],\n  };\n});\n```\n\n**入口文件 `main.ts` 导入：**\n\n```go\nimport 'virtual:svg-icons-register'; \u002F\u002F注册 SVG 图标 ，让 Vite 的 SVG 插件生效。\n```\n\n**注意：在src\u002Fassets下创建icons**\n\n**SVG图标使用**\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"\">\n    \u003Ch1>测试\u003C\u002Fh1>\n    \u003C!-- svg图标使用 -->\n    \u003Csvg style=\"width: 30px;height: 30px;\">\n      \u003Cuse xlink:href=\"#icon-suo\" fill=\"\">\u003C\u002Fuse>\n    \u003C\u002Fsvg>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n```\n\n**SVG 图标全局组件封装 (`src\u002Fcomponents\u002FSvgIcon\u002Findex.vue`)：**\n\n为了方便使用，我们将 SVG 图标封装成一个全局组件 `SvgIcon`。\n\n```vue\n\u003Ctemplate>\n    \u003Csvg  class=\"svg-icon\" :style=\"{ width: width, height: height }\">\n      \u003Cuse :href=\"prefix + name\" :fill=\"color\">\u003C\u002Fuse>\n    \u003C\u002Fsvg>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\ndefineProps({\n  \u002F\u002F xlink:href 属性值的前缀\n  prefix: {\n    type: String,\n    default: '#icon-'    \u002F\u002F default 是 Vue 官方提供的 props 选项，用来给“父组件没传的 prop”一个默认值。\n\t\t\t\n  },\n  \u002F\u002F svg 矢量图的名字\n  name: String,\n  \u002F\u002F svg 图标的颜色\n  color: {\n    type: String,\n    default: \"\"\n  },\n  \u002F\u002F svg 宽度\n  width: {\n    type: String,\n    default: '16px'\n  },\n  \u002F\u002F svg 高度\n  height: {\n    type: String,\n    default: '16px'\n  }\n});\n\u003C\u002Fscript>\n\u003Cstyle scoped>\n   .svg-icon {\n  fill: currentColor;        \u002F\u002F 让 SVG 图形的填充颜色跟随当前元素的字体颜色，保持一致。\n  vertical-align: middle;    \u002F\u002F 是让行内或行内块元素在行高里垂直居中，更美观对齐文本和其他元素。\n}\n\u003C\u002Fstyle>\n\n\n```\n\n**全局组件注册 (`src\u002Fcomponents\u002Findex.ts`)：**\n\n```typescript\n\u002F\u002F 引入项目中的全部组件\nimport SvgIcon from '.\u002FSvgIcon\u002Findex.vue';\nimport type { App, Component } from 'vue';\n\u002F\u002F 全局对象\nconst components: Record\u003Cstring, Component> = { SvgIcon };\n\u002F\u002F 对外暴露插件对象\nexport default {\n    install(app: App) {\n        \u002F\u002F 注册项目全部的全局组件\n        Object.keys(components).forEach((key: string) => {\n            \u002F\u002F 注册为全局组件\n            app.component(key, components[key]!); \u002F\u002F !告诉ts不可能为空\n        });\n    }\n};\n```\n\n注意：\n\n`类型注解：`Record\u003Cstring, Component>`\n\n这是TypeScript的**工具类型**（Utility Type），用于约束`components`对象的结构：\n\n- `Record\u003CK, V>`表示：**键的类型为`K`，值的类型为`V`的对象**。\n- 这里`K`是`string`（键必须是字符串，即组件的名称），`V`是`Component`（值必须是Vue组件，来自`vue`的类型定义）。\n\n**作用**：确保`components`对象的键只能是字符串（组件名），值只能是Vue组件，避免类型错误（比如不小心放入非组件的值）。`\n\n\n\n**在 `main.ts` 中安装全局组件：引入自定义插件**\n\n```javascript\n\u002F\u002F 引入自定义插件\nimport gloablComponent from '.\u002Fcomponents\u002Findex';\n\u002F\u002F 安装自定义插件\napp.use(gloablComponent);\n```\n\n**使用**\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\"\">\n    \u003Ch1>测试\u003C\u002Fh1>\n    \u003C!-- svg图标使用 -->\n    \u003Csvg-icon name=\"suo\" color=\"red\" width=\"100px\" height=\"100px\">\u003C\u002Fsvg-icon>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport SvgIcon from '@\u002Fcomponents\u002FSvgIcon\u002Findex.vue'\n\u003C\u002Fscript>\n```\n\n`注意`：\n\n为什么使用组件是svg-icon呢，而不是SvgIcon\n\nVue官网推荐组件名（大驼峰）\n\nVue 会自动将 PascalCase （大驼峰）转换为 kebab-case （短横线）：\n\n自定义标签名：\n\n```typescript\nconst components = {\n  'my-custom-icon': SvgIcon,  \u002F\u002F ← 自定义标签名\n};\n```\n\n## 3.5 集成 Sass：增强样式开发能力\n\n项目已通过 Stylelint 配置安装了 `sass` 和 `sass-loader`，可以直接在 Vue 组件中使用 SCSS 语法，只需在 `\u003Cstyle>` 标签上添加 `lang=\"scss\"`。\n\n**引入全局样式和变量：**\n\n在 `src\u002Fstyles` 目录下创建 `index.scss` (用于引入全局重置样式) 和 `variable.scss` (用于定义全局 SCSS 变量)。\n\n**`src\u002Fstyles\u002Findex.scss`：清楚默认样式**\n\n`npm官网`\n\n```scss\nhtml, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed, \nfigure, figcaption, footer, header, hgroup, \nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tfont-size: 100%;\n\tfont: inherit;\n\tvertical-align: baseline;\n}\n\u002F* HTML5 display-role reset for older browsers *\u002F\narticle, aside, details, figcaption, figure, \nfooter, header, hgroup, menu, nav, section {\n\tdisplay: block;\n}\nbody {\n\tline-height: 1;\n}\nol, ul {\n\tlist-style: none;\n}\nblockquote, q {\n\tquotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n\tcontent: '';\n\tcontent: none;\n}\ntable {\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}\n```\n\n**`main.ts` 引入全局样式：**\n\n```go\nimport '@\u002Fstyles\u002Findex.scss';\n```\n\n**`src\u002Fstyles\u002Findex.scss`：**\n\n```scss\n@use '.\u002Freset.scss'; \u002F\u002F 引入重置样式,npm官网复制\n\t\t\t\t\t\u002F\u002F 其他全局样式\n```\n\n**`vite.config.ts` 配置全局 SCSS 变量：**\n\n为了在所有组件中都能使用全局 SCSS 变量，需要在 Vite 配置中进行设置。\n\n```typescript\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs\u002Fplugin-vue';\nimport path from 'path';\n\n\u002F\u002F 引入svg需要用到的插件\nimport { createSvgIconsPlugin } from 'vite-plugin-svg-icons';\n\nexport default defineConfig({\n  plugins: [\n    vue(),\n    createSvgIconsPlugin({\n      \u002F\u002F 指定需要缓存的图标文件夹\n      iconDirs: [path.resolve(process.cwd(), 'src\u002Fassets\u002Ficons')],\n      \u002F\u002F 指定 symbolId 的格式\n      symbolId: 'icon-[dir]-[name]',\n    }),\n  ],\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, '.\u002Fsrc'),\n    },\n  },\n  css: {\n    preprocessorOptions: {\n      scss: {\n        javascriptEnabled: true,\n        additionalData: `@use \"@\u002Fstyles\u002Fvariable.scss\" as *;`, \u002F\u002F 引入全局变量\n      },\n    },\n  },\n});\n\n```\n\n## 3.6 Mock 数据：前端独立开发利器\n\n在后端接口尚未完成时，Mock 数据能够让前端独立进行开发和测试，提高开发效率。项目使用了 `vite-plugin-mock` 和 `mockjs`。\n\n**安装依赖：**\n\n```mipsasm\npnpm install -D vite-plugin-mock mockjs\n```\n\n```typescript\nexport default defineConfig(({ mode }) => {\n  const isDev = mode === 'development';\n    \u002F\u002F mode 的值：\n  \t\u002F\u002F - 'development'  开发环境\n  \t\u002F\u002F - 'production' 生产环境\n    \u002F\u002F - 'test'       测试环境\n\n  return {\n    plugins: [\n      vue(),\n      createSvgIconsPlugin({\n        \u002F\u002F 指定需要缓存的图标文件夹\n        iconDirs: [path.resolve(process.cwd(), 'src\u002Fassets\u002Ficons')],\n        \u002F\u002F 指定 symbolId 的格式\n        symbolId: 'icon-[dir]-[name]',\n      }),\n        \u002F\u002F 条件加载插件\n      isDev &&\n        viteMockServe({\n          mockPath: 'mock',\n          enable: true,\n        }),\n    ],\n```\n\n在项目根目录下创建\n\n**`mock\u002Fuser.ts` 示例：**\n\n在 `mock` 文件夹下创建 `user.ts` 文件，定义模拟的用户登录和信息接口。\n\n```typescript\n\u002F\u002F mock\u002Fuser.ts\n\u002F\u002F 用户信息数据\n\u002F\u002F 该函数返回一个数组，包含用户的两个信息\nfunction createUserList() {\n    return [\n        {\n            userId: 1,\n            avatar:\n                'https:\u002F\u002Fwpimg.wallstcn.com\u002Ff778738c-e4f8-4870-b634-56703b4acafe.gif',\n            username: 'admin',\n            password: '111111',\n            desc: '平台管理员',\n            roles: ['平台管理员'],\n            buttons: ['cuser.detail'],\n            routes: ['home'],\n            token: 'Admin Token',\n        },\n        {\n            userId: 2,\n            avatar:\n                'https:\u002F\u002Fwpimg.wallstcn.com\u002Ff778738c-e4f8-4870-b634-56703b4acafe.gif',\n            username: 'system',\n            password: '111111',\n            desc: '系统管理员',\n            roles: ['系统管理员'],\n            buttons: ['cuser.detail', 'cuser.user'],\n            routes: ['home'],\n            token: 'System Token',\n        },\n    ];\n}\n\u002F\u002F 对外暴露一个数组：数组里面包含两个接口\n\u002F\u002F 登录假的接口，获取用户信息的假的接口\nexport default [\n    \u002F\u002F 用户登录接口\n    {\n        url: '\u002Fapi\u002Fuser\u002Flogin', \u002F\u002F 请求地址\n        method: 'post', \u002F\u002F 请求方式\n        response: ({ body }) => {\n            \u002F\u002F 获取请求体携带过来的用户名与密码\n            const { username, password } = body;\n            \u002F\u002F 调用获取用户信息函数,用于判断是否有此用户\n            const checkUser = createUserList().find(\n                (item) => item.username === username && item.password === password,\n            );\n            \u002F\u002F 没有用户返回失败信息\n            if (!checkUser) {\n                return { code: 201, data: { message: '账号或者密码不正确' } };\n            }\n            \u002F\u002F 如果有返回成功信息\n            const { token } = checkUser;\n            return { code: 200, data: { token } };\n        },\n    },\n    \u002F\u002F 获取用户信息\n    {\n        url: '\u002Fapi\u002Fuser\u002Finfo',\n        method: 'get',\n        response: (request) => {\n            \u002F\u002F 获取请求头携带token\n            const token = request.headers.token;\n            \u002F\u002F 查看用户信息是否包含有次token用户\n            const checkUser = createUserList().find((item) => item.token === token);\n            \u002F\u002F 没有返回失败的信息\n            if (!checkUser) {\n                return { code: 201, data: { message: '获取用户信息失败' } };\n            }\n            \u002F\u002F 如果有返回成功信息\n            return { code: 200, data: { checkUser } };\n        },\n    },\n];\n\n```\n\n## 3.7 Axios 二次封装：统一网络请求与错误处理\n\n在前端项目中，Axios 是常用的 HTTP 客户端。对其进行二次封装，可以实现请求拦截、响应拦截、统一错误处理等功能，极大地提升开发效率和代码健壮性。\n\n**安装 Axios：**\n\n```mipsasm\npnpm install axios\n```\n\n**`src\u002Futils\u002Frequest.ts` Axios 二次封装：**\n\n```typescript\n\u002F\u002F 进行axios二次封装:使用请求与相应拦截器\nimport axios from 'axios';\nimport { ElMessage } from 'element-plus';\n\n\u002F\u002F 第一步：利用axios对象的create方法，去创建axios实例（其他的配置：基础路径，超时的时间）\nconst request = axios.create({\n  baseURL: import.meta.env.VITE_APP_BASE_API, \u002F\u002F 基础路径会携带上\u002Fapi\n  timeout: 5000, \u002F\u002F超时的时间\n});\n\u002F\u002F 第二步：request实例添加请求与响应拦截器\nrequest.interceptors.request.use((config) => {\n  \u002F\u002F config配置对象，headers属性请求头，经常给服务器端携带公共参数\n  return config;\n});\n\n\u002F\u002F 第三步：响应拦截器\nrequest.interceptors.response.use(\n  (response) => {\n    \u002F\u002F 成功回调\n    \u002F\u002F 简化数据\n\n    return response.data;\n  },\n  (error) => {\n    \u002F\u002F 失败回调，处理http网络错误\n    \u002F\u002F 定义一个变量：存储网络错误信息\n    let message = '';\n    \u002F\u002F http状态码\n    const status = error.response.status;\n    switch (status) {\n      case 401:\n        message = 'token过期';\n        break;\n      case 403:\n        message = '无权访问';\n        break;\n      case 404:\n        message = '请求地址错误';\n        break;\n      case 500:\n        message = '服务器出现问题';\n        break;\n      default:\n        message = '网络出现问题';\n        break;\n    }\n    \u002F\u002F 使用 Element Plus 的 ElMessage 显示错误信息\n    ElMessage({\n      type: 'error',\n      message: message,\n    });\n    return Promise.reject(error); \u002F\u002F继续向下传递错误\n  }\n);\n\u002F\u002F 对外暴露\nexport default request;\n\n```\n\n## 3.8 API 接口统一管理：清晰的接口结构\n\n为了避免接口地址硬编码、提高代码可读性和维护性，项目采用了统一的 API 接口管理方式。\n\n**在 `src` 目录下创建 `api` 文件夹，并按模块（如 `user`、`product`、`acl`）进行分类管理。**\n\n**`src\u002Fapi\u002Fuser\u002Findex.ts` 示例：**\n\n```typescript\n\u002F\u002F 统一管理项目用户相关的接口\nimport request from '@\u002Futils\u002Frequest';\n\nimport type { loginForm, loginResponse, userResponseData } from '.\u002Ftype';\n\u002F\u002F 统一管理接口\nenum API {\n  LOGIN_URL = '\u002Fuser\u002Flogin',\n  USERINFO_URL = '\u002Fuser\u002Finfo',\n}\n\u002F\u002F 暴露请求函数\n\u002F\u002F 登录接口方法\nexport const reqLogin = (data: loginForm) => request.post\u003Cany, loginResponse>(API.LOGIN_URL, data);\n\u002F\u002F 获取用户信息接口方法\nexport const reqUserInfo = () => request.get\u003Cany, userResponseData>(API.USERINFO_URL);\n\n```\n\n**`src\u002Fapi\u002Fuser\u002Ftype.ts` 示例 (定义数据类型)：**\n\n```typescript\n\u002F\u002F 登录接口需要携带参数ts类型\nexport interface loginForm {\n  username: string;\n  password: string;\n}\n\ninterface dataType {\n  token: string;\n}\n\u002F\u002F 登录接口返回的数据类型\nexport interface loginResponse {\n  code: number;\n  data: dataType;\n}\n\ninterface userInfo {\n  userId: number;\n  avater: string;\n  username: string;\n  password: string;\n  desc: string;\n  roles: string[];\n  buttons: string[];\n  routes: string[];\n  token: string;\n}\n\u002F\u002F 定义服务器返回用户信息相关的数据类型\ninterface user {\n  checkUser: userInfo;\n}\nexport interface userResponseData {\n  code: number;\n  data: user;\n}\n\n```\n\n这种模块化的接口管理方式，使得接口定义清晰、易于查找和维护，并且结合 TypeScript 提供了强大的类型检查，进一步提升了代码质量。\n\n\n\n# 四，项目的实现\n\n## 4.1 路由配置\n\n用 vue-router@4 配置路由\n\n安装\n\n`pnpm install vue-router`\n\n`src\u002Fviews`新建三个页面组件文件\u002Flogin\u002Findex.vue，\u002Fhome\u002Findex.vue，\u002F404\u002Findex.vue\n\n`src\u002Frouter\u002Findex.ts`创建路由器(路由器配置)\n\n```typescript\n\u002F\u002F 通过vue-router插件实现模板路由配置\nimport { createRouter, createWebHashHistory } from 'vue-router';\n\nimport { constantRoute } from '.\u002Frouters';\n\u002F\u002F创建路由器\nconst router = createRouter({\n  \u002F\u002F 使用 hash 模式（URL 带 #）\n  history: createWebHashHistory(),\n  \u002F\u002F 路由规则数组  \n  routes: constantRoute,\n  \u002F\u002F 页面切换时的滚动行为\n  scrollBehavior() {\n    return {\n      left: 0,\n      top: 0,\n    };\n  },\n});\n\nexport default router;\n\n```\n\n`src\u002Frouter\u002Frouters.ts`配置路由（路由规则的定义）\n\n```typescript\n\u002F\u002F 对外暴漏配置路由\nexport const constantRoute = [\n  {\n    \u002F\u002F 登录\n     \u002F\u002FURL 路径\n    path: '\u002Flogin',\n      \u002F\u002F 对应的页面组件\n    component: () => import('@\u002Fviews\u002Flogin\u002Findex.vue'),\n      \u002F\u002F 路由名称（用于编程式导航）\n    name: 'login',\n  },\n  {\n    \u002F\u002F 登录成功以后展示数据的路由\n    path: '\u002F',\n    component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n    name: 'layout',\n      children: [\n  {\n    path: 'home',  \u002F\u002F 子路由写相对路径\n    component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n    meta: {\n      title: '首页',\n      hidden: false,\n    },\n  },\n  },\n  {\n    \u002F\u002F404\n    path: '\u002F404',\n    component: () => import('@\u002Fviews\u002F404\u002Findex.vue'),\n    name: '404',\n  },\n  \u002F\u002F 重定向到其他路由\n  {\n    path: '\u002F:pathMatch(.*)*',\n    redirect: '\u002F404',\n    name: 'Any',\n  },\n];\n\n```\n\n`注意：懒加载特性`\n\n语法 () => import('组件路径') \n\n作用 按需加载页面组件 \n\n优势 减少首次加载时间、提升用户体验 \n\n打包 每个页面单独打包 \n\n推荐 所有路由都使用懒加载\n\n**子路由路径**\n\n子路由一律写相对路径\n\n\n\n`Vue Router 的两种模式`\n\n**Hash 模式 （当前使用）**\n\n```typescript\nimport { createWebHashHistory } from 'vue-router';\n\nconst router = createRouter({\n  history: createWebHashHistory(),\n  routes: constantRoute,\n});\n```\n\n```text\nurl风格\t带 # 号\nhttp:\u002F\u002Flocalhost:5173\u002F#\u002Flogin\nhttp:\u002F\u002Flocalhost:5173\u002F#\u002Fhome\nhttp:\u002F\u002Flocalhost:5173\u002F#\u002Fabout\n```\n\n**History 模式**\n\n```typescript\nimport { createWebHistory } from 'vue-router';\n\nconst router = createRouter({\n  history: createWebHistory(),\n  routes: constantRoute,\n});\n```\n\n```reStructuredText\nurl风格\nhttp:\u002F\u002Flocalhost:5173\u002Flogin\nhttp:\u002F\u002Flocalhost:5173\u002Fhome\nhttp:\u002F\u002Flocalhost:5173\u002Fabout\n```\n\n## 4.2 登录静态页面\n\n静态页面搭建\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"login_container\">\n        \u003Cel-row>\n            \u003Cel-col :span=\"12\" :xs=\"0\">\u003C\u002Fel-col>\n            \u003Cel-col :span=\"12\" :xs=\"24\">\n                \u003Cel-form class=\"login_form\" :model=\"loginForm\">\n                    \u003Ch1>Hello\u003C\u002Fh1>\n                    \u003Ch2>欢迎来来到硅谷甄选\u003C\u002Fh2>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.username\" placeholder=\"请输入用户名\" :prefix-icon=\"User\">\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.password\" placeholder=\"请输入密码\" :prefix-icon=\"Lock\" type=\"password\"\n                            show-password>\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-button class=\"login_btn\" type=\"primary\" size=\"default\">登录\u003C\u002Fel-button>\n                    \u003C\u002Fel-form-item>\n                \u003C\u002Fel-form>\n            \u003C\u002Fel-col>\n        \u003C\u002Fel-row>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { Lock, User } from '@element-plus\u002Ficons-vue'\nimport { reactive } from 'vue';\n\n\u002F\u002F手机账号和密码的数据\nconst loginForm = reactive({\n    username: 'admin',\n    password: '111111'\n})\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.login_container {\n    width: 100%;\n    height: 100vh;\n    \u002F\u002F background-color: skyblue;\n    background: url('@\u002Fassets\u002Fimages\u002Fbackground.jpg') no-repeat;\n    background-size: cover;\n}\n\n.login_form {\n    position: relative;\n    width: 70%;\n    top: 30vh;\n    background: url('@\u002Fassets\u002Fimages\u002Flogin_form.png');\n    padding: 40px;\n}\n\nh1 {\n    color: white;\n    font-size: 40px;\n}\n\nh2 {\n    font-size: 20px;\n    color: white;\n    margin: 20px 0px;\n}\n\n.login_btn {\n    width: 100%;\n}\n\u003C\u002Fstyle>\n\n```\n\n![image-20260203161820084](images\u002Fimage-20260203161820084.png)\n\n### 4.2.1 什么是栅格布局（先把“感觉”建立起来）\n\n 1️⃣栅格布局 = 把一行切成很多等宽的小格子\n\n想象一条横线：\n\n```\n|--------------------------------|\n```\n\nElement Plus 规定：\n\n> **一行 = 24 个小格子**\n\n```\n|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|\n```\n\n2️⃣ 页面里的任何一块内容\n\n本质上就是：\n\n> **“我占这一行的第几格到第几格”**\n\n### 4.2.2 el-row \u002F el-col 在干什么（核心）\n\nel-row 是什么？\n\n👉 **一条“横着的参考线”**\n\n```\n\u003Cel-row>\n  ...\n\u003C\u002Fel-row>\n```\n\n它只干一件事：\n\n> 告诉浏览器：\n>  **现在开始按 24 栅格来分配宽度**\n\nel-col 是什么？\n\n👉 **在这一行里占多少格**\n\n```\n\u003Cel-col :span=\"6\">\u003C\u002Fel-col>\n```\n\n意思是：\n\n```\n我占 6 \u002F 24 = 25% 宽度\n```\n\n### 4.2.3 span 是怎么工作的（一定要吃透）\n\n一行总宽 = 24\n\n| span | 占比  |\n| ---- | ----- |\n| 24   | 100%  |\n| 12   | 50%   |\n| 8    | 33%   |\n| 6    | 25%   |\n| 4    | 16.6% |\n\n例子 1：左右两栏\n\n```\n\u003Cel-row>\n  \u003Cel-col :span=\"12\">左\u003C\u002Fel-col>\n  \u003Cel-col :span=\"12\">右\u003C\u002Fel-col>\n\u003C\u002Fel-row>\n```\n\n视觉效果：\n\n```\n| 左 左 左 左 左 左 | 右 右 右 右 右 右 |\n```\n\n### 4.2.4 响应式是怎么回事（重点）\n\n屏幕不是只有一种大小\n\n- 手机 📱\n- 平板 💻\n- 电脑 🖥️\n\n👉 所以 **同一列，在不同屏幕占的宽度可以不一样**\n\n------\n\nElement Plus 的断点（你只要记 4 个）\n\n| 属性 | 含义     |\n| ---- | -------- |\n| xs   | 手机     |\n| sm   | 平板     |\n| md   | 普通电脑 |\n| lg   | 大屏     |\n| xl   | 超大屏   |\n\n### 4.2.5 例子：你现在的登录页（拆解）\n\n```\n\u003Cel-col :span=\"12\" :xs=\"0\">\u003C\u002Fel-col>\n\u003Cel-col :span=\"12\" :xs=\"24\">\u003C\u002Fel-col>\n```\n\n在电脑上：\n\n```\n| 左空白 12 | 登录表单 12 |\n```\n\n在手机上：\n\n```\n| 登录表单 24 |\n```\n\n左边那一列直接消失（`xs=0`）\n\n\n\n## 4.3 模板封装登录业务\n\n`安装pinia管理数据`\n\n`npm i pinia`\n\n**src下新建store文件**\n\n**创建大仓库store\u002Findex.ts**\n\n```typescript\n\u002F\u002F 大仓库\nimport { createPinia } from 'pinia';\n\u002F\u002F 创建大仓库\nconst pinia = createPinia();\n\u002F\u002F对外暴露，入口文件需要安装仓库\nexport default pinia;\n\n```\n\n**创建小仓库store\u002Fmodules\u002Fuser.ts**\n\n```typescript\n\u002F\u002F 选项式api\n\u002F\u002F 创建用户相关的小仓库\nimport { defineStore } from 'pinia';\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n  \u002F\u002F 小仓库存储数据的地方\n  state: () => {\n    return {};\n  },\n  \u002F\u002F 异步|逻辑的地方\n  actions: {},\n  getters: {},\n});\n\n\u002F\u002F 对外暴露获取小仓库方法\nexport default useUserStore;\n\n\n\u002F\u002F 组合式api\nimport { defineStore } from 'pinia';\nimport { ref, computed } from 'vue';\n\nconst useUserStore = defineStore('User', () => {\n  \u002F\u002Fstate\n  const user = ref(null);\n\n  \u002F\u002Fgetters\n\n  \u002F\u002Factions\n\n\u002F\u002F 对外暴露state,getters,actions\n})\nexport default useUserStore;\n\n\n```\n\n**src\u002Fmodules\u002Fuser.ts选项式api写法**\n\n```typescript\n\u002F\u002F 创建用户相关的小仓库\nimport { el } from 'element-plus\u002Fes\u002Flocales.mjs';\nimport { defineStore } from 'pinia';\n\n\u002F\u002F 引入接口\nimport { reqLogin } from '@\u002Fapi\u002Fuser';\n\u002F\u002F 引入数据类型\nimport type { loginForm } from '@\u002Fapi\u002Fuser\u002Ftype';\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n  \u002F\u002F 小仓库存储数据的地方\n  state: () => {\n    return {\n      token: localStorage.getItem('TOKEN'), \u002F\u002F用户唯一标识token\n    };\n  },\n  \u002F\u002F 异步|逻辑的地方\n  actions: {\n    \u002F\u002F用户登录的方法\n    async userLogin(data: loginForm) {\n      \u002F\u002F登录请求\n      const result = await reqLogin(data);\n      \u002F\u002F 登录请求:成功200->token\n      \u002F\u002F 登录请求:失败201->登录失败错误的信息\n      if (result.code === 200) {\n        \u002F\u002F pinia仓库存储一下token\n        this.token = result.data.token;\n        \u002F\u002F由于pinia\u002Fvuex存储数据其实利用js对象\n        \u002F\u002F本地存储持久化存储一份\n        localStorage.setItem('TOKEN', result.data.token);\n        \u002F\u002F 能保证当前async函数返回一个成功的promise\n        return 'ok';\n      } else {\n        return Promise.reject(new Error(result.data.message));\n      }\n    },\n  },\n  getters: {},\n});\n\n\u002F\u002F 对外暴露获取小仓库方法\nexport default useUserStore;\n\n```\n\n\n\n**views\u002Flogin\u002Findex.vue 登录页面代码**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"login_container\">\n        \u003Cel-row>\n            \u003Cel-col :span=\"12\" :xs=\"0\">\u003C\u002Fel-col>\n            \u003Cel-col :span=\"12\" :xs=\"24\">\n                \u003Cel-form class=\"login_form\" :model=\"loginForm\">\n                    \u003Ch1>Hello\u003C\u002Fh1>\n                    \u003Ch2>欢迎来来到硅谷甄选\u003C\u002Fh2>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.username\" placeholder=\"请输入用户名\" :prefix-icon=\"User\">\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-input v-model=\"loginForm.password\" placeholder=\"请输入密码\" :prefix-icon=\"Lock\" type=\"password\"\n                            show-password>\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n                    \u003Cel-form-item>\n                        \u003Cel-button class=\"login_btn\" type=\"primary\" size=\"default\" @click=\"login\"\n                            :loading=\"loading\">登录\u003C\u002Fel-button>\n                    \u003C\u002Fel-form-item>\n                \u003C\u002Fel-form>\n            \u003C\u002Fel-col>\n        \u003C\u002Fel-row>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { Lock, User } from '@element-plus\u002Ficons-vue';\nimport { ElNotification } from 'element-plus';\nimport { reactive, ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\n\u002F\u002F 引入用户相关的小仓库\nimport useUserStore from '@\u002Fstore\u002Fmodules\u002Fuser';\nimport { fa } from 'element-plus\u002Fes\u002Flocales.mjs';\n\nlet useStore = useUserStore();\n\u002F\u002F 获取路由器\nlet $router = useRouter();\n\u002F\u002F 定义加载效果开始为false\nlet loading = ref(false)\n\u002F\u002F收集账号和密码的数据\nconst loginForm = reactive({\n    username: 'admin',\n    password: '111111',\n});\n\u002F\u002F 登录按钮的回调\nconst login = async () => {\n    \u002F\u002F 点击登录出现加载效果\n    loading.value = true\n    \n    \u002F\u002F 点击登录按你以后能干什么？\n    \u002F\u002F 通知仓库发登录请求\n    \u002F\u002F 请求成功->首页展示数据的地方\n    \u002F\u002F 请求失败->弹出登录失败信息\n    \n    try {\n        \u002F\u002F调用用户仓库的登录方法，发送登录请求，等待返回\n        await useStore.userLogin(loginForm);\n        \u002F\u002F登录成功提示信息\n        ElNotification({\n            type: 'success',\n            message: '登录成功',\n        });\n        \u002F\u002F编程式导航跳转到展示数据首页\n        $router.push('\u002F');\n        \u002F\u002F登录成功，加载按钮也消失\n        loading.value = false\n\n    } catch (error) {\n        \u002F\u002F 如果登录请求失败（如账号密码错误或网络错误），执行这里的代码\n        \u002F\u002F 关闭加载状态，隐藏“加载中”\n        loading.value = false\n        ElNotification({\n            type: 'error',\n            message: (error as Error).message, \u002F\u002F 告诉 TS error 是 Error 类型(断言)\n        });\n\n    }\n};\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.login_container {\n    width: 100%;\n    height: 100vh;\n    \u002F\u002F background-color: skyblue;\n    background: url('@\u002Fassets\u002Fimages\u002Fbackground.jpg') no-repeat;\n    background-size: cover;\n}\n\n.login_form {\n    position: relative;\n    width: 70%;\n    top: 30vh;\n    background: url('@\u002Fassets\u002Fimages\u002Flogin_form.png');\n    padding: 40px;\n}\n\nh1 {\n    color: white;\n    font-size: 40px;\n}\n\nh2 {\n    font-size: 20px;\n    color: white;\n    margin: 20px 0px;\n}\n\n.login_btn {\n    width: 100%;\n}\n\u003C\u002Fstyle>\n\n```\n\n\n\n## 4.4 用户仓库数据ts类型定义与封装token\n\n**定义用户仓库state数据类型store\u002Fmodules\u002Ftypes\u002Ftype.ts**\n\n```typescript\nexport interface UserState {\n  token: string | null;\n}\n\n```\n\n**修改store\u002Fmodules\u002Fuser.ts中store数据类型**\n\n```typescript\n\u002F\u002F 创建用户相关的小仓库\nimport { defineStore } from 'pinia';\n\n\u002F\u002F 引入接口\n\nimport { reqLogin } from '@\u002Fapi\u002Fuser';\n\n\u002F\u002F 引入操作存储的工具方法\nimport { SET_TOKEN, GET_TOKEN } from '@\u002Futils\u002Ftoken';\n\n\u002F\u002F 引入数据类型\nimport type { loginForm, loginResponseData } from '@\u002Fapi\u002Fuser\u002Ftype';\n\nimport type { UserState } from '.\u002Ftypes\u002Ftype';\n\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n    \n  \u002F\u002F 小仓库存储数据的地方 使用UserState规范state数据类型\n  state: (): UserState => {\n    return {\n      token: GET_TOKEN(), \u002F\u002F用户唯一标识token\n    };\n  }\n```\n\n**封装本地存储，存储数据与读取数据方法**\n\n用于用户登录后的身份验证\n\n优势：\n\n- 代码简洁\n- 统一管理\n- 易于维护\n- 类型安全\n\n**新建utils\u002Ftoken.ts**\n\n```typescript\n\u002F\u002F 封装本地存储存储数据与读取数据方法\n\n\u002F\u002F 存储数据\nexport const SET_TOKEN = (token: string) => {\n  localStorage.setItem('TOKEN', token);\n};\n\u002F\u002F 本地存储获取数据\nexport const GET_TOKEN = () =>{\n    return localStorage.getItem('TOKEN'),\n}\n```\n\n**修改store\u002Fmodules\u002Fuser.ts中存储和获取token方法**\n\n```typescript\n\u002F\u002F 创建用户相关的小仓库\nimport { defineStore } from 'pinia';\n\n\u002F\u002F 引入接口\n\nimport { reqLogin } from '@\u002Fapi\u002Fuser';\n\n\u002F\u002F 引入操作存储的工具方法\nimport { SET_TOKEN, GET_TOKEN } from '@\u002Futils\u002Ftoken';\n\n\u002F\u002F 引入数据类型\nimport type { loginForm, loginResponseData } from '@\u002Fapi\u002Fuser\u002Ftype';\n\nimport type { UserState } from '.\u002Ftypes\u002Ftype';\n\n\u002F\u002F创建用户小仓库\nconst useUserStore = defineStore('User', {\n  \u002F\u002F 小仓库存储数据的地方\n  state: (): UserState => {\n    return {\n      token: GET_TOKEN(), \u002F\u002F用户唯一标识token\n    };\n  },\n  \u002F\u002F 异步|逻辑的地方\n  actions: {\n    \u002F\u002F用户登录的方法\n    async userLogin(data: loginForm) {\n      \u002F\u002F登录请求\n      const result: loginResponseData = await reqLogin(data);\n      \u002F\u002F 登录请求:成功200->token\n      \u002F\u002F 登录请求:失败201->登录失败错误的信息\n      if (result.code === 200) {\n        \u002F\u002F pinia仓库存储一下token\n        this.token = result.data.token as string;\n        \u002F\u002F由于pinia\u002Fvuex存储数据其实利用js对象\n        \u002F\u002F本地存储持久化存储一份\n        SET_TOKEN(result.data.token as string);\n        \u002F\u002F 能保证当前async函数返回一个成功的promise\n        return 'ok';\n      } else {\n        return Promise.reject(new Error(result.data.message));\n      }\n    },\n  },\n  getters: {},\n});\n\n\u002F\u002F 对外暴露获取小仓库方法\nexport default useUserStore;\n\n```\n\n## 4.5 登录模板表单校验\n\n登录表单校验的作用，是在前端提前拦截不合法输入，提升用户体验、减少无效请求、让系统更安全、更专业。\n\n**表单绑定model**\n\n```vue\n\u003Cel-form class=\"login_form\" :model=\"loginForm\" :rules=\"rules\" ref=\"loginForms\">\u003C\u002Fel-form>\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F收集账号和密码的数据\nconst loginForm = reactive({\n    username: 'admin',\n    password: '111111',\n});\n\u003C\u002Fscript>\n```\n\n**定义校验规则（rules），并且绑定rules**\n\n```vue\n\u003Cel-form class=\"login_form\" :model=\"loginForm\" :rules=\"rules\" ref=\"loginForms\">\u003C\u002Fel-form>\n\n\u003Cscript setup lang=\"ts\">\n \t\u002F\u002F 定义表单校验需要配置对象\nconst rules = {\n    \u002F\u002F规则对象属性\n    \u002F\u002F required,代表这个字段务必要校验的\n    \u002F\u002F min:文本长度至少多少位\n    \u002F\u002F max:文本长度最多多少位\n    \u002F\u002F message:错误的提示信息\n    \u002F\u002F trigger:触发校验表单的时机，change->文本发生变化触发校验，blur->失去焦点校验\n    username: [\n        { required: true, min: 6, max: 10, message: '账号长度至少六位，最多十位', trigger: 'change' }\n    ],\n    password: [\n        { required: true, min: 6, message: '密码至少六位', trigger: 'change' }\n    ]\n}\n\u003C\u002Fscript>\n```\n\n**在 el-form-item 上写 `prop`**\n\n```vue\n\u003Cel-form-item prop=\"username\">\n                        \u003Cel-input v-model=\"loginForm.username\" placeholder=\"请输入用户名\" :prefix-icon=\"User\">\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n\u003Cel-form-item prop=\"password\">\n                        \u003Cel-input v-model=\"loginForm.password\" placeholder=\"请输入密码\" :prefix-icon=\"Lock\" type=\"password\"\n                            show-password>\u003C\u002Fel-input>\n                    \u003C\u002Fel-form-item>\n```\n\n**给 el-form 设置 ref（为了手动触发校验，validate的使用）**\n\n```vue\n\u003Cel-form class=\"login_form\" :model=\"loginForm\" :rules=\"rules\" ref=\"loginForms\">\u003C\u002Fel-form>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F获取el-form组件\nlet loginForms = ref()\n\n\u002F\u002F 登录按钮的回调\nconst login = async () => {\n    \u002F\u002F保证全部表单相校验通过在发请求\n    await loginForms.value.validate()    \n    \u002F\u002F 点击登录出现加载效果\n    loading.value = true\n    \u002F\u002F 点击登录按你以后能干什么？\n    \u002F\u002F 通知仓库发登录请求\n    \u002F\u002F 请求成功->首页展示数据的地方\n    \u002F\u002F 请求失败->弹出登录失败信息\n    try {\n        \u002F\u002F 保证登录成功\n        useStore.userLogin(loginForm);\n        \u002F\u002F编程式导航跳转到展示数据首页\n        \u002F\u002F登录成功提示信息\n        ElNotification({\n            type: 'success',\n            message: '登录成功',\n        });\n        \u002F\u002F编程式导航跳转到展示数据首页\n        $router.push('\u002F');\n        \u002F\u002F登录成功，加载按钮也消失\n        loading.value = false\n\n    } catch (error) {\n        \u002F\u002F 登录失败 加载效果消失\n        loading.value = false\n        ElNotification({\n            type: 'error',\n            message: (error as Error).message,\n        });\n\n    }\n};\n\u003C\u002Fscript>\n\n\nvalidate() 是一个 Promise\n成功时（表单校验通过）：Promise 变成 resolved，await validate() 后代码继续执行\n失败时（有校验不通过）：Promise 变成 rejected，await validate() 抛出异常，可以用 catch 捕获\n\nVue 3 的 ref + template ref 机制\n你在模板里给某个组件或元素加上 ref=\"loginForms\"，Vue 会把对应的组件实例或 DOM 元素，自动赋值给你定义的 loginForms 变量。\nVue 编译模板时，发现 ref=\"loginForms\"\n运行时，会把 \u003Cel-form> 的实例赋给 loginForms.value\n```\n\n### 4.5.1 自定义校验规则使用（可写正则）\n\n**给 `\u003Cel-form-item>` 设置 `prop`**\n\n这个 `prop` 对应表单绑定数据的字段名（`loginForm.username`）\n\n**在rules对象中自定义校验规则**\n\n```typescript\nconst rules = {\n  username: [\n    { trigger: 'change', validator: validatorUserName }\n  ],\n  password: [\n    { trigger: 'change', validator: validatorPassword }\n  ]\n}\n\n```\n\n**写自定义校验函数**\n\n函数签名固定，接受三个参数\n\n```typescript\nconst validatorUserName = (rule, value, callback) => {\n  \u002F\u002F rule：校验规则对象\n  \u002F\u002F value：当前表单字段的值\n  \u002F\u002F callback：校验结果的回调\n};\n\u002F\u002F 自定义校验规则\nconst validatorUserName = (rule: any, value: any, callback: any) => {\n    \u002F\u002F rule:即为校验规则对象\n    \u002F\u002F value：即为表单元素文本内容\n    \u002F\u002F 函数：如果符合条件callback放行通过\n    \u002F\u002F 如果不符合条件callback方法，注入错误提示信息\n    \u002F\u002F 可以写正则\n    if (value.length >= 5) {\n        callback()\n    } else {\n        callback(new Error('账号长度至少五位'))\n    }\n}\n```\n\n**`把rules` 传给 `\u003Cel-form>` 组件**\n\n\n\n## 4.6 _layout组件的静态搭建\n\n**创建src\u002Flayout\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">123\u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.layout_container {\n    width: 100%;\n    height: 100vh;\n    background-color: skyblue;\n    \n.layout_slider {\n    width: $base-menu-width;\n    height: 100vh;\n    background-color: $base-menu-background;\n}\n\n.layout_tabbar {\n    position: fixed;\n    left: $base-menu-width ;\n    top: 0;\n    width: calc(100% - $base-menu-width);\n    height: $base-tabbar-height;\n    background-color: springgreen;\n}\n\n.layout_main {\n    position: absolute;\n    width: calc(100% - $base-menu-width);\n    height: calc(100vh - $base-tabbar-height );\n    background-color: red;\n    left: $base-menu-width ;\n    top: $base-tabbar-height;\n    padding: 20px;\n    overflow: auto;\n}\n    }\n    \n\u003C\u002Fstyle>\n```\n\n**配置全局SCSS样式 src\u002Fstyles\u002Fvariable.scss**\n\n```scss\n\u002F\u002F 项目提供scss全局变量\n\u002F\u002F 左侧的菜单的宽度\n$base-menu-width:260px;\n\u002F\u002F 左侧菜单的背景颜色\n$base-menu-background:#00152b;\n\u002F\u002F 顶部导航的高度\n$base-tabbar-height:50px;\n\n```\n\n**滚动条设置src\u002Fstyles\u002Findex.scss**\n\n```scss\n\u002F\u002F 滚动条外观设置\n::-webkit-scrollbar {\n    width: 10px;\n}\n\n::-webkit-scrollbar-track{\n    background-color: $base-menu-background;\n}\n\n::-webkit-scrollbar-thumb{\n    width: 10px;\n    background-color: yellow;\n    border-radius: 10px;\n}\n\n```\n\n**修改src\u002Frouter\u002Frouters.ts**\n\n将登录成功后看到的页面\n\n```typescript\n \u002F\u002F 登录成功以后展示数据的路由\n    path: '\u002F',\n    component: () => import('@\u002Flayout\u002Findex.vue'),\n    name: 'layout',\n  },\n```\n\n\n\n## 4.7 logo组件的封装\n\n封装 Logo 是为了用配置统一控制显示与样式，做到一处修改、全局生效，提升代码可维护性。\n\n**新建src\u002Fsetting.ts用来管理配置logo和标题**\n\n```typescript\n\u002F\u002F 用于项目logo,标题的配置\nexport default {\n  title: '硅谷甄选运营平台', \u002F\u002F 项目的标题\n  logo: 'public\u002Fimage.png', \u002F\u002F 项目的logo\n  logoHidden: true, \u002F\u002F logo组件是否隐藏设置\n};\n\n```\n\n**新建src\u002Flayout\u002Flogo\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"logo\" v-if=\"setting.logoHidden\">\n        \u003Cimg :src='setting.logo' alt=\"\">\n        \u003Cp>{{ setting.title }}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入设置标题与logo这配置文件\nimport setting from '@\u002Fsetting';\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n    \n.logo {\n    width: 100%;\n    height: $base-menu-logo-height;\n    color: white;\n    display: flex;\n    align-items: center;\n    font-size: $base-logo-tittle-fontsize;\n    padding: 10px;\n\nimg {\n    width: 40px;\n    height: 40px;\n}\n\np {\n    margin-left: 10px;\n}\n    }\n\u003C\u002Fstyle>\n```\n\n注意：样式收敛到 logo 内，这样以后你在这个组件里再加别的 `img \u002F p` 也不会乱。\n\n**src\u002Flayout\u002Findex.vue引入logo组件**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">\n            \u003CLogo>\u003C\u002FLogo>\n        \u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\u003Cscript setup lang=\"ts\">\nimport Logo from '.\u002Flogo\u002Findex.vue'\n\u003C\u002Fscript>\n```\n\n**全局variable.scss样式设置**\n\n```scss\n\u002F\u002F左侧菜单logo高度设置\n$base-menu-logo-height:50px;\n\u002F\u002F左侧菜单logo右侧文字大小\n$base-logo-tittle-fontsize:20px;\n\n```\n\n\n\n## 4.8 左侧菜单静态搭建\n\n**src\u002Flayout\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">\n            \u003CLogo>\u003C\u002FLogo>\n            \u003C!-- 展示菜单 -->\n            \u003C!-- 滚动菜单 -->\n            \u003Cel-scrollbar class=\"scrollbar\">\n                \u003C!-- 菜单组件 -->\n                \u003Cel-menu class=\"el_menu\">\n                    \u003Cel-menu-item index=\"1\">首页\u003C\u002Fel-menu-item>\n                    \u003Cel-menu-item index=\"2\">数据大屏\u003C\u002Fel-menu-item>\n                    \u003C!-- 折叠菜单 -->\n                    \u003Cel-sub-menu index=\"3\">\n                        \u003Ctemplate #title>\n                            \u003Cspan>权限管理\u003C\u002Fspan>\n                        \u003C\u002Ftemplate>\n                        \u003Cel-menu-item index=\"3-1\">用户管理\u003C\u002Fel-menu-item>\n                        \u003Cel-menu-item index=\"3-2\">角色管理\u003C\u002Fel-menu-item>\n                        \u003Cel-menu-item index=\"3-3\">菜单管理\u003C\u002Fel-menu-item>\n                    \u003C\u002Fel-sub-menu>\n                \u003C\u002Fel-menu>\n            \u003C\u002Fel-scrollbar>\n        \u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport Logo from '.\u002Flogo\u002Findex.vue'\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.layout_container {\n    width: 100%;\n    height: 100vh;\n    color: white;\n\n    .layout_slider {\n        width: $base-menu-width;\n        height: 100vh;\n        background-color: $base-menu-background;\n\n        .scrollbar {\n            width: 100%;\n            height: calc(100vh - $base-menu-logo-height);\n\n            .el_menu {\n                --el-menu-bg-color: #00152b;\n                --el-menu-text-color: #fff;\n            }\n        }\n    }\n\n    .layout_tabbar {\n        position: fixed;\n        left: $base-menu-width ;\n        top: 0;\n        width: calc(100% - $base-menu-width);\n        height: $base-tabbar-height;\n        background-color: springgreen;\n    }\n\n    .layout_main {\n        position: absolute;\n        width: calc(100% - $base-menu-width);\n        height: calc(100vh - $base-tabbar-height );\n        background-color: red;\n        left: $base-menu-width ;\n        top: $base-tabbar-height;\n        padding: 20px;\n        overflow: auto;\n    }\n}\n\u003C\u002Fstyle>\n```\n\n### 4.8.1 滚动菜单的使用（`el-scrollbar`）\n\n当侧边菜单内容过多，超出固定高度时，使用 `el-scrollbar` 给菜单区域增加自定义滚动条，避免页面整体滚动。\n\n```vue\n典型用法结构\n\n\u003Cel-scrollbar class=\"scrollbar\">\n  \u003Cel-menu class=\"menu\">\n    \u003Cel-menu-item index=\"1\">首页\u003C\u002Fel-menu-item>\n    \u003Cel-sub-menu index=\"2\">\n      \u003Ctemplate #title>\n        \u003Cspan>权限管理\u003C\u002Fspan>\n      \u003C\u002Ftemplate>\n      \u003Cel-menu-item index=\"2-1\">用户管理\u003C\u002Fel-menu-item>\n    \u003C\u002Fel-sub-menu>\n  \u003C\u002Fel-menu>\n\u003C\u002Fel-scrollbar>\n\n样式配合（关键点）\nel-scrollbar 高度固定或计算，保证菜单超出时出现滚动条\n\u003Cstyle scoped lang=\"scss\">\n    .scrollbar {\n \t\t height: calc(100vh - $base-menu-logo-height);\n \t\t \u002F* 高度 = 全屏高度 - logo 高度 *\u002F\n}\n\u003C\u002Fstyle>\n```\n\n**注意：**\n\n`el-scrollbar` 包裹的内容区域高度要超过自身高度，滚动条才会出现\n\n`el-scrollbar` 只能有一个直接子元素（通常是菜单容器）\n\n保证菜单组件的高度不要超出 `el-scrollbar` 的高度\n\n### 4.8.2 el-menu组件的使用\n\n* 普通菜单（`el-menu-item`）\n\n  没有下级菜单的入口\n\n  ```vue\n  \u003Cel-menu>\n  \t\u003Cel-menu-item index=\"1\">首页\u003C\u002Fel-menu-item>\n  \u003C\u002Fel-menu>\n  ```\n\n* 展开菜单 \u002F 子菜单（`el-sub-menu`）\n\n  有下级菜单，需要展开\u002F收起\n\n  ```vue\n  \u003Cel-menu>\n  \t\u003Cel-sub-menu index=\"2\">\n      具名插槽写子菜单的标题\n    \t\t\u003Ctemplate #title>\n      \t\t\u003Cspan>权限管理\u003C\u002Fspan>\n    \t\t\u003C\u002Ftemplate>\n  \n    \t\t\u003Cel-menu-item index=\"2-1\">用户管理\u003C\u002Fel-menu-item>\n    \t\t\u003Cel-menu-item index=\"2-2\">角色管理\u003C\u002Fel-menu-item>\n  \t\u003C\u002Fel-sub-menu>\n  \u003C\u002Fel-menu>\n  ```\n\n* 样式设置\n\n  ```scss\n  .menu {\n    border-right: none;  \u002F\u002F 去掉右侧边框\n    --el-menu-bg-color: #00152b;  \u002F\u002F 背景颜色\n    --el-menu-text-color: #fff;\t\u002F\u002F 文本颜色\n    --el-menu-hover-bg-color: #409eff;  \u002F\u002F 鼠标悬停颜色\n    --el-menu-active-color: #409eff;    \u002F\u002F 鼠标点击颜色\n  }\n  ```\n\n**注意：**\n\n`el-menu`：菜单容器\n\n`el-menu-item`：普通菜单项\n\n`el-sub-menu`：子菜单\n\n`index`：**唯一标识（必须）**\n\n### 4.8.3 递归组件生成动态菜单\n\n通过递归组件实现菜单的无限层级和结构复用，简化代码维护，提高扩展性和可读性\n\n\n\n**src\u002Frouter\u002Frouters.ts**\n\n在路由中起每个菜单的名字，并配置展示逻辑，写入子路由\n\n```typescript\n\u002F\u002F 对外暴漏配置路由\nexport const constantRoute = [\n  {\n    \u002F\u002F 登录\n    path: '\u002Flogin',\n    component: () => import('@\u002Fviews\u002Flogin\u002Findex.vue'),\n    name: 'login',\n    meta: {\n      title: '登录',\n      hidden: true, \u002F\u002F 代表路由标题在菜单中是否隐藏，true隐藏。false不隐藏\n    },\n  },\n  {\n    \u002F\u002F 登录成功以后展示数据的路由\n    path: '\u002F',\n    component: () => import('@\u002Flayout\u002Findex.vue'),\n    name: 'layout',\n    meta: {\n      title: 'layout',\n      hidden: false,\n    },\n    children: [\n      {\n        path: 'home',\n        component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n        meta: {\n          title: '首页',\n          hidden: false,\n        },\n      },\n      {\n        path: 'text',\n        component: () => import('@\u002Fviews\u002Fhome\u002Findex.vue'),\n        meta: {\n          title: '测试',\n          hidden: false,\n        },\n      },\n    ],\n  },\n  {\n    \u002F\u002F404\n    path: '\u002F404',\n    component: () => import('@\u002Fviews\u002F404\u002Findex.vue'),\n    name: '404',\n    meta: {\n      title: '404',\n      hidden: true,\n    },\n  },\n  \u002F\u002F 重定向\n  {\n    path: '\u002F:pathMatch(.*)*',\n    redirect: '\u002F404',\n    name: 'Any',\n    meta: {\n      title: 'Any',\n      hidden: true,\n    },\n  },\n];\n\n\u002F\u002F 子路由一律写相对路径\n\n```\n\n\n\n**创建src\u002Flayout\u002Fmenu\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n  \u003Ctemplate v-for=\"(item, index) in menuList\" :key=\"item.path\">\n    \u003C!-- 没有子路由 -->\n    \u003Ctemplate v-if=\"!item.children\">\n      \u003Cel-menu-item :index=\"item.path\" v-if=\"!item.meta.hidden\">\n        \u003Cspan>标\u003C\u002Fspan>\n        \u003Cspan>{{ item.meta.title }}\u003C\u002Fspan>\n      \u003C\u002Fel-menu-item>\n    \u003C\u002Ftemplate>\n    \u003C!-- 有子路由但是只有一个子路由 -->\n    \u003Ctemplate v-if=\"item.children && item.children.length == 1\">\n      \u003Cel-menu-item :index=\"item.children[0].path\" v-if=\"!item.children[0].meta.hidden\">\n        \u003Cspan>{{ item.children[0].meta.title }}\u003C\u002Fspan>\n      \u003C\u002Fel-menu-item>\n    \u003C\u002Ftemplate>\n    \u003C!-- 有子路由且个数大于1 -->\n    \u003Cel-sub-menu :index=\"item.path\" v-if=\"item.children && item.children.length > 1\">\n      \u003Ctemplate #title>\n        \u003Cspan>{{ item.meta.title }}\u003C\u002Fspan>\n      \u003C\u002Ftemplate>\n      \u003C!-- 使用递归 -->\n      \u003C!-- 组件调用自己  处理无限层级、代码复用、易于维护 -->\n      \u003CSideMenu :menuList=\"item.children\">\u003C\u002FSideMenu>\n    \u003C\u002Fel-sub-menu>\n  \u003C\u002Ftemplate>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 给组件起名字\n\u002F\u002F 告诉 Vue：\n\u002F\u002F “递归时，请用 组件名注册表 去找自己”    \ndefineOptions({\n  name: 'SideMenu'\n})\n\u002F\u002F 有一个名为 menuList 的 props，来自父组件传递的数据\ndefineProps(['menuList']);\n\n\n\u003C\u002Fscript>\n\n\u003Cstyle scoped>\u003C\u002Fstyle>\n\n```\n\n**注意：**\n\n在 Vue 3 + `\u003Cscript setup>` 里：\n `defineOptions({ name: 'Menu' })`\n 就是用来“给组件起名字”的\n\n它等价于你以前写的：\n\n```vue\nexport default {\n  name: 'Menu'\n}\n```\n\n只是 **`script setup` 专用写法**。\n\n```vue\nv-if=\"!item.meta.hidden\" 写入是否展示逻辑，有的一级路由不需要展示,login,404,Any\n```\n\n**v-if 是 Vue 的指令，用来根据表达式的真假决定是否渲染这个节点。**\n\n\n\n**写入动态生成路由组件src\u002Flayout\u002Findex.vue**\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\"layout_container\">\n        \u003C!-- 左侧菜单 -->\n        \u003Cdiv class=\"layout_slider\">\n            \u003CLogo>\u003C\u002FLogo>\n            \u003C!-- 展示菜单 -->\n            \u003C!-- 滚动菜单 -->\n            \u003Cel-scrollbar class=\"scrollbar\">\n                \u003C!-- 菜单组件 -->\n                \u003Cel-menu class=\"el_menu\">\n                    \u003C!-- 根据路由动态生成菜单 -->\n                    \u003C!-- 给子组件传路由数组 -->\n                    \u003CSideMenu :menuList=\"useStore.menuRouters\">\u003C\u002FSideMenu> \n                \u003C\u002Fel-menu>\n            \u003C\u002Fel-scrollbar>\n        \u003C\u002Fdiv>\n        \u003C!-- 顶部导航 -->\n        \u003Cdiv class=\"layout_tabbar\">456\u003C\u002Fdiv>\n        \u003C!-- 内容展示区域 -->\n        \u003Cdiv class=\"layout_main\">\n\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F 引入左侧菜单logo子组件\nimport Logo from '.\u002Flogo\u002Findex.vue'\n\u002F\u002F 引入菜单组件\nimport SideMenu from '.\u002Fmenu\u002Findex.vue'\n\n\u002F\u002F 获取用户相关的小仓库\nimport useUserStore from '@\u002Fstore\u002Fmodules\u002Fuser';\nlet useStore = useUserStore()\n\n\n\n\u003C\u002Fscript>\n\u003Cstyle scoped lang=\"scss\">\n.layout_container {\n    width: 100%;\n    height: 100vh;\n    color: white;\n\n    .layout_slider {\n        width: $base-menu-width;\n        height: 100vh;\n        background-color: $base-menu-background;\n\n        .scrollbar {\n            width: 100%;\n            height: calc(100vh - $base-menu-logo-height);\n\n            .el_menu {\n                --el-menu-bg-color: #00152b;\n                --el-menu-text-color: #fff;\n                --el-menu-hover-bg-color: #00152b;\n                border-right: none; \u002F\u002F 去掉右侧边框\n            }\n        }\n    }\n\n    .layout_tabbar {\n        position: fixed;\n        left: $base-menu-width ;\n        top: 0;\n        width: calc(100% - $base-menu-width);\n        height: $base-tabbar-height;\n        background-color: springgreen;\n    }\n\n    .layout_main {\n        position: absolute;\n        width: calc(100% - $base-menu-width);\n        height: calc(100vh - $base-tabbar-height );\n        background-color: red;\n        left: $base-menu-width ;\n        top: $base-tabbar-height;\n        padding: 20px;\n        overflow: auto;\n    }\n}\n\u003C\u002Fstyle>\n```\n\n",99,"2026-04-07T16:22:29.810Z","2026-04-07T16:22:29.815Z","2026-05-25T22:05:44.500Z",{"id":83,"categoryName":89,"slug":90,"description":91,"sort":31,"isEnable":32,"createTime":92,"updateTime":93,"deleteTime":31},[]]