在后台管理、支付回调、表单提交等场景中,结果页几乎是必出现的页面形态。用户完成一个操作后,系统需要明确告诉他操作是成功还是失败,或者正处于处理中的等待状态。很多项目的做法是每个页面各写一套,导致图标、文案、按钮样式五花八门,后期维护成本很高。用 Vue 3 把它抽象成一个通用组件,不仅能统一视觉规范,还能通过插槽和路由集成获得很强的扩展能力。下面我们从设计思路、核心实现、进阶用法几个层面完整展开。

一、结果页组件的设计思路
设计通用组件前,先梳理清楚结果页的信息结构。无论成功、失败还是等待,一个结果页通常由四部分组成:状态图标、标题文案、描述文字、操作区。操作区可能是返回列表按钮、重试按钮或者跳转链接。把这四部分抽象成组件的固定骨架,再通过 props 控制内容、通过插槽开放定制,就是最合理的设计方向。
状态管理上有两种常见方案。第一种是用 type 属性配合条件渲染,模板里通过 v-if 分支展示不同图标和默认文案;第二种是注册多个子组件,用 component 动态渲染。前者实现简单、直观,适合状态间结构差异不大的情况;后者扩展性更强,适合成功页要展示订单号、失败页要展示错误堆栈这类差异化很大的需求。大多数业务场景推荐第一种,再辅以插槽处理个性化内容,代码量少且容易理解。
还需要考虑的是状态的枚举收敛。建议把 success、error、waiting 这几个状态定义成常量对象,配合 TypeScript 的字面量联合类型,让调用方传入非法值时在编译期就能报错,避免拼写错误导致的静默失败。
二、核心实现:props 驱动与插槽扩展
组件骨架包含图标区、标题、描述和默认插槽。图标可以引入图标库,也可以用 SVG 内联。以下是一个完整实现,采用 <script setup> 语法并支持 TypeScript:
<template>
<div class="result-page">
<div class="result-icon" :class="`result-icon--${type}`">
<svg v-if="type === 'success'" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="22" fill="#52c41a" />
<path d="M14 24l7 7 13-13" stroke="#fff" stroke-width="4"
fill="none" stroke-linecap="round" />
</svg>
<svg v-else-if="type === 'error'" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="22" fill="#ff4d4f" />
<path d="M17 17l14 14M31 17L17 31" stroke="#fff"
stroke-width="4" stroke-linecap="round" />
</svg>
<svg v-else viewBox="0 0 48 48" class="waiting-icon">
<circle cx="24" cy="24" r="20" fill="none"
stroke="#1890ff" stroke-width="4"
stroke-dasharray="90 40" stroke-linecap="round" />
</svg>
</div>
<h2 class="result-title">{{ title || defaultTitle }}</h2>
<p v-if="description" class="result-desc">{{ description }}</p>
<div class="result-extra">
<slot name="extra">
<button v-if="type !== 'waiting'" @click="$emit('retry')">
{{ type === 'success' ? '返回列表' : '重试' }}
</button>
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
type ResultType = 'success' | 'error' | 'waiting'
const props = withDefaults(defineProps<{
type?: ResultType
title?: string
description?: string
}>(), {
type: 'success'
})
defineEmits<{ (e: 'retry'): void }>()
const defaultTitle = computed(() => {
const map: Record<ResultType, string> = {
success: '操作成功',
error: '操作失败',
waiting: '处理中,请稍候'
}
return map[props.type]
})
</script>这段代码有几个值得注意的点。首先,withDefaults 给出了默认值,调用方只传 type 就能得到完整结果页。其次,默认标题通过 computed 映射得到,避免在模板里写一长串三元表达式。最后,extra 插槽有默认内容,同时对外抛出 retry 事件,父组件既可以直接用默认按钮,也可以完全接管操作区。等待状态的图标用 stroke-dasharray 配合 CSS 动画就能实现旋转效果,无需额外图片资源。
样式部分建议按状态拆分类名前缀,比如 result-icon--success、result-icon--error,这样后续要接入设计系统的主题色,只需要调整这几个类对应的变量即可,不会影响组件逻辑。
三、进阶用法:倒计时跳转与异步状态切换
真实业务里,成功页经常需要倒计时若干秒后自动跳回列表页。可以利用 onUnmounted 清理定时器,避免组件销毁后定时器继续执行导致内存泄漏:
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const seconds = ref(3)
let timer: number | undefined
timer = window.setInterval(() => {
seconds.value--
if (seconds.value <= 0) {
clearInterval(timer)
router.push('/order/list')
}
}, 1000)
onUnmounted(() => clearInterval(timer))
</script>
<template>
<ResultPage
type="success"
title="订单提交成功"
:description="`${seconds} 秒后自动返回订单列表`"
/>
</template>等待状态则常见于异步任务轮询场景。提交任务后先渲染 waiting 类型的结果页,后台定时查询任务结果,拿到最终状态后再切换到 success 或 error。切换可以通过一个响应式的 status 变量绑定到 type 属性,配合 <Transition> 组件给状态变化加过渡动画,用户体验会明显提升。
如果结果页需要被多个项目复用,建议进一步封装:把图标、文案映射表通过 provide 注入,或者干脆把组件打包成独立的 UI 库入口,并提供 renderResultPage 这样的编程式调用方法,在非组件环境下也能快速弹出一个标准结果页。同时在组件内部做好 aria-live 无障碍标注,例如等待状态加上 aria-live="polite",让屏幕阅读器在状态变化时能及时播报,这也是高质量组件不可或缺的细节。
最后补充一点测试建议。针对这个组件,核心用例包括三种状态的渲染断言、自定义 title 覆盖默认值的验证、retry 事件的触发以及倒计时跳转逻辑。用 Vitest 加 Vue Test Utils 可以在几十行内覆盖全部场景,确保后续迭代不会悄悄破坏既有行为。把结果页收敛成一个组件后,你会发现新增业务状态只需要扩展映射表,整体维护效率会提升一个台阶。