【前端】Vue3 + AntdVue + Ts + Vite4 + pnpm + Pinia 实战

打印 上一主题 下一主题

主题 1863|帖子 1863|积分 5589

背景:如今前端框架多、版本多、迭代快。以 vue 为例,就有 vue2/vue3,依赖管理工具就有 npm/yarn,打包工具就有 webpack/vite,UI 框架就有 antd-vue/element_ui/element_ui_plus,没有统一的路径可循,影象心智负担较重。
为了方便后续快速搭建项目,能利用最新的框架,而且减轻心智负担,本文采用 vue3 + yarn + vite + antd-vue,方便快速上手。
教程
一、项目搭建

1.1 生态工具对比

包管理 pnpm(npm、yarn)



  • 最早是 npm2,其node_modules 有多层嵌套问题,导致文件路径凌驾 windows 最长路径限定
  • 之后 yarn 解决了上述路径问题,将 node_modules 全部平铺,但仍有如下问题:

    • 幽灵依赖问题:即旧版本的第三方库B1引用了C库,我们即可在项目A中用 require 引用 C 库。但当B1升级到新版本B2后,不再依赖C库了,C库即会被从 node_modules 中移除。但我们本身的代码A就会报错找不到C库的错误
    • 某库的多版本:占用多份磁盘空间。例如我们本身的项目A引用了C库的C1版本,我们引用的B库又依赖于C库的C2版本。那么 node_modules 中就同时存在 C1 和 C2 两个版本,浪费磁盘空间。

  • 同时,npm 3 也跟进了上述 yarn 的盼望
  • 最新的 pnpm 则完全解决了上述问题,有如下两个优点:

    • 小:其整个电脑的所有项目共用同一份依赖文件(例如将C库的C1和C2两个版本都放在电脑全局的 ~/CommonStore文件夹下,然后A项目的各依赖文件都用软链接or硬链接指向全局路径)。这样就大大节省了各项目A、X、Y之间共用的B库和C库的冗余磁盘占用(究竟只存一份嘛)。
    • 快:因为是链接形式不需要拷贝,所以速度快。
      pnpm 是凭什么对 npm 和 yarn 降维打击的
      pnpm官网

打包工具 vite (webpack)



  • 生产环境速度相当,但开辟环境 vite 比 webpack 快多了, 因为vite分阶段编译(用户点击哪个路由,才编译哪个模块),而 webpack 需要全部编译完。vite 在开辟阶段显着减少了编译等待时间(1min到1s的提升幸福感)。
  • 若用 vite 初始化项目则配置文件为 vite.config.ts,若用 vue-cli 初始化项目则配置文件为 vue.config.js,配置内容近似。
1.2 项目创建

vite 项目搭建教程
vite 官网
1.2.0 项目搭建

此中各下令的表明, 详见下文
  1. nvm install 22
  2. nvm alias default v22.14.0
  3. nvm use 22
  4. node -v # v22.14.0 (nvm ls 可查看/选择版本)
  5. npm install -g corepack # 升级最新版 corepack
  6. corepack -v # 查 corepack 版本 0.31.0
  7. corepack enable # 安装 pnpm
  8. corepack prepare pnpm@latest --activate # 升级 pnpm 到最新版本
  9. pnpm -v # 10.5.2
  10. pnpm create vite fe --template vue-ts # 初始化项目
  11. cd fe && pnpm install && pnpm run dev # 安装依赖并run
  12. pnpm add @types/node --save-dev # 为保证 node 的使用. 这是 Node.js 的 TypeScript 类型定义文件. 详见下文
