feat(All):Initial
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* byte数组转换成base64 数据
|
||||
* @param {Array<byte>} buffer
|
||||
* @return {string} base64 string
|
||||
*/
|
||||
export function _arrayBufferToBase64(buffer) {
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode( bytes[ i ] );
|
||||
}
|
||||
return window.btoa( binary );
|
||||
}
|
||||
|
||||
/**
|
||||
* base64数据转换成byte数组
|
||||
* @param {string} base64
|
||||
* @return {string} buffer
|
||||
*/
|
||||
export function _base64ToArrayBuffer(base64) {
|
||||
const binary_string = window.atob(base64);
|
||||
const len = binary_string.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 10进制转rgba
|
||||
export function decToRgb(number:number) {
|
||||
const blue = number & 0xff;
|
||||
const green = number >> 8 & 0xff;
|
||||
const red = number >> 16 & 0xff;
|
||||
return `rgb(${red}, ${green}, ${blue})`;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @author ErSan
|
||||
* @email mlt131220@163.com
|
||||
* @date 2024/3/26 9:57
|
||||
* @description 全局常量
|
||||
*/
|
||||
// 支持的模型格式
|
||||
export const MODEL_SUPPORT_TYPE = [
|
||||
// 普通模型格式
|
||||
"gltf","glb","fbx","obj","mtl",
|
||||
"3dm","3ds",
|
||||
"3mf","amf",
|
||||
"dae","drc",
|
||||
"kmz","ldr",
|
||||
"mpd","md2",
|
||||
"pcd","ply",
|
||||
"stl","svg",
|
||||
"usdz","vox","vtk","vtp",
|
||||
"wrl",
|
||||
"xyz",
|
||||
"json","zip",
|
||||
// 自解析格式
|
||||
"rvt","rfa","ifc",
|
||||
"3DTiles",
|
||||
//"osgb",
|
||||
// "rvm",
|
||||
// "dgn",
|
||||
/*"jt"*/
|
||||
// "shp",
|
||||
// "stp","step"
|
||||
];
|
||||
|
||||
// 资源中心支持上传的格式
|
||||
export const ASSET_UPLOAD_SUPPORT_TYPE = {
|
||||
'Model': ['glb','gltf','fbx','obj'],
|
||||
'Material': ['zip'],
|
||||
'Texture': ['png','jpg','jpeg','webp'],
|
||||
'Billboard': ['png','jpg','jpeg','webp','svg'],
|
||||
'HDR': ['hdr','exr'],
|
||||
'Tiles': ['zip'],
|
||||
}
|
||||
|
||||
// 需要转换的BIM模型格式
|
||||
export const NEED_CONVERT_BIM_MODEL = ["rvt", "rfa"];
|
||||
|
||||
// 支持的图纸文件类型
|
||||
export const DRAWING_SUPPORT_TYPE = ["dwg", "dxf", "png", "jpg", "jpeg"];
|
||||
|
||||
// 需要转换的图纸格式
|
||||
export const NEED_CONVERT_DRAWING = ["dwg"];
|
||||
|
||||
// 支持的文档类型
|
||||
export const DOC_SUPPORT_TYPE = ["doc","docx","xls","xlsx","xlsm","ppt",'pptx',"pdf","txt"];
|
||||
|
||||
export const SCENE_TYPE = [
|
||||
{label:"城市",value:"城市"},
|
||||
{label:"园区",value:"园区"},
|
||||
{label:"工厂",value:"工厂"},
|
||||
{label:"楼宇",value:"楼宇"},
|
||||
{label:"设备",value:"设备"},
|
||||
{label:"其他",value:"其他"},
|
||||
]
|
||||
|
||||
// 截屏占位图
|
||||
export const DefaultScreenshot = "/static/images/placeholder/截屏占位图.png";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export function dateTimeFormat(fmt) {
|
||||
let myDate = new Date();
|
||||
let o = {
|
||||
"M+": myDate.getMonth() + 1, //月份
|
||||
"d+": myDate.getDate(), //日
|
||||
"H+": myDate.getHours(), //小时
|
||||
"m+": myDate.getMinutes(), //分
|
||||
"s+": myDate.getSeconds(), //秒
|
||||
"q+": Math.floor((myDate.getMonth() + 3) / 3), //季度
|
||||
"S": myDate.getMilliseconds() //毫秒
|
||||
};
|
||||
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (myDate.getFullYear() + "").substr(4 - RegExp.$1.length));
|
||||
for (let k in o)
|
||||
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
|
||||
return fmt;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {pow1024} from "./utils";
|
||||
|
||||
/**
|
||||
* 通过Fetch API下载文件/图片(支持跨域/Blob处理)
|
||||
* @param {string} url 文件地址
|
||||
* @param {string} filename 保存的文件名
|
||||
*/
|
||||
export async function downloadWithFetch(url:string, filename:string = "") {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('The network is responding abnormally');
|
||||
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
if(!filename){
|
||||
filename = url.substring(url.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理内存
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
document.body.removeChild(link);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件大小 字节转换单位
|
||||
* @param {number} size
|
||||
* @returns {string|*}
|
||||
*/
|
||||
export const filterSize = (size:number): string | any => {
|
||||
if (!size) return '0B';
|
||||
if (size < pow1024(1)) return size + ' B';
|
||||
if (size < pow1024(2)) return (size / pow1024(1)).toFixed(2) + ' KB';
|
||||
if (size < pow1024(3)) return (size / pow1024(2)).toFixed(2) + ' MB';
|
||||
if (size < pow1024(4)) return (size / pow1024(3)).toFixed(2) + ' GB';
|
||||
return (size / pow1024(4)).toFixed(2) + ' TB'
|
||||
}
|
||||
|
||||
|
||||
export const getServiceStaticFile = (url:string):string => {
|
||||
if(!url) return "";
|
||||
|
||||
if(url.startsWith("blob:") || url.startsWith("data:") || url.startsWith("http:") || url.startsWith("https:") || url.startsWith('/file/static')) return url;
|
||||
|
||||
return `/file/static` + (url[0] === '/' ? '' : '/') + url;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import "../signals/signalRegister";
|
||||
|
||||
window.URL = window.URL || window.webkitURL;
|
||||
// @ts-ignore
|
||||
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
|
||||
Number.prototype.format = function () {
|
||||
return this.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
};
|
||||
|
||||
/*添加自执行函数,为rem设置自适应的根DOM字体大小,1920px下默认1rem = 20px */
|
||||
(function () {
|
||||
change();
|
||||
|
||||
function change() {
|
||||
const w = document.documentElement.clientWidth > 1280 ? document.documentElement.clientWidth : 1280;
|
||||
document.documentElement.style.fontSize = w * 20 / 1920 + "px";
|
||||
}
|
||||
|
||||
/*监听窗口大小的改变*/
|
||||
window.addEventListener("resize", change, false);
|
||||
})()
|
||||
|
||||
//屏蔽选中
|
||||
document.onselectstart = function (event) {
|
||||
if (window.event) {
|
||||
event = window.event;
|
||||
}
|
||||
try {
|
||||
const the = event.srcElement;
|
||||
// @ts-ignore
|
||||
return (the.tagName === "INPUT" && the.type.toLowerCase() === "text") || the.tagName === "TEXTAREA";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 禁止浏览器默认右键菜单
|
||||
document.addEventListener('contextmenu', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// 保存原始方法
|
||||
const originalStringify = JSON.stringify;
|
||||
const originalParse = JSON.parse;
|
||||
|
||||
// 重写 JSON.stringify
|
||||
JSON.stringify = function (value: any, ...args: any[]) {
|
||||
return originalStringify(value, function (key, val) {
|
||||
// 如果有自定义的 replacer 函数,先执行它
|
||||
if (args[0]) {
|
||||
val = args[0](key, val);
|
||||
}
|
||||
|
||||
if (val === Infinity) return "Infinity";
|
||||
if (val === -Infinity) return "-Infinity";
|
||||
return val;
|
||||
}, args[1]); // args[1] 是 space 参数
|
||||
};
|
||||
|
||||
// 重写 JSON.parse
|
||||
JSON.parse = function (text: string, reviver?: (key: any, value: any) => any) {
|
||||
return originalParse(text, function (key, value) {
|
||||
// 处理 Infinity
|
||||
if (value === "Infinity") return Infinity;
|
||||
if (value === "-Infinity") return -Infinity;
|
||||
|
||||
// 如果有自定义的 reviver 函数,执行它
|
||||
return reviver ? reviver(key, value) : value;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { h } from 'vue';
|
||||
import type { Component } from 'vue'
|
||||
import { NIcon } from 'naive-ui';
|
||||
|
||||
/**
|
||||
* 渲染icon
|
||||
* @param icon vicons
|
||||
* @returns
|
||||
*/
|
||||
export function renderIcon(icon: Component) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) });
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/Addons';
|
||||
import { App } from "@astral3d/engine";
|
||||
|
||||
export interface IModel extends THREE.Object3D {
|
||||
metadata: Object;
|
||||
}
|
||||
|
||||
export function getMaterialName(material) {
|
||||
if (Array.isArray(material)) {
|
||||
const array: any = [];
|
||||
|
||||
for (let i = 0; i < material.length; i++) {
|
||||
array.push(material[i].name);
|
||||
}
|
||||
|
||||
return array.join(',');
|
||||
}
|
||||
|
||||
return material.name;
|
||||
}
|
||||
|
||||
export function getObjectType(object) {
|
||||
if (object.isScene) return 'Scene';
|
||||
if (object.isCamera) return 'Camera';
|
||||
if (object.isLight) return 'Light';
|
||||
if (object.isMesh) return 'Mesh';
|
||||
if (object.isLine) return 'Line';
|
||||
if (object.isPoints) return 'Points';
|
||||
|
||||
return 'Object3D';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中模型 path
|
||||
*/
|
||||
export function getSelectedModelPath() {
|
||||
let pathArr: string[] = [];
|
||||
|
||||
function getPath(obj) {
|
||||
if (obj.parent) {
|
||||
pathArr.unshift(obj.parent.name);
|
||||
getPath(obj.parent);
|
||||
}
|
||||
}
|
||||
|
||||
getPath(App.selected);
|
||||
|
||||
return pathArr.join(' !! ');
|
||||
}
|
||||
|
||||
// 屏幕坐标转世界坐标
|
||||
export function screenToWorld(x: number, y: number) {
|
||||
const vector = new THREE.Vector3();
|
||||
vector.set(
|
||||
(x / window.viewer.container.offsetWidth) * 2 - 1,
|
||||
-(y / window.viewer.container.offsetHeight) * 2 + 1,
|
||||
0.5
|
||||
);
|
||||
vector.unproject(App.camera);
|
||||
|
||||
const dir = vector.sub(App.camera.position).normalize();
|
||||
const distance = -App.camera.position.z / dir.z;
|
||||
return App.camera.position.clone().add(dir.multiplyScalar(distance));
|
||||
}
|
||||
|
||||
export function reBufferGeometryUv(geometry: THREE.BufferGeometry) {
|
||||
const uv = geometry.attributes.uv;
|
||||
if (!uv) return;
|
||||
|
||||
// 获取u和v的范围
|
||||
const box = {
|
||||
min: new THREE.Vector2(Infinity, Infinity),
|
||||
max: new THREE.Vector2(-Infinity, -Infinity),
|
||||
};
|
||||
for (let i = 0; i < uv.count; i++) {
|
||||
const u = uv.getX(i);
|
||||
const v = uv.getY(i);
|
||||
|
||||
box.min.x = Math.min(box.min.x, u);
|
||||
box.min.y = Math.min(box.min.y, v);
|
||||
box.max.x = Math.max(box.max.x, u);
|
||||
box.max.y = Math.max(box.max.y, v);
|
||||
}
|
||||
|
||||
// 计算偏移量和范围
|
||||
const offset = new THREE.Vector2(0 - box.min.x, 0 - box.min.y);
|
||||
const range = new THREE.Vector2(box.max.x - box.min.x, box.max.y - box.min.y);
|
||||
|
||||
// 遍历顶点,修改uv
|
||||
for (let i = 0; i < uv.count; i++) {
|
||||
const u = uv.getX(i);
|
||||
const v = uv.getY(i);
|
||||
|
||||
// 计算新的u和v
|
||||
const newU = (u + offset.x) / range.x;
|
||||
const newV = (v + offset.y) / range.y;
|
||||
|
||||
// 写入新的uv
|
||||
uv.setXY(i, newU, newV);
|
||||
}
|
||||
|
||||
// 通知three.js更新
|
||||
uv.needsUpdate = true;
|
||||
}
|
||||
|
||||
export function setUserData(object: IModel, key: string, value: any) {
|
||||
// key按照.分割,设置到object的userData中
|
||||
const keys = key.split('.');
|
||||
let obj = object.userData;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const k = keys[i];
|
||||
if (!obj[k]) {
|
||||
obj[k] = {};
|
||||
}
|
||||
obj = obj[k];
|
||||
}
|
||||
obj[keys[keys.length - 1]] = value;
|
||||
}
|
||||
|
||||
export function setMetaData(object: IModel, key: string, value: any) {
|
||||
// key按照.分割,设置到object的metaData中
|
||||
const keys = key.split('.');
|
||||
let metadata = object.metadata;
|
||||
|
||||
if (!metadata) {
|
||||
metadata = {};
|
||||
object.metadata = metadata;
|
||||
}
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const k = keys[i];
|
||||
if (!metadata[k]) {
|
||||
metadata[k] = {};
|
||||
}
|
||||
metadata = metadata[k];
|
||||
}
|
||||
metadata[keys[keys.length - 1]] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建基础场景(会用于轻量展示)
|
||||
*/
|
||||
export function createBasicScene(container: HTMLElement) {
|
||||
let scene = new THREE.Scene();
|
||||
|
||||
let camera = new THREE.PerspectiveCamera(60, container.offsetWidth / container.offsetHeight, 0.1, 1000);
|
||||
camera.position.set(0, 2, 8);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
let renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
powerPreference: "high-performance"
|
||||
});
|
||||
|
||||
renderer.setSize(container.offsetWidth, container.offsetHeight);
|
||||
renderer.setClearColor(0x0c1126);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// 添加光源
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 2);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
directionalLight.position.set(5, 5, 5);
|
||||
scene.add(directionalLight);
|
||||
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
backLight.position.set(-5, -5, -5);
|
||||
scene.add(backLight);
|
||||
|
||||
// 初始化轨道控制器
|
||||
let controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.screenSpacePanning = true;
|
||||
|
||||
// @ts-ignore监听视窗变化(节流)
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
let resizeObserver = new ResizeObserver(() => {
|
||||
if (timer) return;
|
||||
timer = setTimeout(function () {
|
||||
camera.aspect = container.offsetWidth / container.offsetHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(container.offsetWidth, container.offsetHeight);
|
||||
|
||||
timer = null;
|
||||
}, 16)
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
|
||||
const animate = () => {
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
renderer.setAnimationLoop(animate);
|
||||
|
||||
const dispose = () => {
|
||||
resizeObserver.unobserve(container);
|
||||
resizeObserver.disconnect();
|
||||
// @ts-ignore
|
||||
resizeObserver = null;
|
||||
|
||||
renderer.setAnimationLoop(null);
|
||||
renderer.dispose();
|
||||
// @ts-ignore
|
||||
renderer = null;
|
||||
|
||||
controls.dispose();
|
||||
// @ts-ignore
|
||||
controls = null;
|
||||
|
||||
camera.clear();
|
||||
// @ts-ignore
|
||||
camera = null;
|
||||
|
||||
scene.clear();
|
||||
// @ts-ignore
|
||||
scene = null;
|
||||
}
|
||||
|
||||
return {
|
||||
scene,
|
||||
camera,
|
||||
renderer,
|
||||
controls,
|
||||
dispose
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import type { TreeOption } from 'naive-ui';
|
||||
import {BASE64_TYPES} from "@astral3d/engine";
|
||||
import {t} from "@/language";
|
||||
|
||||
// 替换字符
|
||||
export function escapeHTML(html:string) {
|
||||
if (typeof html !== "string") return html;
|
||||
|
||||
return html
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* naive UI树结构寻找对应节点位置及所处父节点
|
||||
* @param node 目标节点
|
||||
* @param nodes 树数据
|
||||
*/
|
||||
export function findSiblingsAndIndex(node: TreeOption, nodes?: TreeOption[]): [TreeOption[], number] | [null, null] {
|
||||
if (!nodes) return [null, null];
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
const siblingNode = nodes[i];
|
||||
if (siblingNode.key === node.key) return [nodes, i];
|
||||
|
||||
const [siblings, index] = findSiblingsAndIndex(node, siblingNode.children)
|
||||
if (siblings && index !== null) return [siblings, index];
|
||||
}
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
// base64 转 File
|
||||
export function base64ToFile(dataurl, filename) {
|
||||
let arr = dataurl.split(',');
|
||||
let mime = arr[0].match(/:(.*?);/)[1];
|
||||
let bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
let u8arr = new Uint8Array(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new File([u8arr], `${filename}.${BASE64_TYPES[arr[0]]}`, {type: mime});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 求次幂
|
||||
*/
|
||||
export function pow1024(num) {
|
||||
return Math.pow(1024, num)
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态添加script
|
||||
* @param src
|
||||
* @param async
|
||||
*/
|
||||
export function loadScript(src:string,async:boolean = true){
|
||||
return new Promise((resolve,reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.async = async;
|
||||
|
||||
script.onload = () => {
|
||||
resolve("");
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
reject(`${src} 加载失败!`);
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪切板
|
||||
* @param text
|
||||
*/
|
||||
export function copyToClipboard(text: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 检查浏览器是否支持 Clipboard API
|
||||
if (!navigator.clipboard) {
|
||||
reject(t("prompt['Your browser does not support the Clipboard API, please use another browser']"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 Clipboard API 复制文本
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
resolve(t("prompt['Successfully copied to clipboard']"));
|
||||
}).catch(err => {
|
||||
resolve(`${t("prompt['Failed to copy to clipboard']")}: ${err}`);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取rem的px值
|
||||
*/
|
||||
export function remToPxNumber(rem: number): number {
|
||||
const f = parseFloat(document.documentElement.style.fontSize);
|
||||
return f * rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态注入脚本,并监听执行完毕事件
|
||||
* @param {string} src
|
||||
*/
|
||||
export function injectJS(src) {
|
||||
return new Promise(resolve => {
|
||||
// Warn:script.src !== script.getAttribute('src')
|
||||
const loaded = Array.from(document.scripts).some(it => it.getAttribute('src') === src);
|
||||
if (loaded) {
|
||||
resolve(src);
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
document.head.insertBefore(script, document.head.firstElementChild);
|
||||
script.addEventListener('load', () => {
|
||||
resolve(src);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 在树形结构中查找指定 key 的节点(深度优先搜索)
|
||||
*/
|
||||
export function findTreeNode(tree:TreeOption[], targetKey:string | number):TreeOption | null {
|
||||
// 检查树数据是否为数组
|
||||
if (!Array.isArray(tree)) return null;
|
||||
|
||||
for (const node of tree) {
|
||||
// 检查当前节点是否匹配
|
||||
if (node.key === targetKey) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// 递归搜索子节点
|
||||
if (node.children && node.children.length > 0) {
|
||||
const result = findTreeNode(node.children, targetKey);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用深度优先遍历(DFS)递归算法,为没有子节点的节点添加 isLeaf: true 属性
|
||||
*/
|
||||
export function markLeafNodes(tree:TreeOption[]) {
|
||||
if (!tree || !Array.isArray(tree)) return;
|
||||
|
||||
for (const node of tree) {
|
||||
// 如果有children数组且不为空
|
||||
if (node.children && node.children.length > 0) {
|
||||
// 递归处理子节点
|
||||
markLeafNodes(node.children);
|
||||
}
|
||||
// 如果children不存在,或者存在但为空数组
|
||||
else {
|
||||
// 设置 isLeaf 属性(不覆盖已存在的值)
|
||||
if (!node.hasOwnProperty('isLeaf')) {
|
||||
node.isLeaf = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 用于验证的工具函数集合
|
||||
*/
|
||||
import {t} from "@/language";
|
||||
|
||||
export function formItemIsFile(_, value:File | null){
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// 判断value是否是File类型
|
||||
if (!value || !(value instanceof File)) {
|
||||
reject(Error(t("prompt.The entry can not be null")))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function formItemNotNil(_, value:any){
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (value === null || value === undefined) {
|
||||
reject(Error(t("prompt.The entry can not be null")))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user