Compare commits

..

No commits in common. "9662f41eecd7c42512a9fd650bc964383300f5ba" and "5368e52f0d9e5e1c659337d9f31a9e71cc347037" have entirely different histories.

9 changed files with 197 additions and 408 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?: IDataSet.Item["id"]; dataSourceId?: string;
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() as any; const _json = object.toJSON();
_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,24 +135,19 @@ function updateFileList(fList: UploadFileInfo[]) {
isSprite: data.type ==='sprite', isSprite: data.type ==='sprite',
fileName: file.name fileName: file.name
}).then(htmlPlaneObj => { }).then(htmlPlaneObj => {
object.options.codes = htmlPlaneObj.options.codes; data.codes = htmlPlaneObj.options.codes;
object.options.isSingleHtml = htmlPlaneObj.options.isSingleHtml;
data.codes = object.options.codes; const _json = object.toJSON();
Loader.objectLoader.copyAttrByData(htmlPlaneObj, _json.object)
// Keep object identity (id/uuid) so scene tree and context menu references stay valid. object.parent?.add(htmlPlaneObj);
while (object.element.firstChild) {
object.element.removeChild(object.element.firstChild);
}
while (htmlPlaneObj.element.firstChild) { object.parent?.remove(object);
object.element.appendChild(htmlPlaneObj.element.firstChild);
}
App.selected = object; App.selected = htmlPlaneObj;
Hooks.useDispatchSignal("objectSelected", object);
}).catch((e:Error) => window.$message?.error(e.message)) Hooks.useDispatchSignal("objectSelected", htmlPlaneObj);
.finally(() => URL.revokeObjectURL(tempURL)); }).catch((e:Error) => window.$message?.error(e.message));
} }
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(6)); objectData.position.x = Number(object.position.x.toFixed(3));
objectData.position.y = Number(object.position.y.toFixed(6)); objectData.position.y = Number(object.position.y.toFixed(3));
objectData.position.z = Number(object.position.z.toFixed(6)); objectData.position.z = Number(object.position.z.toFixed(3));
objectData.rotation.x = Number((object.rotation.x * THREE.MathUtils.RAD2DEG).toFixed(6)); objectData.rotation.x = Number((object.rotation.x * THREE.MathUtils.RAD2DEG).toFixed(3));
objectData.rotation.y = Number((object.rotation.y * THREE.MathUtils.RAD2DEG).toFixed(6)); objectData.rotation.y = Number((object.rotation.y * THREE.MathUtils.RAD2DEG).toFixed(3));
objectData.rotation.z = Number((object.rotation.z * THREE.MathUtils.RAD2DEG).toFixed(6)); objectData.rotation.z = Number((object.rotation.z * THREE.MathUtils.RAD2DEG).toFixed(3));
objectData.scale.x = Number(object.scale.x.toFixed(6)); objectData.scale.x = Number(object.scale.x.toFixed(3));
objectData.scale.y = Number(object.scale.y.toFixed(6)); objectData.scale.y = Number(object.scale.y.toFixed(3));
objectData.scale.z = Number(object.scale.z.toFixed(6)); objectData.scale.z = Number(object.scale.z.toFixed(3));
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.0001) { if (object.position.distanceTo(newPosition) >= 0.01) {
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.0001) { if (new THREE.Vector3().setFromEuler(object.rotation).distanceTo(new THREE.Vector3().setFromEuler(newRotation)) >= 0.01) {
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.0001) { if (object.scale.distanceTo(newScale) >= 0.01) {
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="6" :step="1" <EsInputNumber v-model:value="objectData.position.x" size="tiny" :show-button="false" :decimal="3" :step="1"
@change="update('position')" /> @change="update('position')" />
<EsInputNumber v-model:value="objectData.position.y" size="tiny" :show-button="false" :decimal="6" :step="1" <EsInputNumber v-model:value="objectData.position.y" size="tiny" :show-button="false" :decimal="3" :step="1"
@change="update('position')" /> @change="update('position')" />
<EsInputNumber v-model:value="objectData.position.z" size="tiny" :show-button="false" :decimal="6" :step="1" <EsInputNumber v-model:value="objectData.position.z" size="tiny" :show-button="false" :decimal="3" :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="6" :step="1" :show-button="false" <EsInputNumber v-model:value="objectData.rotation.x" size="tiny" :decimal="2" :step="1" :show-button="false"
@change="update('rotation')" unit="°" /> @change="update('rotation')" unit="°" />
<EsInputNumber v-model:value="objectData.rotation.y" size="tiny" :decimal="6" :step="1" :show-button="false" <EsInputNumber v-model:value="objectData.rotation.y" size="tiny" :decimal="2" :step="1" :show-button="false"
@change="update('rotation')" unit="°" /> @change="update('rotation')" unit="°" />
<EsInputNumber v-model:value="objectData.rotation.z" size="tiny" :decimal="6" :step="1" :show-button="false" <EsInputNumber v-model:value="objectData.rotation.z" size="tiny" :decimal="2" :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,WaterPool} from "@astral3d/engine"; import {App,Viewer,Hooks} 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,43 +30,6 @@ 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", ""),
@ -85,8 +48,6 @@ 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: '',
dataSourceId: '', dataSource: '',
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,
dataSourceId: detail.dataSourceId || detail.dataSource ? String(detail.dataSourceId || detail.dataSource) : "" dataSource: detail.dataSourceId || detail.dataSource || ""
}); });
showDataSetModal.value = true showDataSetModal.value = true
} }

View File

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

View File

@ -1,7 +1,6 @@
declare namespace IDataSource { declare namespace IDataSource {
type Id = string | number;
interface Item { interface Item {
id: Id; id: string;
name: string; name: string;
type: string; type: string;
connectionString: string; connectionString: string;
@ -11,24 +10,21 @@ declare namespace IDataSource {
} }
declare namespace IDataSet { declare namespace IDataSet {
type Id = string | number;
interface Item { interface Item {
id: Id; id: string;
groupId: Id; groupId: string;
name: string; name: string;
type: string; type: string;
method?: "GET" | "POST"; method?: "GET" | "POST";
api?: string; api?: string;
dataSourceId?: Id; dataSource?: string;
dataSource?: Id;
sql?: string; sql?: string;
json?: string; json?: string;
createTime?: string;
} }
interface IGroup { interface IGroup {
id: Id; id: string;
pid?: Id; pid?: string;
name: string; name: string;
children?: IGroup[]; children?: IGroup[];
} }

View File

@ -65,8 +65,6 @@ 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,12 +206,6 @@ 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();
@ -1113,11 +1107,6 @@ 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;
} }