找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2417

积分

0

好友

319

主题
发表于 2 小时前 | 查看: 4| 回复: 0

在上一篇入门指南中,我们学习了 Layui Vue 的基础用法。本文将带你进入更深层次的实战应用,分享在构建企业级应用时的最佳实践、性能优化技巧,并通过一个完整的企业员工管理系统案例,展示如何将 Layui Vue 高效地应用于真实的业务场景。

程序员专注编写代码的沉浸式工作场景

01 最佳实践与性能优化

1.1 按需加载与分包策略

对于大型企业级应用,合理的分包策略至关重要,它能有效减少首屏加载体积。例如,在使用 Vite 构建时,可以在配置文件中进行如下优化:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'layui-vue': ['layui-vue'],
          'vendor': ['vue', 'vue-router', 'pinia'],
          'components': ['@/components']
        }
      }
    }
  }
});

这个配置将 layui-vue 组件库、Vue 核心及其生态库、以及项目自定义组件分别打包成独立的块,利用浏览器并行加载能力提升速度。

1.2 组件二次封装

在实际项目中,通常需要对基础组件进行二次封装,以满足业务统一的设计规范和交互逻辑。这样做的好处是统一维护、一处修改全局生效。下面是一个对 lay-table 进行业务封装的例子:

<!-- 封装统一风格的表格组件 -->
<template>
  <div class="business-table">
    <div class="table-header" v-if="$slots.header || title">
      <h3 v-if="title">{{ title }}</h3>
      <div class="header-actions">
        <slot name="header"></slot>
      </div>
    </div>

    <lay-table
      v-bind="$attrs"
      :data-source="data"
      :loading="loading"
      :pagination="pagination"
      @change="handleChange"
    >
      <!-- 透传所有插槽 -->
      <template v-for="(_, name) in $slots" #[name]="slotData">
        <slot :name="name" v-bind="slotData" />
      </template>
    </lay-table>
  </div>
</template>

<script setup>
defineProps({
  title: String,
  data: Array,
  loading: Boolean,
  pagination: Object
});

const emit = defineEmits(['change']);

const handleChange = (pagination, filters, sorter) => {
  emit('change', { pagination, filters, sorter });
};
</script>

<style scoped>
.business-table {
  background: #fff;
  border-radius: 4px;
  padding: 16px;
}

.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}

.table-header h3 {
  margin: 0;
  font-size: 16px;
  font-weight: 500;
}
</style>

1.3 状态管理与数据流

在复杂的企业级应用中,合理管理组件状态至关重要。使用 Pinia 进行集中式状态管理是个好选择,它能清晰地划分数据边界和更新逻辑。下面是一个管理多表格状态的 Store 示例:

// stores/table.js
import { defineStore } from 'pinia';

export const useTableStore = defineStore('table', {
  state: () => ({
    tableData: new Map(),
    tableLoading: new Map(),
    tablePagination: new Map()
  }),

  actions: {
    setTableData(tableId, data) {
      this.tableData.set(tableId, data);
    },

    setTableLoading(tableId, loading) {
      this.tableLoading.set(tableId, loading);
    },

    setTablePagination(tableId, pagination) {
      this.tablePagination.set(tableId, pagination);
    },

    async fetchTableData(tableId, fetchFn) {
      this.setTableLoading(tableId, true);
      try {
        const result = await fetchFn();
        this.setTableData(tableId, result.data);
        this.setTablePagination(tableId, result.pagination);
      } finally {
        this.setTableLoading(tableId, false);
      }
    }
  },

  getters: {
    getTableData: (state) => (tableId) => state.tableData.get(tableId) || [],
    getTableLoading: (state) => (tableId) => state.tableLoading.get(tableId) || false,
    getTablePagination: (state) => (tableId) => state.tablePagination.get(tableId) || {
      current: 1,
      pageSize: 10,
      total: 0
    }
  }
});

