UNIAPP 动态渲染页面demo

UNIAPP 动态渲染页面demo

<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
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容