从组件点击事件到业务统一入口:一次前端操作链的完整解耦实践

从组件点击事件到业务统一入口:一次前端操作链的完整解耦实践

1.业务需求和收益点

业务需求:

每个项目可能支持多种操作动作包括处理模型中暂停 / 恢复 / 重跑 / 删除"的地方相关的接口,每个动作按钮既要展示图标,也要执行对应的业务接口。各个组件内部都写业务导致维护成本高。

将项目操作动作配置化、组件化,让 UI 不再参与业务逻辑,构建统一的、可扩展的动作体系

收益点:

  • 收益点 :UI 与业务逻辑彻底解耦
  • 动作按钮可配置、可扩展
  • 图标、文案、逻辑全部集中管理,易维护

2.文件结构和调用链

文件结构:

arduino 复制代码
ProjectListView/
  ├─ components/
  │    └─ ProjectCard.vue
  │
  ├─ settings/
  │    ├─ hooks/
  │    │    └─ useProjectActions.ts   //统一业务处理中心
  │    │
  │    └─ actionIcon.ts //动作图标配置模块
  │
  └─ index.vue

调用链:

2.原始代码部分示例

原始代码部分示例(4 个操作函数):

ProjectListView/index.vue

typescript 复制代码
// 处理暂停图标点击(切换显示三个操作图标)
const handlePauseClick = async (project: ProjectListItem) => {
  try {
    const response = await ProjectService.pauseTrain({ projectId: project.projectId })
    if (response.code === 1) {
      ElMessage.success('建图已暂停')
      await getProjectList()
    } else {
      ElMessage.error(response.message || '暂停失败')
    }
  } catch (error: unknown) {
    console.error('暂停训练失败:', error)
    const errorMessage = error instanceof Error ? error.message : '暂停训练失败'
    ElMessage.error(errorMessage)
  }
}
​
// 处理恢复图标点击(恢复为只显示暂停图标的状态)
const handleResumeClick = async (projectId: number) => {
  try {
    const response = await ProjectService.resumeTrain({ projectId })
    if (response.code === 1) {
      ElMessage.success('项目已恢复建图')
      await getProjectList()
    } else {
      ElMessage.error(response.message || '恢复失败')
    }
  } catch (error: unknown) {
    console.error('恢复训练失败:', error)
    const errorMessage = error instanceof Error ? error.message : '恢复训练失败'
    ElMessage.error(errorMessage)
  }
}
​
// 处理重跑图标点击
const handleRerunClick = async (projectId: number) => {
  try {
    const response = await ProjectService.restartTrain({ projectId })
    if (response.code === 1) {
      ElMessage.success('项目已开始重跑')
      await getProjectList()
    } else {
      ElMessage.error(response.message || '重跑失败')
    }
  } catch (error: unknown) {
    console.error('重跑项目失败:', error)
    const errorMessage = error instanceof Error ? error.message : '重跑项目失败'
    ElMessage.error(errorMessage)
  }
}
​
// 处理删除图标点击
const handleDeleteClick = async (projectId: number) => {
  try {
    await ElMessageBox.confirm(删除后,项目数据不可恢复, '确认删除该项目?', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      closeOnClickModal: false,
      customClass: 'tenant-confirm-messagebox',
    })
​
    try {
      const response = await ProjectService.deleteProject({ projectId })
      if (response.code === 1) {
        ElMessage.success('删除成功')
        await getProjectList()
      } else {
        ElMessage.error(response.message || '删除失败')
      }
    } catch (error: unknown) {
      console.error('删除项目失败:', error)
      const errorMessage = error instanceof Error ? error.message : '删除项目失败'
      ElMessage.error(errorMessage)
    }
  } 
}

简单概述一下这个代码:

参数名叫 project,他的类型设置为 ProjectListItem(写在 types 文件夹中)

const response = await ProjectService.pauseTrain({ projectId: project.projectId })

{ projectId: project.projectId }是对象字面量中的[键:值]写法,传过去的数据是{projectId:123456}

const response = await ProjectService.deleteProject({ projectId })

{ projectId }是一种简写的写法,{ projectId: projectId}可以直接写成{ projectId }

API 层中的