复制代码
1.2.1 node 版本

  1. # 安装 nvm (~/.zshrc 可配置 nvm 的环境变量如下:)
  2. # nvm
  3. export NVM_DIR="$HOME/.nvm"
  4. [ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"                                       # This loads nvm
  5. [ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" # This loads nvm    bash_completion
  6. nvm use 22
  7. nvm ls
复制代码
node 版本, 不会对整套技术栈有太大影响, 反而建议尽量新, 这样可以支持更多特性:


  • Vue3 对 Node.js 版本的依赖较低,重要依赖于浏览器的兼容性。Node.js 20 和 22 都不会对 Vue3 的兼容性产生显着影响。
  • Ant Design Vue 是一个 UI 组件库,与 Node.js 版本的直接关联较小。只要 Node.js 版本支持 ES Module 和 CommonJS,就不会有兼容性问题。
  • TypeScript 的编译和运行与 Node.js 版本的关联较小。Node.js 20 和 22 都支持 TypeScript 的最新特性。
  • Vite 4 依赖于 Node.js 的模块系统和文件系统 API。Node.js 20 和 22 都会支持 Vite 4 的焦点功能,但建议使用较新的 Node.js 版本以获得更好的性能和稳固性。
  • PNPM 是一个包管理工具,与 Node.js 版本的兼容性较好。Node.js 20 和 22 都支持 PNPM 的最新特性。
  • Pinia 是 Vue 的状态管理库,与 Node.js 版本的直接关联较小。只要 Node.js 版本支持 ES Module,就不会有兼容性问题。
1.2.2 corepack

Corepack 是 Node.js 内置的一个包管理工具管理器(Package Manager Manager),用于管理差别的 JavaScript 包管理工具(如 npm、yarn、pnpm 等)。它的重要目的是简化包管理工具的安装和版本管理,确保开辟者在差别项目中使用同等的包管理工具版本。
Corepack 从 Node.js 16.9.0 开始默认启用,并在后续版本中逐步增强。
corepack enable 是一个下令,用于启用 Corepack 的功能。具体来说:运行 corepack enable 后,Corepack 会在系统中启用,并准备好管理包管理工具。启用后,你可以直接使用 yarn 或 pnpm 等下令,而无需手动安装这些工具。
默认 Node.js 16.9.0 及更高版本中,Corepack 默认是启用的,因此通常不需要手动运行 corepack enable。
示例:
启用 Corepack:corepack enable
安装特定版本的包管理工具:corepack prepare yarn@1.22.19 --activate
在项目中使用指定的包管理工具:在 package.json 中指定包管理工具及其版本:
  1. {
  2.   "packageManager": "yarn@1.22.19"
  3. }
复制代码
然后运行 yarn 或 pnpm 时,Corepack 会自动使用指定的版本
1.2.3 npm install -g

npm install -g vite @vue/cli 安装的是 下令行工具(CLI), 而不是 nodejs 的代码库.


  • 下令行工具(CLI):
    vite 和 @vue/cli 都是下令行工具,用于快速创建、构建和管理项目。
    安装后,你可以在终端中直接运行 vite 或 vue 下令来实行相关操作。
  • 全局安装:
    -g 表示全局安装(–global),这些工具会被安装到系统的全局 node_modules 目录中,而不是当前项目的 node_modules。
    全局安装后,你可以在任何目录下使用这些下令。
  • vite 工具的使用:
    一个现代化的前端构建工具,用于快速启动和开辟项目(尤其是 Vue、React 等框架)。
    安装后,你可以使用 vite 下令来创建新项目、启动开辟服务器或构建生产版本。
    示例:
  1. vite create my-project
  2. cd my-project
  3. vite dev
复制代码


  • @vue/cli 工具的使用:
    Vue.js 的官方下令行工具,用于快速搭建 Vue 项目。
    安装后,你可以使用 vue 下令来创建、管理和构建 Vue 项目。
    示例:
    vue create my-project
    cd my-project
    vue serve
  • 安装后的位置
    全局安装的 cli 工具会被安装到 全局 node_modules 目录中, mac 为 ~/.nvm/versions/node/<版本>/lib/node_modules. 另外这些工具的可实行文件会被链接到系统的 PATH 中, 所以可以直接在终端实行 vite 或 vue 下令.
npm install 可加/不加 -g 参数
pnpm add 可加/不加 -g 参数
上述二者, 都既可以安装 下令行 cli, 也可以安装代码库.
如果一个项目的 package.json 中含 “bin” 则表示为 下令行 cli, 否则表示为代码库
例如 vite 的 package.json 如下:
  1. {
  2.   "name": "vite",
  3.   "bin": {
  4.     "vite": "bin/vite.js"
  5.   }
  6. }
复制代码
例如 lodash 的 package.json 如下:
  1. {
  2.   "name": "lodash",
  3.   "main": "lodash.js"
  4. }
复制代码
如果下令行, 未指定 -g, 则被安装在本项目的 node_modules 目录中(而不在全局 node_modules 目录中), 则可通过 npx 运行, 例如 npx vite
1.2.4 pnpm

配置方式:
  1. Appended new lines to /Users/y/.zshrc
  2. Next configuration changes were made:
  3. export PNPM_HOME="/Users/y/Library/pnpm"
  4. case ":$PATH:" in
  5.   *":$PNPM_HOME:"*) ;;
  6.   *) export PATH="$PNPM_HOME:$PATH" ;;
  7. esac
  8. To start using pnpm, run:
  9. source /Users/y/.zshrc
复制代码
升级最新版 pnpm self-update, (注意项目的 package.json 文件中写死了 pnpm 的旧版本号, 可手动改该文件, 或切换到非项目目录再 pnpm -v).
检察版本 pnpm -v
pnpm store path 可检察其全局存储路径:
  1. # pnpm store path
  2. /Users/abc/Library/pnpm/store/v3
  3. # du -sh /Users/abc/Library/pnpm/store/v3
  4. 335M        /Users/abc/Library/pnpm/store/v3
复制代码
安装生产依赖:
如果包是项目运行所必需的,则可不填 (或 --save)(或 -S):pnpm add lodash
全局安装:
如果包需要全局使用,使用 -g:pnpm add typescript -g
删除包:
如果需要删除包,使用 uninstall:pnpm uninstall @types/node --save-dev
1.2.5 pnpm add @types/node --save-dev

为包管 node 的使用. 这是 Node.js 的 TypeScript 类型定义文件.
–save-dev 是把包作为 开辟依赖 devDependencies 安装(而不是作为生产依赖).
pnpm 会从 npm 堆栈下载 @types/node 包,并将其安装到项目的 node_modules 目录中. @types/node 会被添加到 package.json 的 devDependencies 字段中.
  1. {
  2.   "devDependencies": {
  3.     "@types/node": "^20.0.0"
  4.   }
  5. }
复制代码
安装后,TypeScript 编译器会自动识别 Node.js 的类型,避免类型错误
使用场景:
当你使用 TypeScript 开辟 Node.js 项目时,需要安装 @types/node 来获得 Node.js API 的类型支持。
例如,在代码中使用 fs 模块时,TypeScript 会检查 fs.readFile 的参数和返回值类型。
示例:
假设你在开辟一个 Node.js + TypeScript 项目,代码中使用了 fs 模块:
  1. import fs from 'fs';
  2. fs.readFile('file.txt', 'utf-8', (err, data) => {
  3.   if (err) throw err;
  4.   console.log(data);
  5. });
复制代码
如果没有安装 @types/node,TypeScript 会报错,因为它无法识别 fs 模块的类型。安装 @types/node 后,TypeScript 就能正确识别 fs 的类型。
1.2.6 pnpm create vite

大概有一种征象: vite -v 和 vue -v 都无输出(说明没安装), 但 pnpm create vite fe --template vue-ts 却可以正常创建 vite-vue-ts 的项目.

  • vite -v 和 vue -v 无输出, 是因为没有全局安装.
  • pnpm create vite 是一个复合下令, 在其内部会下载并运行 create-vite 这个工具, 但不会全局安装 vite. 而是会在项目内安装(即在项目内的 node_modules 目录内), 即会在 package.json 的 devDependencies 中 配置如下:
  1. "devDependencies": {
  2.   "vite": "^6.2.0"
  3. }
复制代码
在项目中, pnpm vite -v 即可运行本地安装的 vite
  1. # pnpm vite -v
  2. vite/6.2.0 darwin-arm64 node-v20.18.3
复制代码

  • 建议在项目内安装 vite, 而不是在全局安装 vite. 这样避免因全局差别版本导致的兼容问题. 项目内可通过 package.json 管理, 便于团队协作版本控制.
1.3 集成配置

tsconfig.json