通过这种设计,不同页面的表格状态相互隔离,复用逻辑也变得清晰简单。这是构建可维护 Vue.js 应用的重要一环。

02 实战案例:企业员工管理系统

下面通过一个完整的企业员工管理系统案例,展示 Layui Vue 在实际项目中的应用。

2.1 核心代码实现

主布局组件

一个典型的中后台系统布局通常包含侧边导航栏、顶部栏和内容区。以下是使用 Layui Vue 组件实现的主布局:

<!-- layouts/MainLayout.vue -->
<template>
  <div class="app-container">
    <!-- 侧边导航 -->
    <div class="sidebar" :class="{ collapsed: isCollapsed }">
      <div class="logo">
        <img src="/logo.png" alt="Logo" />
        <span v-if="!isCollapsed">企业管理系统</span>
      </div>

      <lay-menu
        :selected-keys="selectedKeys"
        mode="vertical"
        @select="handleMenuSelect"
      >
        <lay-sub-menu key="employee" title="员工管理" icon="layui-icon-user">
          <lay-menu-item key="employee-list">员工列表</lay-menu-item>
          <lay-menu-item key="employee-add">新增员工</lay-menu-item>
          <lay-menu-item key="employee-import">批量导入</lay-menu-item>
        </lay-sub-menu>

        <lay-sub-menu key="attendance" title="考勤管理" icon="layui-icon-time">
          <lay-menu-item key="attendance-list">考勤记录</lay-menu-item>
          <lay-menu-item key="attendance-stat">统计报表</lay-menu-item>
        </lay-sub-menu>

        <lay-sub-menu key="system" title="系统设置" icon="layui-icon-set">
          <lay-menu-item key="department">部门管理</lay-menu-item>
          <lay-menu-item key="role">角色权限</lay-menu-item>
          <lay-menu-item key="log">操作日志</lay-menu-item>
        </lay-sub-menu>
      </lay-menu>
    </div>

    <!-- 主内容区 -->
    <div class="main-content">
      <div class="header">
        <lay-icon
          :type="isCollapsed ? 'layui-icon-spread-left' : 'layui-icon-shrink-right'"
          @click="toggleSidebar"
        />
        <div class="header-right">
          <lay-badge type="dot">
            <lay-icon type="layui-icon-notice" />
          </lay-badge>
          <lay-dropdown>
            <span class="user-info">
              <lay-avatar size="sm" src="/avatar.png" />
              <span>管理员</span>
            </span>
            <template #content>
              <lay-dropdown-item>个人中心</lay-dropdown-item>
              <lay-dropdown-item>修改密码</lay-dropdown-item>
              <lay-dropdown-item divided>退出登录</lay-dropdown-item>
            </template>
          </lay-dropdown>
        </div>
      </div>

      <div class="content">
        <router-view v-slot="{ Component }">
          <transition name="fade" mode="out-in">
            <component :is="Component" />
          </transition>
        </router-view>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';

const isCollapsed = ref(false);
const route = useRoute();
const router = useRouter();

const selectedKeys = computed(() => [route.name]);

const toggleSidebar = () => {
  isCollapsed.value = !isCollapsed.value;
};

const handleMenuSelect = (key) => {
  router.push({ name: key });
};
</script>

<style scoped>
.app-container {
  display: flex;
  height: 100vh;
}

.sidebar {
  width: 260px;
  background: #28333E;
  color: #fff;
  transition: width 0.3s;
  overflow-y: auto;
}

.sidebar.collapsed {
  width: 80px;
}

.logo {
  height: 60px;
  display: flex;
  align-items: center;
  padding: 0 16px;
  background: #192027;
}

.logo img {
  height: 32px;
  margin-right: 8px;
}

.logo span {
  color: #fff;
  font-size: 16px;
  font-weight: 500;
}

.main-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: #f6f8fa;
}

.header {
  height: 60px;
  background: #fff;
  border-bottom: 1px solid #e4e7ed;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
}

