动态组件和插槽
[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: {// 判断使用的icongetIcon(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>