You've already forked Pandona-Engine
组件
组件是PE引擎中可复用的UI构建块,它们将模板、样式和逻辑封装在一起,使得代码更加模块化和可维护。
什么是组件?
组件是PE应用中独立的、可复用的代码单元,它包含:
- 模板 - 定义组件的结构和元素
- 样式 - 定义组件的外观和样式
- 逻辑 - 定义组件的行为和功能
创建组件
在PE中,组件可以通过创建独立的目录结构来定义:
components/
├── button/
│ ├── index.pe
│ └── index.less
├── card/
│ ├── index.pe
│ └── index.less
└── modal/
├── index.pe
└── index.less
组件模板文件
组件模板文件与场景模板文件结构相同:
<!-- components/button/index.pe -->
<sence>
<box class="pe-button" @click="handleClick">
<text class="button-text">{{ label }}</text>
</box>
</sence>
<script>
// 组件属性
let label = '按钮'
// 组件事件
let onClick = null
// 设置属性
function setLabel(newLabel) {
label = newLabel
const textElement = game.getSceneElement('button-text')
if (textElement) {
textElement.element.textContent = label
}
}
// 设置事件处理器
function setOnClick(handler) {
onClick = handler
}
// 事件处理函数
function handleClick() {
if (typeof onClick === 'function') {
onClick()
}
}
// 组件初始化
onLoad(() => {
console.log('按钮组件加载')
})
onShow(() => {
console.log('按钮组件显示')
})
</script>
组件样式文件
/* components/button/index.less */
.pe-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
&:hover {
background-color: #2980b9;
}
&:active {
transform: translateY(1px);
}
.button-text {
margin: 0;
font-weight: normal;
}
}
使用组件
在场景中使用组件非常简单:
<!-- scenes/home/index.pe -->
<sence>
<!-- 使用按钮组件 -->
<button @click="handleButtonClick"></button>
<!-- 带属性的组件 -->
<button label="提交" @click="handleSubmit"></button>
<button label="取消" @click="handleCancel"></button>
</sence>
<script>
// 导入组件
import Button from '../../components/button/index.pe'
function handleButtonClick() {
console.log('按钮被点击')
}
function handleSubmit() {
console.log('提交按钮被点击')
}
function handleCancel() {
console.log('取消按钮被点击')
}
</script>
组件通信
组件之间可以通过属性和事件进行通信。
Props(属性)
父组件向子组件传递数据:
<!-- 父组件 -->
<sence>
<user-card
name="张三"
age="25"
email="zhangsan@example.com"
@edit="handleEditUser">
</user-card>
</sence>
<!-- 子组件 (components/user-card/index.pe) -->
<sence>
<box class="user-card">
<text class="user-name">{{ name }}</text>
<text class="user-age">年龄: {{ age }}</text>
<text class="user-email">邮箱: {{ email }}</text>
<box class="edit-button" @click="handleEdit">编辑</box>
</box>
</sence>
<script>
// 组件属性
let name = ''
let age = ''
let email = ''
// 组件事件
let onEdit = null
// 设置属性的方法
function setName(value) {
name = value
}
function setAge(value) {
age = value
}
function setEmail(value) {
email = value
}
// 设置事件处理器
function setOnEdit(handler) {
onEdit = handler
}
// 事件处理函数
function handleEdit() {
if (typeof onEdit === 'function') {
onEdit({ name, age, email })
}
}
</script>
Events(事件)
子组件向父组件传递数据:
<!-- 子组件触发事件 -->
<script>
function handleEdit() {
// 触发自定义事件
const event = new CustomEvent('edit', {
detail: { name, age, email }
})
// 获取组件根元素并触发事件
const rootElement = game.getSceneElement('user-card')
if (rootElement) {
rootElement.element.dispatchEvent(event)
}
}
</script>
插槽(Slots)
插槽允许父组件向子组件传递内容:
<!-- 子组件 (components/card/index.pe) -->
<sence>
<box class="card">
<box class="card-header">
<text class="card-title">{{ title }}</text>
</box>
<box class="card-body">
<!-- 默认插槽 -->
<slot></slot>
</box>
<box class="card-footer">
<!-- 具名插槽 -->
<slot name="footer"></slot>
</box>
</box>
</sence>
<script>
let title = '卡片标题'
function setTitle(value) {
title = value
}
</script>
<!-- 父组件中使用插槽 -->
<sence>
<card title="我的卡片">
<!-- 默认插槽内容 -->
<text>这是卡片的主要内容</text>
<!-- 具名插槽内容 -->
<box slot="footer">
<button>确定</button>
<button>取消</button>
</box>
</card>
</sence>
动态组件
PE支持动态组件,可以根据条件渲染不同的组件:
<sence>
<!-- 动态组件 -->
<component :is="currentComponent"
v-for="prop in componentProps"
:key="prop.key"
v-bind="prop.value">
</component>
<!-- 切换组件的按钮 -->
<box class="btn" @click="switchToButton">显示按钮</box>
<box class="btn" @click="switchToCard">显示卡片</box>
</sence>
<script>
let currentComponent = 'button'
let componentProps = {}
function switchToButton() {
currentComponent = 'button'
componentProps = { label: '动态按钮' }
}
function switchToCard() {
currentComponent = 'card'
componentProps = { title: '动态卡片' }
}
</script>
组件生命周期
组件也有自己的生命周期钩子,与场景生命周期类似:
<sence>
<box class="component">组件内容</box>
</sence>
<script>
// 组件生命周期钩子
onLoad(() => {
console.log('组件加载')
// 组件初始化
})
onShow(() => {
console.log('组件显示')
// 组件显示时的操作
})
onHide(() => {
console.log('组件隐藏')
// 组件隐藏时的操作
})
onDestory(() => {
console.log('组件销毁')
// 组件销毁时的清理操作
})
</script>
组件最佳实践
1. 单一职责原则
每个组件应该只负责一个功能:
<!-- 好的做法 -->
<user-avatar></user-avatar>
<user-name></user-name>
<user-email></user-email>
<!-- 避免 -->
<user-info></user-info> <!-- 包含太多功能 -->
2. 合理的组件结构
<!-- components/product-card/index.pe -->
<sence>
<box class="product-card">
<sprite class="product-image"></sprite>
<box class="product-info">
<text class="product-name">{{ name }}</text>
<text class="product-price">¥{{ price }}</text>
<box class="product-actions">
<button @click="addToCart">加入购物车</button>
<button @click="viewDetails">查看详情</button>
</box>
</box>
</box>
</sence>
<script>
// 属性
let name = ''
let price = 0
let image = ''
// 事件
let onAddToCart = null
let onViewDetails = null
// 方法
function setName(value) {
name = value
}
function setPrice(value) {
price = value
}
function setImage(value) {
image = value
}
function setOnAddToCart(handler) {
onAddToCart = handler
}
function setOnViewDetails(handler) {
onViewDetails = handler
}
// 事件处理
function addToCart() {
if (typeof onAddToCart === 'function') {
onAddToCart({ name, price })
}
}
function viewDetails() {
if (typeof onViewDetails === 'function') {
onViewDetails({ name, price })
}
}
</script>
3. 组件样式隔离
/* components/product-card/index.less */
.product-card {
width: 300px;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
.product-image {
width: 100%;
height: 200px;
background-color: #f5f5f5;
}
.product-info {
padding: 15px;
.product-name {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.product-price {
color: #e74c3c;
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
}
.product-actions {
display: flex;
gap: 10px;
.button {
flex: 1;
}
}
}
}
完整示例
以下是一个完整的组件示例:
components/modal/index.pe
<sence>
<box class="modal-overlay" @click="handleOverlayClick">
<box class="modal-container" @click.stop>
<box class="modal-header">
<text class="modal-title">{{ title }}</text>
<box class="modal-close" @click="close">✕</box>
</box>
<box class="modal-body">
<slot></slot>
</box>
<box class="modal-footer">
<slot name="footer">
<button class="btn btn-primary" @click="confirm">确定</button>
<button class="btn btn-secondary" @click="close">取消</button>
</slot>
</box>
</box>
</box>
</sence>
<script>
// 属性
let title = '模态框'
let visible = false
// 事件
let onConfirm = null
let onClose = null
// 方法
function setTitle(value) {
title = value
const titleElement = game.getSceneElement('modal-title')
if (titleElement) {
titleElement.element.textContent = title
}
}
function setVisible(value) {
visible = value
const overlay = game.getSceneElement('modal-overlay')
if (overlay) {
overlay.element.style.display = visible ? 'flex' : 'none'
}
}
function setOnConfirm(handler) {
onConfirm = handler
}
function setOnClose(handler) {
onClose = handler
}
// 事件处理
function handleOverlayClick() {
close()
}
function confirm() {
if (typeof onConfirm === 'function') {
onConfirm()
}
close()
}
function close() {
setVisible(false)
if (typeof onClose === 'function') {
onClose()
}
}
// 生命周期
onLoad(() => {
console.log('模态框组件加载')
// 初始化隐藏
setVisible(false)
})
onShow(() => {
console.log('模态框组件显示')
})
onHide(() => {
console.log('模态框组件隐藏')
})
onDestory(() => {
console.log('模态框组件销毁')
})
</script>
components/modal/index.less
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 300px;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
.modal-title {
font-size: 18px;
font-weight: bold;
margin: 0;
}
.modal-close {
cursor: pointer;
font-size: 20px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
&:hover {
background-color: #f5f5f5;
}
}
}
.modal-body {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
&.btn-primary {
background-color: #3498db;
color: white;
&:hover {
background-color: #2980b9;
}
}
&.btn-secondary {
background-color: #95a5a6;
color: white;
&:hover {
background-color: #7f8c8d;
}
}
}
}
使用模态框组件
<!-- scenes/home/index.pe -->
<sence>
<box class="page">
<button @click="showModal">打开模态框</button>
<!-- 使用模态框组件 -->
<modal title="确认操作" @confirm="handleConfirm" @close="handleClose">
<text>您确定要执行此操作吗?</text>
<box slot="footer">
<button class="btn btn-danger" @click="handleConfirm">确认</button>
<button class="btn btn-secondary" @click="handleClose">取消</button>
</box>
</modal>
</box>
</sence>
<script>
let modalVisible = false
function showModal() {
modalVisible = true
// 更新模态框可见性
const modal = game.getSceneElement('modal')
if (modal) {
modal.setVisible(true)
}
}
function handleConfirm() {
console.log('用户确认操作')
// 执行确认操作
}
function handleClose() {
console.log('用户关闭模态框')
modalVisible = false
}
</script>
通过以上内容,你已经了解了PE引擎的组件系统。组件是构建复杂应用的基础,合理使用组件可以大大提高代码的可维护性和复用性。