【vue3】07 - 调用element组件库

【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)requireimportWebpack 支持两者
Viteimportnew 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:表格高亮
  1. 使用内置属性如 highlight-current-rowstripe 快速实现基础高亮
  2. 使用 row-class-namecell-class-name 进行条件高亮
  3. 使用 cell-style 进行动态样式设置
  4. 使用插槽完全自定义高亮内容和样式
  5. 通过 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是否禁用booleanfalse
size尺寸 (large/default/small)stringdefault
active-value打开时的值boolean / string / numbertrue
inactive-value关闭时的值boolean / string / numberfalse
active-text打开时的文字描述string-
inactive-text关闭时的文字描述string-
active-color打开时的背景色string#409EFF
inactive-color关闭时的背景色string#C0CCDA

9:轮播图carousel

轮播图使用的是<el-carousel>组件

9.1:基础用法

结合使用 el-carouselel-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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值