javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > TS和组件绑定(泛型表格)

TS和组件绑定过程(泛型表格)

作者:淑子啦

文章介绍了使用React+TS和Vue3;TS实现泛型通用表格和标准公共组件的方法,重点在于类型规范、事件规范、插槽规范等,强调了全程无`null`、类型约束等,并提供了配套的全局类型规范文件,这些实践有助于提升代码质量和可复性性

一、React + TS 泛型通用表格

GenericTable.tsx

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;

使用示例

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

<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>

使用:

<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. 类型规范

2. props 默认值

withDefaults 给可选属性设默认值,TS 识别完美,不用自己逻辑判断。

3. 事件规范

4. 多插槽规范

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

// 通用枚举
export type StatusType = 0 | 1 | 2;

// 通用接口返回格式
export interface ResData<T> {
  code: number;
  data: T;
  message: string;
}

// 常用工具类型复用
export type PartialOptional<T> = Partial<T>;

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文