csharp 复制代码
 static async pauseTrain(params: PauseTrainParams): Promise<PauseTrainResponse> {
    const { projectId } = params

const { projectId } = params 为解构写法,如果 params 中存在一个叫 projectId 的值,拿出来变成一个新的同名变量,然后再使用

3.统一动作处理逻辑的封装与使用

在 ProjectListView/settings/hooks,创建 useProjectActions.ts,抽出逻辑

3.1 封装后的使用方式

先看封装后,怎么用,在看怎么封装

1.接入

javascript 复制代码
import {
  getStateUIConfig,
  PROJECT_TYPE_MENU_LIST,
  PROJECT_TYPE_MAP,
  createActionIconConfig,
  useProjectActions,
  useUrlSync,
} from './settings/index'

对应的 hook 统一有 index 管理,所以这么接入

2.将 hook 所需要的函数(刷新列表函数),传给他

php 复制代码
// 项目操作处理
const { handleAction } = useProjectActions({
  refreshProjectList: getProjectList,
})

这行代码本身是一个完全独立的"初始化"步骤,把getProjectList 传给hook,跟后面给 hook 传 id 啥的不在一条线上。

3.将handleAction 传给动作配置项

scss 复制代码
// 操作图标配置
const actionIconConfig = createActionIconConfig(handleAction)

这里的逻辑是,文件 A 中导入了 B,把 B 传给了 C,在 C 中调用 B

4.在模版中使用该配置

ruby 复制代码
<ProjectCard
            :action-icon-config="actionIconConfig"
          />

在父组件中把这个配置单传给子组件,子组件来进行具体的使用。

3.2 封装实现细节

actionIcon.ts 文件:

typescript 复制代码
import type { Component } from 'vue'
import type { ProjectListItem } from '@/types/project'
import PauseIcon from '@/assets/svgs/HomePauseIcon.vue'
import ResumeIcon from '@/assets/svgs/ResumeIcon.vue'
import DeleteIcon from '@/assets/svgs/DeleteIcon.vue'
import RerunIcon from '@/assets/svgs/RerunIcon.vue'
​
//字符串枚举
export type ActionIconType = 'pause' | 'resume' | 'delete' | 'rerun'
​
//动作-图标映射
export const ACTION_ICON_COMPONENTS = {
  pause: PauseIcon,
  resume: ResumeIcon,
  delete: DeleteIcon,
  rerun: RerunIcon,
} as const
​
//一个操作图标,需要的所有配置
export interface ActionIconConfig {
  component: Component
  tooltip: string
  handler: (project: ProjectListItem) => void
}
​
// 统一的操作处理函数的类型
export type HandleAction = (actionType: ActionIconType, project: ProjectListItem) => Promise<void>
​
​
//操作图标映射-统一处理
export function createActionIconConfig(
  handleAction: HandleAction,
): Record<ActionIconType, ActionIconConfig> {
  return {
    pause: {
      component: ACTION_ICON_COMPONENTS.pause,
      tooltip: '暂停',
      handler: (project) => handleAction('pause', project),
    },
    resume: {
      component: ACTION_ICON_COMPONENTS.resume,
      tooltip: '恢复',
      handler: (project) => handleAction('resume', project),
    },
    delete: {
      component: ACTION_ICON_COMPONENTS.delete,
      tooltip: '删除',
      handler: (project) => handleAction('delete', project),
    },
    rerun: {
      component: ACTION_ICON_COMPONENTS.rerun,
      tooltip: '重跑',
      handler: (project) => handleAction('rerun', project),
    },
  }
}
​
​
//判断是否为操作图标类型
export function isActionIconType(actionType: string): actionType is ActionIconType {
  return ['pause', 'resume', 'delete', 'rerun'].includes(actionType)
}

动作-图标映射为什么要加 as const?

为什么要加as const?

不加 as const 的话,TS 会把它推成一个比较宽泛的类型,比如:

less 复制代码
{
  pause: Component
  resume: Component
  delete: Component
  rerun: Component
}

加上 as const,TS 会更精确地认为:value 是这些具体组件本身,而不是随便一个 Component

=> void 的意思?

=> void:这个函数 不关心返回值 / 不需要返回任何东西

export type HandleAction = (actionType: ActionIconType, project: ProjectListItem) => Promise的含义是什么?

"有这么一种函数:

  • 第一个参数叫 actionType,只能是 'pause' | 'resume' | 'delete' | 'rerun'
  • 第二个参数叫 project,是一个 ProjectListItem 对象
  • 这个函数是异步的,返回 Promise

这个函数createActionIconConfig 的输入和产出是什么?

javascript 复制代码
export function createActionIconConfig(
  handleAction: HandleAction,
): Record<ActionIconType, ActionIconConfig>
  • 输入就是 handleAction,对应的类型HandleAction,这是从 index.vue 中传过来的
  • 产出: Record<ActionIconType, ActionIconConfig>

Record<K, V> 可以理解成:

一个对象:

  • key 的类型是 K
  • value 的类型是 V
  • K = ActionIconType → 'pause' | 'resume' | 'delete' | 'rerun'
  • V = ActionIconConfig → 每个动作对应一条配置,结构是:
typescript 复制代码
{
  component: Component
  tooltip: string
  handler: (project: ProjectListItem) => void
}

所以返回的东西大致长这样:

yaml 复制代码
{
  pause:  { component: ..., tooltip: '暂停', handler: (...) => {} },
  resume: { component: ..., tooltip: '恢复', handler: (...) => {} },
  delete: { ... },
  rerun:  { ... },
}

也就是说整个一个返回就是配置单对象

useProjectActions.ts(自定义 hook) 文件:

typescript 复制代码
import { ElMessage, ElMessageBox } from 'element-plus'
import { ProjectService } from '@/services/project'
import type { ProjectListItem } from '@/types/project'
import type { HandleAction } from '../actionIcon'
​
//项目操作处理函数的参数
export interface UseProjectActionsOptions {
  //刷新项目列表的函数
  refreshProjectList: () => Promise<void>
}
​
//项目操作处理
export function useProjectActions(options: UseProjectActionsOptions): {
  handleAction: HandleAction
} {
  const { refreshProjectList } = options
​
  const handlePause = async (project: ProjectListItem) => {
    try {
      const response = await ProjectService.pauseTrain({ projectId: project.projectId })
      if (response.code === 1) {
        ElMessage.success('建图已暂停')
        await refreshProjectList()
      } else {
        ElMessage.error(response.message || '暂停失败')
      }
    } catch (error: unknown) {
      console.error('暂停训练失败:', error)
      const errorMessage = error instanceof Error ? error.message : '暂停训练失败'
      ElMessage.error(errorMessage)
    }
  }
​
  const handleResume = async (project: ProjectListItem) => {
    try {
      const response = await ProjectService.resumeTrain({ projectId: project.projectId })
      if (response.code === 1) {
        ElMessage.success('项目已恢复建图')
        await refreshProjectList()
      } else {
        ElMessage.error(response.message || '恢复失败')
      }
    } catch (error: unknown) {
      console.error('恢复训练失败:', error)
      const errorMessage = error instanceof Error ? error.message : '恢复训练失败'
      ElMessage.error(errorMessage)
    }
  }
​
  const handleRerun = async (project: ProjectListItem) => {
    try {
      const response = await ProjectService.restartTrain({ projectId: project.projectId })
      if (response.code === 1) {
        ElMessage.success('项目已开始重跑')
        await refreshProjectList()
      } else {
        ElMessage.error(response.message || '重跑失败')
      }
    } catch (error: unknown) {
      console.error('重跑项目失败:', error)
      const errorMessage = error instanceof Error ? error.message : '重跑项目失败'
      ElMessage.error(errorMessage)
    }
  }
​
  const handleDelete = async (project: ProjectListItem) => {
    try {
      await ElMessageBox.confirm(`删除后,项目数据不可恢复`, '确认删除该项目?', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        closeOnClickModal: false,
        customClass: 'tenant-confirm-messagebox',
      })
​
      try {
        const response = await ProjectService.deleteProject({ projectId: project.projectId })
        if (response.code === 1) {
          ElMessage.success('删除成功')
          await refreshProjectList()
        } else {
          ElMessage.error(response.message || '删除失败')
        }
      } catch (error: unknown) {
        console.error('删除项目失败:', error)
        const errorMessage = error instanceof Error ? error.message : '删除项目失败'
        ElMessage.error(errorMessage)
      }
    } 
  }
​
  //统一入口函数
  const handleAction: HandleAction = async (actionType, project) => {
    switch (actionType) {
      case 'pause':
        await handlePause(project)
        break
      case 'resume':
        await handleResume(project)
        break
      case 'rerun':
        await handleRerun(project)
        break
      case 'delete':
        await handleDelete(project)
        break
      default:
        console.warn(`未知的操作类型: ${actionType}`)
    }
  }
​
  return {
    handleAction,
  }
}
​

其中

typescript 复制代码
export interface UseProjectActionsOptions {
  refreshProjectList: () => Promise<void>
}

useProjectActions 接收的那个配置对象里,必须有一个 refreshProjectList 函数

arduino 复制代码
export function useProjectActions(options: UseProjectActionsOptions): {
  handleAction: HandleAction
} 

定义了输入输出,输入一个刷新函数,输出一个处理函数

然后就是定义项目处理的操作

然后就是通过将这个函数导出,被父组件传递给createActionIconConfig 操作映射

统一入口函数的参数就是createActionIconConfig 传过去的

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax