一、React + TS 泛型通用表格
GenericTable.tsx
TypeScript
import React from 'react';
// 列配置类型
export type TableColumn<T> = {
key: keyof T;
title: string;
width?: number;
render?: (val: T[keyof T], record: T) => React.ReactNode;
};
// 组件入参
interface GenericTableProps<T> {
columns: TableColumn<T>[];
dataSource: T[];
loading?: boolean;
}
// 泛型组件写法
function GenericTable<T>(props: GenericTableProps<T>) {
const { columns, dataSource, loading = false } = props;
if (loading) return <div>加载中...</div>;
return (
<table border={1} cellPadding={6} cellSpacing={0}>
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.key)} style={{ width: col.width }}>
{col.title}
</th>
))}
</tr>
</thead>
<tbody>
{dataSource.map((record, idx) => (
<tr key={idx}>
{columns.map((col) => {
const val = record[col.key];
return (
<td key={String(col.key)}>
{col.render ? col.render(val, record) : String(val)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
}
export default GenericTable;
使用示例
TypeScript
import GenericTable from './GenericTable';
// 业务实体
interface UserItem {
id: number;
name: string;
status: 0 | 1;
}
function Demo() {
const columns: TableColumn<UserItem>[] = [
{ key: 'id', title: 'ID' },
{ key: 'name', title: '姓名' },
{
key: 'status',
title: '状态',
render: (val) => (val === 1 ? '启用' : '禁用')
}
];
const data: UserItem[] = [
{ id: 1, name: '张三', status: 1 },
{ id: 2, name: '李四', status: 0 }
];
return <GenericTable columns={columns} dataSource={data} />;
}
二、Vue3 + TS 标准公共组件模板
BaseDialog.vue
TypeScript
<template>
<!-- 遮罩层 -->
<div
class="base-dialog-mask"
v-if="visible"
@click.self="handleCloseMask"
>
<!-- 弹窗容器 -->
<div class="base-dialog" :style="dialogStyle">
<!-- 头部 -->
<div class="dialog-header">
<slot name="header">
<span class="title">{{ title }}</span>
</slot>
<span class="close-btn" @click="handleClose">×</span>
</div>
<!-- 默认插槽:主体内容 -->
<div class="dialog-body">
<slot />
</div>
<!-- 底部 -->
<div class="dialog-footer" v-if="showFooter">
<slot name="footer">
<button class="btn cancel-btn" @click="handleClose">取消</button>
<button class="btn confirm-btn" @click="handleConfirm">确定</button>
</slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 👉 1. 先定义类型:单独抽离,不写在行内
type DialogSize = 'small' | 'middle' | 'large';
interface BaseDialogProps {
// 必传
visible: boolean
// 可选 + 默认值
title?: string
size?: DialogSize
width?: string
showFooter?: boolean
closeOnMask?: boolean
}
// 👉 2. props 定义 + 精准默认值(TS 标准写法)
const props = withDefaults(defineProps<BaseDialogProps>(), {
title: '提示',
size: 'middle',
showFooter: true,
closeOnMask: true,
width: ''
})
// 👉 3. 严格定义 emits 事件类型
interface DialogEmits {
(e: 'update:visible', val: boolean): void
(e: 'confirm'): void
(e: 'close'): void
}
const emit = defineEmits<DialogEmits>()
// 👉 4. 计算弹窗宽度(根据 size 适配)
const dialogStyle = computed(() => {
const sizeMap: Record<DialogSize, string> = {
small: '400px',
middle: '600px',
large: '800px'
}
return {
width: props.width || sizeMap[props.size]
}
})
// 👉 5. 内部事件方法
const handleClose = () => {
emit('update:visible', false)
emit('close')
}
const handleConfirm = () => {
emit('confirm')
}
const handleCloseMask = () => {
if (props.closeOnMask) {
handleClose()
}
}
// 👉 6. 对外暴露组件实例方法(父组件可 ref 调用)
defineExpose({
handleClose,
handleConfirm
})
</script>
<style scoped>
.base-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.base-dialog {
background: #fff;
border-radius: 8px;
overflow: hidden;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.title {
font-size: 16px;
font-weight: 600;
}
.close-btn {
cursor: pointer;
font-size: 20px;
color: #999;
}
.dialog-body {
padding: 20px;
}
.dialog-footer {
padding: 12px 20px;
border-top: 1px solid #eee;
text-align: right;
}
.btn {
padding: 6px 16px;
margin-left: 8px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.cancel-btn {
background: #f5f5f5;
}
.confirm-btn {
background: #1677ff;
color: #fff;
}
</style>
使用
TypeScript
<template>
<BaseDialog
v-model:visible="dialogVisible"
title="编辑内容"
size="middle"
:show-footer="true"
@confirm="handleSubmit"
>
这里是弹窗主体内容
</BaseDialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import BaseDialog from '@/components/BaseDialog.vue'
const dialogVisible = ref(false)
const handleSubmit = () => {
console.log('点击确定')
}
</script>
重点拆解:为什么这是企业高级写法
1. 类型规范
- 单独
interface定义 Props、Emits - 用枚举字面量
'small'|'middle'|'large',杜绝乱传字符串 - 全程无 any,所有参数都有类型约束
2. props 默认值
用 withDefaults 给可选属性设默认值,TS 识别完美,不用自己逻辑判断。
3. 事件规范
- 用
update:visible支持v-model:visible双向绑定 - 事件参数类型严格约束,不会乱传参
4. 多插槽规范
- 具名插槽
header/footer+ 默认插槽 - 外部可自定义头部、底部、内容,复用性拉满
5. defineExpose 暴露实例
父组件通过 ref 可以直接调用组件内部方法,适合复杂业务弹窗。
6. 自适应 + 配置化
通过 size、width 灵活控制弹窗大小,适配不同业务场景。
Vue3 + TS 公共组件,固定遵守这 6 条
- 所有 Props 先用
interface定义,绝不写行内对象 - 可选属性统一用
withDefaults给默认值 - 固定值选项用字面量联合类型,不用 string
- Emits 必须用接口约束事件名和参数
- 复杂配置抽
Record、computed统一管理 - 需要父组件调用方法,必须
defineExpose
三、配套全局类型规范(必加)
在项目 src 新建 types/global.d.ts
TypeScript
// 通用枚举
export type StatusType = 0 | 1 | 2;
// 通用接口返回格式
export interface ResData<T> {
code: number;
data: T;
message: string;
}
// 常用工具类型复用
export type PartialOptional<T> = Partial<T>;