2024-08-09

在Element UI的el-table组件中,如果你遇到了右侧滚动条空白占位的问题,可能是由于表格内容的宽度超出了容器的宽度,导致出现水平滚动条。在这种情况下,可以通过设置el-table的max-height属性和table-layout样式属性来处理右侧的空白占位问题。

以下是一个简单的处理方式:

  1. 设置el-table的max-height属性,限制表格的最大高度,确保垂直滚动条能正常显示。
  2. 设置el-table的样式table-layout: fixed;,这样可以防止列宽度的变化导致的布局变化。

示例代码:




<template>
  <el-table
    :data="tableData"
    style="width: 100%; table-layout: fixed;"
    max-height="400"
  >
    <!-- 列定义 -->
  </el-table>
</template>
 
<script>
export default {
  data() {
    return {
      tableData: [
        // 数据列表
      ]
    };
  }
};
</script>
 
<style>
/* 可以在这里添加额外的样式 */
</style>

在这个例子中,el-table的最大高度被设置为400像素,并且table-layout属性被设置为fixed。这样,即使内容宽度超出容器宽度,水平滚动条也不会出现,而是显示垂直滚动条。

2024-08-09

在Element UI的日期时间选择器组件el-date-picker中,可以通过设置disabledDate属性来禁用特定的日期。disabledDate是一个方法,接收当前日期作为参数,并在此日期满足某些条件时返回true,表示该日期被禁用。

下面是一个例子,展示如何禁用今天之前的所有日期:




<template>
  <el-date-picker
    v-model="value"
    type="date"
    placeholder="选择日期"
    :disabled-date="disabledDate"
  ></el-date-picker>
</template>
 
<script>
export default {
  data() {
    return {
      value: ''
    };
  },
  methods: {
    disabledDate(time) {
      return time.getTime() < Date.now() - 8.64e7; // 8.64e7 毫秒数代表一天
    }
  }
};
</script>

在这个例子中,disabledDate方法返回一个布尔值,如果当前日期小于(今天日期减去一天的毫秒数),则表示该日期是禁用的。这里使用Date.now()获取当前时间的毫秒数,并减去一天的毫秒数来禁用今天之前的所有日期。您可以根据需要修改这个方法,以禁用特定的日期范围或单独的日期。

React Native Element是一个开源的跨平台React Native UI工具包,它提供了一系列可以用于构建跨平台应用UI的组件。

以下是如何在你的React Native项目中安装和使用React Native Elements的步骤:

  1. 首先,确保你的React Native环境已经安装好。
  2. 使用npm或者yarn安装React Native Elements:



npm install react-native-elements --save
# 或者
yarn add react-native-elements
  1. 由于React Native Elements需要使用到react-native-vector-icons,你可能还需要安装这个库:



npm install react-native-vector-icons --save
# 或者
yarn add react-native-vector-icons
  1. 对于iOS项目,你可能需要在Xcode中安装这些图标字体。可以通过运行以下命令来安装:



npx react-native link react-native-vector-icons
  1. 在你的React Native项目中使用React Native Elements。以下是一个简单的例子,展示如何使用Button组件:



import React from 'react';
import { View } from 'react-native';
import { Button } from 'react-native-elements';
 
const App = () => (
  <View>
    <Button
      title="Click Me"
      onPress={() => console.log('Button clicked!')}
    />
  </View>
);
 
export default App;

这个例子中,我们导入了Button组件,并在一个简单的React Native视图中创建了一个按钮。当按钮被点击时,它会在控制台中打印出一条消息。

注意:在实际开发中,你可能需要处理平台特有的样式和组件属性,但上述代码提供了一个基本的入门示例。

2024-08-09

'# Vue 项目购物车中 使用ElementUI Table 组件对列求和

一、背景与问题

在电商类 Vue 项目中,购物车功能是核心模块之一。当需要展示商品列表时,ElementUI 的 Table 组件是常用选择。但随着业务复杂度提升,用户常常需要对表格中的某一列(如价格、数量)进行求和统计。

传统做法是通过遍历数据计算总和,但这种方式存在以下问题:

  1. 数据绑定不及时:手动计算总和时,无法自动响应数据变化
  2. 计算逻辑耦合:总和计算与表格渲染逻辑耦合,违反单一职责原则
  3. 性能瓶颈:大数据量时频繁计算可能导致性能问题
  4. 可维护性差:多个地方重复计算逻辑,难以统一管理

本文将深入探讨如何在 Vue 项目中使用 ElementUI 的 Table 组件实现列求和,并分析其原理和最佳实践。


二、基本原理

ElementUI 的 Table 组件本质上是基于 Vue 的响应式系统构建的。其核心原理包括:

  1. 数据绑定:通过 v-for 渲染表格行
  2. 计算属性:使用 computed 属性进行数据处理
  3. 自定义列:通过 slot 自定义列渲染逻辑
  4. 事件处理:通过 @input 等事件监听数据变化

列求和的核心思想是:

  • 在组件内部维护一个计算属性,用于计算指定列的总和
  • 通过 watch 监听数据变化,实时更新总和
  • 利用 slot 自定义显示总和的列

三、环境准备

# 安装依赖
npm install element-ui --save

在 Vue 项目中引入 ElementUI:

// main.js
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(ElementUI)

四、核心实现

1. 基础实现:计算属性 + slot

<template>
  <el-table :data="cartItems" border>
    <el-table-column prop="name" label="商品名称"></el-table-column>
    <el-table-column prop="price" label="单价"></el-table-column>
    <el-table-column prop="quantity" label="数量"></el-table-column>
    <el-table-column label="小计" width="120">
      <template slot-scope="scope">
        {{ scope.row.price * scope.row.quantity }}
      </template>
    </el-table-column>
    <el-table-column label="总计" width="120">
      <template slot-scope="scope">
        {{ total }}
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  data() {
    return {
      cartItems: [
        { name: '商品A', price: 100, quantity: 1 },
        { name: '商品B', price: 200, quantity: 2 },
      ]
    }
  },
  computed: {
    total() {
      return this.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
    }
  }
}
</script>

