【vue3】07 - 调用element组件库
文章目录
一:准备工作
vue2使用的是elementUI, vue3使用的是elementPlus
elementUI官方文档:https://element.eleme.cn/2.13/#/zh-CN/
elementPlus官方文档:https://element-plus.org/zh-CN/#/zh-CN
1:下载element Plus
npm install element-plus
# 或者
yarn add element-plus
2:在main.ts中引入和初始化
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
// -------- 引入 --------
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 通过 createApp 创建一个 Vue 应用程序
// 指定根组件 App.vue, 后面会将App.vue的内容渲染到index.html中的id=app上
const app = createApp(App)
const pinia = createPinia()
app.use(router)
app.use(pinia)
// -------- 初始化 ---------
app.use(ElementPlus);
// 挂载到 DOM,绑定根容器 => id=app 上(在目录的最外层的index.html中,模板html文件
app.mount('#app')
二:常见使用汇总示例
1:下拉选择框select
- 使用到的组件是
<el-select>
和<el-option>
1.1:基础选择框
<template>
<!-- el-select 表示下拉框 -->
<!-- v-model="value" 表示下拉框的值 -->
<!-- 下面是其他常用选项 -->
<!-- clearable 表示下拉框可以清空,如果不配置这个,一旦选择,就没法退回到请选择的原始状态了 -->
<!-- 可以设置disabled 表示下拉框不可用,一般用于特殊情况 -->
<!-- multiple 表示下拉框可以多选,可以选择多个 -->
<el-select v-model="value" clearable placeholder="请选择">
<!-- v-for 要进行遍历的下拉项,声明每一个下拉项叫做item -->
<!-- :key="item.value" 表示每一个下拉项的key值,用来区分每一个下拉项 -->
<!-- :label="item.label" 表示每一个下拉项的label值,用来显示在select中 -->
<!-- :value="item.value" 表示每一个下拉项的value值,用来绑定到select中 -->
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<script setup lang='ts'>
import { ref } from 'vue'
const value = ref('')
const options = [
{ value: 'option1', label: '选项一' },
{ value: 'option2', label: '选项二' },
{ value: 'option3', label: '选项三' },
]
</script>
1.2:分组选项
<template>
<el-select v-model="value" placeholder="请选择">
<!-- el-option-group -> 声明进行分组展示,v-for中的迭代项是分组依据 -->
<!-- :key -> 唯一标识,保证唯一性 -->
<!-- :label -> 分组标题 -->
<el-option-group v-for="group in options" :key="group.label" :label="group.label">
<!-- el-option -> 列表项 -->
<el-option
v-for="item in group.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-option-group>
</el-select>
</template>
<script setup>
import { ref } from 'vue'
const value = ref('')
const options = [
{
label: '热门城市',
options: [
{ value: 'Shanghai', label: '上海' },
{ value: 'Beijing', label: '北京' },
],
},
{
label: '其他城市',
options: [
{ value: 'Chengdu', label: '成都' },
{ value: 'Shenzhen', label: '深圳' },
],
},
]
</script>
1.3:远程搜索
<template>
<!-- filterable -> 启用输入框过滤功能,允许用户输入文字来筛选选项 -->
<!-- remote -> 启用远程搜索功能,用户输入内容时,会触发远程搜索 -->
<!-- reserve-keyword -> 输入框过滤功能,在选中选项后,保留关键词 -->
<!--
remote-method -> 远程搜索方法,返回一个 Promise 对象
Promise 对象 resolve 的是远程搜索结果
-->
<!-- loading -> 输入框过滤功能,在远程搜索时显示加载状态 -->
<el-select
v-model="value"
filterable
remote
reserve-keyword
placeholder="请输入关键词"
:remote-method="remoteMethod"
:loading="loading"
>
<!-- options是选项的内容,通过远程搜索确定选项 -->
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<script setup lang='ts'>
import { ref } from 'vue'
const value = ref('')
const options = ref([])
const loading = ref(false)
// 模拟远程搜索
// 方法格式是:(query) => Promise.resolve([])
const remoteMethod = (query) => {
// 如果输入了搜索词
if (query) {
// 先设置为加载中
loading.value = true
// 使用setTimeout模拟远程搜索,搜索结束了就是先将loading设置为false
// 将搜索结果设置为options,为了上面显示选项使用
setTimeout(() => {
loading.value = false
options.value = [
{ value: query + '1', label: query + '选项1' },
{ value: query + '2', label: query + '选项2' },
]
}, 200) // 模拟搜索时间,演示200ms, 正常来说这里应该是调用接口获取数据
} else {
options.value = [] // 如果没有输入搜索词,就清空选项
}
}
</script>
1.4:自定义下拉框样式
<template>
<el-select v-model="value" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
>
<!-- @slot 自定义选项内容 -->
<!--
var(--el-text-color-secondary) 是一个 CSS 颜色变量
它被定义在 src/styles/variables.scss 文件中。
-->
<!-- 表示下拉选中的展示方式是左边显示label,右边显示value -->
<span style="float: left">{{ item.label }}</span>
<span style="float: right; color: var(--el-text-color-secondary); font-size: 13px">
{{ item.value }}
</span>
</el-option>
</el-select>
</template>
<script setup>
import { ref } from 'vue'
const value = ref('')
const options = ref([
{
value: '选项1',
label: '黄金糕'
},
{
value: '选项2',
label: '双皮奶'
},
{
value: '选项3',
label: '蚵仔煎'
}
])
</script>
1.5:允许创建新条目
<template>
<!-- allow-create 允许创建新标签 -->
<!-- default-first-option 默认第一个选项 -->
<el-select
v-model="tags"
multiple
filterable
allow-create
default-first-option
placeholder="请输入并创建标签"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<script setup>
import { ref } from 'vue'
const tags = ref([])
const options = [
{ value: 'HTML', label: 'HTML' },
{ value: 'CSS', label: 'CSS' },
{ value: 'JavaScript', label: 'JavaScript' },
]
</script>
1.6:可以自定义筛选方法
<template>
<!-- filterable -> 允许过滤 -->
<!-- filter-method -> 自定义过滤方法 -->
<el-select
v-model="value"
filterable
:filter-method="filterMethod"
placeholder="自定义筛选"
>
<!-- 通过computed计算属性来过滤数据 -->
<el-option
v-for="item in filteredOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<script setup>
import { ref, computed } from 'vue'
const value = ref('')
const options = [
{ value: 'apple', label: '苹果' },
{ value: 'banana', label: '香蕉' },
{ value: 'orange', label: '橙子' },
]
const filterText = ref('')
/**
* 通过computed计算属性来过滤数据
* @type {ComputedRef<({label: string, value: string}|{label: string, value: string}|{label: string, value: string})[]>}
*/
const filteredOptions = computed(() => {
return options.filter(item =>
item.label.toLowerCase().includes(filterText.value.toLowerCase())
)
})
/**
* 自定义过滤方法
* @param val
*/
const filterMethod = (val) => {
filterText.value = val
}
</script>
1.7:表单验证
<template>
<!-- 表单容器,绑定数据模型和验证规则 -->
<el-form :model="form" :rules="rules" ref="formRef">
<!-- 表单项,label为显示标签,prop为表单字段名 -->
<el-form-item label="选择项" prop="selected">
<!-- 下拉选择组件,v-model双向绑定到form.selected -->
<el-select v-model="form.selected" placeholder="请选择">
<!-- 循环生成选项,使用options数组中的数据 -->
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- 提交按钮,点击触发submitForm方法 -->
<el-button type="primary" @click="submitForm">提交</el-button>
</el-form>
</template>
<script setup>
// 导入Vue的ref函数,用于创建响应式数据
import { ref } from 'vue'
// 定义表单数据对象,包含选中值selected
const form = ref({
selected: '' // 当前选中的值
})
// 下拉框选项列表,每个选项包含value和label属性
const options = [
{ value: 'option1', label: '选项一' },
{ value: 'option2', label: '选项二' }
]
// 表单验证规则定义
const rules = {
// 针对selected字段的验证规则
selected: [
// 必填验证,未选择时提示信息,change事件触发验证
{ required: true, message: '请选择选项', trigger: 'change' }
]
}
// 表单引用,用于访问表单方法(如验证)
const formRef = ref()
// 表单提交方法
const submitForm = () => {
// 调用表单验证方法
formRef.value.validate((valid) => {
if (valid) {
// 验证通过,弹出成功提示
alert('提交成功!')
} else {
// 验证失败,返回false阻止提交
return false
}
})
}
</script>
2:上传操作upload
- 上传操作使用的是
<el-upload>
和一系列钩子函数,在选择文件的前后能进行一些操作
2.1:基础文件上传
<template>
<!--
action -> 必填,上传的API地址
auto-upload -> 是否自动上传,默认true
on-preview -> 文件被选择上传时的钩子
file-list 已上传的文件列表
multiple 是否支持多选文件
limit 最大允许上传个数
on-exceed 文件超出个数限制时的钩子
before-upload 上传文件之前的钩子,参数为上传的文件,返回false则停止上传
on-success 文件上传成功时的钩子
on-error 文件上传失败时的钩子
on-change 文件状态改变时的钩子
on-remove 文件列表移除文件时的钩子
on-preview 点击已上传的文件链接时的钩子
list-type 文件列表的类型,可选text/picture/picture-card
drag 是否启用拖拽上传
-->
<el-upload
class="upload-demo"
action="https://your-upload-api.com/upload"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:file-list="fileList"
multiple
:limit="3"
:on-exceed="handleExceed"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
</template>
<script>
export default {
data() {
// 初始化文件列表,初始为空数组
return {
fileList: []
};
},
methods: {
// 当文件被从列表中移除时触发
handleRemove(file, fileList) {
console.log(file, fileList);
},
// 当点击已上传的文件链接时触发
handlePreview(file) {
console.log(file);
},
// 当上传文件数量超出限制时触发
handleExceed(files, fileList) {
this.$message.warning(`当前限制选择3个文件,本次选择了${files.length}个文件,共选择了${files.length + fileList.length}个文件`);
},
// 在文件上传前进行校验
beforeUpload(file) {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt500K = file.size / 1024 < 500;
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG/PNG 格式!');
}
if (!isLt500K) {
this.$message.error('上传头像图片大小不能超过 500KB!');
}
// 返回布尔值,决定是否继续上传
return isJPG && isLt500K;
},
// 文件上传成功时的操作
handleSuccess(response, file, fileList) {
this.$message.success('上传成功');
console.log('上传成功', response);
}
}
}
</script>
2.2:图片上传(带预览)
<template>
<el-upload
action="https://your-upload-api.com/upload"
list-type="picture-card"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
:file-list="fileList"
:limit="1"
:on-exceed="handleExceed"
:before-upload="beforeAvatarUpload"
>
<i class="el-icon-plus"></i>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过2MB</div>
</el-upload>
<!-- 上传图片预览 --->
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt="">
</el-dialog>
</template>
<script>
export default {
data() {
return {
dialogImageUrl: '',
dialogVisible: false,
fileList: []
};
},
methods: {
handleRemove(file, fileList) {
console.log(file, fileList);
},
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
},
handleExceed(files, fileList) {
this.$message.warning(`只能上传一张图片`);
},
// 上传图片
beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG/PNG 格式!');
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!');
}
return isJPG && isLt2M;
}
}
}
</script>
2.3:手动上传
<template>
<!-- 注意使用auto-update: false 避免自动上传-->
<!-- 使用:http-request="httpRequest"调用自定义的上传方法完成上传操作 -->
<el-upload
class="upload-demo"
ref="upload"
action="#"
:auto-upload="false"
:http-request="httpRequest"
:on-change="handleChange"
:file-list="fileList"
multiple
>
<!-- 触发上传按钮,slot='trigger' 表示这是自定义的上传触发器 -->
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<!-- 手动上传按钮,点击执行submitUpload方法 -->
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">
上传到服务器
</el-button>
<!-- 上传提示信息 -->
<div slot="tip" class="el-upload__tip">可上传任意格式文件</div>
</el-upload>
</template>
<script setup lang="ts">
import axios from 'axios'
import {reactive} from "vue";
let fileList = reactive([])
function submitUpload() {
this.$refs.upload.submit(); // 调用组件实例的 submit 方法开始上传
}
function handleChange(file, fileList) {
this.fileList = fileList; // 同步最新的文件列表到组件数据
}
function httpRequest(options) {
// 通过formData设置参数
let formData = new FormData(); // 创建 FormData 对象用于封装文件
formData.append('file', options.file); // 将文件添加到表单数据中
// 发起 POST 请求上传文件
axios.post('your-upload-api.com/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data' // 明确设置上传类型为 multipart/form-data
},
// 监听上传进度
onUploadProgress: progressEvent => {
// 计算上传进度百分比
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
options.onProgress({percent: percentCompleted}); // 回调通知进度条更新
}
}).then(res => {
options.onSuccess(res.data); // 上传成功回调
}).catch(err => {
options.onError(err); // 上传失败回调
});
}
</script>
2.4:拖拽上传
<template>
<!-- drag -> 表示允许拖拽上传 -->
<el-upload
class="upload-demo"
drag
action="https://your-upload-api.com/upload"
multiple
:on-success="handleSuccess"
:file-list="fileList"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
</template>
<script>
export default {
data() {
return {
fileList: []
};
},
methods: {
handleSuccess(response, file, fileList) {
this.$message.success('上传成功');
console.log('上传成功', response);
}
}
}
</script>
3:表格相关table
- 表格主要使用的
<el-table>
和<el-table-column>
- 在表格中通过
<template v-slot="scope">
传递的scope获取当前数据,然后就可以操作了 - 图片使用的是
<el-image>
, 视频使用的是<vedio>
- 下载使用的是
<el-button>
+ 事件 - 如果需要进度条使用的是
<el-progress>
和在调用时候的onDownloadProgress: (process) => {}
3.1:表格中显示图片
<template>
<el-table :data="tableData" style="width: 100%">
<!-- 第一列, prop中指定使用的是data中的那个属性,label是标签名称 -->
<el-table-column prop="name" label="文件名" width="180"></el-table-column>
<!-- 第二列, prop中指定使用的是data中的那个属性, label是标签名称 -->
<el-table-column prop="image" label="图片预览">
<template v-slot="scope">
<!-- flex弹性布局,水平居中 -->
<div style="display: flex; align-items: center">
<!-- 注意这里 -->
<el-image
style="width: 300px"
:src="scope.row.image"
fit="fill"
></el-image>
</div>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import { reactive } from "vue";
const tableData = reactive([
{
name: "1.jpg",
image: "/static/1.jpg",
},
{
name: "2.jpg",
image: "/static/2.jpg",
},
])
</script>
注意点一:
:src="scope.row.image"
:src
-> 这是 Vue 的动态属性绑定语法(v-bind:src
的简写) -> 使图片地址能够响应式更新
scope.row
- 在 Element UI 表格的插槽作用域中,
scope.row
表示当前行的完整数据对象 - 如果是第1行数据,
scope.row
={ name: '图片1', image: 'xxx.jpg' }
scope.row.image
- 访问当前行数据中的
image
字段 - 这个字段应该包含图片的完整路径或有效 URL
注意点二:注意图片的地址
因为vite中不支持require,所以再指定图片地址的时候要注意(如果地址是URL没有这个问题)
方案1:使用 ES 模块的 import 语法(推荐)
import img1 from '@/assets/1.jpg'
import img2 from '@/assets/2.jpg'
export default {
data() {
return {
tableData: [
{
name: '图片1',
image: img1
},
{
name: '图片2',
image: img2
}
]
}
}
}
方案2:使用绝对路径(适合 public/static 目录)
export default {
data() {
return {
tableData: [
{
name: '图片1',
image: '/static/1.jpg' // 假设图片放在 public/static 目录
},
{
name: '图片2',
image: '/static/2.jpg'
}
]
}
}
}
方案3:动态导入(适用于动态路径)
export default {
data() {
return {
tableData: [
{
name: '图片1',
image: new URL('@/assets/1.jpg', import.meta.url).href
},
{
name: '图片2',
image: new URL('@/assets/2.jpg', import.meta.url).href
}
]
}
}
}
方案4:配置 Vite 支持 require(不推荐)
如果您确实需要使用 require
,可以在 vite.config.js
中添加:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
export default defineConfig({
plugins: [vue()],
define: {
'require': require
}
})
总结:不同构建工具的处理方式
构建工具 | 推荐方式 | 注意事项 |
---|---|---|
Vue CLI (Webpack) | require 或 import | Webpack 支持两者 |
Vite | import 或 new URL() | 默认不支持 require |
Nuxt | ~/assets/ 路径 | 使用 Nuxt 专用别名 |
3.2:多图预览
<template>
<el-table :data="tableData">
<el-table-column prop="name" label="文件名"></el-table-column>
<el-table-column label="图片预览">
<template v-slot="scope">
<!-- 所谓多图预览,就是在el-image中可以使用v-for展示多张图片 -->
<el-image
v-for="(img, index) in scope.row.images"
:key="index"
style="width: 80px; height: 80px; margin-right: 10px"
:src="img"
:preview-src-list="scope.row.images"
></el-image>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import {reactive} from "vue";
let tableData = reactive([
{
name: '产品1',
images: [
'/static/1.jpg',
'/static/2.jpg'
]
},
{
name: '产品2',
images: [
'/static/2.jpg',
'/static/1.jpg'
]
}
])
</script>
3.3:基础表格中下载
<template>
<el-table :data="tableData">
<el-table-column prop="name" label="文件名"></el-table-column>
<el-table-column prop="size" label="文件大小"></el-table-column>
<!-- 就是在表格列中插入操作相关的按钮 -->
<el-table-column label="操作" width="400px">
<template v-slot="scope">
<el-button type="primary" @click="handleDownload(scope.row)">下载</el-button>
<el-button type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import {reactive} from "vue";
let tableData = reactive([
{
name: '文档1.pdf',
url: 'https://example.com/files/doc1.pdf',
size: '2.5MB'
},
{
name: '表格.xlsx',
url: 'https://example.com/files/table.xlsx',
size: '1.8MB'
}
])
function handleDownload(row) {
// 创建隐藏的a标签实现下载
const link = document.createElement('a')
link.href = row.url
link.download = row.name
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
function handleDelete(row) {
// 删除当前行数据
const index = this.tableData.indexOf(row)
this.tableData.splice(index, 1)
}
</script>
3.4:带进度显示的高级下载
<template>
<div>
<el-button @click="startDownload">下载文件</el-button>
<!-- 下载进度条 -->
<!-- 当前仅当下载进度大于0时显示 -->
<!-- percentage: 进度百分比 使用v-bind表示响应式 -->
<!-- status: 进度条状态 使用v-bind表示响应式-->
<el-progress
v-if="downloadProgress >= 0"
:percentage="downloadProgress"
:status="downloadStatus"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import axios from 'axios'
import type { AxiosProgressEvent } from 'axios'
const downloadProgress = ref(0)
const downloadStatus = ref('')
const startDownload = async () => {
downloadProgress.value = 0
downloadStatus.value = ''
try {
// 先检查headers
const headResponse = await axios.head('https://example.com/large-file.zip')
const contentLength = parseInt(headResponse.headers['content-length'] || '0')
if (!contentLength) {
throw new Error('服务器未返回Content-Length')
}
// 开始下载
// 发起GET请求下载文件,设置响应类型为blob以处理二进制数据
const response = await axios({
url: 'http://xxx.test', // 下载地址
method: 'GET', // 请求方法
responseType: 'blob', // 响应类型为blob,用于处理大文件
// 监听下载进度事件
onDownloadProgress: (progress: AxiosProgressEvent) => {
// 如果progress对象中包含总大小信息
if (progress.total) {
// 计算下载进度百分比并更新到响应式变量
downloadProgress.value = Math.round(
(progress.loaded * 100) / progress.total // 计算加载的百分比
)
}
}
})
// 下载完成后将进度条状态设为成功
downloadStatus.value = 'success'
console.log('下载完成', response.data)
// 创建下载链接
const url = window.URL.createObjectURL(new Blob([response.data]))
const link = document.createElement('a')
link.href = url
link.download = 'file.zip'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('下载失败:', error)
downloadStatus.value = 'exception'
downloadProgress.value = 0
}
}
</script>
3.5:既有图片,又有下载
<template>
<!-- 声明table中使用的数据是tableData -->
<!-- width 100% 表示宽度为100% -->
<el-table :data="tableData" style="width: 100%">
<!-- 第一列的数据描述,是放产品名称,放的是tableData中的name属性 -->
<el-table-column prop="name" label="产品名称" width="180" />
<!-- 第二列的数据描述,是放产品图片,放的是tableData中的image属性 -->
<el-table-column label="产品图片">
<template #default="scope">
<!-- 图片的信息 -->
<!-- scope.row当前行的数据,image -> 当前行数据中的image信息 -->
<el-image
style="width: 100px; height: 100px"
:src="scope.row.image"
fit="cover"
/>
</template>
</el-table-column>
<!-- 第三列的数据描述,是放产品操作,放的是tableData中的操作按钮 -->
<el-table-column label="操作" width="220">
<template #default="scope">
<!-- scope.row -> 当前行的数据 -->
<el-button size="small" @click="handlePreview(scope.row)">预览</el-button>
<el-button size="small" type="primary" @click="handleDownload(scope.row)">下载 </el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="预览图片" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import axios from 'axios'
interface Product {
id: number
name: string
image: string
manualUrl: string
}
const tableData = ref<Product[]>([
{
id: 1,
name: '智能手机',
image: '/static/1.jpg',
manualUrl: 'https://example.com/manuals/phone.pdf'
},
{
id: 2,
name: '笔记本电脑',
image: '/static/2.jpg',
manualUrl: 'https://example.com/manuals/laptop.pdf'
}
])
// 预览图片的dialog
const dialogVisible = ref(false)
// 预览图片的url
const dialogImageUrl = ref('')
// 预览图片
const handlePreview = (row: Product) => {
dialogImageUrl.value = row.image
dialogVisible.value = true
}
// 下载图片
// 注意参数使用了上面定义接口来校验,确保参数的格式正确,传递的参数是当前行数据
const handleDownload = async (row: Product) => {
try {
// 通过axios下载图片, 响应数据类型为blob
const response = await axios({
url: row.manualUrl, // 请求路径是当前行的manualUrl
method: 'GET',
responseType: 'blob',
})
// 创建一个URL对象,将blob数据转换为URL
const url = window.URL.createObjectURL(new Blob([response.data]))
// 创建一个a标签,将URL赋值给a标签的href属性,并设置download属性为文件名
const link = document.createElement('a')
// 点击a标签,实现下载
link.href = url
// 设置文件名
link.download = `${row.name}_使用手册.pdf`
// 将a标签添加到body中,并模拟点击
document.body.appendChild(link)
link.click()
// 释放内存
window.URL.revokeObjectURL(url)
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
}
}
</script>
3.6:表格中显示视频
<el-table-column label="产品图片">
<!-- 通过scope拿到当前行的数据 -->
<template #default="scope">
<!-- 图片的信息 -->
<!-- scope.row当前行的数据 -->
<vedio
:src="scope.row.file"
fit="cover"
controls
style="width: 300px"
/>
</template>
</el-table-column>
3.7:表格高亮
- 使用内置属性如
highlight-current-row
和stripe
快速实现基础高亮 - 使用
row-class-name
和cell-class-name
进行条件高亮 - 使用
cell-style
进行动态样式设置 - 使用插槽完全自定义高亮内容和样式
- 通过 CSS 覆盖默认样式实现个性化效果
行高亮 - 鼠标悬停高亮
<el-table :data="tableData" :highlight-current-row="true">
<!-- 表格列定义 -->
</el-table>
行高亮 - 当前行高亮
<el-table
:data="tableData"
highlight-current-row
@current-change="handleCurrentChange">
<!-- 表格列定义 -->
</el-table>
<script>
export default {
methods: {
handleCurrentChange(val) {
this.currentRow = val;
}
}
}
</script>
条件行高亮
<el-table
:data="tableData"
:row-class-name="tableRowClassName">
<!-- 表格列定义 -->
</el-table>
<script>
export default {
methods: {
tableRowClassName({row, rowIndex}) {
if (row.status === 'success') {
return 'success-row';
} else if (row.status === 'warning') {
return 'warning-row';
}
return '';
}
}
}
</script>
<style>
.el-table .success-row {
background-color: #f0f9eb;
}
.el-table .warning-row {
background-color: #fdf6ec;
}
</style>
列高亮
<el-table
:data="tableData"
:cell-class-name="cellClassName">
<el-table-column prop="date" label="日期"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
</el-table>
<script>
export default {
methods: {
// 在name列进行高亮
// row -> 行信息,column -> 列信息, rowIndex -> 行索引信息 columnIndex -> 列索引信息
cellClassName({row, column, rowIndex, columnIndex}) {
// 如果是是name列,返回higlight-column -> 赋予class
if (column.property === 'name') {
return 'highlight-column';
}
return '';
}
}
}
</script>
<style>
.highlight-column {
background-color: #f0f9eb;
}
</style>
条件单元格高亮
<el-table :data="tableData">
<el-table-column prop="name" label="姓名">
<template #default="{row}">
<!-- 通过响应式样式,分数大于九十分的具有highlight-cell类,否则没有 -->
<span :class="{'highlight-cell': row.score > 90}">
{{ row.name }}
</span>
</template>
</el-table-column>
</el-table>
<style>
/* highlight-cell的样式 */
.highlight-cell {
color: #ff0000;
font-weight: bold;
}
</style>
使用cell-style完成单元格高亮
<el-table
:data="tableData"
:cell-style="cellStyle">
<!-- 表格列定义 -->
</el-table>
<script>
export default {
methods: {
// 单元格样式的逻辑,返回的是单元格的样式信息
cellStyle({row, column, rowIndex, columnIndex}) {
// 如果是分数列,并且分数 < 60分
if (column.property === 'score' && row.score < 60) {
return {
backgroundColor: '#fff8e6',
color: '#ff9900'
};
}
return {};
}
}
}
</script>
斑马纹表格
<el-table :data="tableData" stripe>
<!-- 表格列定义 -->
</el-table>
自定义高亮 - 覆盖默认样式
/* 修改默认高亮行颜色 */
.el-table__body tr.current-row>td {
background-color: #e6f7ff !important;
}
/* 修改悬停颜色 */
.el-table__body tr:hover>td {
background-color: #f5f5f5 !important;
}
/* 修改斑马纹颜色 */
.el-table--striped .el-table__body tr.el-table__row--striped td {
background-color: #fafafa;
}
动态高亮
<template>
<el-table
:data="tableData"
:row-class-name="getRowClassName"
@row-click="handleRowClick">
<!-- 表格列定义 -->
</el-table>
</template>
<script>
export default {
data() {
return {
tableData: [...],
activeRowId: null
};
},
methods: {
getRowClassName({row}) {
return row.id === this.activeRowId ? 'active-row' : '';
},
handleRowClick(row) {
this.activeRowId = row.id;
}
}
}
</script>
<style>
.active-row {
background-color: #e6f7ff !important;
}
.active-row:hover>td {
background-color: #d0e8ff !important;
}
</style>
3.8:表格多选
<template>
<!-- ref -> 获取组件实例 -->
<!-- data -> 获取组件数据, 使用响应式v-bind -->
<!-- style -> 获取组件样式 -->
<!-- @selection-change="handleSelectionChange" -> 监听多选框选中状态 -->
<el-table
ref="multipleTableRef"
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<!-- 第一列, 选择框 -->
<el-table-column type="selection" width="55" />
<!-- 第二列, 日期 -->
<el-table-column prop="date" label="日期" width="120" />
<!-- 第三列, 姓名 -->
<el-table-column prop="name" label="姓名" width="120" />
<!-- 第四列, 地址 -->
<el-table-column prop="address" label="地址" />
</el-table>
<!-- 按钮 -->
<div style="margin-top: 20px">
<!-- 触发toggleSelection方法, 传入的第二行和第三行 -->
<el-button @click="toggleSelection([tableData[1], tableData[2]])">切换第二、第三行的选中状态</el-button>
<!-- 触发clearSelection方法 -->
<el-button @click="clearSelection">取消选择</el-button>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import type { ElTable } from 'element-plus'
// 定义接口,规范数据结构
interface User {
date: string
name: string
address: string
}
// 定义变量
// 1: 定义表格实例
const multipleTableRef = ref<InstanceType<typeof ElTable>>()
// 定义被选中项集合,数据类型是User[]
const multipleSelection = ref<User[]>([])
// 定义表格数据 -> tableData -> 数据类型是User[]
const tableData: User[] = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'John',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Morgan',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Jessy',
address: 'No. 189, Grove St, Los Angeles',
},
]
// 触发选择的时候,传入的选择的行
const toggleSelection = (rows?: User[]) => {
if (rows) {
// 遍历每一行,调用toggleRowSelection方法
rows.forEach((row) => {
multipleTableRef.value!.toggleRowSelection(row, undefined)
})
} else {
// 清空选择
multipleTableRef.value!.clearSelection()
}
}
// 清除选择
const clearSelection = () => {
multipleTableRef.value!.clearSelection()
}
// 监听多选框选中状态
const handleSelectionChange = (val: User[]) => {
multipleSelection.value = val
}
</script>
带有分页功能的
<template>
<el-table
ref="multipleTableRef"
:data="currentPageData"
row-key="id"
@selection-change="handleSelectionChange"
@select="handleSelect"
@select-all="handleSelectAll"
>
<!-- 多选列,保留选中状态 -->
<el-table-column type="selection" width="55" reserve-selection />
<!-- 数据列:日期 -->
<el-table-column label="日期" width="150" prop="date"></el-table-column>
<!-- 数据列:姓名 -->
<el-table-column label="姓名" width="150" prop="name"></el-table-column>
<!-- 数据列:地址 -->
<el-table-column label="地址" width="150" prop="address"></el-table-column>
</el-table>
<!-- 分页组件,用于切换页面 -->
<el-pagination
@current-change="handlePageChange"
:current-page="currentPage"
:page-size="pageSize"
:total="total"
/>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
// 定义用户数据接口,规范对象结构
interface User {
id?: number // 可选的唯一标识符(用于选中状态管理)
date: string // 用户的日期字段
name: string // 用户的姓名字段
address: string // 用户的地址字段
}
// 当前显示的页码,默认为第一页
const currentPage = ref(1)
// 每页展示的数据条数
const pageSize = 2
// 总数据条目数量
const total = ref(4)
// 所有用户数据,使用 ref 包裹以支持响应式更新
const allData = ref<User[]>([
{
id: 1,
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
id: 2,
date: '2016-05-02',
name: 'John',
address: 'No. 189, Grove St, Los Angeles',
},
{
id: 3,
date: '2016-05-04',
name: 'Morgan',
address: 'No. 189, Grove St, Los Angeles',
},
{
id: 4,
date: '2016-05-01',
name: 'Jessy',
address: 'No. 189, Grove St, Los Angeles',
},
])
// 存储当前选中的行数据
const selectedRows = ref<User[]>([])
// 计算属性,获取当前页需要显示的数据
const currentPageData = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return allData.value.slice(start, end)
})
// 处理分页变化事件,从而触发数据更新
const handlePageChange = (page: number) => {
currentPage.value = page
}
// 处理表格选中项变化事件(可用于日志输出或进一步操作)
const handleSelectionChange = (val: User[]) => {
console.log('当前页选中:', val)
}
// 单个行选中/取消选中处理
const handleSelect = (selection: User[], row: User) => {
updateSelectedRows(selection, row)
}
// 全选/取消全选处理
const handleSelectAll = (selection: User[]) => {
updateSelectedRows(selection)
}
// 更新选中行集合的方法
const updateSelectedRows = (selection: User[], row?: User) => {
if (row) {
// 单个选择操作
if (selection.includes(row)) {
// 如果是新增选中,确保在选中数组中不存在后再添加
if (!selectedRows.value.some(item => item.id === row.id)) {
selectedRows.value.push(row)
}
} else {
// 如果是取消选中,则从选中数组中移除
selectedRows.value = selectedRows.value.filter(item => item.id !== row.id)
}
} else {
// 全选或取消全选操作
const currentPageIds = currentPageData.value.map(item => item.id)
if (selection.length > 0) {
// 全选当前页所有数据
selection.forEach(item => {
if (!selectedRows.value.some(selected => selected.id === item.id)) {
selectedRows.value.push(item)
}
})
} else {
// 取消全选当前页数据
selectedRows.value = selectedRows.value.filter(
item => !currentPageIds.includes(item.id)
)
}
}
}
</script>
4:日期和时间date-picker
- 使用
<el-date-picker>
和<el-time-picker>
系列组件 - 一般可以进行工具类操作日期和时间的格式化问题
4.1:基本日期选择
日期的选择器使用<el-date-picker>
<template>
<!-- 对dateValue进行双向绑定 -->
<!-- type="date" -> 类型是date -->
<!-- format -> 使用的格式 -->
<!-- value-format -> 绑定的格式 -->
<el-date-picker
v-model="dateValue"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const dateValue = ref('') // 响应式声明dateValue
</script>
4.2:范围日期
<template>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
// 类型定义, 是否有值,有值就是 [string, string],没有值就是 null
type DateRange = [string, string] | null
// 创建 ref,初始值为 null, 对象是DataRange泛型
const dateRange = ref<DateRange>(null)
</script>
4.3:基本时间选择
时间选择器使用的是<el-time-picker>
<template>
<el-time-picker
v-model="timeValue"
placeholder="选择时间"
format="HH:mm:ss"
value-format="HH:mm:ss"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const timeValue = ref('')
</script>
4.4:范围时间
<template>
<el-time-picker
v-model="timeRange"
is-range
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="HH:mm:ss"
value-format="HH:mm:ss"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
type TimeRange = [string, string] | null
const timeRange = ref<TimeRange>(null)
</script>
4.5:完整日期时间选择
使用<el-date-picker>
同时在其type
属性中指定datetime
<template>
<el-date-picker
v-model="datetimeValue"
type="datetime"
placeholder="选择日期时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const datetimeValue = ref('')
</script>
4.6:范围完整日期时间
<template>
<el-date-picker
v-model="datetimeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期时间"
end-placeholder="结束日期时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
type DateTimeRange = [string, string] | null
const datetimeRange = ref<DateTimeRange>(null)
</script>
4.7:日期时间格式化
// 使用day.js
import { ElMessage } from 'element-plus'
import type { Dayjs } from 'dayjs'
const formatDate = (date: Dayjs | string): string => {
if (typeof date === 'string') {
return date
}
return date.format('YYYY-MM-DD HH:mm:ss')
}
// 可以自定义格式化
export function formatToChineseDate(dateString: string) {
const date: Date = new Date(dateString)
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
}
4.8:日期时间验证
对于验证,主要使用的是<el-form>
中的:rules
属性
需要自定义验证规则和在提交时判断是否验证通过
<template>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="预约日期" prop="reservationDate">
<el-date-picker
v-model="form.reservationDate"
type="date"
placeholder="选择预约日期"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import {ElMessage} from "element-plus";
// 定义接口 - 表单数据的格式是string的,后面用泛型约束
interface ReservationForm {
reservationDate: string
}
const formRef = ref<FormInstance>()
// 约束form的数据是上面定义的接口类型的
const form = reactive<ReservationForm>({
reservationDate: ''
})
// 声明验证规则 - 对应表单中的:rules -> 将会在用户输入数据时触发
const rules = reactive<FormRules<ReservationForm>>({
reservationDate: [
// 设置必填规则,提示信息为"请选择预约日期",触发时机为选择变化时
{ required: true, message: '请选择预约日期', trigger: 'change' },
{
// 自定义日期校验器,验证所选日期是否为过去的时间
validator: (_, value, callback) => {
if (value) {
// 将用户选择的日期转换为 Date 对象
const selectedDate = new Date(value)
// 获取当前时间并清空时分秒部分,仅保留日期对比
const today = new Date()
today.setHours(0, 0, 0, 0)
// 如果选择的日期小于今天日期,则提示错误
if (selectedDate < today) {
callback(new Error('不能选择过去的日期'))
} else {
// 校验通过
callback()
}
} else {
// 如果没有值,跳过此校验规则
callback()
}
},
// 触发时机为日期选择变化时
trigger: 'change'
}
]
})
const submitForm = () => {
// 调用 Element Plus 的表单验证方法
// 使用可选链操作符(?.)防止 formRef.value 为 undefined 时出错
formRef.value?.validate((valid) => {
// 如果 valid 为 true,表示表单通过了所有校验规则
if (valid) {
// 提交成功时显示成功的提示消息
ElMessage.success('提交成功')
}
})
}
</script>
4.9:国际化配置
在main.ts
中配置默认的语言是中文语言
// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus, {
locale: zhCn,
})
app.mount('#app')
🎉 有的时候,你直接import会报一个错误 - Could not find a declaration file for module 'element-plus/dist/locale/zh-cn.mjs'
错误,这是因为 TypeScript 无法找到对应的类型声明文件
1️⃣ 在src/typings/中创建element-plus.d.ts
文件,添加如下的内容
// src/typings/element-plus.d.ts
declare module 'element-plus/dist/locale/zh-cn.mjs' {
import { Language } from 'element-plus/es/locale'
const zhCn: Language
export default zhCn
}
declare module 'element-plus/dist/locale/en.mjs' {
import { Language } from 'element-plus/es/locale'
const en: Language
export default en
}
2️⃣ 在 tsconfig.json
文件中,确保包含了类型声明路径:
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
// 确保包含了类型声明路径:
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./src/typings"],
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
然后就会发现不会报错了【如果还是显示报错,重新启动编译器大概率能解决】
🎉 封装一个国际化语言切换的工具类
// src/utils/locale.ts
import { ref } from 'vue'
import type { Language } from 'element-plus'
// 定义可用的语言类型
type AvailableLanguage = 'zh-cn' | 'en'
// 当前语言状态 - 默认中文
const currentLanguage = ref<AvailableLanguage>('zh-cn')
// 语言映射
const languageMap: Record<AvailableLanguage, () => Promise<Language>> = {
'zh-cn': () => import('element-plus/dist/locale/zh-cn.mjs'),
'en': () => import('element-plus/dist/locale/en.mjs')
}
// 获取当前语言配置
const getElementPlusLocale = async (): Promise<Language> => {
const module = await languageMap[currentLanguage.value]()
return module.default
}
// 切换语言
const changeLanguage = (lang: AvailableLanguage) => {
currentLanguage.value = lang
}
export {
currentLanguage,
getElementPlusLocale,
changeLanguage,
type AvailableLanguage
}
<template>
<!-- el-config-provider 用于设置语言 -->
<el-config-provider :locale="currentLocale">
<!-- 应用内容 -->
<el-select v-model="language" @change="change">
<el-option label="中文" value="zh-cn"/>
<el-option label="English" value="en"/>
</el-select>
</el-config-provider>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import {changeLanguage, getElementPlusLocale} from '../utils/locale'
// 获取element-plus的locale - 设置为响应式的,默认是中文
const currentLocale = getElementPlusLocale();
// 语言选择 - 默认是中文
const language = ref('zh-cn');
// 点击切换语言 -> lang -> zh-cn | en
const change = (lang: AvailableLanguage) => {
changeLanguage(lang);
}
</script>
4.10:工具方法的封装
// utils/date.ts
import type { Dayjs } from 'dayjs'
/**
* 获取两个日期之间的天数差
*/
export const getDaysBetween = (startDate: string | Dayjs, endDate: string | Dayjs): number => {
const start = new Date(startDate)
const end = new Date(endDate)
return Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
}
/**
* 检查日期是否在范围内
*/
export const isDateInRange = (
date: string | Dayjs,
range: [string | Dayjs, string | Dayjs]
): boolean => {
const target = new Date(date)
const start = new Date(range[0])
const end = new Date(range[1])
return target >= start && target <= end
}
/**
* 格式化日期为友好显示
*/
export const formatToFriendlyDate = (date: string | Dayjs): string => {
const now = new Date()
const target = new Date(date)
const diffDays = Math.floor((now.getTime() - target.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays === 0) return '今天'
if (diffDays === 1) return '昨天'
if (diffDays === 2) return '前天'
if (diffDays < 7) return `${diffDays}天前`
return target.toLocaleDateString()
}
4.11:预约问题
结合上面的方法,可以写出一个预约系统日期时间选择的组件
<template>
<div class="reservation-system">
<h1>预约系统</h1>
<!-- 表单 -->
<!-- model -> 响应式 - 对应的form数据 -->
<!-- 表单验证 :rules -> 这里只要求必填 -->
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<!-- 表单项 -->
<!-- prop中指定对应的表单字段 -->
<el-form-item label="预约日期" prop="date">
<!-- 日期选择器 -->
<!-- disabled-date -> 表示禁用的时间范围 -->
<!-- @change -> 表示用户选择了日期后触发 -->
<el-date-picker
v-model="form.date"
type="date"
placeholder="选择日期"
:disabled-date="disabledDate"
@change="handleDateChange"
/>
</el-form-item>
<!-- 时间选择器 -->
<el-form-item label="预约时间" prop="time">
<el-time-select
v-model="form.time"
:picker-options="timeOptions"
placeholder="选择时间"
/>
</el-form-item>
<!-- 时段选择是基本下拉框,选择的是durationOptions中的数据,赋值给form的duration字段 -->
<el-form-item label="预约时段" prop="duration">
<el-select v-model="form.duration" placeholder="选择时段">
<el-option
v-for="item in durationOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- 提交按钮,触发提交事件 -->
<el-form-item>
<el-button type="primary" @click="submitForm">提交预约</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import {reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
interface ReservationForm {
date: string
time: string
duration: number
}
// form实例
const formRef = ref<FormInstance>()
// 表单结构 -> 遵循ReservationForm接口
const form = reactive<ReservationForm>({
date: '',
time: '',
duration: 1
})
// 禁用过去的日期
const disabledDate = (time: Date) => {
return time.getTime() < Date.now() - 24 * 60 * 60 * 1000
}
// 时间选择配置
const timeOptions = {
start: '08:00',
step: '00:30',
end: '18:00'
}
// 时段选项
const durationOptions = [
{ value: 1, label: '1小时' },
{ value: 2, label: '2小时' },
{ value: 3, label: '3小时' }
]
// 表单验证规则
const rules = reactive<FormRules<ReservationForm>>({
date: [
// 必填
{ required: true, message: '请选择预约日期', trigger: 'change' }
],
time: [
// 必填
{ required: true, message: '请选择预约时间', trigger: 'change' }
],
duration: [
// 必填
{ required: true, message: '请选择预约时段', trigger: 'change' }
]
})
const handleDateChange = (date: string) => {
console.log('选择的日期:', date)
// 可以在这里添加日期变化后的逻辑
}
const submitForm = () => {
formRef.value?.validate((valid) => {
if (valid) {
const reservation = {
date: form.date,
time: form.time,
duration: form.duration,
datetime: `${form.date} ${form.time}`
}
ElMessage.success(`预约成功: ${JSON.stringify(reservation)}`)
}
})
}
</script>
<style scoped>
.reservation-system {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
</style>
5:单选框相关radio
单选框涉及的组件是<el-radio>
& <el-radio-group>
& <el-radio-button>
<el-radio-group>
将一些单选框封装到一个单选框组中,里面封装的是<el-radio>
<el-radio>
基本的单选框,必须在<el-radio-group>
中<el-radio-button>
单选框按钮,正常的单选是圆圈,这个可以改成按钮样式
5.1:基本单选框
如果只是简单的列举
<template>
<div>
<el-radio-group v-model="radioValue">
<el-radio label="option1">选项1</el-radio>
<el-radio label="option2">选项2</el-radio>
<el-radio label="option3">选项3</el-radio>
</el-radio-group>
<p>当前选中的值: {{ radioValue }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const radioValue = ref('option1') // 设置默认选中项
return {
radioValue
}
}
})
</script>
如果要使用循环
<template>
<div class="radio-test">
<el-radio-group v-model="chooseItem">
<el-radio
v-for="item in data.items"
:key="item.value"
:label="item.value"
@change="hasChange"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</div>
</template>
<script setup lang="ts">
import {ref, reactive} from "vue";
const chooseItem = ref("");
const data = reactive({
items: [
{label: "选项1", value: "1",},
{label: "选项2", value: "2",},
{label: "选项3", value: "3",},
],
});
function hasChange(value) {
console.log(value);
}
</script>
5.2:带有类型定义的单选框
这里主要设计到ts约束
<template>
<!-- 单选框组, 双向绑定的数据是selectedFruit -->
<el-radio-group v-model="selectedFruit">
<!-- 单选内容是fruits数组中的选项 -->
<!-- 使用的key是fruit.value -->
<!-- :label -> 选项使用的 -->
<!-- 单选内容是fruit.label -->
<el-radio
v-for="fruit in fruits"
:key="fruit.value"
:label="fruit.value"
>
{{ fruit.label }}
</el-radio>
</el-radio-group>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
// 定义选项的类型
type Fruit = {
value: string
label: string
}
// 指定选项的值只能是 apple | banana | orange
type FruitValue = 'apple' | 'banana' | 'orange'
// 定义水果 = 水果选项的数组
const fruits: Fruit[] = [
{value: 'apple', label: '苹果'},
{value: 'banana', label: '香蕉'},
{value: 'orange', label: '橙子'}
]
const selectedFruit = ref<FruitValue>('apple') // 限制只能选择预定义的值
</script>
5.3:结合表单验证
<template>
<!-- :model="form" -> 使用的form数据 -->
<!-- :rules = "rules" -> 校验规则 -->
<!-- ref="formRef" -> 获取form组件实例 -->
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<!-- prop使用的数据是 form.gender -->
<el-form-item label="性别" prop="gender">
<!-- 双向绑定的是 form.gender -->
<!-- label是指的是给后端返回的内容 -->
<el-radio-group v-model="form.gender">
<el-radio label="male">男</el-radio>
<el-radio label="female">女</el-radio>
<el-radio label="other">其他</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
// 表单数据接口 -> 定义表单数据类型
interface FormData {
name: string
gender: string
}
// 获取表单实例
const formRef = ref<FormInstance>()
// 表单数据,归属于表单数据结构
const form = reactive<FormData>({
name: '',
gender: ''
})
// 规则约束
const rules = reactive<FormRules<FormData>>({
name: [
// name要求必填,聚焦的时候生效
{ required: true, message: '请输入姓名', trigger: 'blur' },
// 要求长度在2 ~ 5个字符
{ min: 2, max: 5, message: '长度在 2 到 5 个字符', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
]
})
// 数据提交
const submitForm = () => {
formRef.value?.validate((valid) => {
if (valid) {
ElMessage.success(`提交成功:姓名 ${form.name},性别: ${form.gender}`)
}
})
}
</script>
5.4:后端获取数据渲染
<template>
<el-radio-group v-model="selectedCity">
<!-- 循环渲染城市选项 -->
<el-radio v-for="city in cities" :key="city.id" :label="city.id">
{{ city.name }}
</el-radio>
</el-radio-group>
</template>
<script lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
interface City {
id: string
name: string
}
const cities = ref<City[]>([])
const selectedCity = ref('')
const fetchCities = async () => {
try {
// 获取城市 -> 请求接口axios.get, 获取数据
const response = await axios.get<City[]>('/api/cities')
cities.value = response.data
// 默认选中第一个城市
if (cities.value.length > 0) {
selectedCity.value = cities.value[0].id // 默认选中第一个
}
} catch (error) {
console.error('获取城市列表失败:', error)
}
}
// 组件挂载时执行获取城市列表
onMounted(() => {
fetchCities()
})
</script>
6:审核和评分相关rate
这两个设计到的比较多内容,直接演示完整例子(例子来源于deepseek,所有的内容都进行了完整注释)
审核没有涉及到额外的elementPlus组件,评分涉及到<el-rate>
6.1:项目结构如下
src/
├── components/
│ │── ReviewComponent.vue # 审核组件 - 审核父组件
│ │── ReviewDetail.vue # 审核详情 - 审核子组件
│ │── RatingComponent.vue # 评分组件 - 评分父组件
│ │── RatingDetail.vue # 评分详情 - 评分子组件
├── api/types/
│ ├── review.ts # 审核相关的数据结构定义
│ ├── rating.ts # 评分相关的数据结构定义
├── App.vue # 这里是入口组件,评分组件和审核组件都注册到这里展示
├── main.ts # 这里引入element-plus
6.2:先声明数据结构
审核数据结构 -> 审核项内容
// @/api/types/review.ts
// 审核需要那些信息?
// 定义审核状态 -> panding: 等待审核, approved: 审核通过, rejected: 审核拒绝
export type ReviewStatus = 'pending' | 'approved' | 'rejected'
// 审核列表项接口
export interface ReviewItem {
id: number // 审核项ID
title: string // 待审核项标题
author: string // 待审核项作者
submitTime: string // 提交时间
status: ReviewStatus // 审核状态 - pending: 等待审核, approved: 审核通过, rejected: 审核拒绝
content: string // 待审核项内容
}
评分项数据结构 -> 评分项,评分表单,评分详情
// @/api/types/rating.ts
// 评分都需要那些数据结构?
// 评分项数据结构
export interface RatingItem {
id: number // 评分项ID
name: string // 评分项名称
author: string // 评分项作者
averageScore: number // 评分项平均分
}
// 评分表单数据结构 -> 别人给这个评分项评分的时候弹出来的表单的数据结构
export interface RatingForm {
score: number // 评分项分数
comment: string // 评分项评价
}
// 评分项详情数据结构 -> 查看指定的评分的详情,这个详情的数据结构
export interface RatingDetail {
id: number // 评分项ID
rater: string // 评分项作者
score: number // 评分项分数
comment: string // 评分项评价
time: string // 评分项时间
}
6.3:审核组件
审核父组件
<!-- @/components/ReviewComponent.vue -->
<!-- 审核父组件 -->
<template>
<div class="review-container">
<!-- el-card -> 卡片组件,提供了整洁 & 富有层次感的容器 -->
<el-card class="review-card">
<!-- 卡片头部 -> 内容审核四个字 -->
<template #header>
<div class="card-header">
<span>内容审核</span>
</div>
</template>
<!-- 卡片的主体是表格,用el-table组件 -->
<!-- :data -> 是相应数据,使用的是reviewList数据, 下面表格项中的prop就是reviewList中的字段 -->
<!-- style="width: 100%" -> 表格宽度为100%,border -> 表格边框 -->
<el-table :data="reviewList" style="width: 100%" border>
<!-- 第一列表格项 -->
<!-- prop -> 表示数据项的字段 -->
<!-- label -> 表示数据项的标题 -->
<!-- width -> 表示数据项的宽度 -->
<el-table-column prop="id" label="ID" width="80"/>
<!-- 第二列表格项 -->
<el-table-column prop="title" label="标题"/>
<!-- 第三列表格项 -->
<el-table-column prop="author" label="作者" width="120"/>
<!-- 第四列表格项 -->
<el-table-column prop="submitTime" label="提交时间" width="180"/>
<!-- 第五列表格项 -->
<el-table-column label="状态" width="120">
<!-- 列表项模板,使用#default="{row}",拿到当前行的数据,方便下面进行函数调用的时候参数使用 -->
<template #default="{row}">
<!-- el-tag -> 标签组件,用于显示标记信息 -->
<!-- :type ->
表示标签的类型,通过getStatusTagType方法获取
这个方法是map映射,将 ReviewStatus 转换为对应的标签类型
传递的参数是row.status,表示当前行的状态
返回的结果是warning: 警告, success: 成功, danger: 危险
-->
<!--
内容是状态文本,对于状态文本的获取是通过getStatusText方法获取
这个方法是map映射,将 ReviewStatus 转换为对应的状态文本
传递的参数是row.status,表示当前行的状态
返回的结果是待审核, 已通过, 已拒绝
-->
<el-tag :type="getStatusTagType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<!-- 第六列表格项, 操作栏,这里放的是两个按钮,是通过还是拒绝?表示审核的操作 -->
<el-table-column label="操作" width="300">
<!-- 列表项模板,使用#default="{row}",拿到当前行的数据,方便下面进行函数调用的时候参数使用 -->
<template #default="{ row }">
<!-- el-button -> 按钮组件,用于显示按钮 -->
<!-- size -> 表示按钮的大小,这里设置为small,表示小按钮 -->
<!-- type -> 表示按钮的类型,这里设置为primary,表示主要按钮 -->
<!--
通过点击按钮,调用handleReview方法
传递两个参数,第一个是row.id,第二个是'approve',表示通过
-->
<!--
:disabled -> 表示按钮是否禁用,这里通过判断当前行的状态是否为待审核
如果是,则禁用按钮,即之后待审核的状态才会可以点击按钮进行操作
-->
<el-button
size="small"
type="primary"
@click="handleReview(row.id, 'approve')"
:disabled="row.status !== 'pending'"
>
通过
</el-button>
<!-- el-button -> 按钮组件,用于显示按钮 -->
<!-- size -> 表示按钮的大小,这里设置为small,表示小按钮 -->
<!-- type -> 表示按钮的类型,这里设置为primary,表示主要按钮 -->
<!--
通过点击按钮,调用handleReview方法
传递两个参数,第一个是row.id,第二个是'reject',表示拒绝
-->
<!--
:disabled -> 表示按钮是否禁用,这里通过判断当前行的状态是否为待审核
如果是,则禁用按钮,即之后待审核的状态才会可以点击按钮进行操作
-->
<el-button
size="small"
type="danger"
@click="handleReview(row.id, 'reject')"
:disabled="row.status !== 'pending'"
>
拒绝
</el-button>
<!-- el-button -> 按钮组件,用于显示按钮 -->
<!-- size -> 表示按钮的大小,这里设置为small,表示小按钮 -->
<!-- type -> 表示按钮的类型,这里设置为primary,表示主要按钮 -->
<!--
通过点击按钮,调用showReviewDetail方法
传递一个参数,row.id,表示当前行的id
其实就是将reviewDialogVisible的内容置为true,这样就能显示详情对话框了,这个对话框再调用子组件
-->
<el-button
size="small"
type="primary"
@click="showReviewDetail(row.id)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -> 用于分页 -->
<!-- :current-page ->
表示当前页码,这里绑定了pagination.currentPage
pagination.currentPage是响应式数据,所以这里会自动更新
-->
<!-- :page-size ->
表示每页显示的条数,这里绑定了pagination.pageSize
pagination.pageSize是响应式数据,所以这里会自动更新
-->
<!-- :total ->
表示总条数, 这里绑定了pagination.total
pagination.total是响应式数据,所以这里会自动更新
-->
<!-- :page-sizes ->
表示每页显示的条数,这里绑定了[5, 10, 20, 50]
这个数组是静态数据,所以这里不会自动更新
-->
<!-- layout -> 表示分页布局,这里绑定了'total, sizes, prev, pager, next, jumper'-->
<!-- @current-change ->
表示当前页码改变时,调用fetchReviewList方法
这个方法用于获取当前页码的数据
-->
<!-- @size-change ->
表示每页显示的条数改变时,调用handleSizeChange方法
这个方法用于 重置当前页码大小之后,获取当前页码的数据fetchReviewList
-->
<el-pagination
class="pagination"
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[5, 10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="fetchReviewList"
@size-change="handleSizeChange"
/>
</el-card>
<!-- 审核详情对话框, 在这个对话框中,将会调用子组件ReviewDetail -->
<el-dialog v-model="reviewDialogVisible" title="审核详情" width="50%">
<!-- :content="selectedContent", 父组件向子组件传递参数,传递的内容是selectedContent,父组件通过content接收 -->
<!-- @submit ->
自定义事件submit, 这个在子组件中将会定义和发送
然后将参数传递过来之后,将调用handleReviewSubmit
-->
<!-- @cancel ->
自定义事件cancel, 这个在子组件中将会定义和发送
然后将参数传递过来之后,将调用reviewDialogVisible = false
-->
<ReviewDetail
:content="selectedContent"
@submit="handleReviewSubmit"
@cancel="reviewDialogVisible = false"
/>
</el-dialog>
</div>
</template>
<script setup lang="ts">
// 引入vue的常用方法,ref -> 用于创建响应式数据, onMounted -> 用于在组件挂载完成后执行代码
import {onMounted, ref} from 'vue'
// 引入element-plus的常用方法,ElMessage -> 用于显示消息提示,ElMessageBox -> 用于显示对话框
import {ElMessage, ElMessageBox} from 'element-plus'
// 引入自定义的api,这里引入的是review.ts中的ReviewItem -> 审核项数据结构和审核状态
import type {ReviewItem, ReviewStatus} from '@/api/types/review'
// 引入子组件ReviewDetail, 在上面引入
import ReviewDetail from './ReviewDetail.vue'
// 审核列表数据 -> 初始化为空数组,在挂载(onMounted)之后,会调用fetchReviewList方法赋值
const reviewList = ref<ReviewItem[]>([])
// 选中的内容 -> 初始化为空字符串
// 在调用showReviewDetail方法时,会将当前行的内容赋值给selectedContent
const selectedContent = ref<string>('')
// 审核详情对话框是否可见 -> 初始化为false
// 在调用showReviewDetail方法时,会将reviewDialogVisible设置为true
const reviewDialogVisible = ref<boolean>(false)
// 分页配置 -> 初始化为一个对象,对象中有三个属性,currentPage, pageSize, total
const pagination = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
// 获取审核状态文本
// 就是一个map, key为ReviewStatus, value为对应的文本
// 如果ReviewStatus的值不在map中,则返回未知状态,在上面显示状态的时候使用
const getStatusText = (status: ReviewStatus): string => {
const statusMap: Record<ReviewStatus, string> = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝'
}
return statusMap[status] || '未知状态'
}
// 显示审核详情 -> 点击按钮时调用
// 修改reviewDialogVisible为true,就能显示对话框了
// selectedContent -> 父组件向子组件传递参数,传递的内容是当前行的内容
const showReviewDetail = (id: number) => {
// 获取当前行的数据
const review = reviewList.value.find(item => item.id === id)
// 判断当前行数据是否存在
// 如果存在,则将当前行的数据赋值给selectedContent
// 并将reviewDialogVisible设置为true
// 然后就能显示对话框了
if (review) {
selectedContent.value = review.content
reviewDialogVisible.value = true
}
}
// 获取状态标签类型
// 其实就是一个map, key为ReviewStatus, value为对应的标签类型
// 如果ReviewStatus的值不在map中,则返回空字符串,在显示标签的时候使用,定义标签的type
// 会显示不同的颜色,颜色不同,表示不同的状态
const getStatusTagType = (status: ReviewStatus): string => {
const typeMap: Record<ReviewStatus, string> = {
pending: 'warning',
approved: 'success',
rejected: 'danger'
}
return typeMap[status] || ''
}
// 获取审核列表
// 正常来说,这里是调用接口获取数据,这里模拟数据
const fetchReviewList = async () => {
try {
// 模拟数据
// 这里模拟数据,生成10条数据
// 设置数据reviewList等于生成出来的数据
reviewList.value = Array.from({length: 10}, (_, i) => ({
id: i + 1,
title: `测试内容 ${i + 1}`,
author: `作者 ${i % 3 + 1}`,
submitTime: new Date().toLocaleString(),
status: ['pending', 'approved', 'rejected'][i % 3] as ReviewStatus,
content: `这是第 ${i + 1} 条测试内容的详细内容...`
}))
// 设置分页总数total等于30
pagination.value.total = 30
} catch (error) {
// 显示错误信息
ElMessage.error('获取审核列表失败')
console.error(error)
}
}
// 处理审核操作
// 传入的是当前行的id和操作类型 -> approve: 通过, reject: 拒绝
const handleReview = async (id: number, action: 'approve' | 'reject') => {
try {
// 下面弹出确认框,根据操作类型判断是否通过审核
await ElMessageBox.confirm(
`确定要${action === 'approve' ? '通过' : '拒绝'}该内容吗?`,
'提示',
{confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'}
)
// 更新状态
// 先找到当前行, item.id === id的项就是当前行
const item = reviewList.value.find(item => item.id === id)
if (item) {
// 修改状态
item.status = action === 'approve' ? 'approved' : 'rejected'
}
ElMessage.success('操作成功')
} catch (error) {
console.error(error)
}
}
// 子组件中详情查看完成的按钮,先关闭对话框,然后显示消息,根据当前的操作类型显示不同的消息
const handleReviewSubmit = (action: 'approve' | 'reject') => {
reviewDialogVisible.value = false
ElMessage.success(`已${action === 'approve' ? '通过' : '拒绝'}审核`)
}
// 每页条数变化
// 修改分页配置,然后重新获取数据
const handleSizeChange = (size: number) => {
pagination.value.pageSize = size
fetchReviewList()
}
// 初始化加载数据,直接调用fetchReviewList方法
onMounted(() => {
fetchReviewList()
})
</script>
<style scoped>
.review-container {
padding: 20px; /* 设置内边距是20像素 */
}
.review-card {
margin-bottom: 20px; /* 设置底边的外边距是20像素 */
}
.pagination {
margin-top: 20px; /* 添加20像素的顶边外边距 */
justify-content: flex-end; /* 将分页器放在右侧 */
}
</style>
详情子组件
<template>
<div class="review-detail">
<div class="content-box">
<!-- 显示当前的内容 -->
{{ content }}
</div>
<div class="action-buttons">
<!-- 点击按钮触发自定义事件,发射给父组件,父组件根据事件名称判断是执行通过还是拒绝操作 -->
<el-button type="primary" @click="$emit('submit', 'approve')">通过</el-button>
<el-button type="danger" @click="$emit('submit', 'reject')">拒绝</el-button>
<el-button @click="$emit('cancel')">取消</el-button>
</div>
</div>
</template>
<script setup lang="ts">
// 获取父组件传递的属性
defineProps<{
content: string
}>()
// 声明自定义事件有哪些?template中将通过特殊事件发送对应的事件给父组件完成 子 -> 父
defineEmits<{
(e: 'submit', action: 'approve' | 'reject'): void
(e: 'cancel'): void
}>()
</script>
<style scoped>
.review-detail {
padding: 10px; /* 内边距10px */
}
.content-box {
padding: 15px; /* 内边距15px */
margin-bottom: 20px; /* 底部外边距20px */
border: 1px solid #eee; /* 边框1px,颜色#eee */
border-radius: 4px; /* 圆角4px */
min-height: 200px; /* 最小高度200px */
max-height: 400px; /* 最大高度400px */
overflow-y: auto; /* 垂直滚动 */
}
.action-buttons {
display: flex; /* 弹性布局 */
justify-content: flex-end; /* 按钮居右显示 */
gap: 10px; /* 按钮间距10px */
}
</style>
6.4:评分组件
评分父组件
<template>
<div class="rating-container">
<!-- el-card -> 卡片组件,提供了整洁 & 富有层次感的容器 -->
<el-card class="rating-card">
<!-- 卡片头部 -> 评分管理四个字 -->
<template #header>
<div class="card-header">
<span>评分管理</span>
</div>
</template>
<!-- 卡片的主体是表格,用el-table组件 -->
<!-- :data -> 是相应数据,使用的是ratingList数据, 下面表格项中的prop就是ratingList中的字段 -->
<!-- style="width: 100%" -> 表格宽度为100%,border -> 表格边框 -->
<el-table :data="ratingList" style="width: 100%" border>
<!-- 第一列表格项 -->
<!-- prop -> 表示数据项的字段 -->
<!-- label -> 表示数据项的标题 -->
<!-- width -> 表示数据项的宽度 -->
<el-table-column prop="id" label="ID" width="80" />
<!-- 第二列表格项 -->
<el-table-column prop="name" label="项目名称" />
<!-- 第三列表格项 -->
<el-table-column prop="author" label="作者" width="120" />
<!-- 第四列表格项 -->
<el-table-column label="平均分" width="300">
<template #default="{ row }">
<!--
el-rate -> 评分组件
v-model -> 表示当前评分
disabled -> 表示禁用
max -> 表示最大评分
show-score -> 表示显示评分
text-color -> 表示文字颜色
score-template -> 表示评分模板
-->
<el-rate
v-model="row.averageScore"
disabled
:max="10"
show-score
text-color="#ff9900"
score-template="{value}"
/>
</template>
</el-table-column>
<!-- 第五列表格项 -->
<el-table-column label="操作" width="180">
<!-- 列表项模板,使用#default="{row}",拿到当前行的数据,方便下面进行函数调用的时候参数使用 -->
<template #default="{ row }">
<!-- el-button -> 按钮组件,用于显示按钮 -->
<!-- size -> 表示按钮的大小,这里设置为small,表示小按钮 -->
<!-- type -> 表示按钮的类型,这里设置为primary,表示主要按钮 -->
<!-- 评分按钮
通过点击 按钮,调用showRatingDialog函数,传入当前行的数据,用于显示评分对话框
设置初始化数据和ratingDialogVisible.value = true
-->
<el-button size="small" type="primary" @click="showRatingDialog(row)">评分</el-button>
<!-- 详情按钮
通过点击 按钮,调用showRatingDetails函数,传入当前行的数据,用于显示评分详情对话框
设置初始化数据currentItemId.value = row.id
设置初始化数据detailDialogVisible.value = true
-->
<el-button size="small" @click="showRatingDetails(row.id)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -> 用于分页 -->
<!-- :current-page ->
表示当前页码,这里绑定了pagination.currentPage
pagination.currentPage是响应式数据,所以这里会自动更新
-->
<!-- :page-size ->
表示每页显示的条数,这里绑定了pagination.pageSize
pagination.pageSize是响应式数据,所以这里会自动更新
-->
<!-- :total ->
表示总条数, 这里绑定了pagination.total
pagination.total是响应式数据,所以这里会自动更新
-->
<!-- :page-sizes ->
表示每页显示的条数,这里绑定了[5, 10, 20, 50]
这个数组是静态数据,所以这里不会自动更新
-->
<!-- layout -> 表示分页布局,这里绑定了'total, sizes, prev, pager, next, jumper'-->
<!-- @current-change ->
表示当前页码改变时,调用fetchRatingList方法
这个方法用于获取当前页码的数据
-->
<!-- @size-change ->
表示每页显示的条数改变时,调用handleSizeChange方法
这个方法用于 重置当前页码大小之后,获取当前页码的数据fetchReviewList
-->
<el-pagination
class="pagination"
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[5, 10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="fetchRatingList"
@size-change="handleSizeChange"
/>
</el-card>
<!-- 评分对话框 -->
<el-dialog v-model="ratingDialogVisible" :title="`为 ${currentItem?.name} 评分`" width="40%">
<div class="rating-form">
<!-- el-form -> 表单组件,用于显示表单, :model -> 双向绑定使用的数据结构是ratingForm, 评分和评论 -->
<el-form :model="ratingForm" label-width="80px">
<!-- 评分项 -->
<el-form-item label="评分">
<!-- el-rate -> 评分组件 -->
<!-- v-model -> 双向绑定当前评分 -->
<el-rate
v-model="ratingForm.score"
:colors="['#99A9BF', '#F7BA2A', '#FF9900']"
:max="10"
show-text
text-template="{value}分"
/>
</el-form-item>
<!-- 评价项 -->
<el-form-item label="评价">
<!-- el-input -> 输入框组件,用于显示输入框 -->
<!-- type -> 表示输入框的类型,这里设置为textarea,表示多行输入框 -->
<!-- v-model -> 双向绑定当前评价 -->
<el-input
v-model="ratingForm.comment"
type="textarea"
:rows="4"
placeholder="请输入评价内容"
/>
</el-form-item>
</el-form>
</div>
<!-- 底部,提交按钮和取消按钮 -->
<!-- 如果是取消按钮,则直接关闭对话框 -->
<!-- 如果是提交按钮,则调用submitRating方法 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="ratingDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitRating">提交</el-button>
</span>
</template>
</el-dialog>
<!-- 评分详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="评分详情" width="60%">
<rating-detail :item-id="currentItemId" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import type { RatingItem, RatingForm } from '@/api/types/rating'
import RatingDetail from './RatingDetail.vue'
// 评分列表数据
// 使用ref定义一个响应式数据,用于存储评分列表数据,后续在挂载的时候将调用后端的API接口获取数据
const ratingList = ref<RatingItem[]>([])
// 当前选中的评分项 -> 用于处理当前的评分项用
const currentItem = ref<RatingItem | null>(null)
// 当前评分项ID -> 用于定位当前的评分项
const currentItemId = ref<number>(0)
// 分页配置
const pagination = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
// 对话框控制
const ratingDialogVisible = ref<boolean>(false)
const detailDialogVisible = ref<boolean>(false)
// 评分表单
const ratingForm = ref<RatingForm>({
score: 0,
comment: ''
})
// 获取评分列表
const fetchRatingList = async () => {
try {
// 模拟数据
const mockData: RatingItem[] = Array.from({ length: 10 }, (_, i) => ({
id: i + 1,
name: `项目 ${i + 1}`,
author: `作者 ${i % 5 + 1}`,
averageScore: Number((Math.random() * 5 + 5).toFixed(1))
}))
ratingList.value = mockData
pagination.value.total = 30
} catch (error) {
ElMessage.error('获取评分列表失败')
console.error(error)
}
}
// 显示评分对话框
// 初始化评分项等于当前选中的item, 设置评分表单的评分和评论为0和空
// 设置评分表单对话框可见
const showRatingDialog = (item: RatingItem) => {
currentItem.value = item
ratingForm.value = {
score: 0,
comment: ''
}
ratingDialogVisible.value = true
}
// 显示评分详情
// 获取当前行的数据 & 设置详情对话框可见
const showRatingDetails = (id: number) => {
currentItemId.value = id
detailDialogVisible.value = true
}
// 提交评分
const submitRating = async () => {
// 评分项不存在, 直接返回空
if (!currentItem.value) return
// 评分为0, 直接返回空
if (ratingForm.value.score === 0) {
ElMessage.warning('请先评分')
return
}
try {
// 模拟提交
ElMessage.success('评分提交成功')
// 1:关闭对话框
ratingDialogVisible.value = false
// 2:更新平均分
// 2.1:获取当前评分项
const item = ratingList.value.find(item => item.id === currentItem.value?.id)
// 在评分项存在的情况下,更新平均分
if (item) {
item.averageScore = Number(((item.averageScore + ratingForm.value.score) / 2).toFixed(1))
}
} catch (error) {
ElMessage.error('评分提交失败')
console.error(error)
}
}
// 每页条数变化
const handleSizeChange = (size: number) => {
pagination.value.pageSize = size
fetchRatingList()
}
// 初始化加载数据
onMounted(() => {
fetchRatingList()
})
</script>
<style scoped>
.rating-container {
padding: 20px;
}
.rating-card {
margin-bottom: 20px;
}
.pagination {
margin-top: 20px;
justify-content: flex-end; /* 设置分页组件在容器中的位置为右对齐 */
}
.rating-form {
padding: 0 20px; /* 添加内边距,使表单内容更靠左 */
}
</style>
评分的子组件
<template>
<div class="rating-detail">
<el-table :data="ratingDetails" style="width: 100%" border>
<el-table-column prop="rater" label="评分人" width="120" />
<el-table-column label="评分" width="150">
<template #default="{ row }">
<el-rate
v-model="row.score"
disabled
:max="10"
show-score
text-color="#ff9900"
score-template="{value}分"
/>
</template>
</el-table-column>
<el-table-column prop="comment" label="评价" />
<el-table-column prop="time" label="时间" width="180" />
</el-table>
<el-pagination
class="pagination"
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[5, 10, 20]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="fetchRatingDetails"
@size-change="handleSizeChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { RatingDetail } from '@/api/types/rating'
// 接收父组件传递的itemId
const props = defineProps<{
itemId: number
}>()
// 评分详情数据
const ratingDetails = ref<RatingDetail[]>([])
// 分页配置
const pagination = ref({
currentPage: 1,
pageSize: 5,
total: 0
})
// 获取评分详情
const fetchRatingDetails = async () => {
if (!props.itemId) return
try {
// 模拟数据
const mockData: RatingDetail[] = Array.from({ length: 5 }, (_, i) => ({
id: i + 1,
rater: `用户 ${i + 1}`,
score: Number((Math.random() * 5 + 5).toFixed(1)),
comment: ['很好', '不错', '一般', '有待改进', '很棒'][i % 5],
time: new Date().toLocaleString()
}))
ratingDetails.value = mockData
pagination.value.total = 15
} catch (error) {
ElMessage.error('获取评分详情失败')
console.error(error)
}
}
// 每页条数变化
const handleSizeChange = (size: number) => {
pagination.value.pageSize = size
fetchRatingDetails()
}
// 监听itemId变化
watch(() => props.itemId, (newVal) => {
if (newVal) {
fetchRatingDetails()
}
}, { immediate: true })
</script>
<style scoped>
.rating-detail {
padding: 10px;
}
.pagination {
margin-top: 20px;
justify-content: flex-end;
}
</style>
7:树形下拉选择器treeSelect
7.1:基本树形下拉选择
<template>
<!-- v-model="value" -> 表示双向绑定的是value -->
<!-- 默认情况下,树形选择器会自动渲染子节点,即在展开某个节点时,会自动渲染该节点的子节点。 -->
<!-- :render-after-expand="false" -> 表示在展开某个节点时,不会自动渲染该节点的子节点。 -->
<!-- :data="data" -> 表示数据源 -->
<el-tree-select
v-model="value"
:data="data"
:render-after-expand="false"
style="width: 240px"
/>
<!-- 分割线 -->
<el-divider />
<!-- 如果要显示复选框,则需要设置show-checkbox属性,默认为false,即不显示复选框。 -->
show checkbox:
<el-tree-select
v-model="value"
:data="data"
:render-after-expand="false"
show-checkbox
style="width: 240px"
/>
</template>
<script lang="ts" setup>
import { ref, onUnmounted } from 'vue'
// 定义数据源, 被选中的节点
const value = ref()
// 定义数据,就是通过children添加子节点,孩子套孩子,一层一层的
const data = [
{
value: '1',
label: 'Level one 1',
children: [
{
value: '1-1',
label: 'Level two 1-1',
children: [
{
value: '1-1-1',
label: 'Level three 1-1-1',
},
],
},
],
},
{
value: '2',
label: 'Level one 2',
children: [
{
value: '2-1',
label: 'Level two 2-1',
children: [
{
value: '2-1-1',
label: 'Level three 2-1-1',
},
],
},
{
value: '2-2',
label: 'Level two 2-2',
children: [
{
value: '2-2-1',
label: 'Level three 2-2-1',
},
],
},
],
},
{
value: '3',
label: 'Level one 3',
children: [
{
value: '3-1',
label: 'Level two 3-1',
children: [
{
value: '3-1-1',
label: 'Level three 3-1-1',
},
],
},
{
value: '3-2',
label: 'Level two 3-2',
children: [
{
value: '3-2-1',
label: 'Level three 3-2-1',
},
],
},
],
},
]
onUnmounted(() => {
if (value.value) {
console.log('被选节点的值为:', value.value)
}
console.log('组件销毁了')
})
</script>
7.2:懒加载
<template>
<!-- :load -> 懒加载当前节点的子节点,两个参数是当前节点node, 和子节点的回调solved -->
<!-- :props -> 懒加载节点的属性,默认为 { label: 'label', children: 'children', isLeaf: 'isLeaf' } -->
<!-- :cache-data -> 缓存懒加载的节点数据,用于解决懒加载时,子节点数据不变,但是父节点数据变化时,子节点数据不变的问题 -->
<el-tree-select
v-model="value"
lazy
:load="load"
:props="props"
style="width: 240px"
/>
<el-divider />
<el-tree-select
v-model="value2"
lazy
:load="load"
:props="props"
:cache-data="cacheData"
style="width: 240px"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref()
const value2 = ref(5)
const cacheData = [{ value: 5, label: 'lazy load node5' }]
const props = {
label: 'label',
children: 'children',
isLeaf: 'isLeaf',
}
let id = 0
/**
* 懒加载节点数据的方法
* @param node 当前节点对象
* @param resolve 用于返回子节点数据的回调函数
*/
const load = (node, resolve) => {
// 如果当前节点标记为叶子节点,则返回空数组表示没有子节点
if (node.isLeaf) return resolve([])
// 使用 setTimeout 模拟异步请求延迟加载数据
setTimeout(() => {
// 返回两个模拟的子节点数据
resolve([
{
// 节点值,通过自增 id 生成唯一标识
value: ++id,
// 节点显示标签
label: `lazy load node${id}`,
},
{
value: ++id,
label: `lazy load node${id}`,
// 标记此节点为叶子节点,不再触发懒加载
isLeaf: true,
},
])
}, 400) // 延迟 400 毫秒模拟网络请求
}
</script>
8:开关switch
代码非常的简单,就使用<el-switch>
即可
<template>
<el-switch v-model="value" />
</template>
<script setup>
import { ref } from 'vue'
const value = ref(true)
</script>
<el-switch>
完整的属性列表如下,这些都可以配置:
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
v-model | 绑定值 | boolean / string / number | - |
disabled | 是否禁用 | boolean | false |
size | 尺寸 (large/default/small) | string | default |
active-value | 打开时的值 | boolean / string / number | true |
inactive-value | 关闭时的值 | boolean / string / number | false |
active-text | 打开时的文字描述 | string | - |
inactive-text | 关闭时的文字描述 | string | - |
active-color | 打开时的背景色 | string | #409EFF |
inactive-color | 关闭时的背景色 | string | #C0CCDA |
9:轮播图carousel
轮播图使用的是<el-carousel>
组件
9.1:基础用法
结合使用 el-carousel
和 el-carousel-item
标签就得到了一个走马灯。
每一个页面的内容是完全可定制的,把你想要展示的内容放在 el-carousel-item
标签内。
默认情况下,在鼠标 hover 底部的指示器时就会触发切换。
通过设置 trigger
属性为 click
,可以达到点击触发的效果。
<template>
<div class="carousel-container">
<!-- el-carousel -> 声明是一个轮播图组件, 里面要轮播的内容放在el-carousel-item中 -->
<el-carousel height="400px">
<!-- el-carousel-item -> 轮播图的每一项,里面要放轮播图内容 -->
<!-- 使用 v-for 指令循环渲染轮播图项 :key="item.id" -> 绑定轮播图的唯一标识 -->
<el-carousel-item v-for="item in carouselItems" :key="item.id">
<!-- 指定轮播图项的图是那个, 用image标签 :alt -> 标识属性是图片的标题 -->
<img :src="item.imageUrl" :alt="item.title" class="carousel-image" />
<!-- 轮播图的标题和描述, 根据样式显示在哪里 -->
<div class="carousel-caption">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
</div>
</el-carousel-item>
</el-carousel>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
// 定义接口,规范结构
interface CarouselItem {
id: number
imageUrl: string
title: string
description: string
}
// 声明使用的数据 -> 响应式,并且遵循接口约束
const carouselItems = ref<CarouselItem[]>([
{
id: 1,
imageUrl: '/static/1.jpg',
title: '英雄联盟',
description: '这个是阿卡丽'
},
{
id: 2,
imageUrl: '/static/2.jpg',
title: '英雄联盟',
description: '这个是黎明瑞文和黑夜亚索'
},
])
</script>
<style scoped>
.carousel-container {
margin: 20px auto; /* 边距 */
max-width: 1200px; /* 最大宽度 */
}
.carousel-image {
width: 100%;
height: 100%; /* 图片的尺寸盛满组件 */
object-fit: cover; /* 图片填充方式 */
}
.carousel-caption {
position: absolute; /* 定位方式,绝对定位 */
bottom: 20px;
left: 20px; /* 显示在左下角 */
color: white; /* 文字颜色 */
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); /* 文字阴影 */
}
</style>
9.2:常用属性配置
在<el-carousel>
中,可以添加如下常用的属性,满足个性化的需求
<el-carousel
:interval="5000" // 自动切换时间(ms)
:autoplay="true" // 是否自动切换
:loop="true" // 是否循环播放
:height="'400px'" // 高度
:arrow="'always'" // 箭头显示时机(always/hover/never)
:indicator-position="'outside'" // 指示器位置(inside/outside/none)
:trigger="'click'" // 指示器触发方式(click/hover)
:type="'card'" // 轮播类型(card/default)
@change="handleCarouselChange" // 切换时触发的事件
>
9.3:响应式设计
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const carouselHeight = ref('400px')
// 如果是屏幕宽度小于 < 768 -> 将轮播图的高度改为250
const updateHeight = () => {
carouselHeight.value = window.innerWidth < 768 ? '250px' : '400px'
}
// 在挂载的时候调用更新高度的方法,达成响应式设计,适配不同的屏幕
onMounted(() => {
updateHeight()
// 添加事件resize, 事件绑定的方法是updateHeight
window.addEventListener('resize', updateHeight)
})
// 在结束挂载的时候,移除这个事件监听,恢复
onBeforeUnmount(() => {
window.removeEventListener('resize', updateHeight)
})
</script>
<template>
<el-carousel :height="carouselHeight">
<!-- 内容 -->
</el-carousel>
</template>
10:时间线timeline
10.1:基本时间线
<template>
<div class="timeline-container">
<el-timeline>
<!-- 循环渲染时间轴项 -->
<!-- hollow -> 属性的值,如果为true,则表示 Hollow 样式 -->
<el-timeline-item
v-for="(item, index) in timelineData"
:key="index"
:timestamp="item.timestamp"
:type="item.type"
:color="item.color"
:size="item.size"
:hollow="item.hollow"
>
{{ item.content }}
</el-timeline-item>
</el-timeline>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
// 定义接口
interface TimelineItem {
timestamp: string
content: string
// 类型枚举 -> primary | success | warning | danger | info
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
color?: string
// 尺寸枚举 -> large | normal
size?: 'large' | 'normal'
hollow?: boolean
}
// 根据TimelineItem接口定义数据数组
const timelineData = ref<TimelineItem[]>([
{
timestamp: '2023-05-01',
content: '项目启动',
type: 'primary',
size: 'large'
},
{
timestamp: '2023-05-15',
content: '需求分析完成',
type: 'success'
},
{
timestamp: '2023-06-01',
content: 'UI设计定稿',
type: 'info'
},
{
timestamp: '2023-06-20',
content: '开发阶段',
type: 'warning',
hollow: true
},
{
timestamp: '2023-07-10',
content: '测试阶段',
color: '#f50'
}
])
</script>
<style scoped>
.timeline-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
</style>
10.2:折叠时间线
<template>
<el-timeline>
<!-- 时间轴项 --->
<!-- :key="index" -> 使用index作为唯一标识 -->
<!-- placement="top" -> 时间轴项的位置,可选值有:top、bottom、left、right -->
<el-timeline-item v-for="(item, index) in timelineData" :key="index" placement="top">
<!-- 时间轴项的图标 --->
<!-- #dot -> 具名插槽v-slot:dot -> 时间轴项的图标插槽 -->
<template #dot>
<!-- 展开/折叠图标,在dot插槽中 --->
<el-icon :size="20" @click="toggleExpand(index)">
<!-- 根据item.expanded的值来显示不同的图标 -->
<component :is="item.expanded ? 'ArrowDown' : 'ArrowRight'" />
</el-icon>
</template>
<!-- 时间轴项的头部, 点击的时候触发展开和折叠的切换 --->
<!-- 这部分是始终显示的部分 -->
<div class="timeline-header" @click="toggleExpand(index)">
<span class="timestamp">{{ item.timestamp }}</span>
<span class="title">{{ item.title }}</span>
</div>
<!-- el-collapse-transition -> 折叠动画 -->
<el-collapse-transition>
<!-- 展开/折叠内容 --->
<div v-show="item.expanded" class="timeline-content">
<p>{{ item.content }}</p>
<div v-if="item.details">
<el-divider />
<!-- 详情项 --->
<p v-for="(detail, i) in item.details" :key="i">{{ detail }}</p>
</div>
</div>
</el-collapse-transition>
</el-timeline-item>
</el-timeline>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
interface TimelineItem {
timestamp: string
title: string
content: string
details?: string[]
expanded: boolean
}
const timelineData = ref<TimelineItem[]>([
{
timestamp: '2023-05-01',
title: '项目启动',
content: '项目正式启动,召开启动会议',
details: [
'参会人员:张三、李四、王五',
'会议纪要:确定项目目标和时间节点'
],
expanded: false
},
{
timestamp: '2023-05-05',
title: '需求gathering',
content: '开始gathering需求,确定项目需求',
details: [
'需求人员:张三、李四、王五',
'会议纪要:确定项目目标和时间节点'
],
expanded: false
},
])
// 切换时间线项的展开/折叠状态, 参数是时间线项的索引timelineData.value[index]
// 通过取反状态
const toggleExpand = (index: number) => {
timelineData.value[index].expanded = !timelineData.value[index].expanded
}
</script>
<style scoped>
.timeline-header {
display: flex;
align-items: center;
cursor: pointer;
padding: 8px 0;
.timestamp {
margin-right: 15px;
font-weight: bold;
color: var(--el-text-color-primary);
}
.title {
color: var(--el-text-color-regular);
}
}
.timeline-content {
padding: 10px 0 10px 30px;
}
</style>
10.3:垂直时间线
<template>
<div class="horizontal-timeline">
<!-- 水平时间轴容器 -->
<!-- 时间轴基线 -->
<div class="timeline-line"></div>
<!-- 遍历 timelineData 数据生成每个时间节点 -->
<div
v-for="(item, index) in timelineData"
:key="index"
class="timeline-item"
:style="{ left: `${index * (100 / timelineData.length)}%` }"
>
<!-- 时间节点的圆点,根据 item.type 动态设置样式 -->
<div class="timeline-dot" :class="[`dot-${item.type}`]"></div>
<!-- 时间节点的内容部分 -->
<div class="timeline-content">
<!-- 时间戳信息 -->
<div class="timeline-date">{{ item.timestamp }}</div>
<!-- 标题信息 -->
<div class="timeline-title">{{ item.title }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
interface TimelineItem {
timestamp: string
title: string
type: 'primary' | 'success' | 'warning' | 'danger' | 'info'
}
const timelineData: TimelineItem[] = [
{ timestamp: '2023-01', title: '阶段一', type: 'primary' },
{ timestamp: '2023-03', title: '阶段二', type: 'success' },
{ timestamp: '2023-06', title: '阶段三', type: 'warning' },
{ timestamp: '2023-09', title: '阶段四', type: 'danger' },
{ timestamp: '2023-12', title: '阶段五', type: 'info' }
]
</script>
<style scoped>
.horizontal-timeline {
position: relative; /* 启用绝对定位的子元素 */
height: 120px; /* 设置时间轴整体高度 */
margin: 40px 0; /* 上下外边距,为时间轴内容提供空间 */
/* 时间轴基线样式 */
.timeline-line {
position: absolute; /* 绝对定位在容器中 */
top: 20px; /* 距离顶部 20px 的位置绘制基线 */
left: 0;
right: 0; /* 横跨整个容器宽度 */
height: 4px; /* 基线高度 */
background: var(--el-border-color-light); /* 使用 Element Plus 提供的颜色变量 */
}
/* 每个时间节点的容器 */
.timeline-item {
position: absolute; /* 用于精确控制每个节点的位置 */
top: 0; /* 定位到时间轴上部 */
transform: translateX(-50%); /* 水平居中于指定的 left 百分比位置 */
text-align: center; /* 内容居中 */
width: 120px; /* 固定宽度,确保节点内容整齐显示 */
/* 时间节点圆点样式 */
.timeline-dot {
width: 16px;
height: 16px;
border-radius: 50%; /* 圆形样式 */
margin: 0 auto 8px; /* 上下外边距,让内容与圆点有一定间距 */
border: 3px solid white; /* 边框颜色统一白色 */
/* 根据 type 动态设置背景色 */
&.dot-primary { background: var(--el-color-primary); }
&.dot-success { background: var(--el-color-success); }
&.dot-warning { background: var(--el-color-warning); }
&.dot-danger { background: var(--el-color-danger); }
&.dot-info { background: var(--el-color-info); }
}
/* 时间戳文本样式 */
.timeline-date {
font-size: 12px; /* 较小字体 */
color: var(--el-text-color-secondary); /* 使用 Element Plus 的次级文字颜色 */
}
/* 标题文本样式 */
.timeline-title {
font-weight: bold; /* 加粗标题 */
}
}
}
</style>
10.4:调用后端演示
<template>
<el-timeline v-loading="loading">
<el-timeline-item
v-for="(item, index) in timelineData"
:key="index"
:timestamp="formatDate(item.createdAt)"
placement="top"
>
<!-- el-card 形成一个个的卡片 -->
<el-card>
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
<!-- 对于每一个文件,v-for遍历,形成一个el-tag -->
<div v-if="item.attachments.length" class="attachments">
<el-tag
v-for="(file, i) in item.attachments"
:key="i"
size="small"
@click="downloadFile(file)"
>
{{ file.name }} <!-- 点击的时候触发文件的下载 -->
</el-tag>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
interface TimelineItem {
id: number
title: string
description: string
createdAt: string
attachments: { // 文件列表
name: string
url: string
}[]
}
const loading = ref(false)
// 初始化数据集合为空
const timelineData = ref<TimelineItem[]>([])
// 在这个函数中完成数据的调用 -> 获取时间线数据
// 通过axios 调用后端接口,获取时间线数据
// 根据vite.config.ts中配置的代理
// 请求的url为 /api/timeLine -> http://localhost:8090/timeLine
const fetchTimelineData = async () => {
try {
loading.value = true
const response = await axios.get('api/timeLine')
console.log(response)
// 将结果中的赋值给timelineData
timelineData.value = response.data.data
} catch (error) {
ElMessage.error('获取时间线数据失败')
} finally {
loading.value = false
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString()
}
const downloadFile = (file: { url: string; name: string }) => {
window.open(file.url, '_blank')
}
onMounted(() => {
fetchTimelineData()
})
</script>
<style scoped>
.attachments {
margin-top: 10px;
.el-tag {
margin-right: 8px;
cursor: pointer;
}
}
</style>
11:菜单导航
基本的菜单栏都使用的是<el-menu>
,<el-menu-item>
和<el-sub-menu>
对于面包屑导航使用的是<el--breadcrumb>
对于标签导航使用的是
11.1:基本菜单导航实现
1️⃣ 配置router
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 定义路由数组,指定路由路径,名称和对应的组件
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
2️⃣ 菜单组件
<template>
<!-- 顶部导航栏 --->
<!-- :default-active="activeIndex" -> 默认选中的菜单项 index --->
<!-- mode="horizontal" -> 顶部导航栏 --->
<!-- 如果要设置为纵向的,mode="vertical" --->
<!-- @select="handleSelect" -> 点击菜单项触发的事件 --->
<!-- router -> 配置路由 --->
<el-menu
:default-active="activeIndex"
class="el-menu-demo"
mode="horizontal"
@select="handleSelect"
router
>
<!-- 会通过index + router跳转到对应的路由 -->
<el-menu-item index="/">首页</el-menu-item>
<el-menu-item index="/about">关于</el-menu-item>
<!-- 添加子菜单 --->
<el-sub-menu index="2">
<template #title>更多</template>
<el-menu-item index="/contact">联系我们</el-menu-item>
<el-menu-item index="/help">帮助中心</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const activeIndex = ref(route.path)
// 点击菜单项触发的事件
const handleSelect = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>
11.2:侧边栏导航实现
<template>
<el-container>
<!-- 侧边栏, 指定宽度 -->
<el-aside width="200px">
<!-- el-menu -> 声明这是一个菜单 -->
<!-- :default-active="activeMenu" -> 默认选中的菜单 -->
<!-- :collapse="isCollapse" -> 折叠菜单 -->
<!-- router -> 使用路由 -->
<el-menu
:default-active="activeMenu"
class="el-menu-vertical"
:collapse="isCollapse"
router
>
<!-- el-sub-menu -> 父级菜单 -->
<el-sub-menu index="1">
<!-- #title -> 菜单项的标题 -->
<template #title>
<!-- 图标 -->
<el-icon><location /></el-icon>
<span>导航一</span>
</template>
<!-- el-menu-item -> 菜单项 -->
<el-menu-item index="/page1">选项1</el-menu-item>
<el-menu-item index="/page2">选项2</el-menu-item>
</el-sub-menu>
<!-- el-menu-item -> 菜单项 -->
<el-menu-item index="/page3">
<el-icon><icon-menu /></el-icon>
<template #title>导航二</template>
</el-menu-item>
</el-menu>
</el-aside>
<el-main>
<router-view />
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { Location, Menu as IconMenu } from '@element-plus/icons-vue'
const route = useRoute()
// 侧边栏折叠 -> 默认是false
const isCollapse = ref(false)
// 获取当前路由
const activeMenu = computed(() => {
return route.path
})
</script>
11.3:路由守卫和权限控制
全局前置守卫 - 在router/index.ts中配置
// 引入路由相关
// createRouter 创建路由实例
// createWebHistory 创建 Web 历史模式
// RouteRecordRaw 路由配置项
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 引入用户模块
import { useUserStore } from '@/store/user'
// 定义路由元信息类型
interface RouteMeta {
title: string // 页面标题 - 必填
icon?: string // 图标 - 可选
requiresAuth?: boolean // 是否需要认证 - 可选
hidden?: boolean // 是否隐藏 - 可选
}
// 扩展路由类型
interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta' | 'children'> {
meta?: RouteMeta // 路由元信息
children?: AppRouteRecordRaw[] // 子路由
}
// 路由配置
const routes: AppRouteRecordRaw[] = [
// ...
]
// 创建路由实例, 指定历史模式,并将路由配置传递给它
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫 -> 在进入每一个路由之前,执行一些操作
// 可以理解为拦截器,过滤器之类的
// to -> 当前路由对象
// from -> 上一个路由对象
// next -> 继续导航
router.beforeEach(async (to, from, next) => {
// 获取用户状态管理实例
const userStore = useUserStore()
// 动态设置页面标题
// 如果目标路由有 meta.title,则在标题后添加 " - Vue Admin" 后缀
if (to.meta.title) {
document.title = `${to.meta.title} - Vue Admin`
}
// 路由鉴权逻辑
// 如果目标路由需要认证且用户未登录(token不存在)
if (to.meta.requiresAuth && !userStore.token) {
// 跳转到登录页,并携带当前路径作为 redirect 参数
next({
name: 'Login',
query: { redirect: to.fullPath }
})
} else {
// 否则继续导航
next()
}
})
// 导出路由实例
export default router
11.4:面包屑导航
<template>
<!-- 面包屑, 声明使用的分隔符是/ -->
<el-breadcrumb separator="/">
<!-- 遍历路由匹配结果,将meta.title添加到面包屑中 -->
<!-- :to="item.path" 表示点击该面包屑时跳转到对应的路由 -->
<!-- 遍历的item.meta.title是当前路由的meta.title -->
<el-breadcrumb-item
v-for="(item, index) in breadcrumbs"
:key="index"
:to="item.path"
>
{{ item.meta.title }}
</el-breadcrumb-item>
</el-breadcrumb>
<!-- 组件内容展示区,根据当前的组件展示对应的信息 -->
<router-view />
</template>
<script setup lang="ts">
import {computed, onMounted} from 'vue'
import {useRoute} from 'vue-router'
const route = useRoute()
// 获取面包屑 -> computed -> 计算属性
const breadcrumbs = computed(() => {
// 获取当前路由的匹配结果
return route.matched;
})
onMounted(() => {
console.log(route)
})
</script>
11.5:标签页导航
<template>
<!-- 标签页组件 -->
<!-- v-model是双向绑定的数据,绑定的内容是activeTab变量的值 -->
<!-- type="card":标签页样式为卡片样式 -->
<!-- closable:标签页是否可以关闭 -->
<!-- @tab-remove:标签页关闭时触发的事件 -->
<!-- @tab-change:标签页切换时触发的事件 -->
<el-tabs
v-model="activeTab"
type="card"
closable
@tab-remove="removeTab"
@tab-change="changeTab"
>
<!-- 标签页内容 -->
<!-- v-for循环遍历tabs数组,为每个标签页设置标题和路径 -->
<el-tab-pane
v-for="item in tabs"
:key="item.path"
:label="item.title"
:name="item.path"
/>
</el-tabs>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
// 定义标签页数据结构,包含标题和路径
interface TabItem {
title: string
path: string
}
// 获取当前路由和路由器实例
const route = useRoute()
const router = useRouter()
// 标签页集合,使用ref实现响应式数据
const tabs = ref<TabItem[]>([])
// 当前激活的标签页
const activeTab = ref('')
// 添加标签页方法
const addTab = () => {
// 解构获取当前路由的路径和元信息
const { path, meta } = route
// 判断该标签是否已存在
const exists = tabs.value.some(tab => tab.path === path)
// 如果不存在且meta中定义了title,则添加新标签
if (!exists && meta.title) {
tabs.value.push({
title: meta.title as string,
path
})
}
// 设置当前激活的标签页为当前路由路径
activeTab.value = path
}
// 移除标签页方法
const removeTab = (targetPath: string) => {
// 查找要移除的标签页索引
const index = tabs.value.findIndex(tab => tab.path === targetPath)
// 从数组中移除该标签
tabs.value.splice(index, 1)
// 如果被移除的是当前激活的标签页
if (activeTab.value === targetPath) {
// 获取最后一个标签页
const lastTab = tabs.value[tabs.value.length - 1]
// 如果还有其他标签页,则跳转到最后一个标签页
if (lastTab) {
router.push(lastTab.path)
} else {
// 否则跳转到首页
router.push('/')
}
}
}
// 切换标签页方法
const changeTab = (path: string) => {
// 路由跳转到指定路径
router.push(path)
}
// 监听路由变化,当路由路径变化时自动添加对应标签页
watch(() => route.path, addTab, { immediate: true })
</script>