Compare commits

..

6 Commits

9 changed files with 408 additions and 197 deletions

View File

@ -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;
} }

View File

@ -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) {

View File

@ -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>

View File

@ -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);

View File

@ -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
} }

View File

@ -6,31 +6,44 @@
<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"
expand-trigger="hover"
:options="groupOptions"
check-strategy="all"
show-path
filterable
clearable
label-field="name"
value-field="id"
/>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="12" :label="t('home.Data set type')"> <n-form-item-gi :span="12" :label="t('home.Data set type')" path="type">
<n-select v-model:value="model.type" :options="setTypes" /> <n-select v-model:value="model.type" :options="setTypes" />
</n-form-item-gi> </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
:key="'dataset-method-select'"
v-model:value="model.method"
:options="[
{ label: 'GET', value: 'GET' }, { label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' }, { label: 'POST', value: 'POST' },
]" /> ]"
/>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="24" :label="t('home.API interface')"> <n-form-item-gi :span="24" :label="t('home.API interface')" path="api">
<n-input v-model:value="model.api" type="textarea" /> <n-input v-model:value="model.api" type="textarea" />
</n-form-item-gi> </n-form-item-gi>
</template> </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=" ">
@ -44,7 +57,7 @@
</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>
@ -62,119 +75,256 @@
</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(
const props = withDefaults(defineProps<{ defineProps<{
show: boolean, show: boolean;
model: IDataSet.Item model?: IDataSet.Item;
}>(), { }>(),
{
show: false, show: false,
model: () => ({ model: () => ({
id: '', id: "",
groupId: "", groupId: "",
name: '', name: "",
type: 'SQL', type: "API",
}) method: "GET",
}) api: "",
dataSourceId: "",
sql: "",
json: "",
createTime: "",
}),
}
);
const emits = defineEmits(["update:show", "refresh"]); const emits = defineEmits(["update:show", "refresh"]);
const formRef = useTemplateRef<FormInst>("formRef"); const formRef = useTemplateRef<FormInst>("formRef");
const rules = { function createRequiredRule(message: string, trigger: Array<"input" | "blur" | "change">) {
name: { required: true, message: t("prompt.Please enter a name for the dataset"), trigger: 'blur' } return {
trigger,
validator(_: unknown, value: unknown) {
if (value === undefined || value === null || value === "") {
return new Error(message);
}
if (Array.isArray(value) && value.length === 0) {
return new Error(message);
}
return true;
},
}; };
}
const rules = computed(() => {
const baseRules: Record<string, any> = {
name: { required: true, message: t("prompt.Please enter a name for the dataset"), trigger: ["input", "blur"] },
groupId: createRequiredRule("请选择数据集分组", ["change", "blur"]),
type: { required: true, message: "请选择数据集类型", trigger: ["change", "blur"] },
};
const type = props.model?.type;
if (type === "API") {
baseRules.method = {
required: true,
message: "请选择请求方式",
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 groupOptions = ref<IDataSet.IGroup[]>([]);
const setTypes = [ const setTypes = [
{ label: 'API', value: 'API' }, { label: "API", value: "API" },
{ label: 'SQL', value: 'SQL' }, { label: "SQL", value: "SQL" },
{ label: 'JSON', value: 'JSON' }, { label: "JSON", value: "JSON" },
]; ];
const dataSourceOptions = ref<{ label: string; value: IDataSource.Item["id"] }[]>([]); const dataSourceOptions = ref<{ label: string; value: IDataSource.Item["id"] }[]>([]);
const submitLoading = ref(false); const submitLoading = ref(false);
const isEdit = computed(() => Boolean(props.model?.id));
watch(() => props.show, (show) => { function normalizeDataSourceId(value: unknown): IDataSource.Item["id"] | undefined {
if (value === undefined || value === null || value === "") {
return undefined;
}
return String(value);
}
function extractList<T>(value: unknown): T[] {
if (Array.isArray(value)) {
return value as T[];
}
if (value && typeof value === "object") {
const nested = (value as Record<string, unknown>).result;
if (Array.isArray(nested)) {
return nested as T[];
}
}
return [];
}
async function loadOptions() {
const [groupRes, dataSourceRes] = await Promise.allSettled([fetchDataSetGroupTree(), fetchDataSourceList()]);
if (groupRes.status === "fulfilled") {
groupOptions.value = extractList<IDataSet.IGroup>(groupRes.value.data);
} else {
groupOptions.value = [];
}
if (dataSourceRes.status === "fulfilled") {
const list = extractList<IDataSource.Item>(dataSourceRes.value.data);
dataSourceOptions.value = list.map(item => ({
label: item.name,
value: String(item.id),
}));
} else {
dataSourceOptions.value = [];
}
}
function normalizeJsonValue(value?: IDataSet.Item["json"]) {
if (!value) {
return "";
}
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value, null, 2);
} catch {
return "";
}
}
function resetFieldsForType(type: string, prevType?: string) {
if (!props.model) {
return;
}
if (type !== "API") {
props.model.method = undefined;
props.model.api = "";
}
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 = "";
}
}
function validateSql(sql: string) {
const trimmed = sql.trim();
if (!/^(SELECT|WITH)\b/i.test(trimmed)) {
window.$message?.error("SQL 必须以 SELECT 或 WITH 开头");
return false;
}
if (/[;]|--|\/\*/.test(trimmed)) {
window.$message?.error("SQL 不允许包含危险语句标记");
return false;
}
return true;
}
function buildPayload(): DataSetPayload {
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;
}
watch(
() => props.show,
show => {
if (!show) { if (!show) {
return; return;
} }
loadOptions(); loadOptions();
}); if (props.model) {
props.model.dataSourceId = normalizeDataSourceId(props.model.dataSourceId);
watch(() => props.model.type, (nextType, oldType) => {
if (!nextType || nextType === oldType) {
return;
} }
if (nextType !== "API") { if (props.model?.type === "API" && !props.model.method) {
props.model.method = undefined;
props.model.api = "";
} else if (!props.model.method) {
props.model.method = "GET"; props.model.method = "GET";
} }
if (nextType !== "SQL") { if (props.model?.type === "JSON") {
props.model.dataSource = ""; props.model.json = normalizeJsonValue(props.model.json);
props.model.sql = "";
} }
if (nextType !== "JSON") {
props.model.json = "";
} }
}); );
async function loadOptions() { watch(
const [groupRes, dataSourceRes] = await Promise.all([ () => props.model?.type,
fetchDataSetGroupTree(), (next, prev) => {
fetchDataSourceList() if (!next || next === prev) {
]); return;
groupOptions.value = groupRes.data || [];
dataSourceOptions.value = (dataSourceRes.data || []).map(item => ({
label: item.name,
value: item.id
}));
} }
resetFieldsForType(next, prev);
formRef.value?.restoreValidation();
}
);
function handleClose() { function handleClose() {
emits("update:show", false); emits("update:show", false);
} }
function saveDataSet(e: MouseEvent) { function saveDataSet(e: MouseEvent) {
e.preventDefault() e.preventDefault();
formRef.value?.validate(async (errors) => { formRef.value?.validate(async errors => {
if (!errors) { if (errors || !props.model) {
submitLoading.value = true; return;
const payload: DataSetPayload = {
id: props.model.id || undefined,
name: props.model.name?.trim() || "",
groupId: props.model.groupId,
type: props.model.type
};
if (props.model.type === "API") {
payload.method = props.model.method;
payload.api = props.model.api?.trim();
} else if (props.model.type === "SQL") {
payload.dataSourceId = props.model.dataSource;
payload.sql = props.model.sql?.trim();
} else if (props.model.type === "JSON") {
payload.json = props.model.json;
} }
const res = props.model.id ? await fetchUpdateDataSet(payload) : await fetchCreateDataSet(payload); 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; submitLoading.value = false;
if (res.error) { if (res.error) {
return; return;
} }
window.$message?.success(props.model.id ? t("prompt.Success to update") : t("prompt.Saved successfully!")); window.$message?.success(isEdit.value ? t("prompt.Success to update") : t("prompt.Saved successfully!"));
//
emits("refresh"); emits("refresh");
handleClose(); handleClose();
} });
})
} }
</script> </script>

View File

@ -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[];
} }

View File

@ -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() {

View File

@ -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;
} }