基于vue3+vite实现前端工程化

1.vue3核心语法

1.两种语法

1.1.选项式API

使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 datamethods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

<script>
export default {
  // data() 返回的属性将会成为响应式的状态
  // 并且暴露在 `this` 上
  data() {
    return {
      count: 0
    }
  },

  // methods 是一些用来更改状态与触发更新的函数
  // 它们可以在模板中作为事件处理器绑定
  methods: {
    increment() {
      this.count++
    }
  },

  // 生命周期钩子会在组件生命周期的各个不同阶段被调用
  // 例如这个函数就会在组件挂载完成后被调用
  mounted() {
    console.log(`The initial count is ${this.count}.`)
  }
}
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

1.2. 组合式API(推荐)

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

 对比:

两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。

选项式 API 以“组件实例”的概念为中心 (即上述例子中的 this),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。

组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。

1.3.组合式AP语法

setup
vue3 中新增了 setup ,它的出现是为了解决组件内容庞大后,理解和维护组件变得困难的问题。即 vue
data computed methods watch 等内容非常多以后,同一业务逻辑的 data 中的数据和
methods 中的方法在 vue 文件中 相隔甚远 ,看代码时,经常需要根据 data 中的数据去搜索找到对应
methods 方法,上下跳跃查看代码,非常不方便。而在 setup 中,则可以把 data 中的数据和
methods 方法写在相临的位置,方便查看和维护。
开启了 setup 就不需要在定义 data methods 了,直接在 script 标签写就行
直接在标签上使用 setup
const 定义常量,在 vue3 中被用来定义变量或函数
定义变量时,因为要保证 vue 的响应式,所以配合 ref 函数使用
ref 函数:
作用 : 定义一个响应式的数据
语法 : const xxx = ref(initValue)
创建一个包含响应式数据的引用对象( reference 对象,简称 ref 对象)。
JS 中操作数据: xxx.value
模板中读取数据 : 不需要 .value,直接 {xxx}}
备注: 接收的数据可以是:基本类型、也可以是对象类型。 基本类型的数据:响应式依然是靠
Object.defineProperty() get set 完成的。 对象类型的数据:内部 求助 Vue3.0中的一个新函数 — reactive 函数。
适用于:基本类型 ( 数字、字符串、布尔值)
reactive 函数:
reactive Vue3 中提供的实现响应式数据的方法。
reactive 参数必须是对象 (json / arr)
如果给 reactive 传递了其它对象
默认情况下,修改对象无法实现界面的数据绑定更新。
如果需要更新,需要进行重新赋值。 ( 即不允许直接操作数据,需要放个新的数据来替代原数
)
适用于 对象或数组
<template>
    <div>
     <h1>标题</h1>
     <button @click="ck">点击次数:{{num }}</button>
      <button @click="ck1">点击次数:{{num }}</button>
    </div>
</template>
<!-- 最新的方法 -->
<script setup lang="ts">
import { ref,reactive } from 'vue';

//reactive使用 里面只能是数组和对象
//定义变量
const num1 =reactive({
    age:0
})
const ck1 =() =>{
    num1.age++
}
//ref的使用

const num =ref(0)  //定义变量 等价于 data -> num
const ck = ()=> {   //定义函数 等价于 methods ->ck
    num.value++
}
</script>

<style scoped>

</style>

2.vite介绍

Vite(法语意为 "快速的",发音 /vit/,发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

Vite 意在提供开箱即用的配置,同时它的 插件 API 和 JavaScript API 带来了高度的可扩展性,并有完整的类型支持。

3.vite项目搭建

使用 NPM:

$ npm create vite@latest

使用 Yarn:

$ yarn create vite

使用 PNPM:

$ pnpm create vite

然后按照提示操作即可!

项目工程结构图如下:

4.vue-router

Vue Router是 Vue.js 的官方路由,为 Vue.js 提供富有表现力、可配置的、方便的路由
它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举,他最主要的功能就是路由跳转(切换页面)。
3.1安装:
npm install vue-router@4

3.2使用:

src下创建文件夹:router

再创建路由的 js 文件: index.js
填写代码:
import {createRouter as _createRouter, createWebHistory} from 'vue-router';
// 导入 凡是想要通过路由跳转的,都需要在这:1.导入 2.注册 设置对应vue组件的路径名
import My from '../views/My.vue'
import About from '../views/About.vue'
const routes = [
// 注册 设置对应vue组件的路径名
{ path: '/my', component: My },
{ path: '/', component: About }
]
export function createRouter() {
return _createRouter({
history: createWebHistory(),  //推荐使用这个模式 使用的是/+组件的路径名称
routes
})
}

在main.js中使用

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
//导入vue-router
import { createRouter } from "./router/index.js"
createApp(App).use(createRouter()).mount('#app')
在app.vue中代码如下:
<script setup>
import HelloWorld from "./components/HelloWorld.vue";
</script>

<template>
  <!-- <HelloWorld msg="Vite + Vue" /> -->
  <div>
    <router-link to="/">主页</router-link>|
    <router-link to="/my">我的博客</router-link>|
  </div>
  <div>
    <!--    显示路由跳转的组件的内容-->
    <router-view />
  </div>
</template>

<style scoped></style>
凡是可以通过路由跳转 vue 页面,都必须在路由中注册
路由注册两种方式:
.声明式:静态标签
<router-link to="路径名称">主页</router-link>
编程式:router.push(...)      或者 this.$router.push("")
路由传值:
vue-router 跳转(编程式)的时候,还可以传递数据
可以通过 path+query 进行参数传递,这种特点:参数会跟着 url 进行传输
还可以用params参数,不会再url上传递
跳转使用的是: useRouter
创建一个GoodsList.vue文件
<template>
  <div>
    <h1>商品列表页面</h1>
    <table>
      <thead>
        <tr>
          <th>序号</th>
          <th>名称</th>
          <th>价格</th>
          <th>库存</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="g in goodslist" :key="g.id">
          <td>{{ g.id }}</td>
          <td>{{ g.name }}</td>
          <td>{{ g.price }}</td>
          <td>{{ g.num }}</td>
          <td>
            <button @click="tz">查看详情</button>
            <button @click="tzdetail(g.id, g.name)">详情带数据</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
import {ref} from 'vue';
//1.引入
import { useRouter } from 'vue-router'
//2.实例化
const router = useRouter()

const goodslist=ref([
  {
    id:1,
    name:"苹果手机14pro",
    price:8999,
    num:10
  },
  {
    id:2,
    name:"华为Meta50",
    price:6999,
    num:22
  }
])
const tz=()=>{
  //3.使用 跳转 编程式导航
  router.push('/goodsdetail');
}
const tzdetail = (id,n) => {
  router.push({path:'/goodsdetail',query:{id:id,name:n}});
}
</script>

<style scoped>

</style>

取值用的是:useRoute

创建一个GoodsDetail.vue文件

<template>
  <h1>商品详情页面</h1>
  <h2>路由传递的数据:{{route.query.id}}</h2>
  <h2>路由传递的数据:{{route.query.name}}</h2>
</template>

<script setup>
import { useRoute } from 'vue-router'
const route = useRoute();
</script>

<style scoped>

</style>

路由守卫:

非常重要的功能,可以做登录拦截,无权限访问

  1. 全局前置守卫(Global Before Guards)

    • 在每次路由跳转前触发。

    • 适用于全应用范围内的权限控制或登录状态检查。

  2. 路由独享守卫(Per-Route Guard)

    • 定义在路由配置中,只对特定的路由有效。

    • 适合于需要针对某些具体路由做特殊处理的情况。

  3. 组件内守卫(In-Component Guards)

    • 直接定义在组件内部,如 beforeRouteEnterbeforeRouteUpdate 和 beforeRouteLeave

    • 用于控制组件级别的导航行为

// 全局前置守卫
router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  const isAuthenticated = localStorage.getItem('token') !== null; // 检查是否登录,这里仅作示例

  if (requiresAuth && !isAuthenticated) {
    next({ name: 'Login' }); // 如果未登录且需要登录,则重定向到登录页
  } else {
    next(); // 确保一定要调用 next 函数
  }
});

动态路由:

菜单是动态生成的,而不是前端写死的,所以需要用到动态生成路由的方法

// src/store/modules/permission.ts
import { defineStore } from 'pinia'
import { processedAsyncRoutes } from '@/router'
import { useRouter } from 'vue-router'

interface PermissionState {
  routes: any[]
}

export const usePermissionStore = defineStore('permission', {
  state: (): PermissionState => ({
    routes: []
  }),

  actions: {
    generateRoutes(roles: string[]) {
      const router = useRouter()
      const accessedRoutes = processedAsyncRoutes.filter(route => {
        if (!route.meta || !route.meta.roles) return true
        return roles.some(role => route.meta?.roles?.includes(role))
      })

      accessedRoutes.forEach(route => {
        router.addRoute(route)  //动态生成路由方法
      })

      this.routes = accessedRoutes
      return accessedRoutes
    }
  }
})

5状态管理(vuex,pinia)

5.1vuex

Vuex 是 Vue.js 的状态管理库,它帮助开发者集中管理和组织应用中各个组件的状态。在 Vuex 中,并不是“五大对象”,而是有五个核心概念或组成部分,它们分别是:State、Getter、Mutation、Action 和 Module。每个部分都有其特定的作用:

  1. State

    • 作用:存储应用的核心状态(数据)。它是单一状态树,即整个应用的状态都集中存储在一个对象里面。
  2. Getter

    作用:类似于Vue组件中的计算属性,用于从state派生出一些状态。Getters可以用来处理state中的数据,然后返回处理后的结果。
  3. Mutation

    • 作用:更改Vuex的store中的状态的唯一方法是提交mutation。每一个mutation都有一个字符串类型的事件类型和一个回调函数(handler)。回调函数就是实际进行状态更改的地方,并且它会接受state作为第一个参数。
    • 注意:必须同步执行。
  4. Action

    • 作用:类似mutation,但是是用来处理异步任务的。Actions提交的是mutations而不是直接变更状态,同时它可以包含任意异步操作。
  5. Module

    • 作用:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得复杂时,store 对象就有可能变得相当臃肿。为了解决这个问题,Vuex 允许我们将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块。

安装:

