1.vue3核心语法
1.两种语法
1.1.选项式API
使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data
、methods
和 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语法
<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")是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
-
一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
-
一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
Vite 意在提供开箱即用的配置,同时它的 插件 API 和 JavaScript API 带来了高度的可扩展性,并有完整的类型支持。
3.vite项目搭建
使用 NPM:
$ npm create vite@latest
使用 Yarn:
$ yarn create vite
使用 PNPM:
$ pnpm create vite
然后按照提示操作即可!
项目工程结构图如下:
4.vue-router
npm install vue-router@4
3.2使用:
src下创建文件夹:router
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')
<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>
<router-link to="路径名称">主页</router-link>
<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>
路由守卫:
非常重要的功能,可以做登录拦截,无权限访问
-
全局前置守卫(Global Before Guards)
-
在每次路由跳转前触发。
-
适用于全应用范围内的权限控制或登录状态检查。
-
-
路由独享守卫(Per-Route Guard)
-
定义在路由配置中,只对特定的路由有效。
-
适合于需要针对某些具体路由做特殊处理的情况。
-
-
组件内守卫(In-Component Guards)
-
直接定义在组件内部,如
beforeRouteEnter
、beforeRouteUpdate
和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。每个部分都有其特定的作用:
-
State:
- 作用:存储应用的核心状态(数据)。它是单一状态树,即整个应用的状态都集中存储在一个对象里面。
-
Getter:
作用:类似于Vue组件中的计算属性,用于从state派生出一些状态。Getters可以用来处理state中的数据,然后返回处理后的结果。 -
Mutation:
- 作用:更改Vuex的store中的状态的唯一方法是提交mutation。每一个mutation都有一个字符串类型的事件类型和一个回调函数(handler)。回调函数就是实际进行状态更改的地方,并且它会接受state作为第一个参数。
- 注意:必须同步执行。
-
Action:
- 作用:类似mutation,但是是用来处理异步任务的。Actions提交的是mutations而不是直接变更状态,同时它可以包含任意异步操作。
-
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>
更多使用流程参考:
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,面向设计师和开发者的组件库 饿了吗团队开源,用于后台页面绘制
官网:
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开发两种方式
官网:
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信息存储,权限菜单模块 (后续实现)