Files
school-developer/docs/hooks使用指南.md
2026-03-19 15:55:37 +08:00

36 KiB
Raw Blame History

Hooks 使用指南

本文档详细说明了项目中所有 Hooks 的使用方法,帮助开发者快速上手和正确使用。

目录

  1. useDict - 字典数据获取
  2. useParam - 参数数据获取
  3. useMessage - 消息提示
  4. useMessageBox - 消息确认框
  5. useTable - 表格数据管理

1. useDict - 字典数据获取 Hook

文件位置: src/hooks/dict.ts

功能说明

用于获取字典数据,支持多个字典类型同时获取,具有自动缓存机制,避免重复请求。

基本用法

import { useDict } from '/@/hooks/dict';

// 在 setup 中使用
const { dictType1, dictType2 } = useDict('dictType1', 'dictType2');

完整示例

<template>
  <div>
    <!-- 使用字典数据 -->
    <el-select v-model="form.status">
      <el-option
        v-for="item in userStatus"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      />
    </el-select>
    
    <el-select v-model="form.type">
      <el-option
        v-for="item in userType"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      />
    </el-select>
  </div>
</template>

<script setup lang="ts">
import { useDict } from '/@/hooks/dict';

// 获取多个字典类型
const { userStatus, userType, deptType } = useDict(
  'user_status', 
  'user_type', 
  'dept_type'
);

const form = ref({
  status: '',
  type: ''
});
</script>