npm install vuex@3   //vue2
npm install vuex@4   //vue3

1定义:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    getCountPlusOne(state) {
      return state.count + 1
    }
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync(context) {
      setTimeout(() => {
        context.commit('increment')
      }, 1000)
    }
  }
})

export default store

2.使用

<!-- Counter.vue -->
<template>
  <div>
    <h2>当前计数:{{ count }}</h2>
    <h3>计数加一:{{ getCountPlusOne }}</h3>

    <button @click="increment">+1</button>
    <button @click="incrementAsync">异步 +1</button>
  </div>
</template>

<script>
export default {
  computed: {
    count() {
      return this.$store.state.count
    },
    getCountPlusOne() {
      return this.$store.getters.getCountPlusOne
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    },
    incrementAsync() {
      this.$store.dispatch('incrementAsync')
    }
  }
}
</script>

3高级写法:

// Counter.vue
<template>
  <div>
    <p>Count is {{ count }}</p>
    <p>Count plus one is {{ getCountPlusOne }}</p>
    <button @click="increment">Increment</button>
    <button @click="incrementAsync">Increment Async</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState(['count']),
    ...mapGetters(['getCountPlusOne'])
  },
  methods: {
    ...mapActions(['increment', 'incrementAsync'])
  }
};
</script>

5.2pinia(推荐)

pinia是一个vue的状态存储库,你可以使用它来存储、共享一些跨组件或者页面的数据,使用起来和vuex非常类似。pina相对Vuex来说,更好的ts支持和代码自动补全功能。

pinia在2019年11月开始时候是一个实验项目,目的就是重新设计一个与组合API匹配的vue状态存储。基本原则和原来还是一样的,pinia同时支持vue2和vue3,且不要求你必须使用Vue3的组合API。不管是使用vue2或者vue3,pinia的API是相同的,文档是基于vue3写的。

Pinia 是 Vuex4 的升级版,也就是 Vuex5; Pinia 极大的简化了Vuex的使用,是 Vue3的新的状态管理工具;Pinia 对 ts的支持更好,性能更优, 体积更小,无 mutations,可用于 Vue2 和 Vue3;Pinia支持Vue Devtools、 模块热更新和服务端渲染。

灵活性:Pinia 不强制要求使用 mutations 来改变状态,任何 action 都可以直接修改状态,这提供了更大的灵活性。

无命名空间限制:Vuex 中模块需要考虑命名空间问题,而 Pinia 则通过 store 名称来避免冲突,更加简单直接。

更好的 TypeScript 支持:Pinia 对 TypeScript 提供了更友好的支持,类型推断更加准确。

Setup Store 和 Options Store:Pinia 支持使用 Composition API(Setup Store)和 Options API(Options Store)两种方式定义 store,给开发者更多选择。

pinia(Pinia | The intuitive store for Vue.js)

安装;

npm install pinia
npm i pinia-plugin-persist --save  //持久化

1.使用

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // State
  state: () => ({
    count: 0,
  }),
  // Getters
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  // Actions
  actions: {
    increment() {
      this.count++
    },
    asyncIncrement() {
      setTimeout(() => {
        this.count++
      }, 1000)
    }
  }
})

2.引入

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

3.使用

<template>
  <div>
    <p>Count is {{ counter.count }}</p>
    <p>Double Count is {{ counter.doubleCount }}</p>

    <button @click="counter.increment">Increment</button>
    <button @click="counter.asyncIncrement">Async Increment</button>
  </div>
</template>

<script>
import { useCounterStore } from './stores/counter'

export default {
  setup() {
    const counter = useCounterStore()
    return { counter }
  }
}
</script>

更多使用流程参考:

定义 Store | Pinia

6.axios

安装依赖

npm install axios

Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。

POST 请求 | Axios中文文档 | Axios中文网

使用如下:

axios.get('/user', {
  params: {
    ID: 12345
  }
})
.then(function (response) {
  console.log(response);
})
.catch(function (error) {
  console.error(error);
});


axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

请求接口代码如下:

main.js中使用:

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

// 路由
import router from './router'

// Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'

// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'
axios.defaults.baseURL = '具体请求地址'

// Pinia
import { createPinia } from 'pinia'

// 图标库
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

// 创建应用
const app = createApp(App)

// 注册图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

// 使用插件
app.use(router)
  .use(ElementPlus, { locale: zhCn })
  .use(VueAxios, axios)
  .use(createPinia())
  .mount('#app')

 编写页面代码:

<template>
  <div>
    <div style="margin-top: 20px">
      <el-table :data="students" border style="width: 100%">
        <el-table-column prop="id" label="序号" width="80" />
        <el-table-column prop="number" label="数量" />
        <el-table-column prop="code" label="物料编码" />
        <el-table-column prop="price" label="价格" />
        <el-table-column prop="ctime" label="创建时间" width="200" />

        <!-- 操作列 -->
        <el-table-column label="操作" width="200">
          <template #default="scope">
            <el-button
              type="primary"
              size="small"
              @click="openEditDialog(scope.row)"
            >
              修改
            </el-button>
            <el-button
              type="danger"
              size="small"
              @click="handleDelete(scope.row.id)"
              >删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div>
        <el-pagination
          v-model:current-page="currentPage"
          v-model:page-size="pageSize"
          :page-sizes="[10, 20, 30, 40]"
          layout="total, sizes, ->, prev, pager, next, jumper"
          :total="total"
        />
      </div>
      <!-- 新增/修改弹窗 -->
      <el-dialog
        v-model="dialogVisible"
        :title="isEdit ? '编辑物料单' : '新增物料单'"
        width="50%"
      >
        <el-form :model="currentStudent" label-width="120px">
          <el-form-item label="物料编码:">
            <el-input v-model="currentStudent.code" />
          </el-form-item>
          <el-form-item label="数量:">
            <el-input v-model="currentStudent.number" />
          </el-form-item>
          <el-form-item label="价格:">
            <el-input v-model="currentStudent.price" />
          </el-form-item>
          <el-form-item label="日期:">
            <el-date-picker
              v-model="currentStudent.ctime"
               type="datetime"
               placeholder="请选择日期"
               value-format="YYYY-MM-DD HH:mm:ss"
               format="YYYY-MM-DD HH:mm:ss"
               size="default"
            />
          </el-form-item>

          <el-form-item>
            <el-button type="primary" @click="handleSubmit">提交</el-button>
            <el-button @click="dialogVisible = false">取消</el-button>
          </el-form-item>
        </el-form>
      </el-dialog>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from "vue";
import axios from "axios";

// 响应式数据
const students = ref([]); // 学生列表
const currentStudent = ref({}); // 当前编辑或新增的数据对象
const dialogVisible = ref(false); // 控制弹窗显示
const isEdit = ref(false); // 是否为编辑状态
const currentPage = ref(1)
const pageSize = ref(10)
const total =ref()
// 查询所有数据
const get = () => {
  axios.post("buy/page", { page: currentPage.value, size: pageSize.value}).then((result) => {
    // ✅ 正确写法:使用 .value 来修改 ref 的值
    students.value = result.data.data.records
    total.value = result.data.data.total
  });
};

// 打开新增弹窗
const openAddDialog = () => {
  isEdit.value = false;
  currentStudent.value = {}; // 清空表单
  dialogVisible.value = true;
};

// 打开编辑弹窗
const openEditDialog = (row) => {
  isEdit.value = true;
  currentStudent.value = { ...row }; // 拷贝当前行数据
  dialogVisible.value = true;
};

// 提交新增或修改
const handleSubmit = () => {
  axios.post("buy/save", currentStudent.value).then((res) => {
    if (res.data.code === "0000") {
      get(); // 刷新表格
      dialogVisible.value = false;
    } else {
      alert(isEdit.value ? "修改失败" : "新增失败");
    }
  });
};

// 删除数据
const handleDelete = (id) => {
  if (!confirm("确定要删除这条数据吗?")) return;

  axios.post("/buy/deleteById", [id]).then((res) => {
    if (res.data.code === "0000") {
      get(); // 刷新列表
    } else {
      alert("删除失败");
    }
  });
};
//监听器 监听data中的值变化
// watch(()=>{
//    get();
// })
//传递依赖项 
watch([currentPage, pageSize], () => {
  get()
})
// 页面加载时获取数据
onMounted(() => {
  get();
});
</script>
<style scoped></style>

二次封装axios

// src/utils/request.js
import axios from 'axios';

// 创建 axios 实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // api 的 base_url
  timeout: 5000 // 请求超时时间
});

// request 拦截器
service.interceptors.request.use(
  config => {
    // 在发送请求之前做些什么
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = 'Bearer ' + token; // 让每个请求携带 token
    }
    return config;
  },
  error => {
    // 对请求错误做些什么
    console.error(error);
    return Promise.reject(error);
  }
);

// response 拦截器
service.interceptors.response.use(
  response => {
    const res = response.data;
    // 根据返回的数据判断是否成功,这里假设当 code 不为 20000 时为异常
    if (res.code !== 20000) {
      console.error(res.message || 'Error');
      return Promise.reject(new Error(res.message || 'Error'));
    } else {
      return res;
    }
  },
  error => {
    console.error('Response Error:', error.message);
    return Promise.reject(error);
  }
);

/**
 * 统一请求函数
 * @param {Object} options - 请求配置项
 * @returns {Promise}
 */
export function request(options) {
  if (options.method.toLowerCase() === 'get') {
    // 如果是 GET 请求,则将参数放在 params 中
    return service.get(options.url, { params: options.data });
  } else {
    // 其他类型请求(如 POST)将数据放在 data 中
    return service(options.method.toLowerCase(), options.url, { data: options.data });
  }
}

使用

import { request } from '@/utils/request';

function fetchUser(id) {
  return request({
    method: 'get',
    url: '/user',
    data: { ID: id }
  });
}

fetchUser(12345).then(response => {
  console.log(response);
}).catch(error => {
  console.error(error);
});


import { request } from '@/utils/request';

function createUser(data) {
  return request({
    method: 'post',
    url: '/user',
    data: data
  });
}

createUser({
  firstName: 'Fred',
  lastName: 'Flintstone'
}).then(response => {
  console.log(response);
}).catch(error => {
  console.error(error);
});

7.组件库

