TS 和组件绑定深耕(泛型表格)

一、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. 自适应 + 配置化

通过 sizewidth 灵活控制弹窗大小,适配不同业务场景。

Vue3 + TS 公共组件,固定遵守这 6 条

  1. 所有 Props 先用 interface 定义,绝不写行内对象
  2. 可选属性统一用 withDefaults 给默认值
  3. 固定值选项用字面量联合类型,不用 string
  4. Emits 必须用接口约束事件名和参数
  5. 复杂配置抽 Recordcomputed 统一管理
  6. 需要父组件调用方法,必须 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>;
相关推荐
道清茗2 小时前
【shell编程知识点汇总】第九章 HTML 清洗、多行合并与条件替换
前端·html
噢,我明白了3 小时前
表单的完整 CRUD 练习【极简个人记账本】(含前端后端链接mySQL)
java·前端·数据库·mysql
幽络源小助理3 小时前
MacCMSPro版视频影视系统源码_全开源高可用视频平台解决方案
前端·php·php源码
不会敲代码110 小时前
手写 Zustand:三十分钟带你搞懂状态管理库的核心原理
前端·javascript·源码
神奇的程序员10 小时前
重构了自己5年前写的截图插件
前端·javascript·架构
橙淮10 小时前
从优化到安全再到未来 ——JavaScript 全维度技术指南
javascript
UXbot11 小时前
一人独立交付 UI + 前端:AI 驱动 UI 设计工具的五大功能模块深度评测
前端·低代码·ui·设计模式·交互
kobesdu11 小时前
【ROS2实战笔记-19】ROS2 生命周期节点的启动顺序、状态转换陷阱与热备方案
java·前端·笔记·机器人·ros·ros2
诚实可靠王大锤11 小时前
React Native 输入框与按钮焦点冲突解决方案(rn版本0.70.3)
前端·javascript·react native·react.js