关键代码解释:

  • 使用 computed 属性 total 计算总和
  • 通过 slot-scope 获取当前行数据
  • 在总计列中显示 total 计算结果

2. 带动态更新的实现:watch + 计算属性

<template>
  <el-table :data="cartItems" border>
    <el-table-column prop="name" label="商品名称"></el-table-column>
    <el-table-column prop="price" label="单价"></el-table-column>
    <el-table-column prop="quantity" label="数量"></el-table-column>
    <el-table-column label="小计" width="120">
      <template slot-scope="scope">
        {{ scope.row.price * scope.row.quantity }}
      </template>
    </el-table-column>
    <el-table-column label="总计" width="120">
      <template slot-scope="scope">
        {{ total }}
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  data() {
    return {
      cartItems: [
        { name: '商品A', price: 100, quantity: 1 },
        { name: '商品B', price: 200, quantity: 2 },
      ]
    }
  },
  computed: {
    total() {
      return this.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
    }
  }
}
</script>

关键改进:

  • 当数据变化时,计算属性会自动更新
  • 通过 watch 监听数据变化,确保总和实时更新

3. 自定义列组件实现:封装可复用组件

<template>
  <el-table :data="cartItems" border>
    <el-table-column prop="name" label="商品名称"></el-table-column>
    <el-table-column prop="price" label="单价"></el-table-column>
    <el-table-column prop="quantity" label="数量"></el-table-column>
    <el-table-column label="小计" width="120">
      <template slot-scope="scope">
        {{ scope.row.price * scope.row.quantity }}
      </template>
    </el-table-column>
    <el-table-column label="总计" width="120">
      <template slot-scope="scope">
        {{ total }}
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  data() {
    return {
      cartItems: [
        { name: '商品A', price: 100, quantity: 1 },
        { name: '商品B', price: 200, quantity: 2 },
      ]
    }
  },
  computed: {
    total() {
      return this.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
    }
  }
}
</script>

关键优势:

  • 封装成可复用的组件
  • 通过 props 传递数据
  • 更容易维护和扩展

五、完整案例:购物车功能实现

1. 数据结构设计

// cartItems 示例
[
  { 
    id: 1, 
    name: '商品A', 
    price: 100, 
    quantity: 1, 
    selected: true 
  },
  { 
    id: 2, 
    name: '商品B', 
    price: 200, 
    quantity: 2, 
    selected: true 
  }
]

2. 模板实现

<template>
  <div>
    <el-table :data="cartItems" border>
      <el-table-column prop="name" label="商品名称"></el-table-column>
      <el-table-column prop="price" label="单价"></el-table-column>
      <el-table-column prop="quantity" label="数量"></el-table-column>
      <el-table-column label="小计" width="120">
        <template slot-scope="scope">
          {{ scope.row.price * scope.row.quantity }}
        </template>
      </el-table-column>
      <el-table-column label="总计" width="120">
        <template slot-scope="scope">
          {{ total }}
        </template>
      </el-table-column>
    </el-table>
    <div>总计金额:{{ total }}</div>
  </div>
</template>

3. 脚本实现

export default {
  data() {
    return {
      cartItems: [
        { id: 1, name: '商品A', price: 100, quantity: 1, selected: true },
        { id: 2, name: '商品B', price: 200, quantity: 2, selected: true },
      ]
    }
  },
  computed: {
    total() {
      return this.cartItems.reduce((sum, item) => {
        if (item.selected) {
          return sum + item.price * item.quantity
        }
        return sum
      }, 0)
    }
  }
}

关键改进:

  • 增加了商品选择状态(selected)
  • 总和计算时过滤未选商品
  • 更符合实际购物车场景

六、源码解析

1. 计算属性原理

computed: {
  total() {
    return this.cartItems.reduce((sum, item) => {
      if (item.selected) {
        return sum + item.price * item.quantity
      }
      return sum
    }, 0)
  }
}
  • reduce 方法遍历数组
  • 每次计算时都会触发 getter 重新计算
  • 当 cartItems 发生变化时,计算属性会自动更新

2. slot-scope 机制

<template slot-scope="scope">
  {{ scope.row.price * scope.row.quantity }}
</template>
  • slot-scope 是 Vue 的作用域插槽语法
  • scope 包含当前行的 row 数据
  • 可以访问 row 的任何属性

3. 响应式更新机制

// 修改商品数量时触发更新
this.$set(this.cartItems, index, {
  ...item,
  quantity: newValue
})
  • 使用 this.$set 确保响应式更新
  • 避免直接赋值导致的响应性问题
  • 保持数据结构的 immutability

七、进阶使用

1. 多列求和

computed: {
  total() {
    return this.cartItems.reduce((sum, item) => {
      if (item.selected) {
        return sum + item.price * item.quantity
      }
      return sum
    }, 0)
  },
  totalPrice() {
    return this.cartItems.reduce((sum, item) => {
      if (item.selected) {
        return sum + item.price * item.quantity
      }
      return sum
    }, 0)
  }
}

2. 动态列名支持

<el-table-column :label="columnLabel" width="120">
  <template slot-scope="scope">
    {{ scope.row.price * scope.row.quantity }}
  </template>
</el-table-column>

3. 表格分页处理

computed: {
  paginatedItems() {
    const start = this.pageSize * this.currentPage
    const end = start + this.pageSize
    return this.cartItems.slice(start, end)
  }
}

八、性能与工程实践

1. 性能优化

大数据量处理:

  • 使用分页(pageSize + currentPage)
  • 使用虚拟滚动(vue-virtual-scroll-list)
  • 使用防抖/节流处理用户输入

优化建议:

// 防抖处理
this.debouncedUpdate = _.debounce(() => {
  this.updateTotal()
}, 300)

2. 异常处理

