从组件点击事件到业务统一入口:一次前端操作链的完整解耦实践
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 层中的
csharpstatic async pauseTrain(params: PauseTrainParams): Promise<PauseTrainResponse> { const { projectId } = paramsconst { 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 传过去的