vue3双向邦定:使用defineModel替换defineProps+defineEmits

defineProps+defineEmits

TypeScript 复制代码
<script setup lang="ts">
/**
 * 基础分页
 * 统一封装 el-pagination 的 size-change / current-change 事件处理逻辑:
 * - 每页大小变化时重置页码为 1,避免越界页码导致返回空页
 * - 页码变化时重新加载数据
 *
 * 支持两种使用模式(二选一):
 * 1. Store 模式(复杂页面):传入实现 PaginatedStore 接口的 store,自动获得 isResetting 屏蔽能力
 * 2. 手动模式(简单页面):直接传入 total / current / pageSize,通过 v-model 双向绑定
 *
 * @example
 * // Store 模式
 * <BasePagination :store="store" :loader="store.getCapitalInfos" />
 *
 * @example
 * // 手动模式(无 store)
 * <BasePagination
 *   :total="total"
 *   v-model:current="queryRequest.current"
 *   v-model:page-size="queryRequest.size"
 *   :loader="fetchCapitalInfos" />
 */
defineOptions({
  name: "BasePagination"
});
import type { PaginatedStore } from "@/types";
import { computed, nextTick } from "vue";

// ========== 依赖 ==========
interface Props {
  /**
   * 分页 Store / 控制器(Store 模式)
   * - 提供 isResetting / setCurrentPage / queryRequest / total,自动获得并发屏蔽能力
   */
  store?: PaginatedStore;
  /**
   * 记录总数(手动模式,store 未提供时使用)
   */
  total?: number;
  /**
   * 当前页码(手动模式,支持 v-model:current)
   */
  current?: number;
  /**
   * 每页大小(手动模式,支持 v-model:page-size)
   */
  pageSize?: number;
  /**
   * 数据加载器
   * - 由本组件在「每页大小变化」或「页码变化」时调用
   * - 参数 `forceRefresh = true` 表示强制从后端拉取最新数据
   * - 返回 Promise,本组件会 `await` 它,用于控制加载顺序
   */
  loader: (forceRefresh?: boolean) => Promise<unknown>;
  /** 每页大小可选项,默认 [20, 50, 100, 200, 500] */
  pageSizes?: number[];
  /** 布局字符串,默认 "total, sizes, prev, pager, next, jumper" */
  layout?: string;
}
const props = withDefaults(defineProps<Props>(), {
  pageSizes: () => [20, 50, 100, 200, 500],
  layout: "total, sizes, prev, pager, next, jumper"
});

const emit = defineEmits<{
  /**
   * 当前页码变化(手动模式下配合 v-model:current 使用)
   * @param value 新的当前页码
   */
  "update:current": [value: number];
  /**
   * 每页大小变化(手动模式下配合 v-model:page-size 使用)
   * @param value 新的每页大小
   */
  "update:pageSize": [value: number];
}>();

// ========== 计算属性(Getters / Setters) ==========
/** 是否处于 Store 模式 */
const isStoreMode = computed(() => !!props.store);

/** 当前页码(桥接两种模式的 v-model:current-page) */
const currentModel = computed({
  get: () => (isStoreMode.value ? props.store!.queryRequest.current : props.current) ?? 1,
  set: (value: number) => {
    if (isStoreMode.value) {
      props.store!.queryRequest.current = value;
    } else {
      emit("update:current", value);
    }
  }
});

/** 每页大小(桥接两种模式的 v-model:page-size) */
const pageSizeModel = computed({
  get: () => (isStoreMode.value ? props.store!.queryRequest.size : props.pageSize) ?? 20,
  set: (value: number) => {
    if (isStoreMode.value) {
      props.store!.queryRequest.size = value;
    } else {
      emit("update:pageSize", value);
    }
  }
});

/** 记录总数 */
const totalModel = computed(() => (isStoreMode.value ? props.store!.total : props.total) ?? 0);

// ========== 事件处理 ==========
/**
 * 每页大小改变(重置页码为 1,避免越界页码导致返回空页)
 */