为什么要用到组件,因为在vue中万物都是组件,实际上就是为了解决绘制页面时相同的页面结构(例如输入框,表格等)多次出现,不可能每次都写一遍代码,所以抽离成组件,处处可用,其实就是封装成方法的思想。

7.1Element-UI

基于 Vue 3,面向设计师和开发者的组件库 饿了吗团队开源,用于后台页面绘制

官网:

Button 按钮 | Element Plus

7.2 Vant

Vant 是一个轻量、可定制的移动端组件库,于 2017 年开源。

目前 Vant 官方提供了 Vue 2 版本Vue 3 版本微信小程序版本,并由社区团队维护 React 版本支付宝小程序版本。有赞开源

官网:

Vant 4 - A lightweight, customizable Vue UI library for mobile web apps.

7.3 Uni-App

uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。他不是一个组件库,可以理解他是编译成多套系统的框架,方便开发者,不用原生语法去开发APP和小程序,很繁琐,改成使用vue的语法去开发,例如开发APP,有安卓和ios原生开发,还有uni-App开发两种方式

官网:

uni-app官网

8.实现简易版电商后台系统(手搓)

8.1项目创建

安装依赖

//创建项目
npm create vite@latest
//安装依赖
npm install vue-router@4
npm install pinia
npm i pinia-plugin-persist --save  //持久化
npm install axios
npm install element-plus --save
npm install @element-plus/icons-vue

8.2编码

清空style.css样式,不需要使用

main.js注入组件  引入依赖

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

// 路由
import router from './router'

// Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'

// Axios
import axios from 'axios'
import VueAxios from 'vue-axios'
axios.defaults.baseURL = '请求后端地址'

// Pinia
import { createPinia } from 'pinia'

// 图标库
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

// 创建应用
const app = createApp(App)

// 注册图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

// 使用插件
app.use(router)
  .use(ElementPlus, { locale: zhCn })
  .use(VueAxios, axios)
  .use(createPinia())
  .mount('#app')

router文件夹下index.js  注入路由

import { createRouter , createWebHistory } from 'vue-router';
import Blog from '../views/Blog.vue'
import V3Student from '../views/V3Student.vue'
import MaterialManage from '../views/Material.vue'
import BuyManage from '../views/Buy.vue'