computed: {
  total() {
    try {
      return this.cartItems.reduce((sum, item) => {
        if (item.selected) {
          return sum + item.price * item.quantity
        }
        return sum
      }, 0)
    } catch (e) {
      console.error('计算总和时出错:', e)
      return 0
    }
  }
}

3. 安全考虑

数据校验:

beforeUpdate() {
  this.cartItems.forEach(item => {
    if (typeof item.price !== 'number' || typeof item.quantity !== 'number') {
      console.error('数据类型不正确:', item)
    }
  })
}

4. 工程实践

组件封装:

<template>
  <el-table :data="items" border>
    <el-table-column prop="name" label="商品名称"></el-table-column>
    <el-table-column prop="price" label="单价"></el-table-column>
    <el-table-column prop="quantity" label="数量"></el-table-column>
    <el-table-column label="小计" width="120">
      <template slot-scope="scope">
        {{ scope.row.price * scope.row.quantity }}
      </template>
    </el-table-column>
    <el-table-column label="总计" width="120">
      <template slot-scope="scope">
        {{ total }}
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true
    }
  },
  computed: {
    total() {
      return this.items.reduce((sum, item) => {
        if (item.selected) {
          return sum + item.price * item.quantity
        }
        return sum
      }, 0)
    }
  }
}
</script>

九、常见问题与踩坑

1. 数据更新不及时

错误示例:

this.cartItems = this.cartItems.map(item => ({
  ...item,
  quantity: item.quantity + 1
}))

问题分析:

  • 直接赋值会导致 Vue 的响应式系统无法检测到变化
  • 导致计算属性 total 不更新

解决方案:

this.$set(this.cartItems, index, {
  ...item,
  quantity: newValue
})

2. 计算错误

错误示例:

total: function() {
  return this.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
}

问题分析:

  • 如果 quantity 是字符串类型,会导致计算错误
  • 如果 price 是 null,也会导致 NaN

解决方案:

total: function() {
  return this.cartItems.reduce((sum, item) => {
    const price = parseFloat(item.price) || 0
    const quantity = parseInt(item.quantity) || 0
    return sum + price * quantity
  }, 0)
}

3. 性能问题

错误示例:

watch: {
  cartItems: {
    deep: true,
    handler() {
      this.calculateTotal()
    }
  }
}

问题分析:

  • 频繁触发 calculateTotal 会导致性能问题
  • 特别是在大数据量时

解决方案:

watch: {
  cartItems: {
    deep: true,
    handler() {
      this.debouncedCalculateTotal()
    }
  }
}

十、最佳实践

1. 推荐使用场景

  • 购物车/结算页的金额统计
  • 财务报表的金额汇总
  • 配置表单的参数累计
  • 数据统计面板的数值汇总

2. 不推荐使用场景

  • 需要动态计算的复杂数学运算
  • 需要实时计算的场景(如股票行情)
  • 数据量极大且需要频繁更新的场景
  • 需要多维度统计的场景(如多条件筛选)

3. 推荐方案

  • 使用 computed 属性进行计算
  • 使用 slot-scope 自定义列展示
  • 对大数据量使用分页处理
  • 对关键计算使用防抖/节流优化
  • 对敏感数据进行校验和转换

4. 工程实践建议

  • 封装成可复用组件
  • 保持计算逻辑与展示逻辑分离
  • 使用 TypeScript 增强类型安全
  • 对关键计算进行单元测试

十一、总结

在 Vue 项目中使用 ElementUI 的 Table 组件实现列求和,需要深入理解其响应式机制和计算属性原理。通过合理使用计算属性、slot-scope 和响应式更新机制,可以实现高效、可维护的列求和功能。

实际开发中,应根据具体业务需求选择合适的实现方案,注意处理大数据量时的性能优化,同时做好异常处理和数据校验。通过封装可复用组件,可以提升代码的可维护性和复用性。

希望本文能帮助你更好地在 Vue 项目中实现列求和功能,同时避免常见的陷阱和性能问题。

2024-08-09

'# Vue ElementPlus el-card、el-input 登入页面

一、背景与问题

在现代Web开发中,用户认证系统是核心功能模块之一。ElementPlus作为基于Vue 3的组件库,其el-card和el-input组件在构建登录页面时具有天然优势。但开发过程中常遇到以下问题:

  1. 表单验证机制不完善导致用户提交错误数据
  2. 布局样式在不同设备上显示异常
  3. 输入验证规则与业务逻辑脱节
  4. 组件间状态管理混乱
  5. 安全性漏洞(如XSS攻击)

本文将通过深度技术分析,结合真实开发场景,探讨如何构建一个健壮的登录系统。

二、基本原理

1. 布局机制

el-card组件采用Flex布局实现卡片式设计,其内部结构包含:

<el-card>
  <template #header>
    <div class="card-header">登录</div>
  </template>
  <el-form>
    <!-- 表单项 -->
  </el-form>
</el-card>

其关键特性包括:

  • 响应式布局(通过el-card__body的padding自动调整)
  • 内容区域支持任意HTML内容
  • 自动计算高度(height属性支持百分比)

2. 输入处理机制

el-input组件通过以下机制处理用户输入:

  • 响应式数据绑定(v-model)
  • 输入校验(rules属性)
  • 事件处理(@input、@blur等)
  • 输入过滤(type属性控制输入类型)

其内部使用Vue的Proxy实现数据响应,当输入值变化时会触发更新。

三、环境准备

npm install -g vue-cli
vue create login-system
cd login-system
npm install element-plus --save

项目结构建议:

src/
├── assets/             # 静态资源
├── components/        # 业务组件
├── views/             # 页面组件
│   └── LoginView.vue  # 登录页面
├── utils/             # 工具函数
├── store/            # 状态管理
└── main.js            # 入口文件

四、核心实现

1. 基础登录表单