const handleSizeChange = async () => {
  // Store 模式:依赖 isResetting 屏蔽程序改写引发的并发 change
  if (isStoreMode.value) {
    if (props.store!.isResetting) return;
    await props.store!.setCurrentPage(1);
  } else {
    // 手动模式:直接重置页码为 1(程序修改 prop 不会触发 current-change,无需屏蔽)
    emit("update:current", 1);
    // ⏳ 等待父组件 v-model 回写 props,确保 loader 读取到最新页码
    await nextTick();
  }

  // 加载列表数据(强制刷新)
  await props.loader(true);
};

/**
 * 页码改变(加载列表数据)
 */
const handleCurrentChange = async () => {
  // Store 模式:依赖 isResetting 屏蔽
  if (isStoreMode.value && props.store!.isResetting) return;

  // 加载列表数据(强制刷新)
  await props.loader(true);
};
</script>

<template>
  <el-pagination
    :total="totalModel"
    :page-sizes="pageSizes"
    v-model:page-size="pageSizeModel"
    v-model:current-page="currentModel"
    background
    :layout="layout"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange" />
</template>

<style scoped lang="scss"></style>

defineModel

TypeScript 复制代码
<script setup lang="ts">
/**
 * 基础分页
 * 统一封装 el-pagination 的 size-change / current-change 事件处理逻辑:
 * - 每页大小变化时重置页码为 1,避免越界页码导致返回空页
 * - 页码变化时重新加载数据
 *
 * 支持两种使用模式(二选一):
 * 1. Store 模式(复杂页面):传入实现 PaginatedStore 接口的 store,自动获得 isResetting 屏蔽能力
 * 2. 手动模式(简单页面):直接传入 total / current / pageSize,通过 v-model 双向绑定
 *
 * @example
 * // Store 模式
 * <BasePagination :store="store" :loader="store.getCapitalInfos" />
 *
 * @example
 * // 手动模式(无 store)
 * <BasePagination
 *   :total="total"
 *   v-model:current="queryRequest.current"
 *   v-model:page-size="queryRequest.size"
 *   :loader="fetchCapitalInfos" />
 */
defineOptions({
  name: "BasePagination"
});
import type { PaginatedStore } from "@/types";
import { computed, nextTick } from "vue";

// ========== 依赖 ==========
interface Props {
  /**
   * 分页 Store / 控制器(Store 模式)
   * - 提供 isResetting / setCurrentPage / queryRequest / total,自动获得并发屏蔽能力
   */
  store?: PaginatedStore;
  /**
   * 记录总数(手动模式,store 未提供时使用)
   */
  total?: number;
  /**
   * 数据加载器
   * - 由本组件在「每页大小变化」或「页码变化」时调用
   * - 参数 `forceRefresh = true` 表示强制从后端拉取最新数据
   * - 返回 Promise,本组件会 `await` 它,用于控制加载顺序
   */
  loader: (forceRefresh?: boolean) => Promise<unknown>;
  /** 每页大小可选项,默认 [20, 50, 100, 200, 500] */
  pageSizes?: number[];
  /** 布局字符串,默认 "total, sizes, prev, pager, next, jumper" */
  layout?: string;
}
const props = withDefaults(defineProps<Props>(), {
  pageSizes: () => [20, 50, 100, 200, 500],
  layout: "total, sizes, prev, pager, next, jumper"
});

// ---------- 双向绑定(defineModel,手动模式下配合 v-model:current / v-model:page-size 使用) ----------
/** 当前页码 */
const current = defineModel<number>("current");
/** 每页大小 */
const pageSize = defineModel<number>("pageSize");

// ========== 计算属性(Getters / Setters) ==========
/** 是否处于 Store 模式 */
const isStoreMode = computed(() => !!props.store);

/** 当前页码(桥接两种模式的 v-model:current-page) */
const currentModel = computed({
  get: () => (isStoreMode.value ? props.store!.queryRequest.current : current.value) ?? 1,
  set: (value: number) => {
    if (isStoreMode.value) {
      props.store!.queryRequest.current = value;
    } else {
      // defineModel 赋值自动 emit update:current,由父组件 v-model 回写
      current.value = value;
    }
  }
});