tsconfig.json 就用 脚手架 生成的即可, 无需修改
最新的脚手架分为如下三个文件:
tsconfig.json 如下:
  1. {
  2.   "files": [],
  3.   "references": [
  4.     { "path": "./tsconfig.app.json" },
  5.     { "path": "./tsconfig.node.json" }
  6.   ]
  7. }
复制代码
tsconfig.app.json 如下:
  1. {
  2.   "extends": "@vue/tsconfig/tsconfig.dom.json",
  3.   "compilerOptions": {
  4.     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
  5.     /* Linting */
  6.     "strict": true,
  7.     "noUnusedLocals": true,
  8.     "noUnusedParameters": true,
  9.     "noFallthroughCasesInSwitch": true,
  10.     "noUncheckedSideEffectImports": true
  11.   },
  12.   "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
  13. }
复制代码
tsconfig.node.json 如下:
  1. {
  2.   "compilerOptions": {
  3.     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
  4.     "target": "ES2022",
  5.     "lib": ["ES2023"],
  6.     "module": "ESNext",
  7.     "skipLibCheck": true,
  8.     /* Bundler mode */
  9.     "moduleResolution": "bundler",
  10.     "allowImportingTsExtensions": true,
  11.     "isolatedModules": true,
  12.     "moduleDetection": "force",
  13.     "noEmit": true,
  14.     /* Linting */
  15.     "strict": true,
  16.     "noUnusedLocals": true,
  17.     "noUnusedParameters": true,
  18.     "noFallthroughCasesInSwitch": true,
  19.     "noUncheckedSideEffectImports": true
  20.   },
  21.   "include": ["vite.config.ts"]
  22. }
复制代码
vite.config.ts

修改 vite.config.ts 参考. 此中关键是指定 @ 为 src 目录的别名.
(可选) 新建 /src/assets/styles/variables.scss 用于放公共的 scss 变量,其可被 .scss 文件或 .vue 文件 引用。
  1. pnpm add mockjs -S # 用于生成随机数据的 JavaScript 库,主要用于前端开发中的模拟数据(Mock Data)
  2. pnpm add vite-plugin-mock -D # 是一个 Vite 插件,用于在 Vite 项目中集成 Mock 数据功能
  3. pnpm add postcss-px-to-viewport -S # 是一个 PostCSS 插件,用于将 CSS 中的 px 单位转换为 viewport 单位(如 vw、vh),以实现移动端自适应布局
复制代码
  1. // vite.config.ts
  2. import { defineConfig } from 'vite'
  3. import vue from '@vitejs/plugin-vue'
  4. import { viteMockServe } from "vite-plugin-mock";
  5. import * as path from "path";
  6. let backendAddr = process.env.BACKEND_ADDR || 'http://192.168.2.180:334';
  7. export default defineConfig({
  8.   // base: "/foo/", // 开发或生产环境服务的公共基础路径
  9.   base: "/", // 开发或生产环境服务的公共基础路径
  10.   optimizeDeps: {
  11.     force: true, // 强制进行依赖预构建
  12.   },
  13.   css: {
  14.     preprocessorOptions: {
  15.       scss: {
  16.         additionalData: `@use '/src/assets/styles/variables.scss';`, // 引入全局变量文件
  17.       },
  18.     },
  19.   },
  20.   resolve: {
  21.     alias: {
  22.       "@": path.resolve(__dirname, "./src"), // 路径别名
  23.     },
  24.     extensions: [".js", ".ts", ".json"], // 导入时想要省略的扩展名列表
  25.   },
  26.   server: {
  27.     host: true, // 监听所有地址
  28.     proxy: {
  29.       // // 字符串简写写法
  30.       "/foo": "http://localhost:4567",
  31.       // // 选项写法
  32.       "^/api": {
  33.         target: backendAddr,
  34.         changeOrigin: true,
  35.         rewrite: (path: string) => path.replace(/^\/api/, ""),
  36.       },
  37.       "^/ws": {
  38.         target: backendAddr,
  39.         changeOrigin: true,
  40.         rewrite: (path: string) => path.replace(/^\/ws/, ""),
  41.       }
  42.     },
  43.   },
  44.   build: {
  45.     outDir: "dist", // 打包文件的输出目录
  46.     assetsDir: "static", // 静态资源的存放目录
  47.     assetsInlineLimit: 4096, // 图片转 base64 编码的阈值
  48.   },
  49.   plugins: [vue(), viteMockServe()],
  50. });
复制代码
启动脚本