<template>
  <el-card class="login-card">
    <el-form :model="loginForm" :rules="rules" ref="loginFormRef">
      <el-form-item label="用户名" prop="username">
        <el-input v-model="loginForm.username" />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input 
          v-model="loginForm.password" 
          type="password"
          show-password
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="submitForm">登录</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const loginForm = ref({
      username: '',
      password: ''
    })
    
    const rules = ref({
      username: [
        { required: true, message: '请输入用户名', trigger: 'blur' },
        { min: 3, max: 15, message: '长度在3到15个字符', trigger: 'blur' }
      ],
      password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 6, message: '密码至少6位', trigger: 'blur' }
      ]
    })
    
    const submitForm = () => {
      // 简单的表单验证逻辑
      if (!loginForm.value.username || !loginForm.value.password) {
        alert('请输入用户名和密码')
        return
      }
      // 模拟登录逻辑
      alert('登录成功')
    }
    
    return {
      loginForm,
      rules,
      submitForm
    }
  }
}
</script>

<style scoped>
.login-card {
  width: 400px;
  margin: 100px auto;
  padding: 20px;
}
</style>

关键代码解释:

  • 使用ref创建响应式对象
  • 通过rules定义验证规则
  • show-password属性控制密码显示切换
  • @click事件绑定提交逻辑

2. 高级验证逻辑

const validateUsername = (rule, value, callback) => {
  if (!value) {
    callback(new Error('用户名不能为空'))
  } else if (!/^[a-zA-Z0-9]+$/.test(value)) {
    callback(new Error('用户名只能包含字母和数字'))
  } else {
    callback()
  }
}

const validatePassword = (rule, value, callback) => {
  if (!value) {
    callback(new Error('密码不能为空'))
  } else if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{6,}$/.test(value)) {
    callback(new Error('密码需包含大小写字母和数字'))
  } else {
    callback()
  }
}

3. 状态管理方案

// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    user: null,
    token: null
  },
  mutations: {
    setUser(state, user) {
      state.user = user
    },
    setToken(state, token) {
      state.token = token
    }
  },
  actions: {
    async login({ commit }, credentials) {
      // 模拟API调用
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(credentials)
      })
      
      if (response.ok) {
        const data = await response.json()
        commit('setUser', data.user)
        commit('setToken', data.token)
        return true
      }
      return false
    }
  }
})

五、完整案例

1. 登录页面完整实现

<template>
  <el-card class="login-card">
    <el-form 
      :model="loginForm" 
      :rules="rules" 
      ref="loginFormRef"
      @submit.prevent="submitForm"
    >
      <el-form-item label="用户名" prop="username">
        <el-input v-model="loginForm.username" />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input 
          v-model="loginForm.password" 
          type="password"
          show-password
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="submitForm">登录</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script>
import { ref } from 'vue'
import { useStore } from 'vuex'

export default {
  setup() {
    const loginForm = ref({
      username: '',
      password: ''
    })
    
    const rules = ref({
      username: [
        { required: true, message: '请输入用户名', trigger: 'blur' },
        { min: 3, max: 15, message: '长度在3到15个字符', trigger: 'blur' }
      ],
      password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 6, message: '密码至少6位', trigger: 'blur' }
      ]
    })
    
    const store = useStore()
    
    const submitForm = () => {
      if (!loginForm.value.username || !loginForm.value.password) {
        alert('请输入用户名和密码')
        return
      }
      
      // 模拟登录逻辑
      store.dispatch('login', {
        username: loginForm.value.username,
        password: loginForm.value.password
      }).then(() => {
        alert('登录成功')
        // 跳转到首页
        this.$router.push('/')
      }).catch(() => {
        alert('登录失败')
      })
    }
    
    return {
      loginForm,
      rules,
      submitForm
    }
  }
}
</script>

2. 网络请求封装

// utils/api.js
export async function login(credentials) {
  const response = await fetch('/api/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(credentials)
  })
  
  if (!response.ok) {
    throw new Error('登录失败')
  }
  
  return await response.json()
}

六、源码解析

1. el-input 组件源码分析

ElementPlus的el-input组件内部使用了以下关键技术:

  • 使用v-model实现双向绑定
  • 通过@input事件处理输入变化
  • 支持type属性控制输入类型(文本、密码等)
  • 内部使用el-input__inner作为输入框容器

关键代码片段(简化版):

export default {
  name: 'ElInput',
  props: {
    modelValue: String,
    type: {
      type: String,
      default: 'text'
    }
  },
  methods: {
    handleInput(value) {
      this.$emit('update:modelValue', value)
    }
  }
}

2. 表单验证机制

ElementPlus的el-form组件通过以下方式实现验证:

  1. 维护一个rules对象,存储验证规则
  2. 在表单提交时触发验证
  3. 对每个el-form-item进行验证
  4. 显示错误提示信息

七、进阶使用

1. 国际化支持

// locale/zh-CN.js
export default {
  login: {
    username: '用户名',
    password: '密码',
    submit: '登录'
  }
}

2. 与Vuex集成

// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    user: null
  },
  mutations: {
    setUser(state, user) {
      state.user = user
    }
  }
})

3. 使用第三方库

// utils/validator.js
import { required, minLength, maxLength } from 'vuelidate'

export const loginRules = {
  username: [required, minLength(3), maxLength(15)],
  password: [required, minLength(6)]
}

八、性能与工程实践

1. 性能优化

  1. 避免不必要的计算:使用v-once标记静态内容
  2. 减少组件重新渲染:使用v-show代替v-if进行条件渲染
  3. 使用懒加载:对非关键内容使用v-lazy指令
  4. 优化输入验证:使用正则表达式预校验

2. 异常处理

try {
  await store.dispatch('login', credentials)
} catch (error) {
  console.error('登录异常:', error)
  alert('登录失败')
}

3. 安全性考虑

  1. 对用户输入进行过滤:

    const sanitizedUsername = sanitizeHTML(loginForm.username)
  2. 使用HTTPS进行数据传输
  3. 对敏感数据进行加密存储
  4. 防止XSS攻击:

    const safeUsername = encodeURIComponent(loginForm.username)

