mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-04-30 00:21:53 +08:00
Merge 4f73e2b82110a005c33d254cd522bc3c869ba324 into 6f2e8d8d74ed0d8a7ef1eefd0b39b08a90abc6e7
This commit is contained in:
commit
f0f420bd1e
@ -154,6 +154,12 @@ const expandHandler = () => {
|
||||
};
|
||||
|
||||
const nodeClickHandler = (event: MouseEvent) => {
|
||||
const { data, parent } = props;
|
||||
if (data.comInnerModule) {
|
||||
data[data.comInnerModule] = parent?.[data.comInnerModule];
|
||||
data.parentId = parent?.id;
|
||||
data.type = parent?.type;
|
||||
}
|
||||
treeEmit?.('node-click', event, props.data);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -100,7 +100,7 @@ const values = ref<FormValue>({});
|
||||
const curFormConfig = ref<any>([]);
|
||||
const node = computed(() => editorService.get('node'));
|
||||
const nodes = computed(() => editorService.get('nodes'));
|
||||
|
||||
let innerModuleEditingInfo: { name: string; parentId: string; id: string } | null = null;
|
||||
const styleFormConfig = [
|
||||
{
|
||||
tabPosition: 'right',
|
||||
@ -116,7 +116,36 @@ const init = async () => {
|
||||
|
||||
const type = node.value.type || (node.value.items ? 'container' : 'text');
|
||||
curFormConfig.value = await propsService.getPropsConfig(type);
|
||||
values.value = node.value;
|
||||
innerModuleEditingInfo = null;
|
||||
if (node.value.comInnerModule) {
|
||||
// 从左侧点击进入子模块配置
|
||||
innerModuleEditingInfo = {
|
||||
name: node.value.comInnerModule,
|
||||
parentId: node.value.parentId,
|
||||
id: String(node.value.id),
|
||||
};
|
||||
}
|
||||
|
||||
if (innerModuleEditingInfo) {
|
||||
const { name } = innerModuleEditingInfo;
|
||||
const curFormConfigItems = curFormConfig.value[0].items;
|
||||
const { length } = curFormConfigItems;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (curFormConfigItems[i].title !== '属性') {
|
||||
continue;
|
||||
}
|
||||
const len2 = curFormConfigItems[i].items.length;
|
||||
for (let j = 0; j < len2; j++) {
|
||||
if (curFormConfigItems[i].items[j].name === name) {
|
||||
curFormConfigItems[i].items = curFormConfigItems[i].items[j].items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
values.value = node.value[name] || {};
|
||||
} else {
|
||||
values.value = node.value;
|
||||
}
|
||||
};
|
||||
|
||||
watchEffect(init);
|
||||
@ -128,6 +157,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
const submit = async (v: MNode, eventData?: ContainerChangeEventData) => {
|
||||
try {
|
||||
if (innerModuleEditingInfo !== null) {
|
||||
v._innerModuleEditingInfo = innerModuleEditingInfo;
|
||||
v.id = innerModuleEditingInfo.id;
|
||||
}
|
||||
if (!v.id) {
|
||||
v.id = values.value.id;
|
||||
}
|
||||
|
||||
@ -548,14 +548,31 @@ class Editor extends BaseService {
|
||||
|
||||
if (!config?.id) throw new Error('没有配置或者配置缺少id值');
|
||||
|
||||
const info = this.getNodeInfo(config.id, false);
|
||||
let info = this.getNodeInfo(config.id, false);
|
||||
|
||||
if (!info.node) throw new Error(`获取不到id为${config.id}的节点`);
|
||||
|
||||
const node = toRaw(info.node);
|
||||
let node = toRaw(info.node);
|
||||
let tempConfig = config;
|
||||
if (config._innerModuleEditingInfo) {
|
||||
// 如果是组件内嵌模块的属性修改,则找到组件node进行修改(只能是组件内一层的结构 -- 只找了一个parent)
|
||||
const moduleKey = config._innerModuleEditingInfo.name;
|
||||
const parentNode = toRaw(info.parent);
|
||||
|
||||
let newConfig = await this.toggleFixedPosition(toRaw(config), node, root);
|
||||
const nodeSubModule = (node as any)[moduleKey];
|
||||
if (nodeSubModule) {
|
||||
const newObj: Record<string, any> = {};
|
||||
Object.keys(nodeSubModule).forEach((key) => {
|
||||
newObj[key] = (config as any)[key];
|
||||
});
|
||||
(parentNode as any)[moduleKey] = newObj;
|
||||
tempConfig = parentNode!;
|
||||
node = toRaw(info.parent)!;
|
||||
info = this.getNodeInfo(parentNode!.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
let newConfig = await this.toggleFixedPosition(toRaw(tempConfig), node, root);
|
||||
newConfig = mergeWith(cloneDeep(node), newConfig, (objValue, srcValue, key, object: any, source: any) => {
|
||||
if (typeof srcValue === 'undefined' && Object.hasOwn(source, key)) {
|
||||
return '';
|
||||
@ -571,6 +588,14 @@ class Editor extends BaseService {
|
||||
});
|
||||
|
||||
if (!newConfig.type) throw new Error('配置缺少type值');
|
||||
if (config._innerModuleEditingInfo) {
|
||||
// 如果是组件内嵌模块的属性修改, 不做后面的更新
|
||||
return {
|
||||
oldNode: node,
|
||||
newNode: newConfig,
|
||||
changeRecords,
|
||||
};
|
||||
}
|
||||
|
||||
if (newConfig.type === NodeType.ROOT) {
|
||||
this.set('root', newConfig as MApp);
|
||||
|
||||
36
vue-components/game/package.json
Normal file
36
vue-components/game/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"name": "@tmagic/vue-game",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Tencent/tmagic-editor.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmagic/form-schema": "workspace:^",
|
||||
"vue-demi": "^0.14.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tmagic/core": "workspace:^",
|
||||
"@tmagic/vue-runtime-help": "workspace:^",
|
||||
"@vue/composition-api": ">=1.7.2",
|
||||
"typescript": "catalog:",
|
||||
"vue": ">=2.6.0 || >=3.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
},
|
||||
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
6
vue-components/game/src/event.ts
Normal file
6
vue-components/game/src/event.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { COMMON_EVENT_PREFIX } from '@tmagic/core';
|
||||
|
||||
export default {
|
||||
methods: [],
|
||||
events: [{ label: '点击', value: `${COMMON_EVENT_PREFIX}click` }],
|
||||
};
|
||||
43
vue-components/game/src/formConfig.ts
Normal file
43
vue-components/game/src/formConfig.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineFormConfig } from '@tmagic/form-schema';
|
||||
|
||||
export default defineFormConfig([
|
||||
{
|
||||
text: '游戏级别',
|
||||
name: 'level',
|
||||
type: 'select',
|
||||
placeholder: '请选择级别',
|
||||
options: [
|
||||
{ text: '简单', value: 1 },
|
||||
{ text: '困难', value: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'prizeDialog',
|
||||
type: 'hide',
|
||||
items: [
|
||||
{
|
||||
text: '奖品名称',
|
||||
name: 'prizeName',
|
||||
type: 'data-source-input',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
25
vue-components/game/src/index.ts
Normal file
25
vue-components/game/src/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Game from './index.vue';
|
||||
|
||||
export { default as config } from './formConfig';
|
||||
export { default as value } from './initValue';
|
||||
export { default as event } from './event';
|
||||
|
||||
export default Game;
|
||||
282
vue-components/game/src/index.vue
Normal file
282
vue-components/game/src/index.vue
Normal file
@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="demo-game">
|
||||
<div class="game-box">
|
||||
<span
|
||||
class="dot"
|
||||
:style="{ left: dotPosition.x + 'px', top: dotPosition.y + 'px' }"
|
||||
@click="handleDotClick"
|
||||
></span>
|
||||
<div class="click-counter">
|
||||
点击次数: {{ clickCount }}/{{ maxClicks }}/可获取{{ config.prizeDialog?.prizeName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="game-prize-dialog" v-show="isShowPrize">
|
||||
<div class="dialog-body">
|
||||
<div class="game-prize-dialog-content">
|
||||
<div class="game-prize-dialog-title">恭喜你获得 {{ config.prizeDialog?.prizeName }}</div>
|
||||
</div>
|
||||
<div class="game-prize-dialog-footer">
|
||||
<button @click="handlePrizeConfirm">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, type PropType, ref } from 'vue';
|
||||
|
||||
import { useApp } from '@tmagic/vue-runtime-help';
|
||||
// 移除无法解析的类型导入,使用基本类型定义
|
||||
interface MComponent {
|
||||
id?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const COMMON_EVENT_PREFIX = 'tmagic:';
|
||||
|
||||
interface GameSchema extends Omit<MComponent, 'id'> {
|
||||
id?: string;
|
||||
type?: 'game';
|
||||
text: string;
|
||||
level?: number;
|
||||
prizeDialog?: {
|
||||
prizeName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default defineComponent<any, any>({
|
||||
name: 'tmagic-game',
|
||||
|
||||
props: {
|
||||
config: {
|
||||
type: Object as PropType<GameSchema>,
|
||||
required: true,
|
||||
},
|
||||
containerIndex: Number,
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const { app, node } = useApp(props);
|
||||
// 模拟useApp的返回值
|
||||
const gameLevel = props.config?.level || 1;
|
||||
|
||||
// 游戏逻辑
|
||||
const isShowPrize = ref(false); // 是否显示奖品弹窗
|
||||
const dotPosition = ref({ x: 0, y: 0 }); // 点的位置
|
||||
const clickCount = ref(0); // 点击计数
|
||||
const maxClicks = 5; // 中奖所需点击次数
|
||||
let moveInterval: number | null = null; // 移动计时器ID
|
||||
|
||||
// 根据游戏级别设置移动速度(级别越高,速度越快)
|
||||
const getMoveInterval = (level: number) => {
|
||||
return Math.max(100, 1000 - (level - 1) * 200); // 1级1秒,每升一级减少200ms,最快0.1秒
|
||||
};
|
||||
|
||||
// 随机生成新位置,确保在game-box内
|
||||
const getRandomPosition = () => {
|
||||
// 获取game-box的尺寸
|
||||
const gameBox = document.querySelector('.game-box');
|
||||
if (!gameBox) return { x: 0, y: 0 };
|
||||
|
||||
const boxRect = gameBox.getBoundingClientRect();
|
||||
const boxWidth = boxRect.width;
|
||||
const boxHeight = boxRect.height;
|
||||
|
||||
// dot的尺寸(从CSS中获取)
|
||||
const dotSize = 10; // width和height都是10px
|
||||
|
||||
// 计算有效位置范围,确保dot完全在box内部
|
||||
const maxX = boxWidth - dotSize;
|
||||
const maxY = boxHeight - dotSize;
|
||||
|
||||
// 生成随机位置
|
||||
return {
|
||||
x: Math.floor(Math.random() * maxX),
|
||||
y: Math.floor(Math.random() * maxY),
|
||||
};
|
||||
};
|
||||
|
||||
// 移动点
|
||||
const moveDot = () => {
|
||||
const newPosition = getRandomPosition();
|
||||
dotPosition.value = newPosition;
|
||||
};
|
||||
|
||||
// 开始移动
|
||||
const startMoving = () => {
|
||||
if (moveInterval) {
|
||||
clearInterval(moveInterval);
|
||||
}
|
||||
|
||||
// 初始化位置
|
||||
dotPosition.value = getRandomPosition();
|
||||
|
||||
// 设置移动间隔
|
||||
moveInterval = window.setInterval(moveDot, getMoveInterval(gameLevel));
|
||||
};
|
||||
|
||||
// 停止移动
|
||||
const stopMoving = () => {
|
||||
if (moveInterval) {
|
||||
clearInterval(moveInterval);
|
||||
moveInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置游戏
|
||||
const resetGame = () => {
|
||||
// 重置点击计数
|
||||
clickCount.value = 0;
|
||||
|
||||
// 隐藏奖品弹窗
|
||||
isShowPrize.value = false;
|
||||
|
||||
// 重新开始移动
|
||||
startMoving();
|
||||
};
|
||||
|
||||
// 处理奖品弹窗确认
|
||||
const handlePrizeConfirm = () => {
|
||||
resetGame();
|
||||
};
|
||||
// 处理点的点击事件
|
||||
const handleDotClick = () => {
|
||||
// 增加点击计数(避免使用++运算符)
|
||||
clickCount.value = clickCount.value + 1;
|
||||
|
||||
// 发送点击事件(保留原有功能)
|
||||
if (app && node && typeof app.emit === 'function') {
|
||||
app.emit(`${COMMON_EVENT_PREFIX}click`, node);
|
||||
}
|
||||
|
||||
// 检查是否达到中奖次数
|
||||
if (clickCount.value >= maxClicks) {
|
||||
// 显示中奖弹窗
|
||||
isShowPrize.value = true;
|
||||
|
||||
// 暂时停止移动,方便用户点击弹窗
|
||||
if (moveInterval) {
|
||||
clearInterval(moveInterval);
|
||||
moveInterval = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 暂时禁用编辑器特定的代码,以避免类型错误
|
||||
if (app && app.platform === 'editor' && app.page) {
|
||||
console.log('-- props.config --', props, app, app.page.on);
|
||||
// 监听编辑器中的 TreeNode 点击事件
|
||||
const onEditorSelectHandler = (info: any, _path: any[]) => {
|
||||
if (info.node?.comInnerModule === 'prizeDialog' && info.node?.parentId === props.config?.id) {
|
||||
isShowPrize.value = true;
|
||||
} else {
|
||||
isShowPrize.value = false;
|
||||
}
|
||||
console.log('-- onEditorSelectHandler --', info, props.config);
|
||||
};
|
||||
|
||||
// editor model
|
||||
setTimeout(() => {
|
||||
app.page.on('editor:select', onEditorSelectHandler);
|
||||
}, 1000); // 需要延迟监听,在保存后再刷新的情况下,不延时监听不到
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
startMoving();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopMoving();
|
||||
});
|
||||
|
||||
return {
|
||||
handleDotClick,
|
||||
handlePrizeConfirm,
|
||||
dotPosition,
|
||||
isShowPrize,
|
||||
clickCount,
|
||||
maxClicks,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.game-box {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
height: 300px;
|
||||
border: 2px solid #333;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.game-box .dot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: #007bff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.game-box .dot:hover {
|
||||
background-color: #0056b3;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.game-box .click-counter {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
text-align: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.game-prize-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.game-prize-dialog .dialog-body {
|
||||
background-color: #fff;
|
||||
width: 300px;
|
||||
min-height: 200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.game-prize-dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.game-prize-dialog-footer {
|
||||
text-align: center;
|
||||
}
|
||||
.game-prize-dialog-footer button {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
30
vue-components/game/src/initValue.ts
Normal file
30
vue-components/game/src/initValue.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export default {
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '200',
|
||||
border: 0,
|
||||
backgroundColor: '#fb6f00',
|
||||
},
|
||||
level: 1,
|
||||
prizeDialog: {
|
||||
prizeName: '可乐糖',
|
||||
},
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user