创建vue3项目一

发布时间:2026/6/27 23:36:35
创建vue3项目一 第一步执行创建命令npm create vuelatest第二步回答交互式选项提示内容推荐选择说明Project name:输入项目名如my-vue-app默认是vue-project可以改Add TypeScript?Yes必须选现在 Vue 3 标配Add JSX Support?No除非你要写 React 风格一般不用Add Vue Router for Single Page Application development?Yes路由管理必选Add Pinia for state management?Yes状态管理必选Add Vitest for unit testing?No暂时不选先跑起来Add an end-to-end testing solution?No暂时不选Add ESLint for code quality?Yes代码规范推荐选Add Prettier for code formatting?Yes代码格式化推荐选这个“包名称”Package Name是你在创建项目时脚手架工具npm create vuelatest问你的第二个问题。它对应的是项目根目录下package.json文件里的name字段 “包名称”是做什么的简单来说它就是你项目的“身份证名字”主要用在以下几个地方用途说明项目唯一标识在 npm 生态中这个名称用于唯一标识你的项目。如果你将来要把项目发布到 npm 仓库作为公共包或私有包这个名字必须全局唯一。依赖引用如果你的项目被其他项目引用比如你写了一个 UI 组件库别人就是通过这个名称来npm install your-package-name的。版本管理配合package.json中的version字段用于标识项目的不同版本。项目显示名在 VS Code 的 npm 脚本面板、或者 CI/CD 构建日志中会显示这个名字来区分不同项目。✍️ 怎么填有什么规则你不需要想得太复杂随便填一个英文名就行只要满足 npm 的命名规则只能用小写字母、数字、连字符-和下划线_不能以点.或下划线_开头长度不超过 214 个字符不能有空格或特殊字符如!、、#等⚡️ 选项一使用 Oxfmt 替代 Prettier这关乎你项目的代码格式化工具。它们是什么Prettier一个非常流行的、市场占有率极高的代码格式化工具。Oxfmt一个由Rust语言编写的新一代代码格式化工具。为什么要替换速度极快Oxfmt 的性能是 Prettier 的30到45倍。处理大型项目时格式化几乎可以瞬间完成。无缝衔接Oxfmt 已经通过了 Prettier100%的兼容性测试这意味着切换后代码格式几乎不会有任何变化。建议对于新项目强烈推荐勾选此项。它能帮你省下大量等待格式化完成的时间。 选项二Vue 3.6测试版这关乎你的Vue框架核心版本。它是什么Vue 3.6 的测试版Beta。它包含了实验性的新特性不适合用于生产环境。有什么新东西Vapor Mode蒸汽模式一个去除虚拟DOM的全新编译模式。它能让应用的首屏渲染速度提升近3倍内存占用减少40%以上。Alien Signals一个全新的、性能更强的响应式系统核心。建议对于你的通用后台管理系统项目强烈不推荐勾选此项。测试版可能存在未知Bug或API变动稳定性无法保证。等Vue 3.6正式发布后再考虑升级也不迟。第三步安装依赖并启动安装 pnpmnpm install -g pnpm使用 pnpmpnpm install第四步安装网络请求库对接后端 APIpnpm add axios第五步可选安装 Tailwind CSSpnpm add -D tailwindcss postcss autoprefixer第六步安装ant-designpnpm add ant-design-vue在页面中使用 Ant Design 组件打开src/App.vue替换成下面的示例代码看看 antd 是否正常工作template div stylepadding: 40px; h1UniAdmin 后台管理/h1 a-button typeprimary我是 Ant Design 的按钮/a-button a-button typedefault普通按钮/a-button a-button typedashed虚线按钮/a-button /div /template script setup langts // 不需要手动 import 组件因为我们在 main.ts 里全局注册了 /script style scoped /style按需导入更优方案1. 安装按需导入插件pnpm add -D unplugin-vue-components2. 修改vite.config.tsimport { defineConfig } from vite import vue from vitejs/plugin-vue import Components from unplugin-vue-components/vite import { AntDesignVueResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ vue(), Components({ resolvers: [ AntDesignVueResolver({ importStyle: less, // 如果你用了 less }), ], }), ], })3. 修改main.tsimport { createApp } from vue import { createPinia } from pinia import App from ./App.vue import router from ./router // 只引入基础样式 import ant-design-vue/dist/reset.css const app createApp(App) app.use(createPinia()) app.use(router) // 注意不要 app.use(Antd) 了 app.mount(#app) 遇到的错误日志里有一行关键信息[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: core-js3.49.0Run pnpm approve-builds to pick which dependencies should be allowed to run scripts.core-js是一个 JavaScript 标准库的 polyfill它在安装后需要执行一个脚本来完成构建。pnpm 出于安全考虑默认不运行任何依赖的安装后脚本导致这个步骤被跳过了从而让 Vite 或其他依赖认为环境不完整启动失败。解决方案执行 pnpm 提示的命令批准core-js运行安装后脚本pnpm approve-builds执行后终端会列出所有需要运行脚本的依赖用方向键上下移动按空格键选中core-js然后按回车确认。完成后再重新启动项目pnpm dev 第七步配置路由和基本布局先把“架子”搭好创建 MainLayout.vue在src/layouts/目录下新建MainLayout.vue文件template a-layout stylemin-height: 100vh !-- 侧边栏 -- a-layout-sider v-model:collapsedcollapsed :triggernull collapsible themedark width240 !-- Logo 区域 -- div classlogo span v-if!collapsedUniAdmin/span span v-elseUA/span /div !-- 侧边菜单 -- a-menu themedark modeinline v-model:selectedKeysselectedKeys clickhandleMenuClick a-menu-item key/dashboard dashboard-outlined / span仪表盘/span /a-menu-item a-sub-menu keysystem template #title setting-outlined / span系统管理/span /template a-menu-item key/system/user用户管理/a-menu-item a-menu-item key/system/role角色管理/a-menu-item a-menu-item key/system/menu菜单管理/a-menu-item a-menu-item key/system/settings系统设置/a-menu-item /a-sub-menu a-sub-menu keybusiness template #title shop-outlined / span业务管理/span /template a-menu-item key/business/order订单管理/a-menu-item a-menu-item key/business/product商品管理/a-menu-item /a-sub-menu a-menu-item key/attendance clock-circle-outlined / span打卡管理/span /a-menu-item a-menu-item key/message message-outlined / span站内信/span /a-menu-item a-menu-item key/screen area-chart-outlined / span数据大屏/span /a-menu-item /a-menu /a-layout-sider !-- 右侧内容区 -- a-layout !-- 顶部导航 -- a-layout-header stylebackground: #fff; padding: 0 24px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 1px 4px rgba(0,21,41,.08); div styledisplay: flex; align-items: center; gap: 16px; !-- 折叠按钮 -- menu-unfold-outlined v-ifcollapsed classtrigger clicktoggleCollapsed / menu-fold-outlined v-else classtrigger clicktoggleCollapsed / a-breadcrumb a-breadcrumb-item首页/a-breadcrumb-item a-breadcrumb-item v-ifcurrentBreadcrumb{{ currentBreadcrumb }}/a-breadcrumb-item /a-breadcrumb /div div styledisplay: flex; align-items: center; gap: 16px; !-- 通知 -- a-badge :count5 bell-outlined stylefont-size: 18px; cursor: pointer; / /a-badge !-- 用户信息 -- a-dropdown div stylecursor: pointer; display: flex; align-items: center; gap: 8px; a-avatar sizesmall stylebackground-color: #1677ff; {{ userInitials }} /a-avatar span管理员/span /div template #overlay a-menu a-menu-item keyprofile个人中心/a-menu-item a-menu-divider / a-menu-item keylogout stylecolor: #ff4d4f;退出登录/a-menu-item /a-menu /template /a-dropdown /div /a-layout-header !-- 内容区域 -- a-layout-content stylemargin: 24px; padding: 24px; background: #fff; min-height: 280px; router-view / /a-layout-content !-- 页脚 -- a-layout-footer styletext-align: center; background: #fff; border-top: 1px solid #f0f0f0; UniAdmin ©2026 Created by You /a-layout-footer /a-layout /a-layout /template script setup langts import { ref, computed } from vue import { useRouter, useRoute } from vue-router import { DashboardOutlined, SettingOutlined, ShopOutlined, ClockCircleOutlined, MessageOutlined, AreaChartOutlined, MenuUnfoldOutlined, MenuFoldOutlined, BellOutlined, } from ant-design/icons-vue const router useRouter() const route useRoute() // 侧边栏折叠状态 const collapsed refboolean(false) // 当前选中的菜单 key const selectedKeys refstring[]([route.path]) // 当前面包屑 const currentBreadcrumb computed(() { const menuMap: Recordstring, string { /dashboard: 仪表盘, /system/user: 用户管理, /system/role: 角色管理, /system/menu: 菜单管理, /system/settings: 系统设置, /business/order: 订单管理, /business/product: 商品管理, /attendance: 打卡管理, /message: 站内信, /screen: 数据大屏, } return menuMap[route.path] || }) // 用户头像首字母 const userInitials computed(() 管.charAt(0)) // 切换折叠状态 const toggleCollapsed () { collapsed.value !collapsed.value } // 菜单点击跳转 const handleMenuClick ({ key }: { key: string }) { router.push(key) } /script style scoped .logo { height: 64px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 20px; font-weight: bold; background: rgba(255, 255, 255, 0.1); margin: 16px 16px 8px 16px; border-radius: 8px; letter-spacing: 1px; } .trigger { font-size: 18px; cursor: pointer; transition: color 0.3s; } .trigger:hover { color: #1677ff; } /* 暗色侧边栏的菜单项 hover 效果优化 */ :deep(.ant-menu-dark .ant-menu-item:hover) { background: rgba(255, 255, 255, 0.08); } :deep(.ant-menu-dark .ant-menu-item-selected) { background: #1677ff !important; } :deep(.ant-layout-sider-trigger) { background: #001529; } /style修改路由配置编辑src/router/index.ts把刚才定义的页面路由挂到布局下面import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: /, name: layout, component: () import(/layouts/MainLayout.vue), redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: () import(/views/dashboard/index.vue), }, { path: system/user, name: User, component: () import(/views/system/user.vue), }, { path: system/role, name: Role, component: () import(/views/system/role.vue), }, { path: system/menu, name: Menu, component: () import(/views/system/menu.vue), }, { path: system/settings, name: Settings, component: () import(/views/system/settings.vue), }, { path: business/order, name: Order, component: () import(/views/business/order.vue), }, { path: business/product, name: Product, component: () import(/views/business/product.vue), }, { path: attendance, name: Attendance, component: () import(/views/attendance/index.vue), }, { path: message, name: Message, component: () import(/views/message/index.vue), }, { path: screen, name: Screen, component: () import(/views/screen/index.vue), }, ], }, { path: /login, name: Login, component: () import(/views/login/index.vue), }, { path: /404, name: NotFound, component: () import(/views/NotFound.vue), }, { path: /:pathMatch(.*)*, redirect: /404, }, ], }) export default router创建占位页面为了让布局不报错需要创建对应的页面文件先写最简单的占位内容src/views/dashboard/index.vuetemplateh1 仪表盘/h1p欢迎来到 UniAdmin 后台管理系统/p/templatesrc/views/system/user.vuetemplateh1 用户管理/h1p用户列表将在这里展示/p/template依此类推其他页面先放同样的占位内容后面再逐步完善。安装图标库你可能需要安装 Ant Design Vue 的图标库如果还没有装的话pnpm add ant-design/icons-vue当前项目结构src/├── layouts/│ └── MainLayout.vue # ✅ 已创建├── views/│ ├── dashboard/│ │ └── index.vue # ✅ 已创建│ ├── system/│ │ ├── user.vue│ │ ├── role.vue│ │ ├── menu.vue│ │ └── settings.vue│ ├── business/│ │ ├── order.vue│ │ └── product.vue│ ├── attendance/│ │ └── index.vue│ ├── message/│ │ └── index.vue│ ├── screen/│ │ └── index.vue│ └── NotFound.vue└── router/└── index.ts # ✅ 已更新打开src/App.vue看看是不是这样的template router-view / /template script setup langts // 空的内容 /script还有一个容易被忽略的问题路径别名你的路由配置里用了/layouts/MainLayout.vue这个是 Vite 的路径别名默认指向src/。如果 Vite 没有正确识别会导致页面加载失败。检查一下项目根目录的vite.config.ts确保有这段配置import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src), }, }, })如果vite.config.ts里没有resolve.alias配置补上即可。创建登录页面在src/views/login/index.vue中写入以下代码template div classlogin-container a-card classlogin-card :borderedfalse h1 styletext-align:center; margin-bottom: 24px;UniAdmin 后台/h1 a-form :modelformState namelogin finishonFinish finishFailedonFinishFailed a-form-item label用户名 nameusername :rules[{ required: true, message: 请输入用户名! }] a-input v-model:valueformState.username placeholderadmin / /a-form-item a-form-item label密码 namepassword :rules[{ required: true, message: 请输入密码! }] a-input-password v-model:valueformState.password placeholderadmin123 / /a-form-item a-form-item a-button typeprimary html-typesubmit block登录/a-button /a-form-item /a-form /a-card /div /template script setup langts import { reactive } from vue import { useRouter } from vue-router import { message } from ant-design-vue const router useRouter() const formState reactive({ username: admin, password: admin123, }) // 模拟登录接口 const mockLogin (username: string, password: string): Promiseboolean { return new Promise((resolve) { setTimeout(() { // 写死的账号密码admin / admin123 if (username admin password admin123) { resolve(true) } else { resolve(false) } }, 500) }) } const onFinish async (values: any) { const success await mockLogin(values.username, values.password) if (success) { message.success(登录成功) // 模拟存储 token真实项目会存后端返回的 token localStorage.setItem(token, fake-token) // 跳转到仪表盘 router.push(/dashboard) } else { message.error(用户名或密码错误) } } const onFinishFailed (errorInfo: any) { console.log(登录失败:, errorInfo) } /script style scoped .login-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f0f2f5; } .login-card { width: 400px; padding: 24px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border-radius: 8px; } /style添加路由守卫防止未登录访问后台打开src/router/index.ts在createRouter之后添加一个全局前置守卫// ... 原有的 import 和 routes 配置 ... const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes, }) // ✅ 添加路由守卫 router.beforeEach((to, from, next) { // 检查本地是否有 token const token localStorage.getItem(token) // 如果访问的是登录页 if (to.path /login) { // 如果已经登录跳转到仪表盘否则留在登录页 if (token) { next(/dashboard) } else { next() } return } // 如果访问的不是登录页且没有 token跳转到登录页 if (!token) { next(/login) } else { next() } }) export default router好系统设置是一个典型的CRUD 模块涵盖了列表展示、新增、编辑、删除、分页、搜索这些核心功能。做完这个模块你就能掌握后台开发的80% 通用模式。第一步创建文件结构src/├── api/│ └── settings.ts # API 请求封装├── views/│ └── system/│ └── settings.vue # 系统设置页面└── types/└── settings.ts # TypeScript 类型定义第三步编写 TypeScript 类型定义export interface SettingItem { id: string key: string value: string description: string category: basic | email | security status: boolean createdAt: string updatedAt: string } export interface SettingQueryParams { page: number pageSize: number keyword?: string category?: string }第四步封装 API 请求创建src/api/settings.ts先用Mock 数据模拟后面再对接真实后端import type { SettingItem, SettingQueryParams } from /types/settings // 模拟数据 const mockSettings: SettingItem[] [ { id: 1, key: site_name, value: UniAdmin, description: 网站名称, category: basic, status: true, createdAt: 2026-01-01 10:00:00, updatedAt: 2026-06-25 14:30:00, }, { id: 2, key: site_logo, value: /logo.png, description: 网站Logo路径, category: basic, status: true, createdAt: 2026-01-01 10:00:00, updatedAt: 2026-06-20 09:15:00, }, { id: 3, key: smtp_host, value: smtp.example.com, description: SMTP服务器地址, category: email, status: true, createdAt: 2026-01-15 08:30:00, updatedAt: 2026-06-10 16:20:00, }, { id: 4, key: smtp_port, value: 465, description: SMTP端口, category: email, status: true, createdAt: 2026-01-15 08:30:00, updatedAt: 2026-06-10 16:20:00, }, { id: 5, key: jwt_secret, value: xxxxxxxxxxxxx, description: JWT加密密钥, category: security, status: false, createdAt: 2026-02-01 12:00:00, updatedAt: 2026-06-01 10:00:00, }, ] // 模拟延迟 const delay (ms: number 500) new Promise(resolve setTimeout(resolve, ms)) // 获取设置列表 export const getSettingList async (params: SettingQueryParams) { await delay() let list [...mockSettings] // 搜索过滤 if (params.keyword) { list list.filter(item item.key.includes(params.keyword!) || item.description.includes(params.keyword!) ) } // 分类过滤 if (params.category) { list list.filter(item item.category params.category) } const total list.length const start (params.page - 1) * params.pageSize const end start params.pageSize const data list.slice(start, end) return { code: 0, data: { list: data, total, page: params.page, pageSize: params.pageSize, }, message: success, } } // 新增设置 export const createSetting async (data: OmitSettingItem, id | createdAt | updatedAt) { await delay() const newItem: SettingItem { ...data, id: String(Date.now()), createdAt: new Date().toISOString().replace(T, ).slice(0, 19), updatedAt: new Date().toISOString().replace(T, ).slice(0, 19), } mockSettings.unshift(newItem) return { code: 0, data: newItem, message: 创建成功, } } // 更新设置 export const updateSetting async (id: string, data: PartialSettingItem) { await delay() const index mockSettings.findIndex(item item.id id) if (index -1) { return { code: 404, data: null, message: 设置项不存在, } } mockSettings[index] { ...mockSettings[index], ...data, updatedAt: new Date().toISOString().replace(T, ).slice(0, 19), } return { code: 0, data: mockSettings[index], message: 更新成功, } } // 删除设置 export const deleteSetting async (id: string) { await delay() const index mockSettings.findIndex(item item.id id) if (index -1) { return { code: 404, data: null, message: 设置项不存在, } } mockSettings.splice(index, 1) return { code: 0, data: null, message: 删除成功, } }