九、常见问题与踩坑

1. 常见错误

问题原因解决方案
表单验证不生效未正确绑定model属性检查v-model绑定是否正确
密码显示功能失效未正确设置type属性确认show-password属性是否正确使用
布局错位未设置宽度使用width属性或CSS设置宽度
状态未更新未使用响应式对象使用ref创建响应式对象

2. 常见坑点

  1. 未正确使用ref:

    // 错误示例
    const loginForm = {}
    
    // 正确示例
    const loginForm = ref({})
  2. 验证规则未触发:

    // 错误示例
    <el-form-item prop="username">
    
    // 正确示例
    <el-form-item label="用户名" prop="username">
  3. 未处理异步验证:

    // 错误示例
    async function validate() {
      await new Promise(resolve => setTimeout(resolve, 1000))
    }
    
    // 正确示例
    async function validate() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      return true
    }

十、最佳实践

  1. 使用响应式对象:始终使用ref创建响应式数据
  2. 分离验证逻辑:将验证规则封装为独立模块
  3. 合理使用组件:对复杂表单使用el-form组件
  4. 保持代码简洁:避免过度使用计算属性
  5. 注意安全风险:对用户输入进行过滤和验证
  6. 优化用户体验:添加加载状态和错误提示
  7. 遵循设计规范:保持UI风格与ElementPlus一致

十一、总结

通过本文的深入探讨,我们了解到如何利用ElementPlus的el-card和el-input组件构建一个安全、高效的登录系统。在开发过程中需要特别注意:

  • 正确使用响应式系统和表单验证机制
  • 合理处理用户输入和数据验证
  • 考虑安全性问题(如XSS攻击)
  • 优化用户体验(如加载状态提示)
  • 遵循最佳实践保持代码可维护性

在实际项目中,这种方案适用于需要快速构建美观登录界面的场景,但需要注意:

  • 不适合需要高度定制化输入控件的场景
  • 不适合对性能有极高要求的场景
  • 不适合需要复杂验证逻辑的场景

通过合理使用ElementPlus组件和Vue 3的特性,我们可以构建出既美观又健壮的登录系统。

2024-08-09

'# ElementUI中el-table表格列宽自适应以及封装

一、背景与问题

在复杂的业务场景中,表格组件经常需要适应不同的数据展示需求。ElementUI的el-table组件虽然功能强大,但其默认的列宽设置存在两个典型问题:

  1. 固定列宽的局限性:在数据量大或内容长度不确定时,固定列宽容易导致内容溢出或空白过多
  2. 动态调整需求:业务场景中常需要根据数据内容自动调整列宽,如动态展示图表数据、多语言支持等

传统解决方案往往通过设置min-width或max-width属性,但这在复杂场景中难以满足需求。本文将深入探讨如何实现基于内容的自动列宽调整,并通过封装组件实现可复用的解决方案。

二、基本原理

ElementUI的el-table底层使用的是<table>元素,其列宽控制主要依赖以下机制:

  1. 列宽计算:通过el-table-column的width属性定义初始宽度
  2. DOM布局:表格通过<tr>和<td>元素构建布局,列宽受table-layout: fixed或auto影响
  3. 动态调整:需要通过JS获取DOM节点,计算内容宽度并更新列宽

关键实现原理如下:

// 获取表格DOM元素
const table = this.$refs.table.$el;

// 计算列宽的函数
function calculateColumnWidth() {
  const headers = table.querySelectorAll('th');
  const rows = table.querySelectorAll('tr');
  
  headers.forEach((header, index) => {
    let maxWidth = 0;
    
    // 计算该列所有行的最大宽度
    rows.forEach(row => {
      const cell = row.children[index];
      maxWidth = Math.max(maxWidth, cell.offsetWidth);
    });
    
    // 设置列宽
    header.style.width = `${maxWidth}px`;
  });
}

三、环境准备

npm install element-ui --save

需要引入ElementUI并创建Vue实例:

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI);

四、核心实现

1. 基础列宽自适应

<template>
  <el-table ref="table" :data="tableData" border>
    <el-table-column prop="name" label="姓名" />
    <el-table-column prop="address" label="地址" />
  </el-table>
</template>

<script>
export default {
  data() {
    return {
      tableData: [
        { name: '张三', address: '北京市朝阳区' },
        { name: '李四', address: '上海市浦东新区' }
      ]
    };
  },
  mounted() {
    this.calculateColumnWidth();
  },
  methods: {
    calculateColumnWidth() {
      const table = this.$refs.table.$el;
      const headers = table.querySelectorAll('th');
      const rows = table.querySelectorAll('tr');
      
      headers.forEach((header, index) => {
        let maxWidth = 0;
        
        rows.forEach(row => {
          const cell = row.children[index];
          maxWidth = Math.max(maxWidth, cell.offsetWidth);
        });
        
        header.style.width = `${maxWidth}px`;
      });
    }
  }
};
</script>

关键点解释:

  • 使用ref获取表格DOM
  • 遍历所有行计算最大宽度
  • 动态设置style.width实现自适应

2. 带固定列的自适应

<template>
  <el-table ref="table" :data="tableData" border>
    <el-table-column type="index" width="50" />
    <el-table-column prop="name" label="姓名" />
    <el-table-column prop="address" label="地址" />
  </el-table>
</template>

在计算时需要排除固定列:

calculateColumnWidth() {
  const table = this.$refs.table.$el;
  const headers = table.querySelectorAll('th');
  const rows = table.querySelectorAll('tr');
  const fixedColumns = 1; // 固定列数量
  
  headers.forEach((header, index) => {
    if (index < fixedColumns) return;
    
    let maxWidth = 0;
    
    rows.forEach(row => {
      const cell = row.children[index];
      maxWidth = Math.max(maxWidth, cell.offsetWidth);
    });
    
    header.style.width = `${maxWidth}px`;
  });
}

3. 响应式布局