通常,本地启动前端后,需要连本机环境和联调环境(和后端同砚调接口),则可在 vite.config.js 中设置如下(上文已包含):
  1. let backendAddr = process.env.BACKEND_ADDR || 'http://192.168.2.99:334'; // 若有环境变量则连本地后端服务,若无则连联调机器的后端服务
  2. // 并在 server.proxy 中引用该变量即可,示例如下:
  3.   server: {
  4.     host: true, // 监听所有地址
  5.     proxy: {
  6.       "^/ws": {
  7.         target: backendAddr,
  8.         changeOrigin: true,
  9.         rewrite: (path) => path.replace(/^\/ws/, ""),
  10.       },
  11.     }
复制代码
则指定环境变量启动即可连本地后端服务:
  1. BACKEND_ADDR='http://127.0.0.1:9999' pnpm run dev
复制代码
eslint(可选)

  1. pnpm add eslint eslint-plugin-vue --save-dev
  2. pnpm add @typescript-eslint/parser --save-dev
复制代码
创建配置文件: .eslintrc.js
  1. module.exports = {
  2.     parser: 'vue-eslint-parser',
  3.     parserOptions: {
  4.         parser: '@typescript-eslint/parser',
  5.         ecmaVersion: 2020,
  6.         sourceType: 'module',
  7.         ecmaFeatures: {
  8.             jsx: true
  9.         }
  10.     },
  11.     extends: [
  12.         'plugin:vue/vue3-recommended',
  13.         'plugin:@typescript-eslint/recommended',
  14.     ],
  15.     rules: {
  16.         // override/add rules settings here, such as:
  17.     }
  18. };
复制代码
创建忽略文件:.eslintignore
  1. node_modules/
  2. dist/
  3. index.html
复制代码
下令行式运行:修改 package.json
  1. {
  2.     ...
  3.     "scripts": {
  4.         ...
  5.         "eslint:comment": "使用 ESLint 检查并自动修复 src 目录下所有扩展名为 .js 和 .vue 的文件",
  6.         "eslint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
  7.     }
  8.     ...
  9. }
复制代码
集成 prittier(可选)

  1. pnpm add prettier eslint-config-prettier eslint-plugin-prettier --save-dev
复制代码
创建配置文件: prettier.config.js 或 .prettierrc.js
  1. module.exports = {
  2.   // 一行最多 80 字符
  3.   printWidth: 80,
  4.   // 使用 4 个空格缩进
  5.   tabWidth: 4,
  6.   // 不使用 tab 缩进,而使用空格
  7.   useTabs: false,
  8.   // 行尾需要有分号
  9.   semi: true,
  10.   // 使用单引号代替双引号
  11.   singleQuote: true,
  12.   // 对象的 key 仅在必要时用引号
  13.   quoteProps: "as-needed",
  14.   // jsx 不使用单引号,而使用双引号
  15.   jsxSingleQuote: false,
  16.   // 末尾使用逗号
  17.   trailingComma: "all",
  18.   // 大括号内的首尾需要空格 { foo: bar }
  19.   bracketSpacing: true,
  20.   // jsx 标签的反尖括号需要换行
  21.   jsxBracketSameLine: false,
  22.   // 箭头函数,只有一个参数的时候,也需要括号
  23.   arrowParens: "always",
  24.   // 每个文件格式化的范围是文件的全部内容
  25.   rangeStart: 0,
  26.   rangeEnd: Infinity,
  27.   // 不需要写文件开头的 @prettier
  28.   requirePragma: false,
  29.   // 不需要自动在文件开头插入 @prettier
  30.   insertPragma: false,
  31.   // 使用默认的折行标准
  32.   proseWrap: "preserve",
  33.   // 根据显示样式决定 html 要不要折行
  34.   htmlWhitespaceSensitivity: "css",
  35.   // 换行符使用 lf
  36.   endOfLine: "auto",
  37. };
复制代码
修改 .eslintrc.js 配置
  1. module.exports = {
  2.     ...
  3.     extends: [
  4.         'plugin:vue/vue3-recommended',
  5.         'plugin:@typescript-eslint/recommended',
  6.         'prettier',
  7.         'plugin:prettier/recommended'
  8.     ],
  9.     ...
  10. };
复制代码
下令行式运行:修改 package.json
  1. {
  2.     ...
  3.     "scripts": {
  4.         ...
  5.         "prettier:comment": "自动格式化当前目录下的所有文件",
  6.         "prettier": "prettier --write"
  7.     }
  8.     ...
  9. }
复制代码
效果如下:
![](https://i-blog.csdnimg.cn/direct/d773c52752e44981a8bed24b1997401c.png = 200x200)
pinia

踩坑教训: 不可用时, 先检查是否 pnpm add 乐成了, (因为 vscode 提示时好时坏)
  1. pnpm add pinia # 若报错, 则可先停止 pnpm run dev 再执行
复制代码
新建 src/store 目录并在其下面创建 index.ts,导出 store
  1. import { createPinia } from 'pinia'
  2. const store = createPinia()
  3. export default store
复制代码
在 main.ts 中引入并使用
  1. import { createApp } from "vue";
  2. import "./style.css";
  3. import App from "./App.vue";
  4. import store from "./store";
  5. const app = createApp(App); // 创建vue实例
  6. app.use(store); // 挂载pinia
  7. app.mount("#app"); // 挂载实例
复制代码
定义State: 在 src/store 下面创建一个 user.ts
  1. import { defineStore } from "pinia";
  2. export const useUserStore = defineStore("user", {
  3.     state: () => {
  4.         return {
  5.             name: "张三",
  6.         };
  7.     },
  8.     actions: {
  9.         updateName(name: string) {
  10.             this.name = name;
  11.         },
  12.     },
  13. });
复制代码
获取和修改 State: 在 src/components/usePinia.vue 中使用
  1. <template>
  2.     <div>{{ userStore.name }}</div>
  3. </template>
  4. <script lang="ts" setup>
  5. import { useUserStore } from "@/store/user";
  6. const userStore = useUserStore();
  7. userStore.updateName("张三");
  8. </script>
复制代码
注意, 因为此中用到了 “@”, 所以需在 tsconfig.json 或 tsconfig.app.json 中定义如下:
  1.   "compilerOptions": {
  2.     "baseUrl": ".",
  3.     "paths": {
  4.       "@/*": ["src/*"]
  5.     }
  6.   }
复制代码
集成 vue-router4

  1. pnpm add vue-router
复制代码
新建 src/router 目录并在其下面创建 index.ts,导出 router
  1. import { createRouter, createWebHashHistory } from 'vue-router';
  2. import type { RouteRecordRaw } from 'vue-router';
  3. const routes: Array<RouteRecordRaw> = [
  4.     // {
  5.     //     path: "/login",
  6.     //     name: "Login",
  7.     //     meta: {
  8.     //         title: "登录",
  9.     //         keepAlive: true,
  10.     //         requireAuth: false,
  11.     //     },
  12.     //     component: () => import("@/pages/login.vue"),
  13.     // },
  14.     {
  15.         path: "/aa",
  16.         name: "VueUse",
  17.         meta: {
  18.             title: "鼠标",
  19.             keepAlive: true,
  20.             requireAuth: true,
  21.         },
  22.         component: () => import("@/pages/vueUse.vue"), // 组件的 懒加载
  23.     },
  24.     // {
  25.     //     path: "/hello",
  26.     //     name: "HelloWorld",
  27.     //     meta: {
  28.     //         title: "计数器",
  29.     //         keepAlive: true,
  30.     //         requireAuth: true,
  31.     //     },
  32.     //     component: () => import("@/components/HelloWorld.vue"),
  33.     // },
  34.     // {
  35.     //     path: "/request",
  36.     //     name: "request",
  37.     //     meta: {
  38.     //         title: "请求页",
  39.     //         keepAlive: true,
  40.     //         requireAuth: true,
  41.     //     },
  42.     //     component: () => import("@/pages/request.vue"),
  43.     // }
  44. ];
  45. const router = createRouter({
  46.     history: createWebHashHistory(),
  47.     routes,
  48. });
  49. export default router;
复制代码
在 main.ts 中引入 import router from '@/router'; 并使用
  1. import { createApp } from "vue";
  2. import "./style.css";
  3. import App from "./App.vue";
  4. import store from "./store";
  5. import router from "./router"; // 或 '@/router'
  6. const app = createApp(App); // 创建vue实例
  7. app.use(store).use(router); // 挂载组件
  8. app.mount("#app"); // 挂载实例
复制代码
修改 App.vue
  1. <script setup lang="ts">
  2. import HelloWorld from "./components/HelloWorld.vue";
  3. </script>
  4. <template>
  5.   <h1>abc</h1>
  6.   <div>123</div>
  7.   <HelloWorld msg="def" />
  8.   <RouterView />
  9. </template>
  10. <style>
  11. #app {
  12.   font-family: Avenir, Helvetica, Arial, sans-serif;
  13.   -webkit-font-smoothing: antialiased;
  14.   -moz-osx-font-smoothing: grayscale;
  15.   text-align: center;
  16.   color: #2c3e50;
  17.   margin-top: 60px;
  18. }
  19. </style>
复制代码
集成 vueUse

VueUse 是一个基于 Composition API 的实用函数聚集。
  1. pnpm add @vueuse/core
复制代码
新建 src/pages/vueUse.vue 如下:useMouse 只是 vueuse 的一个最根本的函数库
  1. <template>
  2.   <h1>测试 use 鼠标坐标</h1>
  3.   <h3>Mouse: {{ x }} x {{ y }}</h3>
  4. </template>
  5. <script lang="ts">
  6. import { defineComponent } from "vue";
  7. import { useMouse } from "@vueuse/core";
  8. export default defineComponent({
  9.   name: "VueUse",
  10.   setup() {
  11.     const { x, y } = useMouse();
  12.     return {
  13.       x,
  14.       y,
  15.     };
  16.   },
  17. });
  18. </script>
复制代码
localhost:5173 如下:

localhost:5173/aa 如下:

还有很多,总会有一个适合你;更多函数官方文档
localhost:/5173/hello 如下:

集成 sass

  1. pnpm add -D sass
复制代码
新建 src/assets/styles/variables.scss 文件, 内容如下:
  1. $blue: #007bff;
  2. $red: #dc3545;
  3. $primary-color: red;
  4. $secondary-color: blue;
复制代码
使用在 .vue 文件, 因为上文已在 vite.config.ts 中 @use '/src/assets/styles/variables.scss';, 所以直接使用即可. 例如设置 color 为 red:
  1. <template>
  2.     <h1>测试 use 鼠标坐标</h1>
  3.     <h6 class="c1">{{ x }} x {{ y }}</h6>
  4. </template>
  5. <script lang="ts">
  6. import { defineComponent } from "vue";
  7. import { useMouse } from "@vueuse/core";
  8. export default defineComponent({
  9.   name: "VueUse",
  10.   setup() {
  11.     const { x, y } = useMouse();
  12.     return {
  13.       x,
  14.       y,
  15.     };
  16.   },
  17. });
  18. </script>
  19. <style lang="scss">
  20. .c1 {
  21.     color: blue; // 亲测, red/blue/yellow等不需要自己定义, 默认已经有了. 其实在 vite.config.ts 里已经 @use '/src/assets/styles/variables.scss 了, 可以再其中再定义变量, 如 $blue 等
  22. }
  23. </style>
复制代码
localhost://5173 效果如下:

集成 axios

axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
  1. pnpm add axios
复制代码
新建 src/utils/axios.ts
  1. import axios from 'axios';
  2. import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
  3. const service = axios.create();
  4. // Request interceptors
  5. service.interceptors.request.use(
  6.     (config: InternalAxiosRequestConfig) => {
  7.         // do something
  8.         return config;
  9.     },
  10.     (error: any) => {
  11.         return Promise.reject(error);
  12.     }
  13. );
  14. // Response interceptors
  15. service.interceptors.response.use(
  16.     async (response: AxiosResponse) => {
  17.         console.log("response", response);
  18.         // do something
  19.         return response;
  20.     },
  21.     (error: any) => {
  22.         // do something
  23.         return Promise.reject(error);
  24.     }
  25. );
  26. export default service;
复制代码
在页面中使用即可, 此中涉及 await 调接口, setup() 的机会调用, 调用后赋值给变量, 并在 <template> 内渲染, 完整 vue 代码和关键步骤如下:

  1. <template>
  2.     <h1>测试 use 鼠标坐标</h1>
  3.     <h2 v-if="d">接口响应结果为 {{ d }}</h2>
  4.     <h3 class="c1">Mouse: {{ x }} x {{ y }}</h3>
  5. </template>
  6. <script lang="ts">
  7. import { defineComponent, ref } from "vue";
  8. import { useMouse } from "@vueuse/core"; // 安装后该引用将正常解析
  9. import request from '@/utils/axios';
  10. export default defineComponent({
  11.     name: "VueUse",
  12.     setup() {
  13.         const d = ref<any>(null); // 定义响应式变量, 用于存接口返回的数据
  14.         const requestRes = async () => {// 定义函数, 其调接口
  15.             try {
  16.                 const result = await request({
  17.                     url: '/api/abc-server/ping',
  18.                     method: 'get'
  19.                 })
  20.                 console.log(result);
  21.                 d.value = result.data; // 把接口响应的数据, 赋值给, 响应式变量
  22.             } catch (error) {
  23.                 console.error("请求失败", error);
  24.             }
  25.         }
  26.         requestRes(); // 组件初始化时, 调函数
  27.         const { x, y } = useMouse(); // 将正确获取鼠标坐标
  28.         return { x, y, d }; // 所有组件内的 变量, 都要 return 出去, template 才能使用
  29.     },
  30. });
  31. </script>
  32. <style lang="scss">
  33. @import '@/assets/styles/variables.scss';
  34. .c1 {
  35.     color: blue;
  36. }
  37. </style>
复制代码
其对应的 vite.config.ts 的关键配置如下, 具体内容上文有先容.

用 go 写个 http server 如下:
  1. func main() {
  2.         http.HandleFunc("/ping", Ping)
  3.         http.ListenAndServe(":9999", nil)
  4. }
  5. func Ping(w http.ResponseWriter, req *http.Request) {
  6.         fmt.Fprintf(w, "pong\n")
  7. }
  8. // curl --location --request GET 'http://localhost:9999/ping'
  9. pong
复制代码
封装哀求参数和相应数据的所有 api

新建 src/api/index.ts:
  1. import * as login from './module/login';
  2. import * as ping from './module/ping';
  3. export default Object.assign({}, login, ping);
  4. /*
  5. 其中 `import * as` 将 ./module/login 模块中的所有导出内容, 作为一个命名空间对象导入。
  6. 例如,如果 login.ts 导出了 loginUser 和 logoutUser 两个函数,那么 login 对象将包含这两个函数:
  7. login = {
  8.   loginUser: Function,
  9.   logoutUser: Function,
  10. };
  11. */
  12. /*
  13. Object.assign 是 JavaScript 的方法,用于将一个或多个对象的属性合并到目标对象中。
  14. 第一个参数是目标对象(这里是空对象 {}),后面的参数是源对象(这里是 login 和 index)。
  15. 合并后,目标对象将包含 login 和 index 的所有属性。
  16. 例如若
  17. login = {
  18.   loginUser: Function,
  19.   logoutUser: Function,
  20. };
  21. index = {
  22.   getData: Function,
  23.   setData: Function,
  24. };
  25. 则合并后的对象为
  26. {
  27.   loginUser: Function,
  28.   logoutUser: Function,
  29.   getData: Function,
  30.   setData: Function,
  31. }
  32. 属性冲突的情况:
  33. 其中, 如果 login 和 index 中有同名属性,Object.assign 会以后面的对象为准。例如:
  34. login = { foo: 'login' };
  35. index = { foo: 'index' };
  36. 则 Object.assign({}, login, index); // { foo: 'index' }
  37. 浅拷贝:
  38. Object.assign 是浅拷贝,如果属性值是对象,拷贝的是引用而不是值。
  39. */
  40. /*
  41. export default 将 Object.assign({}, login, index) 的结果作为默认导出。
  42. 其他模块可以通过 import combined from './path/to/module'; 导入这个合并后的对象
  43. */
复制代码
新建 src/api/module/login.ts
  1. import request from '@/utils/axios';
  2. // 登录
  3. // model
  4. interface IResponseType<P = {}> {
  5.     code?: number;
  6.     status: number;
  7.     msg: string;
  8.     data: P;
  9. }
  10. interface ILogin {
  11.     token: string;
  12.     expires: number;
  13. }
  14. // function
  15. export const login = (username: string, password: string) => {
  16.     return request<IResponseType<ILogin>>({
  17.         url: '/api/auth/login',
  18.         method: 'post',
  19.         data: {
  20.             username,
  21.             password
  22.         }
  23.     });
  24. };
复制代码
新建 src/api/module/ping.ts
  1. import request from '@/utils/axios';
  2. // function
  3. export const ping = () => {
  4.     return request<String>({
  5.         url: '/api/auth/login',
  6.         method: 'get'
  7.     });
  8. };
复制代码
由于使用了 typescript,所以需新增 src/types/shims-axios.d.ts
  1. import { AxiosRequestConfig } from 'axios';
  2. // 自定义扩展axios模块
  3. declare module 'axios' {
  4.     export interface AxiosInstance {
  5.         <T = any>(config: AxiosRequestConfig): Promise<T>;
  6.         request<T = any>(config: AxiosRequestConfig): Promise<T>;
  7.         get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
  8.         delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
  9.         head<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
  10.         post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
  11.         put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
  12.         patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
  13.     }
  14. }
复制代码
新建 src/pages/request.vue 页面,并在此中使用
  1. <template>
  2.     <h2>这是 request 请求页</h2>
  3.     <br />
  4.     <router-link to="/">点击跳转至首页</router-link>
  5.     <button @click="requestRes()"></button>
  6. </template>
  7. <script lang="ts">
  8. import { defineComponent } from 'vue';
  9. import request from '@/utils/axios';
  10. import API from "@/api";
  11. export default defineComponent({
  12.     name: "RequestPage",
  13.     setup() {
  14.         const requestRes = async () => {
  15.             let result = await request({
  16.                 url: "/api/corgi/ping",
  17.                 method: "get,"
  18.             })
  19.             console.log(result);
  20.         }
  21.         const requestResAPI = async () => {
  22.             let result = await API.login("zhangsan", "123456");
  23.             console.log(result);
  24.         }
  25.         return {
  26.             requestRes,
  27.             requestResAPI
  28.         }
  29.     }
  30. })
  31. </script>
复制代码
效果如下:

引入 antdvue

AntdVue,引入方式参考官网
  1. pnpm add ant-design-vue
  2. pnpm add vite-plugin-style-import --save-dev # 非必须
  3. pnpm add vue-svg-icon
复制代码
在 main.ts 全局完整引入如下:
  1. import { createApp } from 'vue'
  2. import './style.css'
  3. import "ant-design-vue/dist/reset.css";
  4. import Antd from 'ant-design-vue';
  5. import App from './App.vue'
  6. import store from './store'
  7. import router from './router'
  8. createApp(App).use(router).use(store).use(Antd).mount('#app')
复制代码
需要注意的是,样式文件需要单独引入。
使用 antdvue

新建 test.vue,此中用到了 <a-button> 标签
  1. <template>
  2.     <h2>测试</h2>
  3.     <a-button type="primary">添加用户</a-button>
  4. </template>
  5. <script lang="ts">
  6. import { defineComponent } from "vue";
  7. import request from "@/utils/axios";
  8. import API from "@/api";
  9. export default defineComponent({
  10.     name: "RequestPage",
  11.     setup() {
  12.         const requestRes = async () => {
  13.             let result = await request({
  14.                 url: "/api/corgi/ping",
  15.                 method: "get",
  16.             });
  17.             console.log(result);
  18.         };
  19.         const requestResAPI = async () => {
  20.             let result = await API.login("zhangsan", "123456");
  21.             console.log(result);
  22.         };
  23.         return {
  24.             requestRes,
  25.             requestResAPI,
  26.         };
  27.     },
  28. });
  29. </script>
复制代码
效果如下,说明跑通了

二、vue 用法

2.1 动画

2.1.1 原生 css 动画

transition

  1. <template>
  2.   <div class="box" :style="{ width: width + 'px' }"></div>
  3.   <button @click="change">click</button>
  4. </template>
  5. <script lang="ts" setup>
  6. import { ref } from "vue";
  7. let width = ref(100);
  8. function change() {
  9.   width.value += 100;
  10. }
  11. </script>
  12. <style>
  13. .box {
  14.   background: red;
  15.   height: 100px;
  16. }
  17. </style>
复制代码
效果如下:


为了优化效果,可以把样式改为如下,即 width 属性需要线性过度,时间为 1s:
  1. <style>
  2. .box {
  3.   background: red;
  4.   height: 100px;
  5.   transition: width 1s linear;
  6. }
  7. </style>
复制代码
效果如下:
![](https://img-blog.csdnimg.cn/1c31481b245640d19c22cdeb9dac63e1.gif =300)
animation

  1. <template>
  2.   <div class="box" :style="{ width: width + 'px' }"></div>
  3.   <button @click="change">click</button>
  4. </template>
  5. <script lang="ts" setup>
  6. import { ref } from "vue";
  7. let width = ref(30);
  8. function change() {
  9.   width.value += 100;
  10. }
  11. </script>
  12. <style>
  13. .box {
  14.   width: 30px;
  15.   height: 30px;
  16.   position: relative;
  17.   background: #d88986;
  18.   animation: move 2s linear infinite; /*持续 2s,线性变化,无限循环*/
  19.   /*
  20.   move:指定动画的名称,对应 @keyframes move 定义的动画。
  21. 2s:动画的持续时间为 2 秒。
  22. linear:动画的时间函数为线性(匀速)。
  23. infinite:动画无限循环。
  24. */
  25. }
  26. /* 定制动画在0%,50%,100%的位置 */
  27. @keyframes move {
  28.   0% {
  29.     left: 0px;
  30.   }
  31.   50% {
  32.     left: 200px;
  33.   }
  34.   100% {
  35.     left: 0;
  36.   }
  37. }
  38. /*
  39. @keyframes 定义了动画的关键帧,具体含义如下:
  40. 0%:动画开始时,元素的 left 值为 0px。
  41. 50%:动画进行到一半时,元素的 left 值为 200px。
  42. 100%:动画结束时,元素的 left 值回到 0px。
  43. 3. 动画效果
  44. 元素 .box 会从初始位置(left: 0px)向右移动到 left: 200px,然后再回到初始位置(left: 0px)。
  45. 整个动画持续 2 秒,匀速运动,并且无限循环。
  46. 4. 代码的完整行为
  47. 初始状态:
  48. .box 的宽度为 30px,高度为 30px,背景色为 #d49d9b。
  49. 动画开始前,元素位于 left: 0px。
  50. 动画过程:
  51. 在 0% 时,元素位于 left: 0px。
  52. 在 50% 时(1 秒后),元素移动到 left: 200px。
  53. 在 100% 时(2 秒后),元素回到 left: 0px。
  54. 循环:
  55. 动画完成后,重新开始,无限循环。
  56. 按钮点击:
  57. 点击按钮时,.box 的宽度会增加 100px,但动画效果不受影响,继续按照 @keyframes 的定义运行。
  58. 5. 可视化效果
  59. 你会看到一个宽度为 30px 的方块,在水平方向上左右移动:
  60. 从最左侧(left: 0px)向右移动到 left: 200px,然后再回到最左侧。
  61. 每次点击按钮,方块的宽度会增加 100px,但动画的移动范围(left: 0px 到 left: 200px)不变。
  62. */
  63. </style>
复制代码
效果如下:

2.1.2 vue 动画

  1. <template>
  2.     <button @click="toggle">click</button>
  3.     <transition name="fade">
  4.         <h1 v-if="showTitle">你好</h1>
  5.     </transition>
  6. </template>
  7. <script lang="ts" setup>
  8. import { ref } from "vue";
  9. let showTitle = ref(true);
  10. function toggle() {
  11.     showTitle.value = !showTitle.value;
  12. }
  13. </script>
  14. <style>
  15. .fade-enter-active,
  16. .fade-leave-active {
  17.     transition: opacity 0.5s linear;
  18. }
  19. .fade-enter-from,
  20. .fade-leave-to {
  21.     opacity: 0;
  22. }
  23. </style>
复制代码
vue 的 transition 约定如下:

效果如下:



2.2 jsx

简单示例

定义 Heading.jsx 如下:
  1. import { defineComponent, h } from "vue";
  2. export default defineComponent({
  3.   props: {
  4.     level: {
  5.       type: Number,
  6.       required: true,
  7.     },
  8.   },
  9.   setup(props, { slots }) {
  10.     return () =>
  11.       h(
  12.         "h" + props.level, // 标签名
  13.         {}, // prop 或 attribute
  14.         slots.default() // 子节点
  15.       );
  16.   },
  17. });
复制代码
在 about.vue 中使用,如下:
  1. <template>
  2.   <Heading :level="1">hello xy</Heading>
  3. </template>
  4. <script lang="ts" setup>
  5. import Heading from "@/components/Heading.jsx";
  6. </script>
复制代码
当 level 传 1时,效果如下:

当 level 传 6 时,效果如下:

通过 pnpm add @vitejs/plugin-vue-jsx -D 可安装 jsx 插件。
在 vite.config.ts 中配置如下:
  1. import vueJsx from '@vitejs/plugin-vue-jsx';
  2. export default defineConfig({
  3.   plugins: [vue(), viteMockServe(), vueJsx()],
  4. });
复制代码
修改 Heading.jsx 如下:
  1. import { defineComponent, h } from "vue";
  2. export default defineComponent({
  3.   props: {
  4.     level: {
  5.       type: Number,
  6.       required: true,
  7.     },
  8.   },
  9.   setup(props, { slots }) {
  10.     const tag = "h" + props.level;
  11.     return () => <tag>{slots.default()}</tag>;
  12.     // return () =>
  13.     //   h(
  14.     //     "h" + props.level, // 标签名
  15.     //     {}, // prop 或 attribute
  16.     //     slots.default() // 子节点
  17.     //   );
  18.   },
  19. });
复制代码
todo.jsx 示例

todo.jsx 如下:
  1. import { defineComponent, ref } from "vue";
  2. export default defineComponent({
  3.   setup(props) {
  4.     let title = ref("");
  5.     let todos = ref([
  6.       { title: "pc", done: true },
  7.       { title: "android", done: false },
  8.     ]);
  9.     function addTodo() {
  10.       todos.value.push({ title: title.value });
  11.       title.value = "";
  12.     }
  13.     return () => (
  14.       <div>
  15.         <input type="text" vModel={title.value} />
  16.         <button onClick={addTodo}>click</button>
  17.         <ul>
  18.           {todos.value.length ? (
  19.             todos.value.map((todo) => {
  20.               return <li>{todo.title}</li>;
  21.             })
  22.           ) : (
  23.             <li>no data</li>
  24.           )}
  25.         </ul>
  26.       </div>
  27.     );
  28.   },
  29. });
复制代码
效果如下:

三、AntdVue 组件库使用

业务开辟,重要就是用UI组件库(如antd vue)了,可以在 github 搜项目,例如vue3-antd-admin 和 vue-antd-admin,学习别人的组织思路。
3.1 弹窗

弹窗通常用 a-model 组件
父组件:
  1. <script setup lang="ts">
  2. import { ref } from 'vue';
  3. const visible = ref(false);
  4. <template>
  5.   <div>
  6.     <a-button type="primary" class="btn-upload" ghost @click="handleCreate">
  7.       <template #icon>
  8.         <svg-icon class="icon" icon="icon-upload" />
  9.       </template>
  10.       打开弹窗
  11.     </a-button>
  12.         <!-- 子组件。向子组件传参为 model,接收子组件的 @save 事件 -->
  13.     <form-user
  14.       v-model:visible="visible"
  15.       :model="currentProcedure"
  16.       @save="handleSave"
  17.     />
  18.   </div>
  19. </template>
复制代码
子组件:
  1. <script setup lang="ts">
  2. import { User } from '@/models/user';
  3. import { antdModal } from '@/utils/antd';
  4. import { reactive, ref, watch } from 'vue';
  5. import rules from './rules';
  6. // 数据
  7. export type FormStateModel = User;
  8. // 父组件传来的 prop
  9. const props = defineProps<{
  10.   model?: FormStateModel;
  11.   visible: boolean;
  12. }>();
  13. // 向父组件发送的 emit
  14. const emits = defineEmits<{
  15.   (e: 'save', model: FormStateModel): void;
  16.   (e: 'update:visible', visible: boolean): void;
  17. }>();
  18. // 将数据变为响应式
  19. const formState = reactive<FormStateModel>({
  20.   userName: '',
  21.   password: '',
  22. });
  23. // 用于二次确认的业务逻辑。将数据转为JSON字符串。当点击“取消“按钮时,将“现在的数据“和“刚打开弹窗时的数据”做对比,若有变动则需二次确认
  24. let initModelJSON = '';
  25. // 当 props.visible 时,调用 init 函数
  26. watch(() => props.visible, init);
  27. function init() {
  28.   // 组件初始化时,得到 initModelJSON 的原始值
  29.   const model = props.model;
  30.   if (!model) { // 若为“新增”业务,则父组件未传入 props.model,则先手动赋值为 ‘’ 空字符串做保护,再序列化
  31.     (formState as User).userName = '';
  32.     formState.password = '';
  33.     initModelJSON = JSON.stringify(formState);
  34.     return;
  35.   }
  36.   initModelJSON = JSON.stringify(formState); // 若为“编辑”业务,则父组件传入了 props.model,直接序列化即可
  37. }
  38. // 组件的引用:下文的 <a-form ref="formRef"/>
  39. const formRef = ref();
  40. async function handleOk() {
  41.   try {
  42.     await formRef.value.validate(); // a-form 官方提供了 validate() 方法
  43.     emits('save', normalizeModel()); // 向父组件emit save函数,参数为 normalizeModel()
  44.   } catch (e) {
  45.     console.error(e);
  46.   }
  47. }
  48. async function handleCancel() {
  49.   if (
  50.     // 点击取消按钮时,对比新旧值
  51.     JSON.stringify(formState) !== initModelJSON &&
  52.     !(await antdModal('尚未保存,确定取消?'))
  53.   ) {
  54.     emits('update:visible', true); // 当点击取消时, 向父组件 emit visible = true,让子组件(即本弹窗组件)可见
  55.     return;
  56.   }
  57.   emits('update:visible', false); // 当点击取消时, 向父组件 emit visible = false,让子组件(即本弹窗组件)不可见
  58. }
  59. // 将 normalizeModel 暴露给父组件,因为本组件在 <script setup> 中的变量默认是不被父组件可见的
  60. defineExpose({ normalizeModel });
  61. function normalizeModel() {
  62.   return { ...formState };
  63. }
  64. </script>
  65. <template>
  66.   <!-- v-bind="$attrs" 将调用组件时的组件标签上绑定的非props的特性(class和style除外)向下传递。在子组件中应当添加inheritAttrs: false(避免父作用域的不被认作props的特性绑定应用在子组件的根元素上)。-->
  67.   <a-modal
  68.     v-bind="$attrs"
  69.     :title="(formState as Procedure).id ? '编辑用户' : '添加用户'"
  70.     :visible="visible"
  71.     @ok="handleOk"
  72.     @cancel="handleCancel"
  73.   >
  74.     <a-form ref="formRef" class="form-device" :rules="rules" :model="formState">
  75.       <a-form-item label="用户账号" class="form-item" name="name">
  76.         <a-input v-model:value="formState.name" placeholder="请输入名称" />
  77.       </a-form-item>
  78.     </a-form>
  79.   </a-modal>
  80. </template>
  81. <style lang="scss" scoped>
  82. @import '~/css/variables';
  83. </style>
复制代码


  • 资料

    • Vue 原理视频教程
    • 入门项目实战源码
    • 完整网站vue3+vite+antd-vue,用于模仿


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

麻花痒

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表