特点

  • 支持传入多个字典类型参数
  • 自动缓存机制,避免重复请求
  • 返回响应式引用对象(使用 toRefs
  • 字典数据包含:labelvalueelTagTypeelTagClass

返回数据结构

{
  label: string,        // 显示文本
  value: string,        // 值
  elTagType: string,   // 标签类型(用于 el-tag
  elTagClass: string   // 标签样式类(用于 el-tag
}

注意事项

  • 字典类型参数为字符串类型
  • 返回的对象是响应式的,可以直接在模板中使用
  • 首次请求会从服务器获取,后续会使用缓存

2. useParam - 参数数据获取 Hook

文件位置: src/hooks/param.ts

功能说明

用于获取系统参数值,支持缓存机制,避免重复请求。

基本用法

import { useParam } from '/@/hooks/param';

// 在 setup 中使用
const paramValue = useParam('paramType');

完整示例

<template>
  <div>
    <div>最大上传大小{{ uploadMaxSize }}</div>
    <div>系统名称{{ systemName }}</div>
    <div>系统版本{{ systemVersion }}</div>
  </div>
</template>

<script setup lang="ts">
import { useParam } from '/@/hooks/param';

// 获取单个参数
const uploadMaxSize = useParam('upload_max_size');
const systemName = useParam('system_name');
const systemVersion = useParam('system_version');
</script>

特点

  • 单个参数类型获取
  • 自动缓存机制
  • 返回响应式引用(ref
  • 参数值类型为字符串

注意事项

  • 每次调用只能获取一个参数类型
  • 如需获取多个参数,需要多次调用
  • 返回的是响应式引用,在模板中会自动解包

3. useMessage - 消息提示

文件位置: src/hooks/message.ts

功能说明

统一的消息提示功能,封装了 Element Plus 的 ElMessage,提供统一的配置和样式。

基本用法

import { useMessage } from '/@/hooks/message';

// 在 setup 中使用
const message = useMessage();

方法列表

方法 说明 显示时间
info(title: string) 普通提示 3秒
warning(title: string) 警告提示 3秒
success(title: string) 成功提示 3秒
error(title: string) 错误提示 2秒

完整示例

<template>
  <div>
    <el-button @click="handleInfo">普通提示</el-button>
    <el-button @click="handleWarning">警告提示</el-button>
    <el-button @click="handleSuccess">成功提示</el-button>
    <el-button @click="handleError">错误提示</el-button>
  </div>
</template>

<script setup lang="ts">
import { useMessage } from '/@/hooks/message';

const message = useMessage();

const handleInfo = () => {
  message.info('这是一条普通提示信息');
};

const handleWarning = () => {
  message.warning('这是一条警告提示信息');
};

const handleSuccess = () => {
  message.success('操作成功!');
};

const handleError = () => {
  message.error('操作失败,请重试!');
};
</script>

实际应用场景

// 保存成功后提示
const handleSave = async () => {
  try {
    await saveData(formData);
    message.success('保存成功!');
  } catch (error) {
    message.error('保存失败:' + error.message);
  }
};

// 删除操作提示
const handleDelete = async () => {
  try {
    await deleteData(id);
    message.success('删除成功!');
  } catch (error) {
    message.error('删除失败:' + error.message);
  }
};

默认配置

  • 显示时间: 普通/警告/成功 3秒错误 2秒
  • 显示关闭按钮:
  • 距离顶部偏移: 20px

注意事项

  • 消息提示会自动消失
  • 可以同时显示多条消息
  • 错误提示显示时间较短,便于快速查看

4. useMessageBox - 消息确认框

文件位置: src/hooks/message.ts

功能说明

统一的消息确认框功能,封装了 Element Plus 的 ElMessageBox,支持提示框、确认框和输入框。

基本用法

import { useMessageBox } from '/@/hooks/message';

// 在 setup 中使用
const messageBox = useMessageBox();

方法列表

方法 说明 返回值
info(msg: string) 普通提示框 void
warning(msg: string) 警告提示框 void
success(msg: string) 成功提示框 void
error(msg: string) 错误提示框 void
confirm(msg: string) 确认对话框 Promise
prompt(msg: string) 输入对话框 Promise<{ value: string }>

完整示例

4.1 提示框Alert

<script setup lang="ts">
import { useMessageBox } from '/@/hooks/message';

const messageBox = useMessageBox();

// 普通提示框
const handleInfo = () => {
  messageBox.info('这是一条信息提示');
};

// 警告提示框
const handleWarning = () => {
  messageBox.warning('这是一条警告信息');
};

// 成功提示框
const handleSuccess = () => {
  messageBox.success('操作成功!');
};

// 错误提示框
const handleError = () => {
  messageBox.error('操作失败!');
};
</script>

4.2 确认对话框Confirm

<template>
  <el-button @click="handleDelete">删除</el-button>
</template>

<script setup lang="ts">
import { useMessageBox } from '/@/hooks/message';
import { useMessage } from '/@/hooks/message';

const messageBox = useMessageBox();
const message = useMessage();

const handleDelete = async () => {
  try {
    // 显示确认对话框
    await messageBox.confirm('确定要删除这条记录吗?删除后无法恢复!');
    
    // 用户点击确认后执行
    await deleteData(id);
    message.success('删除成功!');
  } catch {
    // 用户点击取消,不执行任何操作
    console.log('用户取消了删除操作');
  }
};
</script>

4.3 输入对话框Prompt

<template>
  <el-button @click="handleRename">重命名</el-button>
</template>

<script setup lang="ts">
import { useMessageBox } from '/@/hooks/message';
import { useMessage } from '/@/hooks/message';

const messageBox = useMessageBox();
const message = useMessage();

const handleRename = async () => {
  try {
    // 显示输入对话框
    const { value } = await messageBox.prompt('请输入新名称');
    
    // 用户输入了值并点击确认
    if (value && value.trim()) {
      await renameData(id, value);
      message.success('重命名成功!');
    } else {
      message.warning('名称不能为空');
    }
  } catch {
    // 用户点击取消
    console.log('用户取消了重命名操作');
  }
};
</script>

实际应用场景

// 删除确认
const handleDelete = async (id: number) => {
  try {
    await messageBox.confirm('确定要删除这条记录吗?');
    await deleteApi(id);
    message.success('删除成功!');
    getDataList();
  } catch {
    // 用户取消,不执行任何操作
  }
};

// 批量删除确认
const handleBatchDelete = async () => {
  if (selectedIds.length === 0) {
    message.warning('请先选择要删除的记录');
    return;
  }
  
  try {
    await messageBox.confirm(`确定要删除选中的 ${selectedIds.length} 条记录吗?`);
    await batchDeleteApi(selectedIds);
    message.success('批量删除成功!');
    getDataList();
  } catch {
    // 用户取消
  }
};

// 重置密码确认
const handleResetPassword = async (userId: number) => {
  try {
    await messageBox.confirm('确定要重置该用户的密码吗?');
    const result = await resetPasswordApi(userId);
    messageBox.info(`重置后密码为:${result.data.password}`);
  } catch {
    // 用户取消
  }
};

特点

  • 支持国际化(自动使用 i18n
  • confirmprompt 返回 Promise便于链式调用
  • 统一的按钮文本(确认/取消)
  • 自动处理用户取消操作

注意事项

  • confirmprompt 返回 Promise需要使用 async/await.then()/.catch() 处理
  • 用户点击取消会触发 Promise 的 reject,需要在 catch 中处理
  • 提示框方法(infowarningsuccesserror)不返回 Promise

5. useTable - 表格数据管理 Hook

文件位置: src/hooks/table.ts

功能说明

提供完整的表格数据管理功能,包括数据加载、分页、排序、文件下载等,简化表格相关开发。

基本用法

import { useTable } from '/@/hooks/table';
import { fetchList } from '/@/api/your-api';

const { 
  getDataList,           // 获取数据列表方法
  currentChangeHandle,   // 页码改变处理方法
  sizeChangeHandle,      // 每页条数改变处理方法
  sortChangeHandle,      // 排序改变处理方法
  downBlobFile,          // 下载文件方法
  tableStyle            // 表格样式
} = useTable({
  pageList: fetchList,
  queryForm: {}
});

配置选项

属性 类型 默认值 说明
createdIsNeed boolean true 是否在创建时自动加载数据
isPage boolean true 是否需要分页
queryForm any {} 查询条件表单对象
pageList Function - 数据列表查询接口(必填)
pagination Pagination 见下方 分页配置
props object { item: 'records', totalCount: 'total' } 数据属性映射
validate Function - 验证函数
onLoaded Function - 数据加载完成回调
onCascaded Function - 级联数据回调

分页配置Pagination

{
  current: 1,                    // 当前页码
  size: 10,                      // 每页显示条数
  total: 0,                      // 总条数
  pageSizes: [10, 20, 50, 100], // 每页条数选择器选项
  layout: 'total, sizes, prev, pager, next, jumper' // 分页组件布局
}

完整示例

<template>
  <div>
    <!-- 搜索表单 -->
    <el-form :model="searchForm" inline>
      <el-form-item label="姓名">
        <el-input v-model="searchForm.name" placeholder="请输入姓名" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleSearch">查询</el-button>
        <el-button @click="handleReset">重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 表格 -->
    <el-table
      :data="state.dataList"
      :cell-style="tableStyle.cellStyle"
      :header-cell-style="tableStyle.headerCellStyle"
      @sort-change="sortChangeHandle"
      v-loading="state.loading"
      border
    >
      <el-table-column type="index" label="序号" width="60" />
      <el-table-column prop="name" label="姓名" sortable="custom" />
      <el-table-column prop="status" label="状态" />
      <el-table-column label="操作" width="200">
        <template #default="scope">
          <el-button type="primary" link size="small" @click="handleEdit(scope.row)">
            编辑
          </el-button>
          <el-button type="danger" link size="small" @click="handleDelete(scope.row)">
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页 -->
    <pagination
      :current="state.pagination.current"
      :size="state.pagination.size"
      :total="state.pagination.total"
      @currentChange="currentChangeHandle"
      @sizeChange="sizeChangeHandle"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue';
import { useTable } from '/@/hooks/table';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { fetchList, deleteData } from '/@/api/your-api';

// 消息提示
const message = useMessage();
const messageBox = useMessageBox();

// 搜索表单
const searchForm = reactive({
  name: '',
  status: ''
});

// 表格管理
const { 
  getDataList, 
  currentChangeHandle, 
  sizeChangeHandle,
  sortChangeHandle,
  tableStyle,
  state
} = useTable({
  // 查询条件
  queryForm: searchForm,
  
  // 数据列表查询接口
  pageList: fetchList,
  
  // 分页配置(可选)
  pagination: {
    current: 1,
    size: 10,
    total: 0,
    pageSizes: [10, 20, 50, 100],
    layout: 'total, sizes, prev, pager, next, jumper'
  },
  
  // 数据属性映射(可选,根据后端返回结构调整)
  props: {
    item: 'records',      // 数据列表字段名
    totalCount: 'total'   // 总数字段名
  },
  
  // 验证函数(可选)
  validate: async (state) => {
    // 可以在这里进行表单验证
    if (!searchForm.name && !searchForm.status) {
      message.warning('请至少输入一个查询条件');
      return false;
    }
    return true;
  },
  
  // 数据加载完成回调(可选)
  onLoaded: async (state) => {
    console.log('数据加载完成', state.dataList);
    // 可以在这里处理加载完成后的逻辑
  }
});

// 查询
const handleSearch = () => {
  getDataList(); // 刷新并跳转到第一页
};

// 重置
const handleReset = () => {
  searchForm.name = '';
  searchForm.status = '';
  getDataList();
};

// 编辑
const handleEdit = (row: any) => {
  // 编辑逻辑
};

// 删除
const handleDelete = async (row: any) => {
  try {
    await messageBox.confirm('确定要删除这条记录吗?');
    await deleteData(row.id);
    message.success('删除成功!');
    getDataList(false); // 刷新但不跳转第一页
  } catch {
    // 用户取消
  }
};
</script>

返回的方法说明

getDataList(refresh?)

获取数据列表方法。

参数:

  • refresh (可选): boolean - 是否刷新并跳转到第一页,默认为 true

使用示例:

// 刷新并跳转到第一页(默认)
getDataList();

// 刷新但保持当前页
getDataList(false);

currentChangeHandle(val)

页码改变处理方法,用于分页组件的 @currentChange 事件。

参数:

  • val: number - 新的页码

使用示例:

<pagination
  @currentChange="currentChangeHandle"
/>

sizeChangeHandle(val)

每页条数改变处理方法,用于分页组件的 @sizeChange 事件。

参数:

  • val: number - 新的每页条数

使用示例:

<pagination
  @sizeChange="sizeChangeHandle"
/>

sortChangeHandle(column)

排序改变处理方法,用于表格的 @sort-change 事件。

参数:

  • column: object - 排序列信息
    • prop: 列属性名
    • order: 排序方式(ascending | descending | null

使用示例:

<el-table @sort-change="sortChangeHandle">
  <el-table-column prop="name" sortable="custom" />
</el-table>

downBlobFile(url, query, fileName)

下载文件方法。

参数:

  • url: string - 文件下载地址
  • query: object - 请求参数
  • fileName: string - 文件名

使用示例:

const handleExport = () => {
  downBlobFile(
    '/api/export',
    { type: 'excel' },
    '数据导出.xlsx'
  );
};

tableStyle

表格样式配置对象。

结构:

{
  cellStyle: { textAlign: 'center' },
  headerCellStyle: {
    textAlign: 'center',
    background: 'var(--el-table-row-hover-bg-color)',
    color: 'var(--el-text-color-primary)'
  }
}

使用示例:

<el-table
  :cell-style="tableStyle.cellStyle"
  :header-cell-style="tableStyle.headerCellStyle"
>
</el-table>

State 对象说明

useTable 返回的 state 对象包含以下属性:

{
  dataList: [],              // 表格数据列表
  loading: false,            // 加载状态
  pagination: {              // 分页信息
    current: 1,
    size: 10,
    total: 0
  },
  dataListSelections: [],   // 选中的数据
  queryForm: {},             // 查询表单
  // ... 其他配置属性
}

实际应用场景

场景1带验证的查询

const { getDataList, state } = useTable({
  queryForm: searchForm,
  pageList: fetchList,
  validate: async (state) => {
    if (!searchForm.name && !searchForm.status) {
      message.warning('请至少输入一个查询条件');
      return false;
    }
    return true;
  }
});

场景2数据加载后处理

const { getDataList, state } = useTable({
  queryForm: searchForm,
  pageList: fetchList,
  onLoaded: async (state) => {
    // 数据加载完成后,处理字典转换
    state.dataList = state.dataList.map(item => {
      // 转换状态字典
      const statusItem = statusDict.find(d => d.value === item.status);
      item.statusName = statusItem?.label || '';
      return item;
    });
  }
});

场景3无分页表格

const { getDataList, state } = useTable({
  queryForm: searchForm,
  pageList: fetchList,
  isPage: false,  // 禁用分页
  createdIsNeed: true
});

场景4自定义数据字段映射

const { getDataList, state } = useTable({
  queryForm: searchForm,
  pageList: fetchList,
  props: {
    item: 'data.list',        // 后端返回的数据列表路径
    totalCount: 'data.total'  // 后端返回的总数路径
  }
});

弹窗编辑后刷新表格

在弹窗编辑/新增完成后,需要刷新表格数据。有两种方式:

方式一:使用 getDataList(false)(推荐)

父组件(列表页):

<template>
  <!-- 表格 -->
  <el-table :data="state.dataList" v-loading="state.loading">
    <!-- 表格列 -->
  </el-table>
  
  <!-- 弹窗组件监听 refresh 事件 -->
  <form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
</template>

<script setup lang="ts">
import { useTable } from '/@/hooks/table';
import { fetchList } from '/@/api/user';

const { getDataList, state, tableStyle } = useTable({
  queryForm: {},
  pageList: fetchList
});

const formDialogRef = ref();
</script>

子组件(弹窗表单):

<script setup lang="ts">
const emit = defineEmits(['refresh']);

// 提交表单
const onSubmit = async () => {
  const valid = await dataFormRef.value.validate().catch(() => {});
  if (!valid) return false;

  try {
    loading.value = true;
    form.id ? await putObj(form) : await addObj(form);
    useMessage().success(form.id ? '修改成功' : '添加成功');
    visible.value = false;
    emit('refresh'); // 触发 refresh 事件
  } catch (err: any) {
    useMessage().error(err.msg);
  } finally {
    loading.value = false;
  }
};
</script>

说明:

  • getDataList(false) - 刷新数据但不跳转到第一页,保持当前页
  • getDataList()getDataList(true) - 刷新数据并跳转到第一页

方式二:手动管理(不使用 useTable

父组件:

<template>
  <el-table :data="tableData" v-loading="tableLoading">
    <!-- 表格列 -->
  </el-table>
  
  <form-dialog ref="formDialogRef" @refresh="handleRefresh" />
</template>

<script setup lang="ts">
const tableData = ref([]);
const tableLoading = ref(false);
const page = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0
});

// 刷新数据
const handleRefresh = () => {
  getList(page);
};

const getList = (page: any) => {
  tableLoading.value = true;
  fetchList({
    current: page.currentPage,
    size: page.pageSize
  }).then((response: any) => {
    tableData.value = response.data.record.records;
    page.total = response.data.record.total;
  }).finally(() => {
    tableLoading.value = false;
  });
};
</script>

刷新时机的选择

场景 推荐方法 说明
编辑后刷新 getDataList(false) 保持当前页,用户可以看到刚才编辑的数据
新增后刷新 getDataList() 跳转到第一页,因为新数据通常在第一页
删除后刷新 getDataList(false) 保持当前页,如果当前页没数据了会自动调整
查询后刷新 getDataList() 跳转到第一页,显示查询结果

完整示例

<template>
  <div>
    <!-- 搜索表单 -->
    <el-form :model="searchForm" inline>
      <el-form-item>
        <el-button type="primary" @click="handleSearch">查询</el-button>
      </el-form-item>
    </el-form>

    <!-- 操作按钮 -->
    <div class="mb15">
      <el-button type="primary" @click="handleAdd">新增</el-button>
    </div>

    <!-- 表格 -->
    <el-table :data="state.dataList" v-loading="state.loading">
      <el-table-column prop="name" label="姓名" />
      <el-table-column label="操作">
        <template #default="scope">
          <el-button @click="handleEdit(scope.row)">编辑</el-button>
          <el-button @click="handleDelete(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页 -->
    <pagination
      :current="state.pagination.current"
      :size="state.pagination.size"
      :total="state.pagination.total"
      @currentChange="currentChangeHandle"
      @sizeChange="sizeChangeHandle"
    />

    <!-- 弹窗表单 -->
    <form-dialog ref="formDialogRef" @refresh="handleFormRefresh" />
  </div>
</template>

<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useTable } from '/@/hooks/table';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { fetchList, deleteData } from '/@/api/user';

const message = useMessage();
const messageBox = useMessageBox();
const formDialogRef = ref();

const searchForm = reactive({
  name: ''
});

const { 
  getDataList, 
  currentChangeHandle, 
  sizeChangeHandle,
  state 
} = useTable({
  queryForm: searchForm,
  pageList: fetchList
});

// 查询
const handleSearch = () => {
  getDataList(); // 查询后跳转到第一页
};

// 新增
const handleAdd = () => {
  formDialogRef.value?.openDialog();
};

// 编辑
const handleEdit = (row: any) => {
  formDialogRef.value?.openDialog(row.id);
};

// 删除
const handleDelete = async (row: any) => {
  try {
    await messageBox.confirm('确定要删除吗?');
    await deleteData(row.id);
    message.success('删除成功');
    getDataList(false); // 删除后保持当前页
  } catch {
    // 用户取消
  }
};

// 弹窗提交后的刷新处理
const handleFormRefresh = () => {
  // 可以根据操作类型选择刷新方式
  // 如果是新增,跳转到第一页
  // 如果是编辑,保持当前页
  getDataList(false); // 默认保持当前页
};
</script>

注意事项

  • pageList 方法是必填的,需要传入数据查询接口函数
  • queryForm 会在请求时自动合并到请求参数中
  • state.loading 会自动管理,无需手动设置
  • 排序功能会自动将驼峰命名转换为下划线命名(如 userNameuser_name
  • 错误会自动通过 ElMessage.error 提示
  • 弹窗提交成功后,记得 emit('refresh') 触发刷新事件
  • 根据业务场景选择合适的刷新方式(是否跳转第一页)

6. useTable vs 正常使用 Table 对比

概述

useTable Hook 是对表格数据管理的封装,提供了统一的状态管理和方法,相比手动管理表格状态,具有明显的优势。

对比表格

对比项 使用 useTable Hook 正常使用 Table手动管理
代码量 少,配置化 多,需要手动编写大量代码
状态管理 自动管理分页、loading、数据 需要手动管理所有状态
初始化 自动在 onMounted 时加载数据 需要手动在 onMounted 中调用
分页处理 自动处理页码和每页条数变化 需要手动实现 currentChangesizeChange
排序处理 自动处理排序,自动转换命名 需要手动实现排序逻辑
错误处理 自动错误提示 需要手动 try-catch 和错误提示
Loading 状态 自动管理 需要手动设置 tableLoading.value = true/false
数据刷新 统一方法 getDataList() 需要手动调用 getList(page)
代码复用 高度复用,配置即可 每个页面都要重复编写
维护性 集中维护,修改一处即可 分散在各处,修改困难
统一性 所有表格行为一致 每个页面可能实现不同

代码对比示例

使用 useTable Hook推荐

<script setup lang="ts">
import { reactive } from 'vue';
import { useTable } from '/@/hooks/table';
import { fetchList } from '/@/api/user';

// 搜索表单
const searchForm = reactive({
  name: '',
  status: ''
});

// 一行配置,自动获得所有功能
const { 
  getDataList, 
  currentChangeHandle, 
  sizeChangeHandle,
  sortChangeHandle,
  tableStyle,
  state
} = useTable({
  queryForm: searchForm,
  pageList: fetchList
});

// 查询 - 只需一行
const handleSearch = () => {
  getDataList();
};
</script>

<template>
  <el-table
    :data="state.dataList"
    :cell-style="tableStyle.cellStyle"
    :header-cell-style="tableStyle.headerCellStyle"
    @sort-change="sortChangeHandle"
    v-loading="state.loading"
  >
    <!-- 表格列 -->
  </el-table>
  
  <pagination
    :current="state.pagination.current"
    :size="state.pagination.size"
    :total="state.pagination.total"
    @currentChange="currentChangeHandle"
    @sizeChange="sizeChangeHandle"
  />
</template>

代码行数:约 30 行

正常使用 Table手动管理

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { fetchList } from '/@/api/user';
import { ElMessage } from 'element-plus';

// 需要手动定义所有状态
const tableData = ref([]);
const tableLoading = ref(false);
const page = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0
});
const params = ref({});
const search = reactive({
  name: '',
  status: ''
});

// 需要手动实现数据加载
const getList = (page: any) => {
  tableLoading.value = true;
  fetchList(Object.assign({
    current: page.currentPage,
    size: page.pageSize
  }, params.value)).then((response: any) => {
    tableData.value = response.data.record.records;
    page.total = response.data.record.total;
    tableLoading.value = false;
  }).catch((error: any) => {
    ElMessage.error(error.msg || '加载失败');
    tableLoading.value = false;
  });
};

// 需要手动实现分页处理
const currentChange = (val: number) => {
  page.currentPage = val;
  getList(page);
};

const handleSizeChange = (val: number) => {
  page.pageSize = val;
  page.currentPage = 1;
  getList(page);
};

// 需要手动实现排序(如果需要)
const sortChangeHandle = (column: any) => {
  // 手动实现排序逻辑...
  getList(page);
};

// 需要手动在 onMounted 中调用
onMounted(() => {
  getList(page);
});

// 查询
const handleSearch = () => {
  params.value = { ...search };
  page.currentPage = 1;
  getList(page);
};
</script>

<template>
  <el-table
    :data="tableData"
    v-loading="tableLoading"
    @sort-change="sortChangeHandle"
  >
    <!-- 表格列 -->
  </el-table>
  
  <pagination
    :current="page.currentPage"
    :size="page.pageSize"
    :total="page.total"
    @currentChange="currentChange"
    @sizeChange="handleSizeChange"
  />
</template>

代码行数:约 80+ 行

主要优势总结

1. 代码量减少 60%+

  • useTable约 30 行代码
  • 手动管理:约 80+ 行代码
  • 减少约 50 行重复代码

2. 自动状态管理

  • 自动管理 loading 状态
  • 自动管理分页状态(currentsizetotal
  • 自动管理数据列表
  • 自动处理错误提示

3. 自动初始化

// useTable 自动在 onMounted 时加载数据
onMounted(() => {
  if (state.createdIsNeed) {
    query(); // 自动调用
  }
});

4. 统一的方法接口

  • getDataList() - 统一的数据刷新方法
  • currentChangeHandle() - 统一的分页处理
  • sizeChangeHandle() - 统一的每页条数处理
  • sortChangeHandle() - 统一的排序处理(自动转换命名)

5. 统一的表格样式

// 所有使用 useTable 的表格样式一致
tableStyle: {
  cellStyle: { textAlign: 'center' },
  headerCellStyle: { textAlign: 'center', ... }
}

6. 更好的维护性

  • 修改 Hook 代码,所有使用的地方自动生效
  • 统一的错误处理逻辑
  • 统一的 API 响应结构处理

7. 扩展功能

  • 自动排序字段转换(驼峰 → 下划线)
  • 支持验证函数
  • 支持数据加载回调
  • 支持文件下载方法
  • 支持无分页模式

使用建议

推荐使用 useTable 的场景

  1. 标准 CRUD 页面 - 列表、分页、查询、排序
  2. 数据展示页面 - 需要分页的数据表格
  3. 需要统一风格的表格 - 保持项目一致性
  4. 快速开发 - 减少重复代码,提高开发效率

⚠️ 可以考虑手动管理的场景

  1. 特殊业务逻辑 - 分页逻辑与标准差异很大
  2. 性能优化需求 - 需要精细控制加载时机
  3. 复杂的数据处理 - 需要大量自定义数据处理逻辑
  4. 遗留代码 - 已有大量手动管理的代码,迁移成本高

迁移建议

如果现有代码使用手动管理,建议逐步迁移到 useTable

  1. 新页面:直接使用 useTable
  2. 旧页面:在重构时逐步迁移
  3. 复杂页面:可以先迁移基础功能,保留特殊逻辑

总结

useTable Hook 通过封装常见的表格管理逻辑,显著减少了代码量,提高了开发效率和代码质量。对于大多数标准表格场景,强烈推荐使用 useTable


综合使用示例

以下是一个完整的页面示例,展示如何综合使用所有 Hooks

<template>
  <div class="layout-padding">
    <div class="layout-padding-auto layout-padding-view">
      <!-- 搜索表单 -->
      <el-form :model="searchForm" inline>
        <el-form-item label="姓名">
          <el-input v-model="searchForm.name" placeholder="请输入姓名" />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="searchForm.status" clearable>
            <el-option
              v-for="item in userStatus"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">查询</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>

      <!-- 操作按钮 -->
      <div class="mb15">
        <el-button type="primary" icon="FolderAdd" @click="handleAdd">
          新增
        </el-button>
        <el-button 
          type="warning" 
          plain 
          icon="Download" 
          @click="handleExport"
          class="ml10">
          导出
        </el-button>
      </div>

      <!-- 表格 -->
      <el-table
        :data="state.dataList"
        :cell-style="tableStyle.cellStyle"
        :header-cell-style="tableStyle.headerCellStyle"
        @sort-change="sortChangeHandle"
        v-loading="state.loading"
        border
      >
        <el-table-column type="index" label="序号" width="60" />
        <el-table-column prop="name" label="姓名" sortable="custom" />
        <el-table-column prop="status" label="状态">
          <template #default="scope">
            <el-tag v-for="item in userStatus.filter(s => s.value === scope.row.status)" :key="item.value">
              {{ item.label }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200">
          <template #default="scope">
            <el-button type="primary" link size="small" @click="handleEdit(scope.row)">
              编辑
            </el-button>
            <el-button type="danger" link size="small" @click="handleDelete(scope.row)">
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页 -->
      <pagination
        :current="state.pagination.current"
        :size="state.pagination.size"
        :total="state.pagination.total"
        @currentChange="currentChangeHandle"
        @sizeChange="sizeChangeHandle"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive } from 'vue';
import { useDict } from '/@/hooks/dict';
import { useParam } from '/@/hooks/param';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useTable } from '/@/hooks/table';
import { fetchList, deleteData, exportData } from '/@/api/user';

// 1. 字典数据
const { userStatus } = useDict('user_status');

// 2. 参数数据
const systemName = useParam('system_name');

// 3. 消息提示
const message = useMessage();
const messageBox = useMessageBox();

// 4. 搜索表单
const searchForm = reactive({
  name: '',
  status: ''
});

// 5. 表格管理
const { 
  getDataList, 
  currentChangeHandle, 
  sizeChangeHandle,
  sortChangeHandle,
  downBlobFile,
  tableStyle,
  state
} = useTable({
  queryForm: searchForm,
  pageList: fetchList,
  validate: async (state) => {
    // 可以添加验证逻辑
    return true;
  },
  onLoaded: async (state) => {
    console.log('数据加载完成,共', state.dataList.length, '条');
  }
});

// 查询
const handleSearch = () => {
  getDataList();
};

// 重置
const handleReset = () => {
  searchForm.name = '';
  searchForm.status = '';
  getDataList();
};

// 新增
const handleAdd = () => {
  message.info('打开新增对话框');
  // 新增逻辑
};

// 编辑
const handleEdit = (row: any) => {
  message.info('打开编辑对话框');
  // 编辑逻辑
};

// 删除
const handleDelete = async (row: any) => {
  try {
    await messageBox.confirm('确定要删除这条记录吗?删除后无法恢复!');
    await deleteData(row.id);
    message.success('删除成功!');
    getDataList(false); // 刷新但不跳转第一页
  } catch {
    // 用户取消,不执行任何操作
  }
};

// 导出
const handleExport = () => {
  downBlobFile(
    '/api/user/export',
    searchForm,
    `用户数据_${new Date().getTime()}.xlsx`
  );
};
</script>

最佳实践

1. 字典数据使用

  • setup 顶层调用 useDict,确保响应式
  • 字典数据可以直接在模板中使用
  • 使用 el-tag 时可以利用 elTagTypeelTagClass

2. 消息提示使用

  • 操作成功使用 message.success()
  • 操作失败使用 message.error()
  • 需要用户确认的操作使用 messageBox.confirm()
  • 删除等危险操作必须使用确认框

3. 表格管理使用

  • 查询条件变化时调用 getDataList() 刷新数据
  • 删除、新增等操作后调用 getDataList(false) 保持当前页
  • 使用 validate 进行查询条件验证
  • 使用 onLoaded 处理数据加载后的逻辑

4. 错误处理

  • 所有异步操作使用 try-catch 处理错误
  • 使用 message.error() 提示错误信息
  • 用户取消操作不需要提示错误

更新记录

  • 2024-XX-XX: 创建 Hooks 使用指南文档
  • 文档版本v1.0

维护者: 前端开发团队
最后更新: 2024年