const routes = [
  {
    path: '/',
    redirect: '/student/blog',
  },
  {
    path: '/student',
    children: [
      { path: 'blog', component: Blog, meta: { title: '博客管理' } },
      { path: 'info', component: V3Student, meta: { title: '学生信息' } }
    ]
  },
  {
    path: '/purchase',
    children: [
      { path: 'material', component: MaterialManage, meta: { title: '物料管理' } },
      { path: 'buy', component: BuyManage, meta: { title: '购买管理' } }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

componennts组件下的SideMenu.vue(侧边菜单组件)

<template>
  <el-menu
    default-active="$route.path"
    router
    class="el-menu-vertical-demo"
    :collapse="false"
  >
    <!-- 标题 -->
    <div class="menu-title">电商管理系统</div>

    <!-- 学生管理 -->
    <el-sub-menu index="student">
      <template #title>
        <el-icon><Document /></el-icon>
        <span>学生管理</span>
      </template>
      <el-menu-item index="/student/blog">
        <el-icon><Message /></el-icon>
        博客管理
      </el-menu-item>
      <el-menu-item index="/student/info">
        <el-icon><User /></el-icon>
        学生信息
      </el-menu-item>
    </el-sub-menu>

    <!-- 采购管理 -->
    <el-sub-menu index="purchase">
      <template #title>
        <el-icon><ShoppingCart /></el-icon>
        <span>采购管理</span>
      </template>
      <el-menu-item index="/purchase/material">
        <el-icon><Box /></el-icon>
        物料管理
      </el-menu-item>
      <el-menu-item index="/purchase/buy">
        <el-icon><Coin /></el-icon>
        购买管理
      </el-menu-item>
    </el-sub-menu>
  </el-menu>
</template>

<script setup>
import { ref } from 'vue'
const isCollapse = ref(false)
</script>

<style scoped>
.menu-title {
  height: 50px;
  line-height: 50px;
  text-align: center;
  font-size: 16px;
  color: #e6e6e6;
  background-color: #1E2329;
  font-weight: bold;
  letter-spacing: 1px;
}

.el-menu {
  border-right: none;
  background-color: #1E2329;
  color: #e6e6e6;
}

/* 一级菜单标题 */
::v-deep(.el-sub-menu__title) {
  background-color: #1E2329 !important;
  color: #e6e6e6 !important;
}
::v-deep(.el-sub-menu__title:hover) {
  background-color: #1E2329 !important;
  color: #e6e6e6 !important;
}

/* 子菜单项 */
.el-menu-item {
  padding-left: 30px !important;
  color: #e6e6e6;
  background-color: #252C35;
}

/* 图标颜色统一为浅白 */
.el-icon {
  color: #e6e6e6;
}

/* 子菜单项悬停时背景变深,不变白 */
.el-menu-item:hover {
  background-color: #2C3643 !important;
  color: #e6e6e6 !important;
}

/* 当前激活的子菜单项字体变蓝,背景稍深 */
.el-menu-item.is-active {
  background-color: #2C3643 !important;
  color: #409EFF !important;
}

/* 一级菜单容器 hover 也保持不变色(保险) */
::v-deep(.el-sub-menu:hover) {
  background-color: #1E2329 !important;
}


</style>

app.vue

<template>
  <div style="display: flex; min-height: 100vh;">
    <!-- 左侧菜单 -->
    <side-menu />

    <!-- 右侧内容 -->
    <div style="flex: 1; padding: 20px;">
      <router-view />
    </div>
  </div>
</template>

<script setup>
import SideMenu from './components/SideMenu.vue'
</script>

<style scoped>
/* 清除页面边距 */
:global(body) {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* 保证整体布局紧贴边缘 */
div {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
</style>

这里只上传Blog.Vue代码(都是一样的差不多增删改查结构)

<template>
  <div>
    <el-button type="primary" @click="openAddDialog" style="margin-top: 20px">新增</el-button>
  </div>
  <div>
      <el-form :inline="true" :model="formInline" class="demo-form-inline">
    <el-form-item label="博客内容">
      <el-input v-model="formInline.blogText" placeholder="请输入博客内容" clearable />
    </el-form-item>
    <el-form-item label="博客标题">
      <el-input v-model="formInline.blogTitle" placeholder="请输入博客标题" clearable />
    </el-form-item>
    <el-form-item label="博客类型">
      <el-select
        v-model="formInline.creation"
        placeholder="请选择博客类型"
        clearable
      >
        <el-option label="原创" value= 1 />
        <el-option label="转载" value=2 />
      </el-select>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="onSubmit">查询</el-button>
    </el-form-item>
  </el-form>
  </div>

  <el-table :data="blogData" stripe style="width: 100%">
    <el-table-column prop="blogText" label="博客内容" width="180" />
    <el-table-column prop="blogTitle" label="博客标题" width="180" />
    <el-table-column prop="copyrightLink" label="版权地址" />
    <el-table-column prop="createUser" label="创建人" />
    <el-table-column prop="createTime" label="创建时间" width="200" />

    <!-- 是否原创 -->
    <el-table-column label="是否原创">
      <template #default="scope">
        {{ scope.row.creation === 1 ? '是原创' : '非原创' }}
      </template>
    </el-table-column>

    <!-- 是否删除 -->
    <el-table-column label="是否删除">
      <template #default="scope">
        <el-tag :type="scope.row.deleteFlag === 0 ? 'danger' : 'success'" size="small">
          {{ scope.row.deleteFlag === 0 ? '已删除' : '未删除' }}
        </el-tag>
      </template>
    </el-table-column>

    <el-table-column prop="lookNumber" label="浏览量" />
    <el-table-column prop="starNumber" label="收藏量" />
    <el-table-column prop="talkNumber" label="评论数量" />

    <!-- 操作列 -->
    <el-table-column label="操作" width="200">
      <template #default="scope">
        <el-button type="primary" size="small" @click="openEditDialog(scope.row)">修改</el-button>
        <el-button type="danger" size="small" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
    </el-table-column>
  </el-table>

  <!-- 分页组件 -->
  <el-pagination
    v-model:current-page="page"
    v-model:page-size="size"
    :page-sizes="[10, 20, 30, 40]"
    layout="total, sizes, prev, pager, next, jumper"
    :total="total"
    style="margin-top: 20px"
  />

  <!-- 新增/修改弹窗 -->
  <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑博客' : '新增博客'" width="50%">
    <el-form :model="blog" label-width="120px">
      <el-form-item label="博客内容">
        <el-input v-model="blog.blogText" />
      </el-form-item>
      <el-form-item label="博客标题">
        <el-input v-model="blog.blogTitle" />
      </el-form-item>
      <el-form-item label="版权链接">
        <el-input v-model="blog.copyrightLink" />
      </el-form-item>
      <el-form-item label="是否原创">
        <el-select v-model="blog.creation" placeholder="请选择">
          <el-option label="是原创" :value="1" />
          <el-option label="非原创" :value="0" />
        </el-select>
      </el-form-item>

      <el-form-item label="日期">
        <el-date-picker
          v-model="blog.createTime"
          type="datetime"
          placeholder="请选择日期"
          value-format="YYYY-MM-DD HH:mm:ss"
          format="YYYY-MM-DD HH:mm:ss"
          size="default"
        />
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="handleSubmit">提交</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </el-form-item>
    </el-form>
  </el-dialog>
</template>

<script setup>
import axios from 'axios'
import { ref, onMounted,reactive, watch } from 'vue'
const formInline = reactive({
  blogText: '',
  blogTitle: '',
  creation: 0,
})

const onSubmit = () => {
 axios.post("blog/page", { page: page.value, size: size.value,params:{
  blogText: formInline.blogText,blogTitle:formInline.blogTitle,creation:formInline.creation
 } }).then((res) => {
    if (res.data.code === "0000") {
      blogData.value = res.data.data.records
      total.value = res.data.data.total
    }
  })
}
const blogData = ref([])
const blog = ref({})
const page = ref(1)
const size = ref(10)
const total = ref()
const dialogVisible = ref(false)
const isEdit = ref(false)

// 获取博客列表
const getBlogList = () => {
  axios.post("blog/page", { page: page.value, size: size.value }).then((res) => {
    if (res.data.code === "0000") {
      blogData.value = res.data.data.records
      total.value = res.data.data.total
    }
  })
}

// 打开新增弹窗
const openAddDialog = () => {
  isEdit.value = false
  blog.value = {} // 清空表单
  dialogVisible.value = true
}

// 打开编辑弹窗
const openEditDialog = (row) => {
  isEdit.value = true
  blog.value = { ...row } // 拷贝当前行数据
  dialogVisible.value = true
}

// 提交新增或修改
const handleSubmit = () => {
  axios.post("blog/save", blog.value).then((res) => {
    if (res.data.code === "0000") {
      getBlogList(); // 刷新表格
      dialogVisible.value = false;
    } else {
      alert(isEdit.value ? "修改失败" : "新增失败");
    }
  });
};

// 删除数据
const handleDelete = (id) => {
  if (!confirm("确定要删除这条数据吗?")) return;

  axios.post("blog/deleteById", [id]).then((res) => {
    if (res.data.code === "0000") {
      get(); // 刷新列表
    } else {
      alert("删除失败");
    }
  });
};

// 页面加载时获取数据
onMounted(() => {
  getBlogList()
})

watch([page, size], () => {
  getBlogList()
})
</script>

<style scoped></style>

npm run dev 启动项目

展示如下:

未实现登录功能,pinia信息存储,权限菜单模块  (后续实现)

<think>我们正在讨论的是如何使用Vue3Vite创建一个自定义的前端项目脚手架。根据引用内容,我们可以知道Vite是一个新型的前端构建工具,能够提供更快的开发体验。而Vue3Vue.js的最新版本。下面我将详细介绍如何基于Vue3Vite创建自定义项目脚手架。###步骤1:环境准备首先,确保你的系统中已经安装了Node.js(建议版本14.18+或16+)。可以通过以下命令检查Node.js是否安装:```bashnode-vnpm-v```如果未安装,请到[Node.js官网](https://nodejs.org/)下载并安装。###步骤2:使用Vite创建Vue3项目Vite提供了命令行工具,可以快速初始化项目。1.**使用npm创建项目**:```bashnpmcreatevite@latest```然后按照提示操作:-输入项目名称-选择框架:`Vue`-选择变体:`Vue`(表示Vue3)或`Vue-ts`(表示Vue3withTypeScript)2.**使用yarn**:```bashyarncreatevite```同样按照提示选择。###步骤3:进入项目并安装依赖创建完成后,进入项目目录并安装依赖:```bashcdyour-project-namenpminstall#或yarn```###步骤4:运行项目使用以下命令启动开发服务器:```bashnpmrundev#或yarndev```此时,项目将在`http://localhost:5173`(默认端口)运行。###步骤5:项目结构说明使用Vite创建的Vue3项目结构如下:```your-project-name/├──node_modules/├──public/#静态资源├──src/│├──assets/#图片等资源│├──components/#组件│├──App.vue#主组件│└──main.js#入口文件├──index.html#页面入口├──package.json├──vite.config.js#Vite配置文件└──...#其他配置文件(如.eslintrc等)```###步骤6:自定义脚手架如果你需要创建一个自定义的脚手架(例如,包含一些预设配置、插件和工具),你可以通过以下方式:1.**修改`vite.config.js`**:根据需求添加插件(如`@vitejs/plugin-vue`、`vite-plugin-eslint`等)和配置(如别名、代理等)。```javascriptimport{defineConfig}from&#39;vite&#39;importvuefrom&#39;@vitejs/plugin-vue&#39;exportdefaultdefineConfig({plugins:[vue()],resolve:{alias:{&#39;@&#39;:path.resolve(__dirname,&#39;./src&#39;)}},server:{port:3000,//自定义端口proxy:{&#39;/api&#39;:&#39;http://localhost:4000&#39;}}})```2.**添加常用依赖**:例如,添加VueRouter、Pinia(状态管理)、ElementPlus等:```bashnpminstallvue-router@4piniaelement-plus```3.**预设目录结构**:你可以设计一个更丰富的目录结构,例如:```src/├──api/#接口├──router/#路由├──store/#状态管理├──utils/#工具函数└──views/#页面```4.**创建自定义模板**:如果你想将这套配置作为模板,可以将其发布到npm,或者使用`degit`等工具来提供模板克隆功能。也可以使用VueCLI的自定义模板功能(但注意VueCLI默认创建Vue2项目,而Vite更适合Vue3)。###步骤7:发布自定义脚手架(可选)如果你希望团队或他人可以使用你的脚手架,可以:1.将项目模板推送到GitHub仓库。2.使用`npminitvite`的`--template`参数指定模板(需要满足Vite模板规范)。或者,你可以创建一个自定义的脚手架生成工具,类似于`create-vite-app`,但这需要编写更多的脚本。###相关问题1.如何在Vite项目中使用TypeScript?2.Vite与Webpack的主要区别是什么?3.如何在Vite中配置环境变量?4.如何优化Vite项目的生产构建?###引用说明本文参考了Vite官方文档以及相关Vue3教程[^2][^3]。</think>## 使用 Vue 3Vite 创建自定义前端项目脚手架 ### 完整步骤指南 #### 1. 环境准备 ```bash # 安装 Node.js (建议 v16+) node -v # 检查版本 [^1] # 配置 npm 镜像加速 npm config set registry https://registry.npmmirror.com ``` #### 2. 创建基础项目 ```bash # 使用 Vite 创建 Vue 3 项目 npm create vite@latest my-custom-scaffold -- --template vue # 进入项目目录 cd my-custom-scaffold ``` #### 3. 自定义脚手架配置 **项目结构优化**: ``` my-custom-scaffold/ ├── templates/ # 自定义模板 │ ├── views/ │ ├── components/ │ └── layouts/ ├── generator.js # 脚手架逻辑 ├── package.json └── prompts.js # 用户交互问题 ``` **prompts.js** (用户交互配置): ```javascript module.exports = [ { type: &#39;input&#39;, name: &#39;projectName&#39;, message: &#39;项目名称:&#39;, validate: input => !!input || &#39;名称不能为空&#39; }, { type: &#39;checkbox&#39;, name: &#39;features&#39;, message: &#39;选择功能:&#39;, choices: [ { name: &#39;Vue Router&#39;, value: &#39;router&#39; }, { name: &#39;Pinia 状态管理&#39;, value: &#39;pinia&#39; }, { name: &#39;Element Plus UI&#39;, value: &#39;element&#39; } ] } ]; ``` #### 4. 实现脚手架逻辑 (generator.js) ```javascript const fs = require(&#39;fs-extra&#39;); const path = require(&#39;path&#39;); module.exports = async (answers) => { const { projectName, features } = answers; // 创建项目目录 const projectPath = path.join(process.cwd(), projectName); await fs.ensureDir(projectPath); // 复制基础模板 await fs.copy(path.join(__dirname, &#39;templates/base&#39;), projectPath); // 添加可选功能 if (features.includes(&#39;router&#39;)) { await fs.copy(path.join(__dirname, &#39;templates/router&#39;), projectPath); } if (features.includes(&#39;pinia&#39;)) { await fs.copy(path.join(__dirname, &#39;templates/pinia&#39;), projectPath); } // 更新 package.json const pkgPath = path.join(projectPath, &#39;package.json&#39;); const pkg = await fs.readJson(pkgPath); pkg.name = projectName; await fs.writeJson(pkgPath, pkg, { spaces: 2 }); console.log(`\n✅ 项目创建成功!执行以下命令启动:`); console.log(`cd ${projectName}`); console.log(`npm install`); console.log(`npm run dev`); }; ``` #### 5. 配置 package.json ```json { "name": "my-custom-scaffold", "version": "1.0.0", "bin": { "create-my-app": "./bin/cli.js" }, "dependencies": { "fs-extra": "^11.0.0", "inquirer": "^9.0.0" } } ``` #### 6. 创建 CLI 入口 (bin/cli.js) ```javascript #!/usr/bin/env node const inquirer = require(&#39;inquirer&#39;); const prompts = require(&#39;../prompts&#39;); const generator = require(&#39;../generator&#39;); inquirer.prompt(prompts).then(generator).catch(console.error); ``` #### 7. 测试与发布 ```bash # 本地测试 node bin/cli.js # 全局安装测试 npm link create-my-app # 发布到 npm npm publish ``` ### 使用自定义脚手架 ```bash # 全局安装 npm install -g my-custom-scaffold # 创建项目 create-my-app # 根据提示操作 ? 项目名称: my-vue-project ? 选择功能: (按空格选择) Vue Router, Pinia ``` ### 核心优势 1. **极速启动**:Vite 的 ES 模块原生加载速度比传统打包工具快 10-100 倍[^2] 2. **按需配置**:通过交互式问答实现功能模块化 3. **模板定制**:可预置企业级最佳实践(路由、状态管理、API 封装) 4. **热更新**:Vite 的 HMR 更新速度不受项目规模影响 ### 最佳实践建议 1. **模板设计**: - 使用 `<script setup>` 语法 - 集成 `unplugin-auto-import` 自动导入 API - 配置路径别名 `@/*` 2. **功能扩展**: ```javascript // 添加测试选项 { type: &#39;confirm&#39;, name: &#39;needTests&#39;, message: &#39;是否添加单元测试?&#39;, default: false } ``` 3. **版本控制**: ```bash # 在模板中预置 .gitignore node_modules/ dist/ .env.local ``` ### 相关问题 1. 如何在 Vite 中配置环境变量? 2. Vue 3 组合式 API 与选项式 API 如何选择? 3. 如何优化 Vite 项目的生产环境构建? 4. 怎样为脚手架添加自动更新检查功能? > 通过这种模块化设计,你可以创建符合团队规范的前端脚手架,显著提升项目初始化效率[^3]。实际开发中建议结合团队技术栈封装通用业务组件和工具函数。 [^1]: Vue脚手架的安装以及创建Vue2,Vue3Vite+Vue3项目 [^2]: Vue3通过Vite实现工程化 [^3]: vue脚手架以及Vite搭建项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值