/** 每页大小(桥接两种模式的 v-model:page-size) */
const pageSizeModel = computed({
  get: () => (isStoreMode.value ? props.store!.queryRequest.size : pageSize.value) ?? 20,
  set: (value: number) => {
    if (isStoreMode.value) {
      props.store!.queryRequest.size = value;
    } else {
      pageSize.value = value;
    }
  }
});

/** 记录总数 */
const totalModel = computed(() => (isStoreMode.value ? props.store!.total : props.total) ?? 0);

// ========== 事件处理 ==========
/**
 * 每页大小改变(重置页码为 1,避免越界页码导致返回空页)
 */
const handleSizeChange = async () => {
  // Store 模式:依赖 isResetting 屏蔽程序改写引发的并发 change
  if (isStoreMode.value) {
    if (props.store!.isResetting) return;
    await props.store!.setCurrentPage(1);
  } else {
    // 手动模式:直接重置页码为 1(程序修改 prop 不会触发 current-change,无需屏蔽)
    currentModel.value = 1;
    // ⏳ 等待父组件 v-model 回写 props,确保 loader 读取到最新页码
    await nextTick();
  }

  // 加载列表数据(强制刷新)
  await props.loader(true);
};

/**
 * 页码改变(加载列表数据)
 */
const handleCurrentChange = async () => {
  // Store 模式:依赖 isResetting 屏蔽
  if (isStoreMode.value && props.store!.isResetting) return;

  // 加载列表数据(强制刷新)
  await props.loader(true);
};
</script>

<template>
  <el-pagination
    :total="totalModel"
    :page-sizes="pageSizes"
    v-model:page-size="pageSizeModel"
    v-model:current-page="currentModel"
    background
    :layout="layout"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange" />
</template>

<style scoped lang="scss"></style>

改造完成,`vue-tsc` 零错误。Vue 3.4.21 支持`defineModel` ,改动点:

  1. 删除`defineEmits` 整块 ,替换为两个 model( L58-62 ):

ts

`/** 当前页码 */

const current = defineModel<number>("current");

/** 每页大小 */

const pageSize = defineModel<number>("pageSize");`

同时从 Props 接口移除了`current` /`pageSize` ---`defineModel` 会自动声明同名 prop,重复声明会冲突。

  1. 桥接 computed 的手动分支改为直接赋值 ( L76 、 L89 ):

ts

`current.value = value; // defineModel 自动 emit update:current`

  1. handleSizeChange 里`emit("update:current", 1)` →`currentModel.value = 1` ( L107 )

对外行为完全不变:父组件的`v-model:current` /`v-model:page-size` 用法照旧,只是组件内部省掉了手写 emit 契约的样板代码。Store 模式不受影响(model 未被父组件绑定时不会被触碰,分支由`isStoreMode` 保护)。

相关推荐
志尊宝2 小时前
Vue3 零基础每日笔记(047):动态路由与路由参数——:id 传参、query 传参、props 解耦
前端·javascript·vue.js·笔记·html5
计算机毕设定制辅导-无忧学长3 小时前
《基于SpringBoot的中学教师数字胜任力测评网站的设计与实现》
java·vue.js·spring boot·mysql·中学教师数字胜任力测评网站
flash俊杰5 小时前
Vue 3 响应式陷阱与状态单例:从 TDZ 白屏到跨进程 Proxy 剥离
vue.js
志尊宝5 小时前
Vue3 零基础每日笔记(046):声明式导航与编程式导航——useRouter / useRoute 双子星
vue.js·笔记·#vue #前端 #前端开发·#javascript·#vue.js
你别说话了1 天前
Vue实现高效拖拽效果 vue-Draggable
前端·vue.js
志尊宝1 天前
Vue3 零基础每日笔记(036):动手封装 useTitle 与 useToggle——最小的组合式函数
vue.js·#vue #前端 #前端开发·#javascript·#vue.js
专业程序开发源1 天前
springboot旅游推荐系统82074-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·php·课程设计·旅游
晚安日记wanna1 天前
Vue3 script setup 的四层追问答到第三层才算过关
前端·vue.js·面试
six_1 天前
RiverPlusCOM Web:不用安装的在线串口调试助手
vue.js·程序员