You've already forked Pandona-Engine
715 lines
13 KiB
Markdown
715 lines
13 KiB
Markdown
# 组件
|
||
|
||
组件是PE引擎中可复用的UI构建块,它们将模板、样式和逻辑封装在一起,使得代码更加模块化和可维护。
|
||
|
||
## 什么是组件?
|
||
|
||
组件是PE应用中独立的、可复用的代码单元,它包含:
|
||
|
||
1. **模板** - 定义组件的结构和元素
|
||
2. **样式** - 定义组件的外观和样式
|
||
3. **逻辑** - 定义组件的行为和功能
|
||
|
||
## 创建组件
|
||
|
||
在PE中,组件可以通过创建独立的目录结构来定义:
|
||
|
||
```
|
||
components/
|
||
├── button/
|
||
│ ├── index.pe
|
||
│ └── index.less
|
||
├── card/
|
||
│ ├── index.pe
|
||
│ └── index.less
|
||
└── modal/
|
||
├── index.pe
|
||
└── index.less
|
||
```
|
||
|
||
### 组件模板文件
|
||
|
||
组件模板文件与场景模板文件结构相同:
|
||
|
||
```html
|
||
<!-- 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>
|
||
```
|
||
|
||
### 组件样式文件
|
||
|
||
```less
|
||
/* 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;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 使用组件
|
||
|
||
在场景中使用组件非常简单:
|
||
|
||
```html
|
||
<!-- 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(属性)
|
||
|
||
父组件向子组件传递数据:
|
||
|
||
```html
|
||
<!-- 父组件 -->
|
||
<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(事件)
|
||
|
||
子组件向父组件传递数据:
|
||
|
||
```html
|
||
<!-- 子组件触发事件 -->
|
||
<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)
|
||
|
||
插槽允许父组件向子组件传递内容:
|
||
|
||
```html
|
||
<!-- 子组件 (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>
|
||
```
|
||
|
||
```html
|
||
<!-- 父组件中使用插槽 -->
|
||
<sence>
|
||
<card title="我的卡片">
|
||
<!-- 默认插槽内容 -->
|
||
<text>这是卡片的主要内容</text>
|
||
|
||
<!-- 具名插槽内容 -->
|
||
<box slot="footer">
|
||
<button>确定</button>
|
||
<button>取消</button>
|
||
</box>
|
||
</card>
|
||
</sence>
|
||
```
|
||
|
||
## 动态组件
|
||
|
||
PE支持动态组件,可以根据条件渲染不同的组件:
|
||
|
||
```html
|
||
<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>
|
||
```
|
||
|
||
## 组件生命周期
|
||
|
||
组件也有自己的生命周期钩子,与场景生命周期类似:
|
||
|
||
```html
|
||
<sence>
|
||
<box class="component">组件内容</box>
|
||
</sence>
|
||
|
||
<script>
|
||
// 组件生命周期钩子
|
||
onLoad(() => {
|
||
console.log('组件加载')
|
||
// 组件初始化
|
||
})
|
||
|
||
onShow(() => {
|
||
console.log('组件显示')
|
||
// 组件显示时的操作
|
||
})
|
||
|
||
onHide(() => {
|
||
console.log('组件隐藏')
|
||
// 组件隐藏时的操作
|
||
})
|
||
|
||
onDestory(() => {
|
||
console.log('组件销毁')
|
||
// 组件销毁时的清理操作
|
||
})
|
||
</script>
|
||
```
|
||
|
||
## 组件最佳实践
|
||
|
||
### 1. 单一职责原则
|
||
|
||
每个组件应该只负责一个功能:
|
||
|
||
```html
|
||
<!-- 好的做法 -->
|
||
<user-avatar></user-avatar>
|
||
<user-name></user-name>
|
||
<user-email></user-email>
|
||
|
||
<!-- 避免 -->
|
||
<user-info></user-info> <!-- 包含太多功能 -->
|
||
```
|
||
|
||
### 2. 合理的组件结构
|
||
|
||
```html
|
||
<!-- 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. 组件样式隔离
|
||
|
||
```less
|
||
/* 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
|
||
|
||
```html
|
||
<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
|
||
|
||
```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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 使用模态框组件
|
||
|
||
```html
|
||
<!-- 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引擎的组件系统。组件是构建复杂应用的基础,合理使用组件可以大大提高代码的可维护性和复用性。 |