<template>
  <el-table ref="table" :data="tableData" border>
    <el-table-column prop="name" label="姓名" />
    <el-table-column prop="address" label="地址" />
  </el-table>
</template>

配合ResizeObserver实现动态响应:

mounted() {
  this.calculateColumnWidth();
  
  const observer = new ResizeObserver(entries => {
    if (entries.length) {
      this.calculateColumnWidth();
    }
  });
  
  observer.observe(this.$refs.table.$el);
}

五、完整案例

1. 项目结构

src/
├── components/
│   └── AutoWidthTable.vue
├── views/
│   └── DemoPage.vue
└── App.vue

2. 自封装组件(AutoWidthTable.vue)

<template>
  <el-table ref="table" :data="tableData" border>
    <el-table-column v-for="(col, index) in columns" 
                     :key="index" 
                     :prop="col.prop" 
                     :label="col.label">
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  name: 'AutoWidthTable',
  props: {
    columns: {
      type: Array,
      default: () => []
    },
    data: {
      type: Array,
      default: () => []
    }
  },
  mounted() {
    this.calculateColumnWidth();
    
    const observer = new ResizeObserver(entries => {
      if (entries.length) {
        this.calculateColumnWidth();
      }
    });
    
    observer.observe(this.$refs.table.$el);
  },
  methods: {
    calculateColumnWidth() {
      const table = this.$refs.table.$el;
      const headers = table.querySelectorAll('th');
      const rows = table.querySelectorAll('tr');
      const fixedColumns = 0; // 可配置固定列数
      
      headers.forEach((header, index) => {
        if (index < fixedColumns) return;
        
        let maxWidth = 0;
        
        rows.forEach(row => {
          const cell = row.children[index];
          maxWidth = Math.max(maxWidth, cell.offsetWidth);
        });
        
        header.style.width = `${maxWidth}px`;
      });
    }
  }
};
</script>

3. 使用示例(DemoPage.vue)

<template>
  <div>
    <AutoWidthTable 
      :columns="columns" 
      :data="tableData" 
    />
  </div>
</template>

<script>
import AutoWidthTable from './components/AutoWidthTable.vue';

export default {
  components: { AutoWidthTable },
  data() {
    return {
      columns: [
        { prop: 'name', label: '姓名' },
        { prop: 'address', label: '地址' }
      ],
      tableData: [
        { name: '张三', address: '北京市朝阳区' },
        { name: '李四', address: '上海市浦东新区' }
      ]
    };
  }
};
</script>

六、源码解析

  1. DOM操作:通过querySelectorAll获取表头和行元素,遍历计算最大宽度
  2. 响应式监听:使用ResizeObserver监听容器尺寸变化,实现动态调整
  3. 性能优化:通过maxWidth计算避免重复计算,减少DOM操作次数

关键优化点:

let maxWidth = 0;
rows.forEach(row => {
  const cell = row.children[index];
  maxWidth = Math.max(maxWidth, cell.offsetWidth);
});

七、进阶使用

1. 自动收缩列宽

function calculateColumnWidth() {
  const table = this.$refs.table.$el;
  const headers = table.querySelectorAll('th');
  const rows = table.querySelectorAll('tr');
  const fixedColumns = 0;
  
  headers.forEach((header, index) => {
    if (index < fixedColumns) return;
    
    let maxWidth = 0;
    let minWidth = 100; // 最小列宽
    
    rows.forEach(row => {
      const cell = row.children[index];
      maxWidth = Math.max(maxWidth, cell.offsetWidth);
    });
    
    // 自动收缩
    header.style.width = `${Math.min(maxWidth, 300)}px`;
  });
}

2. 响应式断点处理

mounted() {
  this.calculateColumnWidth();
  
  const observer = new ResizeObserver(entries => {
    if (entries.length) {
      const width = entries[0].contentRect.width;
      if (width < 600) {
        this.calculateColumnWidth();
      }
    }
  });
  
  observer.observe(this.$refs.table.$el);
}

八、性能与工程实践

1. 性能优化策略

优化点解决方案
频繁重绘使用requestAnimationFrame
大数据量使用虚拟滚动技术
网络延迟延迟计算,使用防抖函数
function debounce(func, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => func.apply(this, args), delay);
  };
}

2. 异常处理

calculateColumnWidth() {
  try {
    const table = this.$refs.table.$el;
    if (!table) return;
    
    const headers = table.querySelectorAll('th');
    const rows = table.querySelectorAll('tr');
    
    headers.forEach((header, index) => {
      // 异常处理逻辑
    });
  } catch (error) {
    console.error('列宽计算异常:', error);
  }
}

3. 安全性考量

  • 避免用户输入导致的无限宽
  • 对特殊字符进行转义处理
  • 设置最大宽度限制
header.style.width = `${Math.min(maxWidth, 500)}px`;

九、常见问题与踩坑

1. 常见错误

错误示例:

headers.forEach((header, index) => {
  header.style.width = `${cell.offsetWidth}px`;
});

问题:直接设置宽度可能导致布局抖动

解决:使用maxWidth计算并设置最大宽度

2. 兼容性问题

问题:部分浏览器不支持ResizeObserver

解决方案:使用polyfill库如resize-observer-polyfill

3. 表格滚动问题

问题:表格滚动时列宽未更新

解决:监听scroll事件并重新计算

window.addEventListener('resize', this.calculateColumnWidth);

十、最佳实践

  1. 使用场景:

    • 需要动态调整列宽的业务场景
    • 多语言支持的国际化场景
    • 数据内容长度不确定的展示场景
  2. 避免使用场景:

    • 需要固定列宽的展示场景
    • 数据量极大(超过5000行)的表格
    • 需要精确对齐的财务表格
  3. 推荐方案:

    • 对于常规场景:使用本方案实现自适应
    • 对于大数据量:结合虚拟滚动技术
    • 对于固定列宽:保持默认设置

十一、总结

ElementUI的el-table列宽自适应是一个涉及DOM操作、响应式布局和性能优化的综合问题。通过深入分析其工作原理,我们可以实现更灵活的表格展示方案。本文提供的封装方案具有以下特点:

  • 可复用性:通过组件封装实现业务复用
  • 灵活性:支持固定列、响应式断点等扩展
  • 稳定性:包含异常处理和性能优化

在实际项目中,应根据具体需求选择合适的方案。对于需要动态调整列宽的场景,建议采用本方案;对于固定布局或大数据量场景,应考虑其他优化方案。通过合理的设计和实现,可以显著提升表格组件的使用体验。

2024-08-09

'# Element Plus快速开始(Vue 3.x)

一、背景与问题

Element Plus 是基于 Vue 3 的组件库,其核心目标是为开发者提供一套完整、美观且功能丰富的 UI 组件。在现代前端开发中,快速构建界面是提升开发效率的关键,但传统的手动编写 HTML/CSS/JS 会导致重复代码和维护困难。Element Plus 通过组件化设计,结合 Vue 3 的响应式系统和 Composition API,为开发者提供了高效的开发方案。

然而,开发者在使用 Element Plus 时可能会遇到以下问题:

  1. 组件样式未正确加载,导致界面异常
  2. 响应式数据绑定失效
  3. 复杂组件(如表格)的交互逻辑难以实现
  4. 性能优化不足导致页面卡顿
  5. 跨平台兼容性问题

二、基本原理

Element Plus 的核心原理基于 Vue 3 的响应式系统和 Composition API。其底层通过以下技术实现:

  1. 响应式系统:利用 Vue 3 的 reactive 和 ref 实现数据绑定
  2. 组件化架构:通过封装可复用的 UI 元素,降低耦合度
  3. 样式系统:基于 CSS-in-JS 和 CSS Modules 实现样式隔离
  4. 事件系统:通过事件总线和自定义事件实现组件通信
  5. 动画系统:基于 CSS 动画和 JavaScript 动画实现过渡效果

其核心架构包含:

  • 顶层组件(如 el-container)
  • 基础组件(如 el-button)
  • 复杂组件(如 el-table)
  • 全局样式管理
  • 按需加载机制

三、环境准备

1. 项目创建

使用 Vue CLI 创建项目:

npm install -g @vue/cli
vue create element-plus-demo

2. 安装依赖

cd element-plus-demo
npm install element-plus --save
npm install sass --save-dev

3. 配置样式

在 main.js 中引入全局样式:

import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

createApp(App)
  .use(ElementPlus)
  .mount('#app')

4. 配置按需加载(可选)

安装 unplugin-vue-components:

npm install -D unplugin-vue-components

在 vite.config.js 中配置:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'

export default defineConfig({
  plugins: [
    vue(),
    Components({ 
      dirs: ['src/components'],
      extensions: ['vue'],
    }),
  ],
})

四、核心实现

1. 基础组件使用

<template>
  <el-button type="primary" @click="handleClick">点击我</el-button>
  <el-input v-model="inputValue" placeholder="请输入内容" />
  <el-table :data="tableData" border style="width: 100%">
    <el-table-column prop="date" label="日期" width="180" />
    <el-table-column prop="name" label="姓名" width="180" />
    <el-table-column prop="address" label="地址" />
  </el-table>
</template>

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

const inputValue = ref('')
const tableData = ref([
  { date: '2023-04-01', name: '张三', address: '上海市' },
  { date: '2023-04-02', name: '李四', address: '北京市' }
])

const handleClick = () => {
  alert('按钮被点击了')
}
</script>

关键代码解释:

  • v-model:双向绑定实现数据同步
  • ref:创建响应式数据
  • el-table:使用 Vue 3 的响应式数组自动更新

2. 响应式数据绑定

<template>
  <el-card>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-input v-model="inputValue" placeholder="输入内容" />
      </el-col>
      <el-col :span="12">
        <el-tag :type="inputValue.length > 5 ? 'success' : 'info'">
          {{ inputValue }}
        </el-tag>
      </el-col>
    </el-row>
  </el-card>
</template>

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

const inputValue = ref('')
</script>

关键点:

  • el-row 和 el-col 实现栅格布局
  • 动态样式绑定实现条件渲染
  • 响应式变量自动更新 DOM

3. 表单验证(高级用法)

<template>
  <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
    <el-form-item label="用户名" prop="username">
      <el-input v-model="form.username" />
    </el-form-item>
    <el-form-item label="密码" prop="password">
      <el-input v-model="form.password" type="password" />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submitForm">提交</el-button>
    </el-form-item>
  </el-form>
</template>

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

const form = ref({
  username: '',
  password: ''
})

const rules = ref({
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 10, message: '长度在3到10个字符', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, message: '至少6个字符', trigger: 'blur' }
  ]
})

const formRef = ref()

const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      alert('提交成功')
    } else {
      alert('验证失败')
    }
  })
}
</script>

关键点:

  • 使用 ref 捕获表单引用
  • 验证规则通过 rules 对象配置
  • 表单提交通过 validate 方法触发

五、完整案例

1. 项目结构

src/
├── components/
│   └── UserForm.vue
├── views/
│   └── Dashboard.vue
├── App.vue
└── main.js

2. 用户表单组件(UserForm.vue)

<template>
  <el-dialog v-model="dialogVisible" title="用户信息" width="50%">
    <el-form :model="form" :rules="rules" ref="formRef">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="用户名" prop="username">
            <el-input v-model="form.username" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="邮箱" prop="email">
            <el-input v-model="form.email" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-form-item>
        <el-button @click="closeDialog">取消</el-button>
        <el-button type="primary" @click="submitForm">提交</el-button>
      </el-form-item>
    </el-form>
  </el-dialog>
</template>

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

const dialogVisible = ref(false)
const form = ref({
  username: '',
  email: ''
})
const rules = ref({
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 10, message: '长度在3到10个字符', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '请输入邮箱', trigger: 'blur' },
    { type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
  ]
})
const formRef = ref()