.header-right {
  display: flex;
  align-items: center;
  gap: 20px;
}

.user-info {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.content {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

员工列表页面

这是系统的核心功能页,集成了搜索、分页、增删改查等常见操作。

<!-- views/employee/EmployeeList.vue -->
<template>
  <div class="employee-list">
    <div class="page-header">
      <h2>员工管理</h2>
      <div class="header-actions">
        <lay-input
          v-model="searchKeyword"
          placeholder="搜索员工姓名/工号"
          style="width: 250px"
          @keyup.enter="handleSearch"
        >
          <template #prefix>
            <lay-icon type="layui-icon-search" />
          </template>
        </lay-input>
        <lay-button type="primary" @click="handleAdd">
          <lay-icon type="layui-icon-add-1" /> 新增员工
        </lay-button>
        <lay-button @click="handleExport">
          <lay-icon type="layui-icon-export" /> 导出
        </lay-button>
      </div>
    </div>

    <business-table
      title="员工列表"
      :data="tableData"
      :loading="loading"
      :pagination="pagination"
      :columns="columns"
      @change="handleTableChange"
    >
      <template #status="{ value }">
        <lay-badge
          :type="value === '在职' ? 'success' : value === '离职' ? 'danger' : 'warning'"
          :text="value"
        />
      </template>

      <template #action="{ record }">
        <lay-button size="sm" type="primary" @click="editEmployee(record)">编辑</lay-button>
        <lay-button size="sm" type="danger" @click="deleteEmployee(record)">删除</lay-button>
        <lay-button size="sm" @click="viewDetail(record)">详情</lay-button>
      </template>
    </business-table>

    <!-- 新增/编辑弹窗 -->
    <lay-layer v-model="visible" :title="modalTitle" width="600px">
      <employee-form
        v-model="currentEmployee"
        @success="handleFormSuccess"
        @cancel="visible = false"
      />
    </lay-layer>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue';
import { layer } from 'layui-vue';
import BusinessTable from '@/components/BusinessTable.vue';
import EmployeeForm from './EmployeeForm.vue';

const searchKeyword = ref('');
const loading = ref(false);
const visible = ref(false);
const modalTitle = ref('新增员工');
const currentEmployee = ref({});

const tableData = ref([]);
const pagination = reactive({
  current: 1,
  pageSize: 10,
  total: 0
});

const columns = ref([
  { title: '工号', key: 'employeeNo', width: 120 },
  { title: '姓名', key: 'name', width: 120 },
  { title: '部门', key: 'department', width: 150 },
  { title: '职位', key: 'position', width: 150 },
  { title: '入职日期', key: 'joinDate', width: 120 },
  { title: '状态', key: 'status', width: 100, slots: { customRender: 'status' } },
  { title: '操作', key: 'action', width: 200, fixed: 'right', slots: { customRender: 'action' } }
]);

// 获取员工列表
const fetchEmployeeList = async () => {
  loading.value = true;
  try {
    // 实际项目中这里调用API
    await new Promise(resolve => setTimeout(resolve, 500));
    tableData.value = [
      { id: 1, employeeNo: 'EMP001', name: '张三', department: '技术部', position: '高级开发', joinDate: '2024-01-15', status: '在职' },
      { id: 2, employeeNo: 'EMP002', name: '李四', department: '产品部', position: '产品经理', joinDate: '2024-02-01', status: '在职' },
      { id: 3, employeeNo: 'EMP003', name: '王五', department: '市场部', position: '市场专员', joinDate: '2024-02-15', status: '试用' },
      { id: 4, employeeNo: 'EMP004', name: '赵六', department: '技术部', position: '前端开发', joinDate: '2024-01-20', status: '离职' }
    ];
    pagination.total = 20;
  } finally {
    loading.value = false;
  }
};

const handleSearch = () => {
  pagination.current = 1;
  fetchEmployeeList();
};

const handleTableChange = ({ pagination: page }) => {
  pagination.current = page.current;
  pagination.pageSize = page.pageSize;
  fetchEmployeeList();
};

const handleAdd = () => {
  modalTitle.value = '新增员工';
  currentEmployee.value = {};
  visible.value = true;
};

const editEmployee = (record) => {
  modalTitle.value = '编辑员工';
  currentEmployee.value = { ...record };
  visible.value = true;
};

const deleteEmployee = (record) => {
  layer.confirm(`确定要删除员工 ${record.name} 吗?`, {
    title: '删除确认',
    btn: ['确定', '取消']
  }).then(() => {
    // 实际项目中调用删除API
    layer.msg('删除成功', { icon: 1 });
    fetchEmployeeList();
  });
};

const viewDetail = (record) => {
  layer.open({
    type: 'drawer',
    title: '员工详情',
    area: ['500px', '100%'],
    content: `<div style="padding: 20px;">
      <p>工号:${record.employeeNo}</p>
      <p>姓名:${record.name}</p>
      <p>部门:${record.department}</p>
      <p>职位:${record.position}</p>
      <p>入职日期:${record.joinDate}</p>
      <p>状态:${record.status}</p>
    </div>`
  });
};

const handleExport = () => {
  layer.msg('开始导出数据...', { icon: 1 });
  // 实际项目中调用导出API
};

const handleFormSuccess = () => {
  visible.value = false;
  fetchEmployeeList();
  layer.msg('保存成功', { icon: 1 });
};

onMounted(() => {
  fetchEmployeeList();
});
</script>

<style scoped>
.employee-list {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.page-header h2 {
  margin: 0;
  font-size: 18px;
  font-weight: 500;
}

.header-actions {
  display: flex;
  gap: 12px;
}
</style>

03 注意事项与踩坑指南

3.1 版本兼容性

  • Layui Vue 基于 Vue 3 开发,不支持 Vue 2 项目。
  • 如果你使用的是 Layui 2.x 版本,需要升级到 3.x 才能使用 CSS Variable 主题定制功能。
  • Node.js 版本建议使用 14.x 及以上。

3.2 常见问题解决

问题一:组件样式不生效
解决方案:确保正确引入了样式文件:

import 'layui-vue/lib/layui-vue.css';

问题二:按需引入时报错“组件未注册”
解决方案:检查自动导入插件配置是否正确,或手动注册组件:

// 手动注册
import { LayButton } from 'layui-vue';
app.component('LayButton', LayButton);

问题三:主题切换后样式没有更新
解决方案:确保主题类名添加到了正确的DOM元素上,并且CSS变量的作用域覆盖了所有组件:

// 将主题类名添加到html根元素
document.documentElement.className = 'dark-theme';

3.3 性能优化建议

  1. 虚拟滚动:对于长列表,使用虚拟滚动组件减少DOM节点数量。
  2. 组件懒加载:路由级别的组件懒加载,减少首屏加载时间。
  3. 防抖与节流:搜索输入、窗口resize等高频事件添加防抖处理。
  4. 按需引入:避免全量引入组件库,使用按需加载或自动导入。

总结

Layui Vue 组件库将经典设计语言与现代 Vue 3 技术栈进行了很好的融合,为企业级中后台系统开发提供了高效、优雅的解决方案。从快速原型到复杂业务系统,它都能胜任。

无论你是正在启动新项目,还是考虑技术栈升级,Layui Vue 都是一个值得深入评估的选择。它不仅能提升开发效率,还能确保产品界面的一致性和专业性。希望本文的实战案例和最佳实践能为你带来启发。如果想了解更多前沿的前端技术实践和社区讨论,欢迎访问 云栈社区




上一篇:AI 集体“翻车”:为何主流模型都把德哥 (digoal) 误认为女性?
下一篇:Webshell检测与应急响应实战:PHP/JSP/ASP脚本分析与防御指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-18 06:14 , Processed in 0.600792 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表