Compare commits
6 Commits
5368e52f0d
...
9662f41eec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9662f41eec | ||
|
|
fbfaa59b14 | ||
|
|
27b4b720ed | ||
|
|
e62f2b54d5 | ||
|
|
d7c36e31f7 | ||
|
|
985c8d39fc |
@ -17,7 +17,7 @@ export interface DataSetPayload {
|
|||||||
type: "API" | "SQL" | "JSON" | string;
|
type: "API" | "SQL" | "JSON" | string;
|
||||||
method?: IDataSet.Item["method"];
|
method?: IDataSet.Item["method"];
|
||||||
api?: string;
|
api?: string;
|
||||||
dataSourceId?: string;
|
dataSourceId?: IDataSet.Item["id"];
|
||||||
sql?: string;
|
sql?: string;
|
||||||
json?: string;
|
json?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ function update(method: string){
|
|||||||
if(data.type === 'panel' && object.isHtmlPanel) return;
|
if(data.type === 'panel' && object.isHtmlPanel) return;
|
||||||
if(data.type === 'sprite' && object.isHtmlSprite) return;
|
if(data.type === 'sprite' && object.isHtmlSprite) return;
|
||||||
|
|
||||||
const _json = object.toJSON();
|
const _json = object.toJSON() as any;
|
||||||
_json.object.type = data.type === 'panel' ? 'HtmlSprite' : 'HtmlPanel';
|
_json.object.type = data.type === 'panel' ? 'HtmlSprite' : 'HtmlPanel';
|
||||||
_json.object.options.isSprite = !_json.object.options.isSprite;
|
_json.object.options.isSprite = !_json.object.options.isSprite;
|
||||||
|
|
||||||
@ -135,19 +135,24 @@ function updateFileList(fList: UploadFileInfo[]) {
|
|||||||
isSprite: data.type ==='sprite',
|
isSprite: data.type ==='sprite',
|
||||||
fileName: file.name
|
fileName: file.name
|
||||||
}).then(htmlPlaneObj => {
|
}).then(htmlPlaneObj => {
|
||||||
data.codes = htmlPlaneObj.options.codes;
|
object.options.codes = htmlPlaneObj.options.codes;
|
||||||
|
object.options.isSingleHtml = htmlPlaneObj.options.isSingleHtml;
|
||||||
|
|
||||||
const _json = object.toJSON();
|
data.codes = object.options.codes;
|
||||||
Loader.objectLoader.copyAttrByData(htmlPlaneObj, _json.object)
|
|
||||||
|
|
||||||
object.parent?.add(htmlPlaneObj);
|
// Keep object identity (id/uuid) so scene tree and context menu references stay valid.
|
||||||
|
while (object.element.firstChild) {
|
||||||
|
object.element.removeChild(object.element.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
object.parent?.remove(object);
|
while (htmlPlaneObj.element.firstChild) {
|
||||||
|
object.element.appendChild(htmlPlaneObj.element.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
App.selected = htmlPlaneObj;
|
App.selected = object;
|
||||||
|
Hooks.useDispatchSignal("objectSelected", object);
|
||||||
Hooks.useDispatchSignal("objectSelected", htmlPlaneObj);
|
}).catch((e:Error) => window.$message?.error(e.message))
|
||||||
}).catch((e:Error) => window.$message?.error(e.message));
|
.finally(() => URL.revokeObjectURL(tempURL));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCanEdit(name: string) {
|
function getCanEdit(name: string) {
|
||||||
|
|||||||
@ -121,15 +121,15 @@ const updateUI = Utils.throttle(function(object) {
|
|||||||
objectData.type = object.type;
|
objectData.type = object.type;
|
||||||
objectData.uuid = object.uuid;
|
objectData.uuid = object.uuid;
|
||||||
objectData.name = object.name;
|
objectData.name = object.name;
|
||||||
objectData.position.x = Number(object.position.x.toFixed(3));
|
objectData.position.x = Number(object.position.x.toFixed(6));
|
||||||
objectData.position.y = Number(object.position.y.toFixed(3));
|
objectData.position.y = Number(object.position.y.toFixed(6));
|
||||||
objectData.position.z = Number(object.position.z.toFixed(3));
|
objectData.position.z = Number(object.position.z.toFixed(6));
|
||||||
objectData.rotation.x = Number((object.rotation.x * THREE.MathUtils.RAD2DEG).toFixed(3));
|
objectData.rotation.x = Number((object.rotation.x * THREE.MathUtils.RAD2DEG).toFixed(6));
|
||||||
objectData.rotation.y = Number((object.rotation.y * THREE.MathUtils.RAD2DEG).toFixed(3));
|
objectData.rotation.y = Number((object.rotation.y * THREE.MathUtils.RAD2DEG).toFixed(6));
|
||||||
objectData.rotation.z = Number((object.rotation.z * THREE.MathUtils.RAD2DEG).toFixed(3));
|
objectData.rotation.z = Number((object.rotation.z * THREE.MathUtils.RAD2DEG).toFixed(6));
|
||||||
objectData.scale.x = Number(object.scale.x.toFixed(3));
|
objectData.scale.x = Number(object.scale.x.toFixed(6));
|
||||||
objectData.scale.y = Number(object.scale.y.toFixed(3));
|
objectData.scale.y = Number(object.scale.y.toFixed(6));
|
||||||
objectData.scale.z = Number(object.scale.z.toFixed(3));
|
objectData.scale.z = Number(object.scale.z.toFixed(6));
|
||||||
|
|
||||||
if (object.fov !== undefined) {
|
if (object.fov !== undefined) {
|
||||||
objectData.fov = object.fov;
|
objectData.fov = object.fov;
|
||||||
@ -225,19 +225,19 @@ const update = (method: string) => {
|
|||||||
},
|
},
|
||||||
position: () => {
|
position: () => {
|
||||||
const newPosition = new THREE.Vector3(objectData.position.x, objectData.position.y, objectData.position.z);
|
const newPosition = new THREE.Vector3(objectData.position.x, objectData.position.y, objectData.position.z);
|
||||||
if (object.position.distanceTo(newPosition) >= 0.01) {
|
if (object.position.distanceTo(newPosition) >= 0.0001) {
|
||||||
App.execute(new SetPositionCommand(object, newPosition));
|
App.execute(new SetPositionCommand(object, newPosition));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rotation: () => {
|
rotation: () => {
|
||||||
const newRotation = new THREE.Euler(objectData.rotation.x * THREE.MathUtils.DEG2RAD, objectData.rotation.y * THREE.MathUtils.DEG2RAD, objectData.rotation.z * THREE.MathUtils.DEG2RAD);
|
const newRotation = new THREE.Euler(objectData.rotation.x * THREE.MathUtils.DEG2RAD, objectData.rotation.y * THREE.MathUtils.DEG2RAD, objectData.rotation.z * THREE.MathUtils.DEG2RAD);
|
||||||
if (new THREE.Vector3().setFromEuler(object.rotation).distanceTo(new THREE.Vector3().setFromEuler(newRotation)) >= 0.01) {
|
if (new THREE.Vector3().setFromEuler(object.rotation).distanceTo(new THREE.Vector3().setFromEuler(newRotation)) >= 0.0001) {
|
||||||
App.execute(new SetRotationCommand(object, newRotation, undefined));
|
App.execute(new SetRotationCommand(object, newRotation, undefined));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scale: () => {
|
scale: () => {
|
||||||
const newScale = new THREE.Vector3(objectData.scale.x, objectData.scale.y, objectData.scale.z);
|
const newScale = new THREE.Vector3(objectData.scale.x, objectData.scale.y, objectData.scale.z);
|
||||||
if (object.scale.distanceTo(newScale) >= 0.01) {
|
if (object.scale.distanceTo(newScale) >= 0.0001) {
|
||||||
App.execute(new SetScaleCommand(object, newScale, undefined));
|
App.execute(new SetScaleCommand(object, newScale, undefined));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -433,11 +433,11 @@ const handleUserDataClick = () => {
|
|||||||
<div class="sider-scene-attr-item">
|
<div class="sider-scene-attr-item">
|
||||||
<EsKeyFrame :label="t('layout.sider.object.position')" attr="position" />
|
<EsKeyFrame :label="t('layout.sider.object.position')" attr="position" />
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<EsInputNumber v-model:value="objectData.position.x" size="tiny" :show-button="false" :decimal="3" :step="1"
|
<EsInputNumber v-model:value="objectData.position.x" size="tiny" :show-button="false" :decimal="6" :step="1"
|
||||||
@change="update('position')" />
|
@change="update('position')" />
|
||||||
<EsInputNumber v-model:value="objectData.position.y" size="tiny" :show-button="false" :decimal="3" :step="1"
|
<EsInputNumber v-model:value="objectData.position.y" size="tiny" :show-button="false" :decimal="6" :step="1"
|
||||||
@change="update('position')" />
|
@change="update('position')" />
|
||||||
<EsInputNumber v-model:value="objectData.position.z" size="tiny" :show-button="false" :decimal="3" :step="1"
|
<EsInputNumber v-model:value="objectData.position.z" size="tiny" :show-button="false" :decimal="6" :step="1"
|
||||||
@change="update('position')" />
|
@change="update('position')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -445,11 +445,11 @@ const handleUserDataClick = () => {
|
|||||||
<div class="sider-scene-attr-item" v-if="transformRowsVisible.rotation">
|
<div class="sider-scene-attr-item" v-if="transformRowsVisible.rotation">
|
||||||
<EsKeyFrame :label="t('layout.sider.object.rotation')" attr="quaternion" />
|
<EsKeyFrame :label="t('layout.sider.object.rotation')" attr="quaternion" />
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<EsInputNumber v-model:value="objectData.rotation.x" size="tiny" :decimal="2" :step="1" :show-button="false"
|
<EsInputNumber v-model:value="objectData.rotation.x" size="tiny" :decimal="6" :step="1" :show-button="false"
|
||||||
@change="update('rotation')" unit="°" />
|
@change="update('rotation')" unit="°" />
|
||||||
<EsInputNumber v-model:value="objectData.rotation.y" size="tiny" :decimal="2" :step="1" :show-button="false"
|
<EsInputNumber v-model:value="objectData.rotation.y" size="tiny" :decimal="6" :step="1" :show-button="false"
|
||||||
@change="update('rotation')" unit="°" />
|
@change="update('rotation')" unit="°" />
|
||||||
<EsInputNumber v-model:value="objectData.rotation.z" size="tiny" :decimal="2" :step="1" :show-button="false"
|
<EsInputNumber v-model:value="objectData.rotation.z" size="tiny" :decimal="6" :step="1" :show-button="false"
|
||||||
@change="update('rotation')" unit="°" />
|
@change="update('rotation')" unit="°" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref, nextTick, onBeforeUnmount} from 'vue';
|
import {onMounted, ref, nextTick, onBeforeUnmount} from 'vue';
|
||||||
import {App,Viewer,Hooks} from "@astral3d/engine";
|
import {App,Viewer,Hooks,WaterPool} from "@astral3d/engine";
|
||||||
import Toolbar from "./Toolbar.vue";
|
import Toolbar from "./Toolbar.vue";
|
||||||
import PathDrawingOverlay from "./PathDrawingOverlay.vue";
|
import PathDrawingOverlay from "./PathDrawingOverlay.vue";
|
||||||
import {useGlobalConfigStore} from "@/store/modules/globalConfig";
|
import {useGlobalConfigStore} from "@/store/modules/globalConfig";
|
||||||
@ -30,6 +30,43 @@ const pluginStore = usePluginStore();
|
|||||||
|
|
||||||
const viewportRef = ref();
|
const viewportRef = ref();
|
||||||
|
|
||||||
|
function bootstrapWaterPoolWarmup(viewer: Viewer) {
|
||||||
|
const warmupPool = new WaterPool({
|
||||||
|
sky: viewer.scene.environment,
|
||||||
|
name: '__water_pool_warmup__',
|
||||||
|
type: 'cylinder',
|
||||||
|
light: [0.7, 1, -0.3],
|
||||||
|
diameter: 5,
|
||||||
|
height: 5,
|
||||||
|
wallMode: 'none',
|
||||||
|
wallOpacity: 0,
|
||||||
|
useSceneRefraction: 1,
|
||||||
|
surfaceTransmittance: 0.6,
|
||||||
|
normalStrength: 0.5,
|
||||||
|
refractionStrength: 0.035,
|
||||||
|
});
|
||||||
|
|
||||||
|
warmupPool.visible = false;
|
||||||
|
|
||||||
|
// Use scene.add/remove directly to avoid objectAdded/objectRemoved signals and scene tree refresh.
|
||||||
|
viewer.scene.add(warmupPool);
|
||||||
|
|
||||||
|
const removeWarmupPool = () => {
|
||||||
|
if (warmupPool.parent) {
|
||||||
|
warmupPool.parent.remove(warmupPool);
|
||||||
|
}
|
||||||
|
warmupPool.dispose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loaded = (warmupPool as any).loaded;
|
||||||
|
if (loaded && typeof loaded.then === 'function') {
|
||||||
|
loaded.finally(removeWarmupPool);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeWarmupPool();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
App.setConfig({
|
App.setConfig({
|
||||||
theme: globalStore.theme.replace("Theme", ""),
|
theme: globalStore.theme.replace("Theme", ""),
|
||||||
@ -48,6 +85,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
bootstrapWaterPoolWarmup(window.viewer);
|
||||||
|
|
||||||
// 添加astral engine内置插件并监听插件注册
|
// 添加astral engine内置插件并监听插件注册
|
||||||
pluginStore.setPlugins(Array.from(window.viewer.modules.plugin.plugins.values()));
|
pluginStore.setPlugins(Array.from(window.viewer.modules.plugin.plugins.values()));
|
||||||
Hooks.useAddSignal("pluginInstall",pluginStore.addPlugin);
|
Hooks.useAddSignal("pluginInstall",pluginStore.addPlugin);
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const defaultDataSet: IDataSet.Item = {
|
|||||||
type: 'API',
|
type: 'API',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
api: '',
|
api: '',
|
||||||
dataSource: '',
|
dataSourceId: '',
|
||||||
sql: '',
|
sql: '',
|
||||||
json: ''
|
json: ''
|
||||||
};
|
};
|
||||||
@ -179,7 +179,7 @@ async function editDataSet(item) {
|
|||||||
const detail = (res.data || item) as any;
|
const detail = (res.data || item) as any;
|
||||||
resetCurrentDataSet({
|
resetCurrentDataSet({
|
||||||
...detail,
|
...detail,
|
||||||
dataSource: detail.dataSourceId || detail.dataSource || ""
|
dataSourceId: detail.dataSourceId || detail.dataSource ? String(detail.dataSourceId || detail.dataSource) : ""
|
||||||
});
|
});
|
||||||
showDataSetModal.value = true
|
showDataSetModal.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,180 +1,330 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-modal :show="show" @mask-click="handleClose">
|
<n-modal :show="show" @mask-click="handleClose">
|
||||||
<n-card class="w-200 max-w-1200px" :title="t('home.Data set config')">
|
<n-card class="w-200 max-w-1200px" :title="t('home.Data set config')">
|
||||||
<n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto" :disabled="submitLoading">
|
<n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto" :disabled="submitLoading">
|
||||||
<n-grid :cols="24" :x-gap="24">
|
<n-grid :cols="24" :x-gap="24">
|
||||||
<n-form-item-gi :span="12" :label="t('home.Data set name')" path="name">
|
<n-form-item-gi :span="12" :label="t('home.Data set name')" path="name">
|
||||||
<n-input v-model:value="model.name" />
|
<n-input v-model:value="model.name" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="12" :label="t('home.Data set group')">
|
<n-form-item-gi :span="12" :label="t('home.Data set group')" path="groupId">
|
||||||
<n-cascader v-model:value="model.groupId" expand-trigger="hover" :options="groupOptions"
|
<n-cascader
|
||||||
check-strategy="all" show-path filterable clearable label-field="name" value-field="id" />
|
v-model:value="model.groupId"
|
||||||
</n-form-item-gi>
|
expand-trigger="hover"
|
||||||
<n-form-item-gi :span="12" :label="t('home.Data set type')">
|
:options="groupOptions"
|
||||||
<n-select v-model:value="model.type" :options="setTypes" />
|
check-strategy="all"
|
||||||
</n-form-item-gi>
|
show-path
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
label-field="name"
|
||||||
|
value-field="id"
|
||||||
|
/>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="12" :label="t('home.Data set type')" path="type">
|
||||||
|
<n-select v-model:value="model.type" :options="setTypes" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
|
||||||
<template v-if="model.type === 'API'">
|
<template v-if="model.type === 'API'">
|
||||||
<n-form-item-gi :span="12" :label="t('home.Method')">
|
<n-form-item-gi :span="12" :label="t('home.Method')" path="method">
|
||||||
<n-select v-model:value="model.type" :options="[
|
<n-select
|
||||||
{ label: 'GET', value: 'GET' },
|
:key="'dataset-method-select'"
|
||||||
{ label: 'POST', value: 'POST' },
|
v-model:value="model.method"
|
||||||
]" />
|
:options="[
|
||||||
</n-form-item-gi>
|
{ label: 'GET', value: 'GET' },
|
||||||
<n-form-item-gi :span="24" :label="t('home.API interface')">
|
{ label: 'POST', value: 'POST' },
|
||||||
<n-input v-model:value="model.api" type="textarea" />
|
]"
|
||||||
</n-form-item-gi>
|
/>
|
||||||
</template>
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="24" :label="t('home.API interface')" path="api">
|
||||||
|
<n-input v-model:value="model.api" type="textarea" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else-if="model.type === 'SQL'">
|
<template v-else-if="model.type === 'SQL'">
|
||||||
<n-form-item-gi :span="12" :label="t('home.Data sources')">
|
<n-form-item-gi :span="12" :label="t('home.Data sources')" path="dataSourceId">
|
||||||
<n-select v-model:value="model.dataSource" :options="dataSourceOptions" />
|
<n-select :key="'dataset-datasource-select'" v-model:value="model.dataSourceId" :options="dataSourceOptions" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="24" :label="`SQL(${t('home.Query only')})`">
|
<n-form-item-gi :span="24" :label="`SQL(${t('home.Query only')})`" path="sql">
|
||||||
<SQLEditor v-model:value="model.sql as string" />
|
<SQLEditor v-model:value="model.sql as string" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="24" label=" ">
|
<n-form-item-gi :span="24" label=" ">
|
||||||
<n-blockquote>
|
<n-blockquote>
|
||||||
{{ t('home.Parameter is passed as ') }}
|
{{ t('home.Parameter is passed as ') }}
|
||||||
'${id}' ,
|
'${id}' ,
|
||||||
{{ t('home.For example:') }}
|
{{ t('home.For example:') }}
|
||||||
SELECT * FROM table WHERE id='${id}'
|
SELECT * FROM table WHERE id='${id}'
|
||||||
</n-blockquote>
|
</n-blockquote>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="model.type === 'JSON'">
|
<template v-else-if="model.type === 'JSON'">
|
||||||
<n-form-item-gi :span="24" label="JSON">
|
<n-form-item-gi :span="24" label="JSON" path="json">
|
||||||
<JSONEditor v-model:value="model.json as string" />
|
<JSONEditor v-model:value="model.json as string" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<n-gi :span="24">
|
<n-gi :span="24">
|
||||||
<div class="flex justify-end mt-4">
|
<div class="flex justify-end mt-4">
|
||||||
<n-button :disabled="submitLoading" @click="handleClose" class="mr-2">{{ t("other.Cancel") }}</n-button>
|
<n-button :disabled="submitLoading" @click="handleClose" class="mr-2">{{ t("other.Cancel") }}</n-button>
|
||||||
<n-button type="primary" :loading="submitLoading" @click="saveDataSet">{{ t("other.Ok") }}</n-button>
|
<n-button type="primary" :loading="submitLoading" @click="saveDataSet">{{ t("other.Ok") }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch, useTemplateRef } from "vue";
|
import { ref, watch, computed, useTemplateRef } from "vue";
|
||||||
import type { FormInst } from 'naive-ui'
|
import type { FormInst } from "naive-ui";
|
||||||
import { t } from "@/language";
|
import { t } from "@/language";
|
||||||
import { DataSetPayload, fetchCreateDataSet, fetchUpdateDataSet } from "@/http/api/dataSet";
|
import { DataSetPayload, fetchCreateDataSet, fetchUpdateDataSet } from "@/http/api/dataSet";
|
||||||
import { fetchDataSetGroupTree } from "@/http/api/dataSetGroup";
|
import { fetchDataSetGroupTree } from "@/http/api/dataSetGroup";
|
||||||
import { fetchDataSourceList } from "@/http/api/dataSource";
|
import { fetchDataSourceList } from "@/http/api/dataSource";
|
||||||
import SQLEditor from "@/components/code/SQLEditor.vue";
|
import SQLEditor from "@/components/code/SQLEditor.vue";
|
||||||
import JSONEditor from "@/components/code/JSONEditor.vue";
|
import JSONEditor from "@/components/code/JSONEditor.vue";
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
show: boolean;
|
||||||
|
model?: IDataSet.Item;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
show: false,
|
||||||
|
model: () => ({
|
||||||
|
id: "",
|
||||||
|
groupId: "",
|
||||||
|
name: "",
|
||||||
|
type: "API",
|
||||||
|
method: "GET",
|
||||||
|
api: "",
|
||||||
|
dataSourceId: "",
|
||||||
|
sql: "",
|
||||||
|
json: "",
|
||||||
|
createTime: "",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const emits = defineEmits(["update:show", "refresh"]);
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const formRef = useTemplateRef<FormInst>("formRef");
|
||||||
show: boolean,
|
function createRequiredRule(message: string, trigger: Array<"input" | "blur" | "change">) {
|
||||||
model: IDataSet.Item
|
return {
|
||||||
}>(), {
|
trigger,
|
||||||
show: false,
|
validator(_: unknown, value: unknown) {
|
||||||
model: () => ({
|
if (value === undefined || value === null || value === "") {
|
||||||
id: '',
|
return new Error(message);
|
||||||
groupId: "",
|
}
|
||||||
name: '',
|
if (Array.isArray(value) && value.length === 0) {
|
||||||
type: 'SQL',
|
return new Error(message);
|
||||||
})
|
}
|
||||||
})
|
return true;
|
||||||
const emits = defineEmits(["update:show", "refresh"]);
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const formRef = useTemplateRef<FormInst>("formRef");
|
const rules = computed(() => {
|
||||||
const rules = {
|
const baseRules: Record<string, any> = {
|
||||||
name: { required: true, message: t("prompt.Please enter a name for the dataset"), trigger: 'blur' }
|
name: { required: true, message: t("prompt.Please enter a name for the dataset"), trigger: ["input", "blur"] },
|
||||||
};
|
groupId: createRequiredRule("请选择数据集分组", ["change", "blur"]),
|
||||||
const groupOptions = ref<IDataSet.IGroup[]>([]);
|
type: { required: true, message: "请选择数据集类型", trigger: ["change", "blur"] },
|
||||||
const setTypes = [
|
};
|
||||||
{ label: 'API', value: 'API' },
|
const type = props.model?.type;
|
||||||
{ label: 'SQL', value: 'SQL' },
|
if (type === "API") {
|
||||||
{ label: 'JSON', value: 'JSON' },
|
baseRules.method = {
|
||||||
];
|
required: true,
|
||||||
const dataSourceOptions = ref<{ label: string; value: IDataSource.Item["id"] }[]>([]);
|
message: "请选择请求方式",
|
||||||
const submitLoading = ref(false);
|
trigger: ["change", "blur"],
|
||||||
|
};
|
||||||
|
baseRules.api = {
|
||||||
|
required: true,
|
||||||
|
message: "请输入接口地址",
|
||||||
|
trigger: ["input", "blur"],
|
||||||
|
};
|
||||||
|
} else if (type === "SQL") {
|
||||||
|
baseRules.dataSourceId = createRequiredRule("请选择数据源", ["change", "blur"]);
|
||||||
|
baseRules.sql = { required: true, message: "请输入 SQL 语句", trigger: ["blur"] };
|
||||||
|
} else if (type === "JSON") {
|
||||||
|
baseRules.json = { required: true, message: "请输入 JSON 数据", trigger: ["blur"] };
|
||||||
|
}
|
||||||
|
return baseRules;
|
||||||
|
});
|
||||||
|
const groupOptions = ref<IDataSet.IGroup[]>([]);
|
||||||
|
const setTypes = [
|
||||||
|
{ label: "API", value: "API" },
|
||||||
|
{ label: "SQL", value: "SQL" },
|
||||||
|
{ label: "JSON", value: "JSON" },
|
||||||
|
];
|
||||||
|
const dataSourceOptions = ref<{ label: string; value: IDataSource.Item["id"] }[]>([]);
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
const isEdit = computed(() => Boolean(props.model?.id));
|
||||||
|
|
||||||
watch(() => props.show, (show) => {
|
function normalizeDataSourceId(value: unknown): IDataSource.Item["id"] | undefined {
|
||||||
if (!show) {
|
if (value === undefined || value === null || value === "") {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
loadOptions();
|
return String(value);
|
||||||
});
|
}
|
||||||
|
|
||||||
watch(() => props.model.type, (nextType, oldType) => {
|
function extractList<T>(value: unknown): T[] {
|
||||||
if (!nextType || nextType === oldType) {
|
if (Array.isArray(value)) {
|
||||||
return;
|
return value as T[];
|
||||||
}
|
}
|
||||||
if (nextType !== "API") {
|
if (value && typeof value === "object") {
|
||||||
props.model.method = undefined;
|
const nested = (value as Record<string, unknown>).result;
|
||||||
props.model.api = "";
|
if (Array.isArray(nested)) {
|
||||||
} else if (!props.model.method) {
|
return nested as T[];
|
||||||
props.model.method = "GET";
|
}
|
||||||
}
|
}
|
||||||
if (nextType !== "SQL") {
|
return [];
|
||||||
props.model.dataSource = "";
|
}
|
||||||
props.model.sql = "";
|
|
||||||
}
|
|
||||||
if (nextType !== "JSON") {
|
|
||||||
props.model.json = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadOptions() {
|
async function loadOptions() {
|
||||||
const [groupRes, dataSourceRes] = await Promise.all([
|
const [groupRes, dataSourceRes] = await Promise.allSettled([fetchDataSetGroupTree(), fetchDataSourceList()]);
|
||||||
fetchDataSetGroupTree(),
|
|
||||||
fetchDataSourceList()
|
|
||||||
]);
|
|
||||||
groupOptions.value = groupRes.data || [];
|
|
||||||
dataSourceOptions.value = (dataSourceRes.data || []).map(item => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClose() {
|
if (groupRes.status === "fulfilled") {
|
||||||
emits("update:show", false);
|
groupOptions.value = extractList<IDataSet.IGroup>(groupRes.value.data);
|
||||||
}
|
} else {
|
||||||
|
groupOptions.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
function saveDataSet(e: MouseEvent) {
|
if (dataSourceRes.status === "fulfilled") {
|
||||||
e.preventDefault()
|
const list = extractList<IDataSource.Item>(dataSourceRes.value.data);
|
||||||
|
dataSourceOptions.value = list.map(item => ({
|
||||||
|
label: item.name,
|
||||||
|
value: String(item.id),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dataSourceOptions.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
formRef.value?.validate(async (errors) => {
|
function normalizeJsonValue(value?: IDataSet.Item["json"]) {
|
||||||
if (!errors) {
|
if (!value) {
|
||||||
submitLoading.value = true;
|
return "";
|
||||||
const payload: DataSetPayload = {
|
}
|
||||||
id: props.model.id || undefined,
|
if (typeof value === "string") {
|
||||||
name: props.model.name?.trim() || "",
|
return value;
|
||||||
groupId: props.model.groupId,
|
}
|
||||||
type: props.model.type
|
try {
|
||||||
};
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (props.model.type === "API") {
|
function resetFieldsForType(type: string, prevType?: string) {
|
||||||
payload.method = props.model.method;
|
if (!props.model) {
|
||||||
payload.api = props.model.api?.trim();
|
return;
|
||||||
} else if (props.model.type === "SQL") {
|
}
|
||||||
payload.dataSourceId = props.model.dataSource;
|
if (type !== "API") {
|
||||||
payload.sql = props.model.sql?.trim();
|
props.model.method = undefined;
|
||||||
} else if (props.model.type === "JSON") {
|
props.model.api = "";
|
||||||
payload.json = props.model.json;
|
}
|
||||||
}
|
if (type === "API" && !props.model.method) {
|
||||||
|
props.model.method = "GET";
|
||||||
|
}
|
||||||
|
if (type !== "SQL") {
|
||||||
|
props.model.dataSourceId = undefined;
|
||||||
|
props.model.sql = "";
|
||||||
|
} else if (prevType && prevType !== "SQL") {
|
||||||
|
props.model.dataSourceId = undefined;
|
||||||
|
props.model.sql = "";
|
||||||
|
}
|
||||||
|
if (type !== "JSON") {
|
||||||
|
props.model.json = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const res = props.model.id ? await fetchUpdateDataSet(payload) : await fetchCreateDataSet(payload);
|
function validateSql(sql: string) {
|
||||||
submitLoading.value = false;
|
const trimmed = sql.trim();
|
||||||
if (res.error) {
|
if (!/^(SELECT|WITH)\b/i.test(trimmed)) {
|
||||||
return;
|
window.$message?.error("SQL 必须以 SELECT 或 WITH 开头");
|
||||||
}
|
return false;
|
||||||
window.$message?.success(props.model.id ? t("prompt.Success to update") : t("prompt.Saved successfully!"));
|
}
|
||||||
|
if (/[;]|--|\/\*/.test(trimmed)) {
|
||||||
|
window.$message?.error("SQL 不允许包含危险语句标记");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新父页面表格
|
function buildPayload(): DataSetPayload {
|
||||||
emits("refresh");
|
const payload: DataSetPayload = {
|
||||||
|
name: props.model?.name?.trim() || "",
|
||||||
|
groupId: props.model?.groupId ?? "",
|
||||||
|
type: props.model?.type || "API",
|
||||||
|
};
|
||||||
|
if (props.model?.id) {
|
||||||
|
payload.id = props.model.id;
|
||||||
|
}
|
||||||
|
if (payload.type === "API") {
|
||||||
|
payload.method = props.model?.method;
|
||||||
|
payload.api = props.model?.api?.trim();
|
||||||
|
} else if (payload.type === "SQL") {
|
||||||
|
payload.dataSourceId = normalizeDataSourceId(props.model?.dataSourceId);
|
||||||
|
payload.sql = props.model?.sql?.trim();
|
||||||
|
} else if (payload.type === "JSON") {
|
||||||
|
payload.json = normalizeJsonValue(props.model?.json);
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
handleClose();
|
watch(
|
||||||
}
|
() => props.show,
|
||||||
})
|
show => {
|
||||||
}
|
if (!show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadOptions();
|
||||||
|
if (props.model) {
|
||||||
|
props.model.dataSourceId = normalizeDataSourceId(props.model.dataSourceId);
|
||||||
|
}
|
||||||
|
if (props.model?.type === "API" && !props.model.method) {
|
||||||
|
props.model.method = "GET";
|
||||||
|
}
|
||||||
|
if (props.model?.type === "JSON") {
|
||||||
|
props.model.json = normalizeJsonValue(props.model.json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.model?.type,
|
||||||
|
(next, prev) => {
|
||||||
|
if (!next || next === prev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetFieldsForType(next, prev);
|
||||||
|
formRef.value?.restoreValidation();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emits("update:show", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDataSet(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
formRef.value?.validate(async errors => {
|
||||||
|
if (errors || !props.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.model.type === "SQL" && !validateSql(props.model.sql || "")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLoading.value = true;
|
||||||
|
const payload = buildPayload();
|
||||||
|
const res = isEdit.value ? await fetchUpdateDataSet(payload) : await fetchCreateDataSet(payload);
|
||||||
|
submitLoading.value = false;
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.$message?.success(isEdit.value ? t("prompt.Success to update") : t("prompt.Saved successfully!"));
|
||||||
|
emits("refresh");
|
||||||
|
handleClose();
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
16
packages/editor/types/data-source.d.ts
vendored
16
packages/editor/types/data-source.d.ts
vendored
@ -1,6 +1,7 @@
|
|||||||
declare namespace IDataSource {
|
declare namespace IDataSource {
|
||||||
|
type Id = string | number;
|
||||||
interface Item {
|
interface Item {
|
||||||
id: string;
|
id: Id;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
connectionString: string;
|
connectionString: string;
|
||||||
@ -10,21 +11,24 @@ declare namespace IDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare namespace IDataSet {
|
declare namespace IDataSet {
|
||||||
|
type Id = string | number;
|
||||||
interface Item {
|
interface Item {
|
||||||
id: string;
|
id: Id;
|
||||||
groupId: string;
|
groupId: Id;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
method?: "GET" | "POST";
|
method?: "GET" | "POST";
|
||||||
api?: string;
|
api?: string;
|
||||||
dataSource?: string;
|
dataSourceId?: Id;
|
||||||
|
dataSource?: Id;
|
||||||
sql?: string;
|
sql?: string;
|
||||||
json?: string;
|
json?: string;
|
||||||
|
createTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGroup {
|
interface IGroup {
|
||||||
id: string;
|
id: Id;
|
||||||
pid?: string;
|
pid?: Id;
|
||||||
name: string;
|
name: string;
|
||||||
children?: IGroup[];
|
children?: IGroup[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,8 @@ export default class Path extends THREE.Mesh {
|
|||||||
if (Path.flowSignalBound) return;
|
if (Path.flowSignalBound) return;
|
||||||
Path.flowSignalBound = true;
|
Path.flowSignalBound = true;
|
||||||
useAddSignal("sceneRendered", Path.handleFlowTick);
|
useAddSignal("sceneRendered", Path.handleFlowTick);
|
||||||
|
// 立即请求一帧渲染,防止场景静止时 sceneRendered 永远不触发导致流动停止
|
||||||
|
(App.viewer as any)?.pluginRequestRender?.(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unbindFlowSignal() {
|
private static unbindFlowSignal() {
|
||||||
|
|||||||
@ -206,6 +206,12 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
|
|||||||
//整个主场景的box3
|
//整个主场景的box3
|
||||||
public sceneBox3 = new THREE.Box3();
|
public sceneBox3 = new THREE.Box3();
|
||||||
public package: Package;
|
public package: Package;
|
||||||
|
private _pluginNeedsRender = false;
|
||||||
|
|
||||||
|
/** 供插件/内部模块请求下一帧强制渲染,传 true 标记,animate() 检测后自动清除 */
|
||||||
|
pluginRequestRender(value: boolean) {
|
||||||
|
this._pluginNeedsRender = value;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(options: IViewerSetting) {
|
constructor(options: IViewerSetting) {
|
||||||
super();
|
super();
|
||||||
@ -1107,6 +1113,11 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
|
|||||||
needRender = true;
|
needRender = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._pluginNeedsRender) {
|
||||||
|
needRender = true;
|
||||||
|
this._pluginNeedsRender = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.dispatchEvent({type: 'afterAnimation', delta: timeStamp, toBeRender: (_needRender:boolean = false) => {
|
this.dispatchEvent({type: 'afterAnimation', delta: timeStamp, toBeRender: (_needRender:boolean = false) => {
|
||||||
needRender = _needRender;
|
needRender = _needRender;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user