const openDialog = () => {
  dialogVisible.value = true
}

const closeDialog = () => {
  dialogVisible.value = false
  form.value = { username: '', email: '' }
}

const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      alert('提交成功')
      closeDialog()
    } else {
      alert('验证失败')
    }
  })
}
</script>

3. 主页组件(Dashboard.vue)

<template>
  <div class="dashboard">
    <el-button @click="openDialog">添加用户</el-button>
    <el-table :data="users" border style="width: 100%">
      <el-table-column prop="username" label="用户名" />
      <el-table-column prop="email" label="邮箱" />
    </el-table>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import UserForm from './components/UserForm.vue'

const users = ref([
  { username: '张三', email: 'zhangsan@example.com' },
  { username: '李四', email: 'lisi@example.com' }
])

const dialogVisible = ref(false)
const form = ref({
  username: '',
  email: ''
})

const openDialog = () => {
  dialogVisible.value = true
}
</script>

六、源码解析

以 el-table 组件为例,其核心实现涉及:

  1. 虚拟 DOM 构建:通过 vnode 创建表格结构
  2. 数据绑定:使用 reactive 管理数据
  3. 列渲染:通过 el-table-column 动态生成列
  4. 排序功能:实现升序/降序切换
  5. 分页处理:集成 el-pagination 组件

关键代码片段:

function renderTableBody (h, props) {
  const { data, columns } = props
  const rows = data.map(row => {
    return columns.map(column => {
      return h('td', {
        style: { width: column.width }
      }, [row[column.property]])
    })
  })
  return h('tbody', rows)
}

七、进阶使用

1. 自定义组件

创建 CustomButton.vue:

<template>
  <el-button :type="type" @click="onClick">
    <el-icon v-if="icon" :name="icon" class="custom-icon" />
    <span>{{ label }}</span>
  </el-button>
</template>

<script setup>
defineProps({
  type: {
    type: String,
    default: 'primary'
  },
  label: {
    type: String,
    required: true
  },
  icon: {
    type: String,
    default: null
  }
})

defineEmits(['click'])
</script>

2. 深度定制样式

创建 CustomTable.css:

.el-table {
  background-color: #f5f7fa;
}

在组件中使用:

<template>
  <el-table class="custom-table" ...>
    ...
  </el-table>
</template>

3. 表单校验优化

使用 async-validator 实现异步验证:

import { createValidator } from 'async-validator'

const validator = createValidator({
  username: [
    { required: true, message: '请输入用户名' }
  ]
})

const validate = async () => {
  const result = await validator.validate(form)
  if (result) {
    alert('验证通过')
  }
}

八、性能与工程实践

1. 性能优化

  • 按需加载:使用 unplugin-vue-components 按需加载组件
  • 虚拟滚动:使用 vue-virtual-scroller 优化大数据表格
  • 缓存策略:对常用组件使用 keep-alive
  • 懒加载图片:使用 vue-lazy-component 实现图片懒加载

2. 异常处理

try {
  await someAsyncOperation()
} catch (error) {
  console.error('发生错误:', error)
  // 显示错误提示
}

3. 安全性考虑

  • 禁用未使用的组件
  • 使用 vite 的依赖管理
  • 定期更新依赖包
  • 避免直接暴露敏感信息

九、常见问题与踩坑

1. 样式问题

错误示例:

<template>
  <el-button>按钮</el-button>
</template>

问题:未引入样式导致按钮无样式

解决:确保 element-plus/dist/index.css 被正确引入

2. 响应式失效

错误示例:

const count = 1
const increment = () => count++

问题:未使用 ref 或 reactive 导致响应式失效

解决:使用 ref 或 reactive 包裹数据

3. 动画失效

错误示例:

<el-button @click="handleClick">点击</el-button>

问题:未添加动画类导致过渡效果失效

解决:添加 transition 类并定义动画

4. 表单验证失效

错误示例:

const formRef = ref()

问题:未正确使用 ref 导致验证失败

解决:使用 ref 捕获表单实例

十、最佳实践

  1. 组件化开发:将常用组件封装为可复用的组件
  2. 按需加载:使用 unplugin-vue-components 实现按需加载
  3. 样式隔离:使用 CSS Modules 或 SCSS 实现样式隔离
  4. 响应式开发:充分利用 Vue 3 的响应式系统
  5. 代码规范:使用 ESLint 和 Prettier 维护代码质量
  6. 性能监控:使用 Lighthouse 分析页面性能
  7. 文档规范:为组件编写清晰的注释和文档

十一、总结

Element Plus 作为 Vue 3 的官方 UI 组件库,提供了丰富的组件和便捷的开发体验。通过深入理解其响应式系统和组件化架构,开发者可以更高效地构建复杂界面。在实际项目中,合理使用 Element Plus 能显著提升开发效率,但也要注意其局限性。对于需要高度定制化或性能敏感的场景,需要结合其他技术方案进行优化。通过遵循最佳实践,开发者可以充分利用 Element Plus 的优势,构建出高质量的 Vue 3 应用。

2024-08-09

'# Nuxt+vue3+Element-plus+TypeScript

一、背景与问题

在现代前端开发中,Vue3与TypeScript的结合已经成为主流实践。然而,单页应用(SPA)模式在SEO和首屏加载性能方面存在先天局限。Nuxt3作为Vue3的官方SSR框架,通过服务端渲染(SSR)和静态生成(SSG)技术,有效解决了这些问题。同时,Element Plus作为Element UI的Vue3版本,提供了丰富的组件库和TypeScript支持。本文将深入解析这四者如何协同工作,探讨其技术原理、最佳实践以及常见陷阱。

二、基本原理

1. Nuxt3的运行机制

Nuxt3基于Vue3的响应式系统,通过以下核心机制实现SSR:

  • Pages系统:将pages/目录下的.vue文件自动转换为路由
  • App.vue:全局布局组件,包含