[Vue2]动态组件和插槽
动态组件和插槽来实现外部传入自定义渲染
组件
<template>
<!-- 回复的处理进度 -->
<div v-if="steps.length > 0" class="gain-box-header">
<el-steps direction="vertical">
<div class="load-but">
<el-step v-for="item in steps" :key="item.id" :icon="getIcon(item)">
<div slot="title" class="step_title" @click="item.isShow = !item.isShow">
<div>{{ item.title || '加载中' }}</div>
<div>
<i v-if="item.isShow" class="el-icon-arrow-down"></i>
<i v-else class="el-icon-arrow-up"></i>
</div>
</div>
<template slot="description">
<div v-show="item.isShow">
<!-- 内置类型渲染 -->
<template v-if="item.type === NormalSteps.Markdown">
<MdRender v-if="item.content" :content="item.content" />
</template>
<template v-else-if="item.type === NormalSteps.Text">
<div class="step_label">{{ item.content }}</div>
</template>
<template v-else-if="item.type === SpecialStep.RxtPolicyList">
<div class="step_label">{{ item.content }}</div>
</template>
<!-- 自定义组件渲染 -->
<template v-else-if="isCustomComponent(item.type)">
<component
:is="getCustomComponent(item.type)"
:item="item"
:content="item.content"
:meta="item.meta"
v-bind="item.props || {}"
/>
</template>
<!-- 自定义插槽渲染 -->
<template v-else-if="isCustomSlot(item.type)">
<slot
:name="getSlotName(item.type)"
:item="item"
:content="item.content"
:meta="item.meta"
>
<!-- 插槽默认内容 -->
<div class="step_label">{{ item.content }}</div>
</slot>
</template>
<!-- 自定义渲染函数 -->
<template v-else-if="isCustomRender(item.type)">
<div v-html="getCustomRender(item.type, item)"></div>
</template>
<!-- 默认文本渲染 -->
<template v-else>
<div class="step_label">{{ item.content }}</div>
</template>
</div>
</template>
</el-step>
</div>
</el-steps>
</div>
</template>
<script>
import MdRender from '@/components/MdRender/think'
import { NormalSteps, SpecialStep } from '@/dicts/DictSse.js'
export default {
name: 'StepList',
components: { MdRender },
props: {
steps: {
type: Array,
default: () => [
// {
// id: '', // 唯一键
// type: 'md', // 类型
// title: '', // 标题
// content: '', // 内容
// isStop: false, // 是否结束
// isShow: true, // 是否展示
// isError: false, // 是否失败 消息停止时改步骤未结束,则标记为失败
// meta: {}, // 附加属性
// props: {}, // 传递给自定义组件的额外props
// }
]
},
// 自定义组件映射 { 'custom-chart': ChartComponent }
customComponents: {
type: Object,
default: () => ({})
},
// 自定义渲染函数映射 { 'custom-render': (item) => '<div>...</div>' }
customRenders: {
type: Object,
default: () => ({})
}
},
data() {
return {
NormalSteps,
SpecialStep
}
},
methods: {
// 判断使用的icon
getIcon(item) {
if (!item.isStop) {
return 'el-icon-loading'
}
if (item.isError) {
return 'el-icon-error'
}
return 'el-icon-success'
},
// 判断是否为自定义组件
isCustomComponent(type) {
return type && type.startsWith('component:') && this.customComponents[type.replace('component:', '')]
},
// 获取自定义组件
getCustomComponent(type) {
const componentName = type.replace('component:', '')
return this.customComponents[componentName]
},
// 判断是否为自定义插槽
isCustomSlot(type) {
return type && type.startsWith('slot:')
},
// 获取插槽名称
getSlotName(type) {
return type.replace('slot:', '')
},
// 判断是否为自定义渲染函数
isCustomRender(type) {
return type && type.startsWith('render:') && this.customRenders[type.replace('render:', '')]
},
// 获取自定义渲染结果
getCustomRender(type, item) {
const renderName = type.replace('render:', '')
const renderFn = this.customRenders[renderName]
return renderFn ? renderFn(item) : item.content
}
}
}
</script>
<style scoped>
.gain-box-header {
width: 100%;
display: flex;
align-items: center;
}
.load-but {
width: 100%;
padding: 15px 10px;
background: rgba(13, 62, 135, 0.06);
border-radius: 17px;
border: 1px solid rgba(1, 128, 255, 0.03);
}
.load-but > span {
line-height: 29px;
color: #625b88;
}
.step_title {
display: flex;
align-items: center;
justify-content: space-between;
}
.step_label {
white-space: pre-wrap;
font-size: 0.75rem;
color: #606266;
}
::v-deep .el-steps {
width: 100%;
}
::v-deep .el-step {
min-height: 50px;
}
::v-deep .el-step:last-child {
min-height: 0;
}
::v-deep .el-step__icon.is-icon {
background: transparent;
}
::v-deep .el-step.is-vertical .el-step__title {
font-size: 14px;
color: #222222;
}
::v-deep .el-step.is-vertical .el-step__line {
top: 27px;
bottom: 3px;
}
::v-deep .el-icon-success {
color: #4281ed;
}
::v-deep .el-icon-error {
color: #c0c4cc;
}
::v-deep .vuepress-markdown-body {
background: transparent;
}
</style>
外部引用
<template>
<div>
<!-- 使用StepList组件 -->
<StepList
:steps="steps"
:custom-components="customComponents"
:custom-renders="customRenders"
>
<!-- 自定义插槽渲染 -->
<template #custom-table="{ item, content, meta }">
<el-table :data="content" size="mini" border>
<el-table-column prop="name" label="名称" />
<el-table-column prop="value" label="值" />
</el-table>
</template>
<template #custom-progress="{ item, meta }">
<el-progress
:percentage="meta.progress"
:status="meta.status"
:stroke-width="8"
/>
<div style="margin-top: 8px; font-size: 12px; color: #666;">
{{ meta.progressText }}
</div>
</template>
<template #custom-image="{ item, content }">
<div class="image-container">
<img :src="content" :alt="item.title" style="max-width: 100%; height: auto;" />
</div>
</template>
</StepList>
</div>
</template>
<script>
import StepList from './StepList.vue'
import { NormalSteps, SpecialStep } from '@/dicts/DictSse.js'
// 自定义图表组件
const CustomChart = {
props: ['item', 'content', 'meta'],
template: `
<div class="custom-chart">
<div class="chart-title">{{ item.title }}</div>
<div class="chart-content">
<div v-for="(data, index) in content" :key="index" class="chart-bar">
<span class="bar-label">{{ data.label }}</span>
<div class="bar-container">
<div class="bar-fill" :style="{ width: data.value + '%' }"></div>
</div>
<span class="bar-value">{{ data.value }}%</span>
</div>
</div>
</div>
`,
style: `
.custom-chart { padding: 10px; }
.chart-title { font-weight: bold; margin-bottom: 10px; }
.chart-bar { display: flex; align-items: center; margin-bottom: 8px; }
.bar-label { width: 80px; font-size: 12px; }
.bar-container { flex: 1; height: 20px; background: #f0f0f0; margin: 0 10px; position: relative; }
.bar-fill { height: 100%; background: #409eff; transition: width 0.3s; }
.bar-value { font-size: 12px; }
`
}
// 自定义列表组件
const CustomList = {
props: ['item', 'content', 'meta'],
template: `
<div class="custom-list">
<div v-for="(listItem, index) in content" :key="index" class="list-item">
<div class="list-icon">
<i :class="listItem.icon || 'el-icon-check'"></i>
</div>
<div class="list-content">
<div class="list-title">{{ listItem.title }}</div>
<div class="list-desc">{{ listItem.description }}</div>
</div>
</div>
</div>
`,
style: `
.custom-list { padding: 10px 0; }
.list-item { display: flex; align-items: flex-start; margin-bottom: 12px; }
.list-icon { width: 20px; height: 20px; margin-right: 10px; display: flex; align-items: center; justify-content: center; }
.list-content { flex: 1; }
.list-title { font-weight: bold; margin-bottom: 4px; }
.list-desc { font-size: 12px; color: #666; }
`
}
export default {
name: 'StepListExample',
components: { StepList },
data() {
return {
// 自定义组件映射
customComponents: {
'chart': CustomChart,
'list': CustomList
},
// 自定义渲染函数映射
customRenders: {
'highlight': (item) => {
return `<div style="background: #fff3cd; padding: 10px; border-radius: 4px; border-left: 4px solid #ffc107;">
<strong>⚠️ 重要提示</strong><br/>
${item.content}
</div>`
},
'code': (item) => {
return `<pre style="background: #f8f9fa; padding: 12px; border-radius: 4px; overflow-x: auto;"><code>${item.content}</code></pre>`
}
},
// 步骤数据
steps: [
// 内置类型 - Markdown
{
id: '1',
type: NormalSteps.Markdown,
title: '步骤1:分析数据',
content: '## 数据分析结果\n\n- 处理了 **1000** 条记录\n- 发现 `5` 个异常值\n- 准确率达到 **95%**',
isStop: true,
isShow: true,
isError: false,
meta: {}
},
// 内置类型 - Text
{
id: '2',
type: NormalSteps.Text,
title: '步骤2:文本说明',
content: '这是一个普通的文本说明内容,用于展示基本的文本渲染效果。',
isStop: true,
isShow: true,
isError: false,
meta: {}
},
// 自定义组件 - 图表
{
id: '3',
type: 'component:chart',
title: '步骤3:数据可视化',
content: [
{ label: '成功率', value: 85 },
{ label: '处理速度', value: 92 },
{ label: '准确率', value: 78 }
],
isStop: true,
isShow: true,
isError: false,
meta: {},
props: {} // 额外传递给组件的props
},
// 自定义组件 - 列表
{
id: '4',
type: 'component:list',
title: '步骤4:任务清单',
content: [
{
title: '数据预处理',
description: '清洗和格式化原始数据',
icon: 'el-icon-check'
},
{
title: '模型训练',
description: '使用机器学习算法训练模型',
icon: 'el-icon-loading'
},
{
title: '结果验证',
description: '验证模型的准确性和可靠性',
icon: 'el-icon-time'
}
],
isStop: false,
isShow: true,
isError: false,
meta: {}
},
// 自定义插槽 - 表格
{
id: '5',
type: 'slot:custom-table',
title: '步骤5:数据表格',
content: [
{ name: '处理时间', value: '2.5秒' },
{ name: '内存使用', value: '128MB' },
{ name: 'CPU使用率', value: '45%' }
],
isStop: true,
isShow: true,
isError: false,
meta: {}
},
// 自定义插槽 - 进度条
{
id: '6',
type: 'slot:custom-progress',
title: '步骤6:处理进度',
content: '',
isStop: false,
isShow: true,
isError: false,
meta: {
progress: 67,
status: 'active',
progressText: '正在处理中... 67%'
}
},
// 自定义插槽 - 图片
{
id: '7',
type: 'slot:custom-image',
title: '步骤7:结果展示',
content: 'https://via.placeholder.com/300x200?text=Processing+Result',
isStop: true,
isShow: true,
isError: false,
meta: {}
},
// 自定义渲染函数 - 高亮提示
{
id: '8',
type: 'render:highlight',
title: '步骤8:重要提示',
content: '请注意:此操作不可逆,请确保数据已备份!',
isStop: true,
isShow: true,
isError: false,
meta: {}
},
// 自定义渲染函数 - 代码块
{
id: '9',
type: 'render:code',
title: '步骤9:代码示例',
content: `function processData(data) {
return data.map(item => ({
...item,
processed: true,
timestamp: new Date().toISOString()
}));
}`,
isStop: true,
isShow: true,
isError: false,
meta: {}
}
]
}
}
}
</script>
<style scoped>
.image-container {
text-align: center;
padding: 10px;
}
</style>