feat(All):Initial

This commit is contained in:
2025-10-04 23:36:07 +08:00
commit 2b4e5d2668
1321 changed files with 415958 additions and 0 deletions
@@ -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;
}
+57
View File
@@ -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;
}
+70
View File
@@ -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) });
}
+231
View File
@@ -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
}
}
+171
View File
@@ -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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/**
* 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 => {
// Warnscript.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()
}
})
}
@@ -0,0 +1,561 @@
import {App,Utils,Hooks} from "@astral3d/engine";
import {getSelectedModelPath} from "@/utils/common/scenes";
let canvasMouseWheelFn: EventListenerOrEventListenerObject;
/**
* 画布中绘制矩形
* @param {HTMLCanvasElement} canvas 画布对象
* @param {Array<IDrawingMark>} list 矩形数组
**/
export class DrawRect {
private canvas: HTMLCanvasElement;
private parentElement: HTMLDivElement;
private ctx: CanvasRenderingContext2D;
private readonly list: Array<IDrawingMark>;
// 当前选中的矩形下标
public selectRectIndex: number = -1;
// 当前鼠标经过的矩形的下标
private hoverRectIndex: number = -1;
private sX: number = 0;
private sY: number = 0;
// 鼠标按下时的clientXY
private downClientX: number = 0;
private downClientY: number = 0;
private zoom: number = 100;
// 鼠标左键是否按下
private leftMouseDown: boolean = false;
// 画布是否处于拖动状态
private isCanvasDrag: boolean = false;
// 画布拖动后的偏移量
private canvasOffsetX: number = 0;
private canvasOffsetY: number = 0;
// rect是否处于拖动状态
private isDrag: boolean = false;
private isDraged: boolean = false;
//杂项
public rectColor: string = "#15FF00";
public rectSelectColor: string = "#ff0000";
// 拖动的rect的初始数据
private dragRect: IDrawingMark = {x: 0, y: 0, w: 0, h: 0,color:this.rectColor};
constructor(canvas: HTMLCanvasElement, parentElement: HTMLDivElement) {
this.canvas = canvas;
this.parentElement = parentElement;
this.list = App.project.getKey("drawing.markList");
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
this.ctx.strokeStyle = this.rectColor;
this.ctx.lineWidth = 1;
this.canvas.onmousemove = Utils.throttle(this.onmousemove.bind(this), 10);
this.canvas.onmousedown = this.onmousedown.bind(this);
this.canvas.onmouseup = this.onmouseup.bind(this);
this.canvas.onmouseleave = this.onmouseleave.bind(this);
canvasMouseWheelFn = this.onmousewheel.bind(this);
this.canvas.addEventListener("mousewheel", canvasMouseWheelFn);
const drawingDiv = this.parentElement.parentElement as HTMLDivElement;
drawingDiv.onmousedown = this.onParentMouseDown.bind(this);
drawingDiv.onmousemove = Utils.throttle(this.onParentMouseMove.bind(this),16);
drawingDiv.onmouseup = this.onParentMouseUp.bind(this);
drawingDiv.onmouseleave = this.onParentMouseLeave.bind(this);
this.init();
}
init(){
// 若list长度不为0, 则显示已标记框
if (this.list.length !== 0) {
this.list.forEach((value: IDrawingMark) => {
this.ctx.beginPath();
this.ctx.strokeStyle = value.color as string;
// 遍历绘制所有标记框
this.ctx.rect(value.x, value.y, value.w, value.h);
this.ctx.stroke();
});
}
}
/**
* 准备开始画矩形标记框
* @public
*/
public addRect() {
this.canvas.style.cursor = "crosshair";
// 进入绘制流程
App.project.setKey("drawing.isDrawingRect",true);
}
/*
* 退出绘制矩形标记框
*/
public exitRect() {
this.canvas.style.cursor = "default";
// 退出绘制流程
App.project.setKey("drawing.isDrawingRect",false);
}
/**
* 删除rect
*/
public deleteRect() {
this.list.splice(this.selectRectIndex, 1);
this.hoverRectIndex = -1;
this.selectRectIndex = -1;
this.reDrawCanvas();
}
/**
* 图纸复位
*/
public canvasReset(){
this.parentElement.style.left = "0px";
this.parentElement.style.top = "0px";
this.canvasOffsetX = 0;
this.canvasOffsetY = 0;
this.canvas.style.transform = "scale(1)";
this.zoom = 100;
}
/**
* 修改当前绘制的颜色
* @param {string} color 颜色
*/
public setRectColor(color: string) {
this.rectColor = color;
this.list[this.selectRectIndex].color = color;
this.reDrawCanvas(true);
setTimeout(()=>{
this.reDrawCanvas(false);
},800)
}
get selectRectColor(){
return this.list[this.selectRectIndex]?.color;
}
/**
* 高亮选中的模型对应的rect
* @param {string} uuid modelUuid
*/
public selectRect(uuid: string) {
this.selectRectIndex = -1;
this.list.forEach((item, index) => {
if (item.modelUuid !== uuid) return;
this.selectRectIndex = index;
if(this.isDrag) return;
const translate = (zoom:number) => {
setTimeout(() => {
// 移动画布,使对应标记居中放大显示(按当前缩放比)
const x = item.x + item.w / 2;
const y = item.y + item.h / 2;
this.parentElement.style.transition = "all .6s";
this.canvasOffsetX = (-x + this.canvas.offsetWidth / 2) * zoom;
this.canvasOffsetY = (-y + this.canvas.offsetHeight / 2) * zoom;
this.parentElement.style.left = this.canvasOffsetX + "px";
this.parentElement.style.top = this.canvasOffsetY + "px";
setTimeout(() => {
this.parentElement.style.transition = "none";
this.canvas.style.transition = "transform 16ms";
}, 600)
}, 200)
}
// 对应标记框如果宽高最大值小于100px,则显示为100像素大小
const max = Math.max(item.w, item.h);
if (max < 100) {
const z = 100 * 100 / max;
this.zoom = z < 50 ? 50 : (z > 800 ? 800 : z);
}else{
this.zoom = 100;
}
const zoom = this.zoom / 100;
this.canvas.style.transition = "transform .35s";
this.canvas.style.transform = "scale(" + zoom + ")";
translate(zoom);
})
this.reDrawCanvas();
App.project.setKey("drawing.selectedRectIndex",this.selectRectIndex);
}
private onmousemove(em) {
if(em.button !== 0) return;
if (this.isCanvasDrag)return;
// 如果处于绘制流程中
if (App.project.getKey("drawing.isDrawingRect")) {
this.canvas.style.cursor = "crosshair";
if (this.leftMouseDown) {
// 正在绘制矩形
// 如果是处于修改矩形流程中
if (this.selectRectIndex !== -1) {
this.list.splice(this.selectRectIndex, 1, {
x: this.sX,
y: this.sY,
w: em.offsetX - this.sX,
h: em.offsetY - this.sY,
color:this.list[this.selectRectIndex].color,
modelUuid: this.list[this.selectRectIndex].modelUuid,
modelPath: this.list[this.selectRectIndex].modelPath
});
this.reDrawCanvas();
} else {
this.reDrawCanvas();
// 设置边框为虚线
this.ctx.beginPath();
this.ctx.setLineDash([8, 4]);
this.ctx.rect(this.sX, this.sY, em.offsetX - this.sX, em.offsetY - this.sY);
this.ctx.stroke();
}
}
return;
}
/** rect 正在拖动 **/
if (this.isDrag) {
this.list.splice(this.selectRectIndex, 1, {
x: this.dragRect.x + (em.offsetX - this.sX),
y: this.dragRect.y + (em.offsetY - this.sY),
w: this.dragRect.w,
h: this.dragRect.h,
color:this.dragRect.color,
modelUuid: this.dragRect.modelUuid,
modelPath: this.dragRect.modelPath
});
this.reDrawCanvas();
this.isDraged = true;
return;
}
this.sX = em.offsetX;
this.sY = em.offsetY;
/** 界面上无矩形 **/
if (this.list.length === 0) return;
/** 界面上有矩形 **/
this.list.forEach((item, index) => {
let path = new Path2D();
if (this.selectRectIndex === index) {
path.rect(item.x - 4, item.y - 4, item.w + 8, item.h + 8);
} else {
path.rect(item.x, item.y, item.w, item.h);
}
if (this.ctx.isPointInPath(path, em.offsetX, em.offsetY)) {
// 鼠标在矩形内
this.hoverRectIndex = index;
this.canvas.style.cursor = "pointer";
} else {
// 如果鼠标不在之前所在的矩形内,清除hoverRectIndex
if (this.hoverRectIndex === index) {
this.hoverRectIndex = -1;
}
this.canvas.style.cursor = "default";
}
this.reDrawCanvas();
});
}
/**
* 鼠标按下时
* @param ed
* @private
*/
private onmousedown(ed) {
ed.stopPropagation();
if(ed.button !== 0) return;
this.isDraged = false;
this.leftMouseDown = true;
// 记录按下位置(矩形绘制起始位置)
this.sX = ed.offsetX;
this.sY = ed.offsetY;
this.downClientX = ed.clientX;
this.downClientY = ed.clientY;
// 如果处于绘制流程中
if (App.project.getKey("drawing.isDrawingRect")) return;
/** 如果鼠标按下时鼠标在矩形内 **/
if (this.hoverRectIndex !== -1) {
// 还未选中过模型 或者 此时点击的不是之前选中的模型
if (this.selectRectIndex === -1 || this.hoverRectIndex !== this.selectRectIndex) {
// 选中矩形 this.hoverRectIndex
this.selectRectIndex = this.hoverRectIndex;
}
this.handleMouseDown(ed.offsetX, ed.offsetY);
} else {
this.selectRectIndex = -1;
// 如果鼠标按下时鼠标不在矩形内,拖动画布
this.isCanvasDrag = true;
}
App.project.setKey("drawing.selectedRectIndex",this.selectRectIndex);
this.reDrawCanvas();
}
private handleMouseDown(offsetX, offsetY) {
const selectRect = this.list[this.selectRectIndex];
// 判断鼠标点击的是四个角(缩放)还是其他区域(拖动)
const x = offsetX - selectRect.x;
const y = offsetY - selectRect.y;
if (x < 5 && y < 5) {
// 左上角
this.sX = selectRect.x + selectRect.w;
this.sY = selectRect.y + selectRect.h;
App.project.setKey("drawing.isDrawingRect",true);
} else if (x > this.list[this.selectRectIndex].w - 5 && y < 5) {
// 右上角
this.sX = selectRect.x;
this.sY = selectRect.y + selectRect.h;
App.project.setKey("drawing.isDrawingRect",true);
} else if (x < 10 && y > this.list[this.selectRectIndex].h - 5) {
// 左下角
this.sX = selectRect.x + selectRect.w;
this.sY = selectRect.y;
App.project.setKey("drawing.isDrawingRect",true);
} else if (x > this.list[this.selectRectIndex].w - 5 && y > this.list[this.selectRectIndex].h - 5) {
// 右下角
this.sX = selectRect.x;
this.sY = selectRect.y;
App.project.setKey("drawing.isDrawingRect",true);
} else {
// 拖动
this.canvas.style.cursor = "move";
this.dragRect = selectRect;
this.isDrag = true;
// 三维场景定位模型
if(selectRect.modelUuid){
const model = App.getObjectByUuid(selectRect.modelUuid);
Hooks.useDispatchSignal("objectFocused",model);
Hooks.useDispatchSignal("objectSelected",model);
}
}
}
/**
* 鼠标抬起时
* @private
*/
private onmouseup(eu) {
if(eu.button !== 0) return;
this.leftMouseDown = false;
this.isCanvasDrag = false;
// 如果处于绘制流程中
if (App.project.getKey("drawing.isDrawingRect")) {
// 判断是新建矩形还是修改
if (this.selectRectIndex === -1) {
const w = eu.offsetX - this.sX;
const h = eu.offsetY - this.sY;
// 矩形w,h都大于5时才添加
if (Math.abs(w) > 5 && Math.abs(h) > 5) {
// 全取左上角点x,y,使得w,h为正数
const rectItem = {
x: w > 0 ? this.sX : this.sX + w,
y: h > 0 ? this.sY : this.sY + h,
w: Math.abs(w),
h: Math.abs(h),
color:this.rectColor,
modelUuid: App.selected?.uuid,
modelPath:getSelectedModelPath(),
}
this.list.push(rectItem);
Hooks.useDispatchSignal("drawingMarkDone","add",rectItem);
}
} else {
const rectItem = {
// 全取左上角点x,y,使得w,h为正数
x: this.list[this.selectRectIndex].w > 0 ? this.sX : this.sX + this.list[this.selectRectIndex].w,
y: this.list[this.selectRectIndex].h > 0 ? this.sY : this.sY + this.list[this.selectRectIndex].h,
w: Math.abs(this.list[this.selectRectIndex].w),
h: Math.abs(this.list[this.selectRectIndex].h),
color:this.list[this.selectRectIndex].color,
modelUuid: this.list[this.selectRectIndex].modelUuid,
modelPath: this.list[this.selectRectIndex].modelPath,
}
this.list.splice(this.selectRectIndex, 1, rectItem);
Hooks.useDispatchSignal("drawingMarkDone","update",rectItem);
}
this.reDrawCanvas();
//退出绘制流
this.exitRect();
return;
}
// 如果处于拖动流程中
if (this.isDrag) {
if(this.isDraged){
Hooks.useDispatchSignal("drawingMarkDone","update",this.list[this.selectRectIndex]);
}
this.isDrag = false;
this.canvas.style.cursor = "default";
return;
}
}
private onmouseleave(){
this.isDrag = false;
}
private onmousewheel(event){
/**
* 获取当前页面的缩放比
* 若未设置zoom缩放比,则为默认100%,即1,原图大小
*/
/* event.wheelDelta 获取滚轮滚动值并将滚动值叠加给缩放比zoom wheelDelta统一为±120,其中正数表示为向上滚动,负数表示向下滚动 */
let z = event.wheelDelta;
if (Math.abs(event.wheelDelta) > 120) {
z = event.wheelDelta > 0 ? 120 : -120;
}
const lastZoom = this.zoom;
this.zoom += z / 12;
/* 最小范围 和 最大范围 的图片缩放尺度 */
if ( this.zoom >= 50 && this.zoom <= 800) {
this.canvas.style.transform = "scale(" + this.zoom / 100 + ")";
}else{
this.zoom = lastZoom;
}
return false;
}
/**
* 重新绘制画布
*/
reDrawCanvas(showSelectLineColor = false) {
this.ctx.setLineDash([8, 0]);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.list.forEach((value, index) => {
if (index === this.selectRectIndex) {
const r = value.w > 10 ? 4 : 2;
/* 绘制选中部分 */
/* 绘制方框 */
this.ctx.beginPath();
this.ctx.strokeStyle = showSelectLineColor ? value.color as string : this.rectSelectColor;
this.ctx.rect(value.x, value.y, value.w, value.h);
this.ctx.fillStyle = 'RGBA(102,102,102,0.2)'
this.ctx.fillRect(value.x, value.y, value.w, value.h);
this.ctx.stroke();
/* 绘制四个角的点 */
this.ctx.beginPath();
this.ctx.strokeStyle = this.rectSelectColor;
this.ctx.arc(value.x, value.y, r, 0, Math.PI * 2)
this.ctx.fillStyle = this.rectSelectColor;
this.ctx.fill();// 画起点实心圆
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.arc(value.x, value.y + value.h, r, 0, Math.PI * 2);
this.ctx.fillStyle = this.rectSelectColor;
this.ctx.fill();// 画起点纵向实心圆
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.arc(value.x + value.w, value.y + value.h, r, 0, Math.PI * 2);
this.ctx.fillStyle = this.rectSelectColor;
this.ctx.fill();// 画起点横向实心圆
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.arc(value.x + value.w, value.y, r, 0, Math.PI * 2);
this.ctx.fillStyle = this.rectSelectColor;
this.ctx.fill();// 画终点实心圆
this.ctx.stroke();
} else if (this.hoverRectIndex === index) {
/* 绘制鼠标经过部分 */
this.ctx.beginPath();
this.ctx.strokeStyle = value.color as string;
this.ctx.rect(value.x, value.y, value.w, value.h);
this.ctx.fillStyle = 'RGBA(102,102,102,0.2)';
this.ctx.fillRect(value.x, value.y, value.w, value.h);
this.ctx.stroke();
} else {
/* 绘制未选中部分 */
this.ctx.beginPath();
this.ctx.strokeStyle = value.color as string;
this.ctx.rect(value.x, value.y, value.w, value.h);
this.ctx.stroke();
}
});
App.project.setKey("drawing.markList",this.list);
}
/* 父级相关事件监听 */
private onParentMouseDown(e){
this.downClientX = e.clientX;
this.downClientY = e.clientY;
this.isCanvasDrag = true;
}
private onParentMouseUp(){
this.isCanvasDrag = false;
this.canvasOffsetX = this.parentElement.offsetLeft;
this.canvasOffsetY = this.parentElement.offsetTop;
}
private onParentMouseMove(e){
/** 画布正在拖动 **/
if (this.isCanvasDrag) {
this.parentElement.style.left = this.canvasOffsetX + e.clientX - this.downClientX + "px";
this.parentElement.style.top = this.canvasOffsetY + e.clientY - this.downClientY + "px";
return;
}
}
private onParentMouseLeave(){
this.isCanvasDrag = false;
}
dispose() {
this.canvas.onmousemove = null;
this.canvas.onmousedown = null;
this.canvas.onmouseup = null;
this.canvas.onmouseleave = null;
this.canvas.removeEventListener("mousewheel", canvasMouseWheelFn);
const drawingDiv = this.parentElement.parentElement as HTMLDivElement;
drawingDiv.onmousedown = null;
drawingDiv.onmousemove = null;
drawingDiv.onmouseup = null;
drawingDiv.onmouseleave = null;
}
}
@@ -0,0 +1,358 @@
/**
* @author ErSan
* @email mlt131220@163.com
* @date 2024/8/22
* @description 预览菜单操作
*/
import { useFullscreen } from '@vueuse/core'
import {
IPreviewOperation,
usePreviewOperationStoreWithOut
} from "@/store/modules/previewOperation";
import * as THREE from 'three';
import { App,Hooks,ClippedEdgesBox,Measure,MeasureMode,ModelExplode,Roaming,MiniMap } from "@astral3d/engine";
import { t } from "@/language";
const operationStore = usePreviewOperationStoreWithOut();
const { enter:enterFullscreen,exit:exitFullscreen } = useFullscreen();
let roamPdFn, pointerlockFn;
export class MenuOperation {
// 初始化控制器状态
static InitControlsState: string = "{}";
// 开始漫游前的相机位置
static lastRoadCameraPos = new THREE.Vector3();
// 开始漫游前的相机目标位置
static lastRoadCameraTarget = new THREE.Vector3();
// 模型爆炸专用图层
static explodeLayer = 10;
// 当前爆炸的模型
static explodeModel:THREE.Object3D | null = null;
// 剖切盒子
static _clippedEdgesBox:ClippedEdgesBox | null = null;
// 测量类
static _measure:Measure | null = null;
// 爆炸类
static _explode:ModelExplode | null = null;
// 漫游类
static _roaming:Roaming | null = null;
// 小地图
static _miniMap:MiniMap | null = null;
static Init(key: string) {
if (MenuOperation[key]) {
MenuOperation[key]();
} else {
window.$message?.warning("相关模块正在开发中...")
}
}
static get ClippedEdgesBox(){
if(!MenuOperation._clippedEdgesBox){
MenuOperation._clippedEdgesBox = new ClippedEdgesBox(window.viewer);
}
return MenuOperation._clippedEdgesBox;
}
static get Measure():Measure{
if(!MenuOperation._measure){
MenuOperation._measure = new Measure(window.viewer,MeasureMode.Distance);
MenuOperation._measure.addEventListener("complete",() => {
if(!MenuOperation._measure) return;
// 激活清除测量按钮
if (!MenuOperation._measure.isClose && MenuOperation._measure.measureGroup.children.length > 0) {
(<{ [key: string]: IPreviewOperation }>operationStore.menuList.measure.children).clearMeasure.disabled = false;
}
})
}
return MenuOperation._measure;
}
static get Explode():ModelExplode{
if(!MenuOperation._explode){
MenuOperation._explode = new ModelExplode();
}
return MenuOperation._explode;
}
static get Roaming():Roaming{
if(!MenuOperation._roaming){
MenuOperation._roaming = new Roaming(window.viewer);
}
return MenuOperation._roaming;
}
static get MiniMap():MiniMap{
if(!MenuOperation._miniMap){
MenuOperation._miniMap = new MiniMap(window.viewer,{
mapSize: 100,
mapRenderSize: 350,
followTarget: window.viewer.camera,
isShow: false,
});
}
return MenuOperation._miniMap;
}
/**
* 还原视角
*/
static toHome() {
if (MenuOperation.InitControlsState === "{}") {
window.$message?.warning("缺失初始视角信息")
return;
}
window.viewer.modules.controls.fromJSON(MenuOperation.InitControlsState, true);
}
/**
* 自动旋转
*/
static autoRotate() {
operationStore.menuList.autoRotate.active = !operationStore.menuList.autoRotate.active;
}
/**
* 剖切
*/
static cutting() {
operationStore.menuList.cutting.active = !operationStore.menuList.cutting.active;
if (operationStore.menuList.cutting.active) {
MenuOperation.ClippedEdgesBox.open();
} else {
MenuOperation.ClippedEdgesBox.close();
}
}
// 测距
static distance() {
// 上一个测量也许未完成
if (!MenuOperation.Measure.isCompleted) {
MenuOperation.Measure.complete();
}
window.$message?.info(t("prompt['Left click to confirm the drawing point, and right click to complete the drawing.']"),{
duration: 1500
})
MenuOperation.Measure.mode = MeasureMode.Distance;
MenuOperation.Measure.open();
operationStore.menuList.measure.active = true;
}
// 测角度
static angle() {
if (!MenuOperation.Measure.isCompleted) {
MenuOperation.Measure.complete();
}
window.$message?.info(t("prompt['Left click to confirm the drawing point, and right click to complete the drawing.']"),{
duration: 1500
})
MenuOperation.Measure.mode = MeasureMode.Angle;
MenuOperation.Measure.open();
operationStore.menuList.measure.active = true;
}
// 测面积
static area() {
if (!MenuOperation.Measure.isCompleted) {
MenuOperation.Measure.complete();
}
window.$message?.info(t("prompt['Left click to confirm the drawing point, and right click to complete the drawing.']"),{
duration: 1500
})
MenuOperation.Measure.mode = MeasureMode.Area;
MenuOperation.Measure.open();
operationStore.menuList.measure.active = true;
}
// 清除测量结果
static clearMeasure() {
(<{ [key: string]: IPreviewOperation }>operationStore.menuList.measure.children).clearMeasure.disabled = true;
MenuOperation.Measure.clear();
operationStore.menuList.measure.active = false;
}
// 爆炸
static explode(){
if(!App.selected && !MenuOperation.explodeModel){
window.$message?.warning(t("prompt['No object selected.']"));
return;
}
operationStore.menuList.explode.active = !operationStore.menuList.explode.active;
if (operationStore.menuList.explode.active){
if(!App.selected) return;
App.selected.traverse(obj => obj.layers.set(MenuOperation.explodeLayer));
window.viewer.camera.layers.set(MenuOperation.explodeLayer);
MenuOperation.Explode.explodeModel(App.selected,operationStore.explodeScalar);
MenuOperation.explodeModel = App.selected;
}else{
if(!MenuOperation.explodeModel) return;
MenuOperation.explodeModel.traverse(obj => obj.layers.set(0));
window.viewer.camera.layers.set(0);
MenuOperation.Explode.restore();
MenuOperation.explodeModel = null;
}
}
// 漫游
static roaming() {
if (!MenuOperation.Roaming) return;
operationStore.menuList.roaming.active = !operationStore.menuList.roaming.active;
if (operationStore.menuList.roaming.active) {
MenuOperation.enterRoaming();
} else {
MenuOperation.leaveRoaming();
}
}
// 选点进入漫游
static enterRoaming() {
window.$message?.info(t("preview.Please select initial position"));
const canvas = window.viewer.renderer.domElement;
const handlePointerDown = (e: MouseEvent) => {
const raycaster = new THREE.Raycaster();
const x = e.offsetX;
const y = e.offsetY;
const mouse = new THREE.Vector2();
mouse.x = (x / canvas.offsetWidth) * 2 - 1;
mouse.y = -(y / canvas.offsetHeight) * 2 + 1;
raycaster.setFromCamera(mouse, window.viewer.camera);
raycaster.firstHitOnly = true;
const intersectObjects:THREE.Object3D[] = [MenuOperation.Roaming.group];
if(window.viewer.modules.tilesManage.mergeMesh){
intersectObjects.push(window.viewer.modules.tilesManage.mergeMesh);
}
let intersects = raycaster.intersectObjects(intersectObjects, true) || [];
if (intersects && intersects.length > 0) {
const intersect = intersects[0];
// 锁定鼠标指针
window.viewer.modules.controls.lockPointer();
canvas.removeEventListener("pointerdown", roamPdFn);
roamPdFn = undefined;
window.viewer.modules.controls.getTarget(MenuOperation.lastRoadCameraTarget);
window.viewer.modules.controls.getPosition(MenuOperation.lastRoadCameraPos);
const point = new THREE.Vector3(intersect.point.x, intersect.point.y + 2, intersect.point.z);
MenuOperation.Roaming.playerInitPos.copy(point);
MenuOperation.Roaming.startRoaming();
// 第三人称
window.viewer.modules.controls.maxPolarAngle = Math.PI / 2;
window.viewer.modules.controls.minDistance = 0.8;
window.viewer.modules.controls.maxDistance = 0.8;
window.viewer.modules.controls.distance = 0.8;
Hooks.useDispatchSignal("sceneGraphChanged");
}
return null;
}
pointerlockFn = () => {
if (document.pointerLockElement) {
// console.log("指针被锁定到:", document.pointerLockElement);
} else {
// console.log("指针锁定状态现已解锁");
MenuOperation.roaming();
}
}
// 监听鼠标锁定取消
document.addEventListener("pointerlockchange", pointerlockFn);
// 监听选取初始位置
roamPdFn = handlePointerDown.bind(this);
canvas.addEventListener("pointerdown", roamPdFn);
}
// 退出漫游
static leaveRoaming() {
MenuOperation.Roaming.exitRoaming(MenuOperation.lastRoadCameraPos,MenuOperation.lastRoadCameraTarget);
if (roamPdFn) {
window.viewer.renderer.domElement.removeEventListener("pointerdown", roamPdFn);
roamPdFn = null;
}
if (pointerlockFn) {
document.removeEventListener("pointerlockchange", pointerlockFn);
pointerlockFn = null;
}
}
// 小地图
static miniMap() {
if (MenuOperation.MiniMap.isShow) {
operationStore.menuList.miniMap.active = false;
MenuOperation.MiniMap.close();
} else {
operationStore.menuList.miniMap.active = true;
MenuOperation.MiniMap.open();
}
}
// 设置
static settings(){
operationStore.menuList.settings.active = !operationStore.menuList.settings.active;
}
static fullscreen() {
operationStore.menuList.fullscreen.show = false;
operationStore.menuList.exitFullscreen.show = true;
enterFullscreen();
}
static exitFullscreen() {
operationStore.menuList.fullscreen.show = true;
operationStore.menuList.exitFullscreen.show = false;
exitFullscreen();
}
}
+116
View File
@@ -0,0 +1,116 @@
import type { AxiosError, AxiosResponse } from 'axios';
import {
DEFAULT_REQUEST_ERROR_CODE,
DEFAULT_REQUEST_ERROR_MSG,
ERROR_STATUS,
NETWORK_ERROR_CODE,
NETWORK_ERROR_MSG,
REQUEST_TIMEOUT_CODE,
REQUEST_TIMEOUT_MSG
} from '@/config/service';
import { showErrorMsg } from './msg';
import {Service} from "../../../types/network";
type ErrorStatus = keyof typeof ERROR_STATUS;
/**
* 策略模式
* @param actions 每一种可能执行的操作
*/
export function exeStrategyActions(actions: Common.StrategyAction[]) {
actions.some(item => {
const [flag, action] = item;
if (flag) {
action();
}
return flag;
});
}
/**
* 处理axios请求失败的错误
* @param axiosError - 错误
*/
export function handleAxiosError(axiosError: AxiosError) {
const error: Service.RequestError = {
type: 'axios',
code: DEFAULT_REQUEST_ERROR_CODE,
msg: DEFAULT_REQUEST_ERROR_MSG
};
const actions: Common.StrategyAction[] = [
[
// 网路错误
!window.navigator.onLine || axiosError.message === 'Network Error',
() => {
Object.assign(error, { code: NETWORK_ERROR_CODE, msg: NETWORK_ERROR_MSG });
}
],
[
// 超时错误
axiosError.code === REQUEST_TIMEOUT_CODE && axiosError.message.includes('timeout'),
() => {
Object.assign(error, { code: REQUEST_TIMEOUT_CODE, msg: REQUEST_TIMEOUT_MSG });
}
],
[
// 请求不成功的错误
Boolean(axiosError.response),
() => {
const errorCode: ErrorStatus = (axiosError.response?.status as ErrorStatus) || 'DEFAULT';
// @ts-ignore
const msg = axiosError.response?.data?.message || ERROR_STATUS[errorCode] || DEFAULT_REQUEST_ERROR_MSG;
Object.assign(error, { code: errorCode, msg });
}
]
];
exeStrategyActions(actions);
showErrorMsg(error);
return error;
}
/**
* 处理请求成功后的错误
* @param response - 请求的响应
*/
export function handleResponseError(response: AxiosResponse) {
const error: Service.RequestError = {
type: 'axios',
code: DEFAULT_REQUEST_ERROR_CODE,
msg: DEFAULT_REQUEST_ERROR_MSG
};
if (!window.navigator.onLine) {
// 网路错误
Object.assign(error, { code: NETWORK_ERROR_CODE, msg: NETWORK_ERROR_MSG });
} else {
// 请求成功的状态码非200的错误
const errorCode: ErrorStatus = response.status as ErrorStatus;
const msg = response.data.message || ERROR_STATUS[errorCode] || DEFAULT_REQUEST_ERROR_MSG;
Object.assign(error, { type: 'http', code: errorCode, msg });
}
showErrorMsg(error);
return error;
}
/**
* 处理后端返回的错误(业务错误)
* @param backendResult - 后端接口的响应数据
*/
export function handleBackendError(backendResult: Record<string, any>, config: Service.BackendResultConfig) {
const { codeKey, msgKey } = config;
const error: Service.RequestError = {
type: 'backend',
code: backendResult[codeKey],
msg: backendResult[msgKey]
};
showErrorMsg(error);
return error;
}
@@ -0,0 +1,74 @@
/**
* 创建一个handlerFetch
*
* @param limit 并发控制
* @param timeout 超时设
* @return function 返回一个函数
*/
export function fetchController (limit:number, timeout:number | boolean) {
limit = limit || 1;
timeout = timeout || false;
let count = 0, pool:any = [];
return function (url:string, options?:any) {
// 通过AbortController 控制 取消fetch 请求
let controller = new AbortController();
let signal = controller.signal;
// 判断是否需要超时
let isTimeout = options && options.timeout || timeout;
// 控制请求超时
let timeoutPromise = () => {
return new Promise((reject) => {
setTimeout(() => {
// resolve('请求超时');
reject('请求超时');
controller.abort();
}, options?.timeout || timeout)
})
}
// 返回fetch 本身
let taskPromise = () => new Promise((resolve, reject) => {
fetch(url, { signal, ...options }).then(res => {
resolve(res);
}).catch(err => {
reject(err)
})
});
// 通过Promise.race可以控制超时,并在访问结果中 去继续调用等待池中的请求
let task = () => (isTimeout ? Promise.race([timeoutPromise(), taskPromise()]) : taskPromise())
.then((res) => {
options.onSuccess && options.onSuccess(res)
next();
})
.catch((err) => {
options.onError && options.onError(err)
next();
});
// 定一个next 控制等待队列中的请求继续并发调用
let next = () => {
// 每执行一次next count - 1,然后比较当前的count 与 limit
// 如果小于limit 循环执行limit-count 次
count--;
if (count < limit && pool.length) {
let n = limit - count;
for (let i = 0; i < n; i++) {
let curTask:any = pool.shift();
curTask();
++count;
}
}
};
// 比较count与limit 大于等于limit的推入等待队列 小于limit的 count + 1,并执行fetch请求
if (count >= limit) {
pool.push(task);
} else {
++count;
task();
}
}
}
@@ -0,0 +1,47 @@
import {Service} from "~/network";
/** 统一失败和成功的请求结果的数据类型 */
export async function handleServiceResult(error: Service.RequestError | null, data: any, other?: any) {
if (error) {
const fail: Service.FailedResult = {
error,
data: null
};
return fail;
}
const success: any = {
error: null,
data,
other
};
return success;
}
/** 请求结果的适配器:用于接收适配器函数和请求结果 */
export function adapter<T extends Service.ServiceAdapter>(
adapterFun: T,
...args: Service.MultiRequestResult<any>
): Service.RequestResult<ReturnType<T>> {
let result: Service.RequestResult | undefined;
const hasError = args.some((item:any) => {
const flag = Boolean(item.error);
if (flag) {
result = {
error: item.error,
data: null
};
}
return flag;
});
if (!hasError) {
const adapterFunArgs = args.map(item => item.data);
result = {
error: null,
data: adapterFun(...adapterFunArgs)
};
}
return result!;
}
@@ -0,0 +1,2 @@
export * from './error';
export * from './handler';
+29
View File
@@ -0,0 +1,29 @@
import { ERROR_MSG_DURATION, NO_ERROR_MSG_CODE } from '@/config/service';
/** 错误消息栈,防止同一错误同时出现 */
const errorMsgStack = new Map<string | number, string>([]);
function addErrorMsg(error: Service.RequestError) {
errorMsgStack.set(error.code, error.msg);
}
function removeErrorMsg(error: Service.RequestError) {
errorMsgStack.delete(error.code);
}
function hasErrorMsg(error: Service.RequestError) {
return errorMsgStack.has(error.code);
}
/**
* 显示错误信息
* @param error
*/
export function showErrorMsg(error: Service.RequestError) {
if (!error.msg || NO_ERROR_MSG_CODE.includes(error.code) || hasErrorMsg(error)) return;
addErrorMsg(error);
window.console.warn(error.code, error.msg);
window.$message?.error(error.msg, { duration: ERROR_MSG_DURATION });
setTimeout(() => {
removeErrorMsg(error);
}, ERROR_MSG_DURATION);
}
@@ -0,0 +1,30 @@
import {Utils} from '@astral3d/engine';
/**
* cesium 相关页面使用的
*/
const cesiumSignals = [
// cesium/viewPort.ts 停止渲染循环
"cesium_stopLoop",
// cesium融合场景下的threejs scene点击事件
"cesium_clickThreeScene",
//销毁viewPort
"cesium_destroy",
// 飞行定位
"cesium_flyTo",
]
/**
* 其他不便分类的
*/
const otherSignal = [
// 编辑脚本
"editScript",
// 场景树变化
"sceneTreeChange",
]
Utils.SignalsRegisterFn([
...cesiumSignals,
...otherSignal
])
@@ -0,0 +1,63 @@
import type {Storage} from "@astral3d/engine";
export default class Config {
private static storage: Storage;
public static config: { [s: string]: any };
static initialize(storage: Storage) {
Config.storage = storage;
Config.config = {
// 场景界面与扩展面板分割大小
sceneSplitSize: 0.8,
// 图纸绘制区
cad:{
bgColor:0x000000
}
};
for (let key of Object.keys(Config.config)) {
Config.storage.getConfigItem(key).then(_value => {
if (_value === null) {
Config.storage.setConfigItem(key, Config.config[key]);
} else {
let newVal = _value;
// 有可能会在代码开发过程中增加新的配置项
if (Config.config[key] && typeof Config.config[key] === "object") {
newVal = Object.assign({}, Config.config[key], _value);
}
Config.config[key] = newVal;
if (newVal !== _value) {
Config.storage.setConfigItem(key, newVal);
}
}
}).catch(() => {
Config.storage.setConfigItem(key, Config.config[key]);
});
}
}
static getKey(key: string) {
return Config.config[key];
}
static setKey(...args: any[]) {
// key, value, key, value ...
for (let i = 0, l = args.length; i < l; i += 2) {
const key = args[i];
const value = args[i + 1];
Config.config[key] = value;
Config.storage.setConfigItem(key, value);
}
}
static clear() {
for (let key of Object.keys(Config.config)) {
Config.storage.removeConfigItem(key);
}
}
}
// 初始化配置
export const initializeConfig = (storage) => Config.initialize(storage);