<template>
<view class="container">
<!-- 顶部控制栏 -->
<view class="control-bar">
<text class="title">🎨 动态组件渲染器</text>
<view class="btn-group">
<button class="btn btn-primary" @click="loadComponent('card')">加载卡片组件</button>
<button class="btn btn-success" @click="loadComponent('list')">加载列表组件</button>
<button class="btn btn-warning" @click="loadComponent('form')">加载表单组件</button>
<button class="btn btn-danger" @click="clearComponent">清空</button>
</view>
</view>
<!-- 代码编辑区 -->
<view class="editor-section" v-if="showEditor">
<view class="section-header">
<text class="section-title">📝 组件代码(可编辑)</text>
<button class="btn btn-small btn-primary" @click="renderCustomCode">运行代码</button>
</view>
<textarea class="code-editor" v-model="editableCode" placeholder="在此输入 Vue 模板代码..." />
</view>
<!-- 动态渲染区域 -->
<view class="preview-section">
<view class="section-header">
<text class="section-title">👁️ 渲染预览</text>
<text class="status-badge" :class="{ active: currentComponentType }">
{{ currentComponentType ? '运行中' : '未加载' }}
</text>
</view>
<view class="preview-container">
<!-- 根据组件类型进行条件渲染 -->
<view v-if="currentComponentType === 'card'" class="dynamic-component">
<view class="product-card">
<image class="product-image" src="https://picsum.photos/300/200?random=1" mode="aspectFill" />
<view class="product-info">
<text class="product-title">🎧 无线降噪耳机 Pro</text>
<text class="product-desc">主动降噪 · 30小时续航 · 空间音频</text>
<view class="product-footer">
<text class="product-price">¥1,299</text>
<button class="buy-btn" @click="handleCardBuy">立即购买</button>
</view>
</view>
</view>
</view>
<view v-else-if="currentComponentType === 'list'" class="dynamic-component">
<view class="news-list">
<view class="list-header">
<text class="header-title">📰 今日热点</text>
<text class="refresh-btn" @click="handleListRefresh">刷新</text>
</view>
<view v-for="(item, index) in currentNewsList" :key="index" class="news-item" @click="handleNewsClick(item)">
<view class="news-content">
<text class="news-title">{{ item.title }}</text>
<text class="news-meta">{{ item.source }} · {{ item.time }}</text>
</view>
<image class="news-thumb" :src="item.image" mode="aspectFill" />
</view>
</view>
</view>
<view v-else-if="currentComponentType === 'form'" class="dynamic-component">
<view class="quick-form">
<text class="form-title">✍️ 快速反馈</text>
<view class="form-item">
<text class="label">姓名</text>
<input class="input" v-model="currentFormData.name" placeholder="请输入姓名" />
</view>
<view class="form-item">
<text class="label">手机号</text>
<input class="input" v-model="currentFormData.phone" placeholder="请输入手机号" type="number" />
</view>
<view class="form-item">
<text class="label">反馈内容</text>
<textarea class="textarea" v-model="currentFormData.content" placeholder="请输入您的建议..." />
</view>
<view class="form-item">
<text class="label">评分</text>
<view class="rate-box">
<text v-for="n in 5" :key="n" class="star" :class="{ active: n <= currentFormData.rate }" @click="handleFormRate(n)">⭐</text>
</view>
</view>
<button class="submit-btn" @click="handleSubmitForm">提交反馈</button>
</view>
</view>
<view v-else-if="currentComponentType === 'custom'" class="dynamic-component">
<view class="custom-component-container">
<text class="custom-component-note">自定义组件预览区域</text>
<view class="custom-code-preview">
<text class="code-label">当前代码:</text>
<text class="code-content">{{ customComponentCode }}</text>
</view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-icon">📦</text>
<text class="empty-text">点击上方按钮加载组件</text>
</view>
</view>
</view>
<!-- 事件日志 -->
<view class="log-section" v-if="eventLogs.length > 0">
<view class="section-header">
<text class="section-title">📊 事件日志</text>
<button class="btn btn-small" @click="clearLogs">清空</button>
</view>
<scroll-view class="log-list" scroll-y>
<view v-for="(log, index) in eventLogs" :key="index" class="log-item">
<text class="log-time">{{ log.time }}</text>
<text class="log-content">{{ log.message }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
// 在uni-app中,我们不需要使用markRaw,而是直接创建组件
export default {
data() {
return {
// 当前组件类型
currentComponentType: null,
showEditor: true,
editableCode: '',
eventLogs: [],
// 当前新闻列表数据
currentNewsList: [],
// 当前表单数据
currentFormData: {
name: '',
phone: '',
content: '',
rate: 0
},
// 自定义组件代码
customComponentCode: '',
// 组件库
componentLibrary: {
card: {
name: 'ProductCard',
template: `
<view class="product-card">
<image class="product-image" src="https://picsum.photos/300/200?random=1" mode="aspectFill" />
<view class="product-info">
<text class="product-title">🎧 无线降噪耳机 Pro</text>
<text class="product-desc">主动降噪 · 30小时续航 · 空间音频</text>
<view class="product-footer">
<text class="product-price">¥1,299</text>
<button class="buy-btn" @click="handleBuy">立即购买</button>
</view>
</view>
</view>
`,
data() {
return {
count: 0
}
},
methods: {
handleBuy() {
this.count++
this.$emit('custom-event', {
type: 'purchase',
message: `用户点击购买,次数: ${this.count}`,
timestamp: Date.now()
})
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
}
}
},
list: {
name: 'NewsList',
template: `
<view class="news-list">
<view class="list-header">
<text class="header-title">📰 今日热点</text>
<text class="refresh-btn" @click="refresh">刷新</text>
</view>
<view v-for="(item, index) in newsList" :key="index" class="news-item" @click="readMore(item)">
<view class="news-content">
<text class="news-title">{{ item.title }}</text>
<text class="news-meta">{{ item.source }} · {{ item.time }}</text>
</view>
<image class="news-thumb" :src="item.image" mode="aspectFill" />
</view>
</view>
`,
data() {
return {
newsList: [{
title: '人工智能新突破:GPT-5 即将发布',
source: '科技日报',
time: '2小时前',
image: 'https://picsum.photos/100/100?random=2'
},
{
title: '新能源汽车销量创新高',
source: '汽车周刊',
time: '4小时前',
image: 'https://picsum.photos/100/100?random=3'
},
{
title: 'SpaceX 星舰第四次试飞成功',
source: '航天新闻',
time: '6小时前',
image: 'https://picsum.photos/100/100?random=4'
}
]
}
},
methods: {
refresh() {
this.$emit('custom-event', {
type: 'refresh',
message: '用户刷新列表'
})
uni.showLoading({
title: '加载中'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '已更新',
icon: 'success'
})
}, 800)
},
readMore(item) {
this.$emit('custom-event', {
type: 'click',
message: `点击新闻: ${item.title}`
})
}
}
},
form: {
name: 'QuickForm',
template: `
<view class="quick-form">
<text class="form-title">✍️ 快速反馈</text>
<view class="form-item">
<text class="label">姓名</text>
<input class="input" v-model="form.name" placeholder="请输入姓名" />
</view>
<view class="form-item">
<text class="label">手机号</text>
<input class="input" v-model="form.phone" placeholder="请输入手机号" type="number" />
</view>
<view class="form-item">
<text class="label">反馈内容</text>
<textarea class="textarea" v-model="form.content" placeholder="请输入您的建议..." />
</view>
<view class="form-item">
<text class="label">评分</text>
<view class="rate-box">
<text v-for="n in 5" :key="n" class="star" :class="{ active: n <= form.rate }" @click="setRate(n)">⭐</text>
</view>
</view>
<button class="submit-btn" @click="submitForm">提交反馈</button>
</view>
`,
data() {
return {
form: {
name: '',
phone: '',
content: '',
rate: 0
}
}
},
methods: {
setRate(n) {
this.form.rate = n
this.$emit('custom-event', {
type: 'rate',
message: `用户评分: ${n} 星`
})
},
submitForm() {
if (!this.form.name || !this.form.phone) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
this.$emit('custom-event', {
type: 'submit',
message: `提交表单: ${JSON.stringify(this.form)}`
})
uni.showModal({
title: '提交成功',
content: `感谢您的反馈,${this.form.name}!\n评分: ${this.form.rate}星`,
showCancel: false
})
}
}
}
}
}
},
methods: {
async loadComponent(type) {
uni.showLoading({
title: '加载中...'
});
// 添加延迟以确保UI更新
await new Promise(resolve => setTimeout(resolve, 500));
const componentDef = this.componentLibrary[type];
if (componentDef) {
this.editableCode = componentDef.template;
// 设置当前组件类型
this.currentComponentType = type;
// 根据不同类型初始化数据
if (type === 'list') {
// 初始化新闻列表数据
this.currentNewsList = this.componentLibrary.list.data().newsList;
} else if (type === 'form') {
// 重置表单数据
this.currentFormData = { name: '', phone: '', content: '', rate: 0 };
}
this.addLog(`加载组件: ${componentDef.name}`);
}
uni.hideLoading();
},
renderCustomCode() {
if (!this.editableCode.trim()) {
uni.showToast({
title: '代码不能为空',
icon: 'none'
})
return
}
// 存储自定义组件代码
this.customComponentCode = this.editableCode;
this.currentComponentType = 'custom';
this.addLog('渲染自定义代码')
uni.showToast({
title: '渲染成功',
icon: 'success'
})
},
clearComponent() {
this.currentComponentType = null;
this.editableCode = '';
this.customComponentCode = '';
this.addLog('清空组件');
},
// 卡片组件相关方法
handleCardBuy() {
// 模拟购买逻辑
this.cardCount = this.cardCount ? this.cardCount + 1 : 1;
this.handleCustomEvent({
type: 'purchase',
message: `用户点击购买,次数: ${this.cardCount}`,
timestamp: Date.now()
});
uni.showToast({
title: '已添加到购物车',
icon: 'success'
});
},
// 列表组件相关方法
handleListRefresh() {
this.handleCustomEvent({
type: 'refresh',
message: '用户刷新列表'
});
uni.showLoading({
title: '加载中'
});
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '已更新',
icon: 'success'
});
}, 800);
},
handleNewsClick(item) {
this.handleCustomEvent({
type: 'click',
message: `点击新闻: ${item.title}`
});
},
// 表单组件相关方法
handleFormRate(n) {
this.currentFormData.rate = n;
this.handleCustomEvent({
type: 'rate',
message: `用户评分: ${n} 星`
});
},
handleSubmitForm() {
if (!this.currentFormData.name || !this.currentFormData.phone) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
});
return;
}
this.handleCustomEvent({
type: 'submit',
message: `提交表单: ${JSON.stringify(this.currentFormData)}`
});
uni.showModal({
title: '提交成功',
content: `感谢您的反馈,${this.currentFormData.name}!\n评分: ${this.currentFormData.rate}星`,
showCancel: false
});
},
handleCustomEvent(event) {
this.addLog(`[${event.type}] ${event.message}`);
},
addLog(message) {
const now = new Date();
const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`;
this.eventLogs.unshift({
time,
message
});
if (this.eventLogs.length > 20) {
this.eventLogs.pop();
}
},
clearLogs() {
this.eventLogs = [];
}
}
}
</script>
<style>
.container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20rpx;
}
.control-bar {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.btn {
padding: 16rpx 30rpx;
border-radius: 30rpx;
font-size: 26rpx;
border: none;
color: white;
transition: all 0.3s;
}
.btn:active {
transform: scale(0.95);
opacity: 0.8;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.btn-success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.btn-warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.btn-danger {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
color: #333;
}
.btn-small {
padding: 10rpx 24rpx;
font-size: 24rpx;
border-radius: 20rpx;
}
.editor-section,
.preview-section,
.log-section {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #555;
}
.status-badge {
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
background: #eee;
color: #999;
}
.status-badge.active {
background: #d4edda;
color: #155724;
}
.code-editor {
width: 100%;
height: 300rpx;
background: #2d2d2d;
color: #f8f8f2;
font-family: 'Courier New', monospace;
font-size: 24rpx;
line-height: 1.6;
padding: 20rpx;
border-radius: 12rpx;
box-sizing: border-box;
}
.preview-container {
min-height: 400rpx;
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx;
border: 2rpx dashed #ddd;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
color: #999;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
}
.log-list {
max-height: 300rpx;
background: #f8f9fa;
border-radius: 12rpx;
padding: 20rpx;
}
.log-item {
display: flex;
gap: 20rpx;
padding: 16rpx 0;
border-bottom: 2rpx solid #eee;
font-size: 24rpx;
}
.log-time {
color: #667eea;
font-family: monospace;
white-space: nowrap;
}
.log-content {
color: #333;
flex: 1;
word-break: break-all;
}
/* 动态组件样式 */
.product-card,
.news-list,
.quick-form {
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 自定义组件容器样式 */
.custom-component-container {
padding: 30rpx;
}
.custom-component-note {
display: block;
text-align: center;
color: #999;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.custom-code-preview {
background: #2d2d2d;
color: #f8f8f2;
padding: 20rpx;
border-radius: 12rpx;
font-family: 'Courier New', monospace;
font-size: 24rpx;
line-height: 1.6;
}
.code-label {
display: block;
color: #ff6b6b;
margin-bottom: 10rpx;
font-weight: bold;
}
.code-content {
color: #f8f8f2;
word-break: break-all;
white-space: pre-wrap;
}
.product-card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
margin: 20rpx;
}
.product-image {
width: 100%;
height: 300rpx;
}
.product-info {
padding: 30rpx;
}
.product-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10rpx;
}
.product-desc {
font-size: 26rpx;
color: #666;
display: block;
margin-bottom: 20rpx;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 36rpx;
color: #ff6b6b;
font-weight: bold;
}
.buy-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16rpx 40rpx;
border-radius: 30rpx;
font-size: 28rpx;
border: none;
}
.news-list {
background: #fff;
border-radius: 16rpx;
margin: 20rpx;
padding: 20rpx;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.refresh-btn {
font-size: 26rpx;
color: #667eea;
}
.news-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 2rpx solid #f8f8f8;
}
.news-content {
flex: 1;
margin-right: 20rpx;
}
.news-title {
font-size: 30rpx;
color: #333;
line-height: 1.4;
margin-bottom: 10rpx;
display: block;
}
.news-meta {
font-size: 24rpx;
color: #999;
}
.news-thumb {
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
}
.quick-form {
background: #fff;
border-radius: 16rpx;
margin: 20rpx;
padding: 40rpx;
}
.form-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 30rpx;
text-align: center;
}
.form-item {
margin-bottom: 30rpx;
}
.label {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 10rpx;
}
.input {
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
background: #f9f9f9;
width: 100%;
box-sizing: border-box;
}
.textarea {
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
background: #f9f9f9;
height: 200rpx;
width: 100%;
box-sizing: border-box;
}
.rate-box {
display: flex;
gap: 20rpx;
}
.star {
font-size: 40rpx;
opacity: 0.3;
transition: all 0.3s;
}
.star.active {
opacity: 1;
transform: scale(1.2);
}
.submit-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 40rpx;
margin-top: 20rpx;
border: none;
font-size: 32rpx;
width: 100%;
padding: 20rpx 0;
}
</style>
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END










暂无评论内容