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` ,改动点:
- 删除`defineEmits` 整块 ,替换为两个 model( L58-62 ):
ts
`/** 当前页码 */
const current = defineModel<number>("current");
/** 每页大小 */
const pageSize = defineModel<number>("pageSize");`
同时从 Props 接口移除了`current` /`pageSize` ---`defineModel` 会自动声明同名 prop,重复声明会冲突。
- 桥接 computed 的手动分支改为直接赋值 ( L76 、 L89 ):
ts
`current.value = value; // defineModel 自动 emit update:current`
- handleSizeChange 里`emit("update:current", 1)` →`currentModel.value = 1` ( L107 )
对外行为完全不变:父组件的`v-model:current` /`v-model:page-size` 用法照旧,只是组件内部省掉了手写 emit 契约的样板代码。Store 模式不受影响(model 未被父组件绑定时不会被触碰,分支由`isStoreMode` 保护)。