Refactor: modularize toolbar buttons, integrate config, and fix layout positioning
This commit is contained in:
15
demo.html
15
demo.html
@@ -1,24 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>BIM Engine SDK Demo</title>
|
<title>BIM Engine SDK Demo</title>
|
||||||
<script src="./dist/bim-engine-sdk.umd.js"></script>
|
<script src="./dist/bim-engine-sdk.umd.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
<div id="app" style="width: 100%; height: 300px; border: 1px dashed #ccc;"></div>
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app" style="width: 1000px; height: 500px; border: 1px dashed #ccc;"></div>
|
||||||
<script>
|
<script>
|
||||||
// 等待 SDK 加载
|
// 等待 SDK 加载
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
if (window.BimEngineSDK) {
|
if (window.LyzBimEngineSDK) {
|
||||||
// 这里的 BimEngineSDK 是 UMD 的全局变量名(由 vite.config.ts 中的 build.lib.name 定义)
|
const Engine = window.LyzBimEngineSDK.BimEngine;
|
||||||
// 如果是 export default,可能直接挂载在 window.BimEngineSDK,或者需要 .default,取决于打包格式。
|
|
||||||
// 但我们在 index.ts 中使用了具名导出 export { BimEngine },所以应该是 window.BimEngineSDK.BimEngine
|
|
||||||
|
|
||||||
const Engine = window.BimEngineSDK.BimEngine;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const instance = new Engine(document.getElementById('app'));
|
const instance = new Engine(document.getElementById('app'));
|
||||||
console.log('Engine initialized:', instance);
|
console.log('Engine initialized:', instance);
|
||||||
@@ -31,4 +27,5 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
214
dist/bim-engine-sdk.es.js
vendored
214
dist/bim-engine-sdk.es.js
vendored
@@ -1,20 +1,210 @@
|
|||||||
class e {
|
(function(){"use strict";try{if(typeof document<"u"){var o=document.createElement("style");o.appendChild(document.createTextNode(".bim-engine-wrapper{position:relative;width:100%;height:100%;font-family:sans-serif;color:#333;padding:20px;background-color:#e16969;border-radius:8px;border:1px solid #e0e0e0;box-sizing:border-box}.bim-engine-opt-btn-container{position:absolute;bottom:20px;left:50%;transform:translate(-50%);z-index:100}.toolbar-container{display:flex;align-items:center;max-width:100%;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.toolbar-container::-webkit-scrollbar{display:none}.opt-btn-group{overflow:hidden;display:flex;align-items:center;flex-shrink:0;background-color:#111111e0;border-radius:4px;padding:4px 8px}.has-divider{margin-right:16px}.opt-btn-wrapper{position:relative}.opt-btn{display:flex;flex-direction:column;align-items:center;justify-content:center;width:50px;min-height:50px;padding:4px;cursor:pointer;color:#ccc;transition:all .2s;border-bottom:2px solid transparent}.opt-btn:hover{background-color:#444;color:#fff}.opt-btn.active{background-color:#ffffff26;color:#fff;border-bottom:2px solid #fff}.opt-btn.disabled{opacity:.5;cursor:not-allowed}.opt-btn-icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.opt-btn-icon svg{width:100%;height:100%}.opt-btn-label{font-size:10px;margin-top:2px}.opt-btn-arrow{font-size:8px;position:absolute;top:2px;right:2px;opacity:.6;transition:transform .2s ease}.opt-btn-arrow.rotated{transform:rotate(180deg)}.opt-btn.no-label .opt-btn-arrow{top:2px;right:2px}.opt-btn-dropdown{position:fixed;transform:translate(-50%,-100%);background-color:#111111e0;border-radius:4px;overflow:hidden;box-shadow:0 4px 12px #0000004d;min-width:50px;z-index:9999;display:flex;flex-direction:column}.opt-btn-dropdown-item{display:flex;flex-direction:column;align-items:center;justify-content:center;color:#b3b4b4;cursor:pointer;transition:background .2s;white-space:nowrap;min-width:50px;min-height:50px;padding:4px}.opt-btn-dropdown-item:last-child{border-bottom:none}.opt-btn-dropdown-item:hover{background-color:#444;color:#fff}.opt-btn-dropdown-item .opt-btn-icon.small{width:30px;height:30px;margin-right:0;margin-bottom:4px}.opt-btn-dropdown-item span{font-size:10px}.opt-btn.no-label .opt-btn-icon{width:32px;height:32px}")),document.head.appendChild(o)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
|
||||||
|
class a {
|
||||||
container;
|
container;
|
||||||
constructor(n) {
|
options;
|
||||||
const i = typeof n == "string" ? document.getElementById(n) : n;
|
// 改用 Array 存储 Group,方便控制顺序
|
||||||
if (!i) throw new Error("Container not found");
|
groups = [];
|
||||||
this.container = i, this.init();
|
activeBtnIds = /* @__PURE__ */ new Set();
|
||||||
|
btnRefs = /* @__PURE__ */ new Map();
|
||||||
|
dropdownElement = null;
|
||||||
|
hoverTimeout = null;
|
||||||
|
DEFAULT_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
|
||||||
|
constructor(t) {
|
||||||
|
const e = typeof t.container == "string" ? document.getElementById(t.container) : t.container;
|
||||||
|
if (!e) throw new Error("Container not found");
|
||||||
|
this.container = e, this.options = {
|
||||||
|
showLabel: !0,
|
||||||
|
visibility: {},
|
||||||
|
...t
|
||||||
|
}, this.initContainer();
|
||||||
|
}
|
||||||
|
initContainer() {
|
||||||
|
this.container.innerHTML = "";
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 添加按钮组
|
||||||
|
* @param groupId 组ID
|
||||||
|
* @param beforeGroupId 在哪个组之前插入(可选),不传则插入到最后
|
||||||
|
*/
|
||||||
|
addGroup(t, e) {
|
||||||
|
if (this.groups.some((n) => n.id === t)) {
|
||||||
|
console.warn("Group " + t + " already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const i = { id: t, buttons: [] };
|
||||||
|
if (e) {
|
||||||
|
const n = this.groups.findIndex((s) => s.id === e);
|
||||||
|
n !== -1 ? this.groups.splice(n, 0, i) : (console.warn(`Target group ${e} not found, appending ${t} to end.`), this.groups.push(i));
|
||||||
|
} else
|
||||||
|
this.groups.push(i);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 添加按钮
|
||||||
|
* @param config 按钮配置(必须包含 groupId,可选包含 parentId)
|
||||||
|
*/
|
||||||
|
addButton(t) {
|
||||||
|
const { groupId: e, parentId: i } = t;
|
||||||
|
if (!e)
|
||||||
|
throw new Error(`Button ${t.id} config must contain 'groupId'`);
|
||||||
|
const n = this.groups.find((o) => o.id === e);
|
||||||
|
if (!n)
|
||||||
|
throw new Error(`Group ${e} not found. Please call addGroup first.`);
|
||||||
|
const s = {
|
||||||
|
...t,
|
||||||
|
children: t.children || []
|
||||||
|
};
|
||||||
|
if (i) {
|
||||||
|
const o = this.findButton(n.buttons, i);
|
||||||
|
if (!o)
|
||||||
|
throw new Error(`Parent button ${i} not found in group ${e}`);
|
||||||
|
o.children || (o.children = []), o.children.push(s);
|
||||||
|
} else
|
||||||
|
n.buttons.push(s);
|
||||||
|
}
|
||||||
|
findButton(t, e) {
|
||||||
|
for (const i of t) {
|
||||||
|
if (i.id === e) return i;
|
||||||
|
if (i.children) {
|
||||||
|
const n = this.findButton(i.children, e);
|
||||||
|
if (n) return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async init() {
|
||||||
|
const { homeButton: t } = await import("./index-CAJWny5G.mjs"), { locationButton: e } = await import("./index-C12x1apF.mjs"), { walkMenuButton: i } = await import("./index-Wpi9Br9A.mjs"), { walkPersonButton: n } = await import("./index-BXbORK0j.mjs"), { walkBirdButton: s } = await import("./index-Djlk5GIH.mjs"), { settingButton: o } = await import("./index-DsRG5l_h.mjs"), { infoButton: r } = await import("./index-DvZ5eiUH.mjs");
|
||||||
|
this.addGroup("group-1"), this.addButton(t), this.addButton(i), this.addButton(n), this.addButton(s), this.addButton(e), this.addGroup("group-2"), this.addButton(o), this.addButton(r), this.render();
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
this.container.innerHTML = "", this.btnRefs.clear();
|
||||||
|
const t = document.createElement("div");
|
||||||
|
t.className = "toolbar-container", this.groups.forEach((e, i) => {
|
||||||
|
const n = this.renderGroup(e, i, this.groups.length);
|
||||||
|
t.appendChild(n);
|
||||||
|
}), this.container.appendChild(t);
|
||||||
|
}
|
||||||
|
renderGroup(t, e, i) {
|
||||||
|
const n = document.createElement("div");
|
||||||
|
return n.className = "opt-btn-group", e < i - 1 && n.classList.add("has-divider"), t.buttons.forEach((s) => {
|
||||||
|
if (this.isVisible(s.id)) {
|
||||||
|
const o = this.renderButton(s);
|
||||||
|
n.appendChild(o);
|
||||||
|
}
|
||||||
|
}), n;
|
||||||
|
}
|
||||||
|
renderButton(t) {
|
||||||
|
const e = document.createElement("div");
|
||||||
|
e.className = "opt-btn-wrapper";
|
||||||
|
const i = document.createElement("div");
|
||||||
|
i.className = "opt-btn", this.activeBtnIds.has(t.id) && i.classList.add("active"), t.disabled && i.classList.add("disabled"), this.options.showLabel || (i.classList.add("no-label"), t.label && (i.title = t.label));
|
||||||
|
const n = document.createElement("div");
|
||||||
|
if (n.className = "opt-btn-icon", n.innerHTML = this.getIcon(t.icon), i.appendChild(n), this.options.showLabel && t.label) {
|
||||||
|
const s = document.createElement("span");
|
||||||
|
s.className = "opt-btn-label", s.textContent = t.label, i.appendChild(s);
|
||||||
|
}
|
||||||
|
if (t.children && t.children.length > 0) {
|
||||||
|
const s = document.createElement("span");
|
||||||
|
s.className = "opt-btn-arrow", s.textContent = "▼", i.appendChild(s);
|
||||||
|
}
|
||||||
|
return i.addEventListener("click", () => this.handleClick(t)), i.addEventListener("mouseenter", () => this.handleMouseEnter(t, i)), i.addEventListener("mouseleave", () => this.handleMouseLeave()), this.btnRefs.set(t.id, i), e.appendChild(i), e;
|
||||||
|
}
|
||||||
|
handleClick(t) {
|
||||||
|
t.disabled || (!t.children || t.children.length === 0) && (t.keepActive && (this.activeBtnIds.has(t.id) ? this.activeBtnIds.delete(t.id) : this.activeBtnIds.add(t.id), this.updateButtonState(t.id)), this.closeDropdown(), t.onClick && t.onClick(t));
|
||||||
|
}
|
||||||
|
handleSubClick(t) {
|
||||||
|
t.keepActive && (this.activeBtnIds.has(t.id) ? this.activeBtnIds.delete(t.id) : this.activeBtnIds.add(t.id), this.updateButtonState(t.id)), this.closeDropdown(), t.onClick && t.onClick(t);
|
||||||
|
}
|
||||||
|
handleMouseEnter(t, e) {
|
||||||
|
if (this.hoverTimeout && (clearTimeout(this.hoverTimeout), this.hoverTimeout = null), t.children && t.children.length > 0) {
|
||||||
|
this.showDropdown(t, e);
|
||||||
|
const i = e.querySelector(".opt-btn-arrow");
|
||||||
|
i && i.classList.add("rotated");
|
||||||
|
} else
|
||||||
|
this.closeDropdown();
|
||||||
|
}
|
||||||
|
handleMouseLeave() {
|
||||||
|
this.hoverTimeout = window.setTimeout(() => {
|
||||||
|
this.closeDropdown();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
showDropdown(t, e) {
|
||||||
|
if (this.closeDropdown(), !t.children) return;
|
||||||
|
const i = document.createElement("div");
|
||||||
|
i.className = "opt-btn-dropdown";
|
||||||
|
const n = e.getBoundingClientRect(), s = n.left + n.width / 2;
|
||||||
|
i.style.top = n.top - 8 + "px", i.style.left = s + "px", t.children.forEach((o) => {
|
||||||
|
if (this.isVisible(o.id)) {
|
||||||
|
const r = this.renderDropdownItem(o);
|
||||||
|
i.appendChild(r);
|
||||||
|
}
|
||||||
|
}), i.addEventListener("mouseenter", () => {
|
||||||
|
this.hoverTimeout && (clearTimeout(this.hoverTimeout), this.hoverTimeout = null);
|
||||||
|
}), i.addEventListener("mouseleave", () => this.handleMouseLeave()), document.body.appendChild(i), this.dropdownElement = i;
|
||||||
|
}
|
||||||
|
renderDropdownItem(t) {
|
||||||
|
const e = document.createElement("div");
|
||||||
|
e.className = "opt-btn-dropdown-item";
|
||||||
|
const i = document.createElement("div");
|
||||||
|
if (i.className = "opt-btn-icon small", i.innerHTML = this.getIcon(t.icon), e.appendChild(i), this.options.showLabel) {
|
||||||
|
const n = document.createElement("span");
|
||||||
|
n.textContent = t.label, e.appendChild(n);
|
||||||
|
}
|
||||||
|
return e.addEventListener("click", (n) => {
|
||||||
|
n.stopPropagation(), this.handleSubClick(t);
|
||||||
|
}), e;
|
||||||
|
}
|
||||||
|
closeDropdown() {
|
||||||
|
this.dropdownElement && (this.dropdownElement.remove(), this.dropdownElement = null), this.btnRefs.forEach((t) => {
|
||||||
|
const e = t.querySelector(".opt-btn-arrow");
|
||||||
|
e && e.classList.remove("rotated");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateButtonState(t) {
|
||||||
|
const e = this.btnRefs.get(t);
|
||||||
|
e && (this.activeBtnIds.has(t) ? e.classList.add("active") : e.classList.remove("active"));
|
||||||
|
}
|
||||||
|
getIcon(t) {
|
||||||
|
return t || this.DEFAULT_ICON;
|
||||||
|
}
|
||||||
|
isVisible(t) {
|
||||||
|
return this.options.visibility?.[t] !== !1;
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
this.closeDropdown(), this.hoverTimeout && clearTimeout(this.hoverTimeout), this.container.innerHTML = "", this.btnRefs.clear(), this.activeBtnIds.clear(), this.groups = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class c {
|
||||||
|
container;
|
||||||
|
optBtnGroups = null;
|
||||||
|
constructor(t) {
|
||||||
|
const e = typeof t == "string" ? document.getElementById(t) : t;
|
||||||
|
if (!e) throw new Error("Container not found");
|
||||||
|
this.container = e, this.init();
|
||||||
}
|
}
|
||||||
init() {
|
init() {
|
||||||
this.container.innerHTML = `
|
this.container.innerHTML = "";
|
||||||
<div style="font-family: sans-serif; color: #333;">
|
const t = document.createElement("div");
|
||||||
<h1>BimEngine</h1>
|
t.className = "bim-engine-wrapper";
|
||||||
<p>这是一个纯 TypeScript 组件入口。</p>
|
const e = document.createElement("h1");
|
||||||
</div>
|
e.textContent = "BimEngine", e.className = "bim-engine-title";
|
||||||
`;
|
const i = document.createElement("p");
|
||||||
|
i.textContent = "这是一个使用BIM-ENGINE。", i.className = "bim-engine-desc";
|
||||||
|
const n = document.createElement("div");
|
||||||
|
n.id = "opt-btn-groups", n.className = "bim-engine-opt-btn-container", t.appendChild(e), t.appendChild(i), t.appendChild(n), this.container.appendChild(t), this.initOptBtnGroups(n);
|
||||||
|
}
|
||||||
|
initOptBtnGroups(t) {
|
||||||
|
this.optBtnGroups = new a({
|
||||||
|
container: t,
|
||||||
|
showLabel: !0
|
||||||
|
}), this.optBtnGroups.init().catch((e) => {
|
||||||
|
console.error("Failed to initialize OptBtnGroups:", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
this.optBtnGroups && (this.optBtnGroups.destroy(), this.optBtnGroups = null), this.container.innerHTML = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export {
|
export {
|
||||||
e as BimEngine
|
c as BimEngine,
|
||||||
|
a as OptBtnGroups
|
||||||
};
|
};
|
||||||
//# sourceMappingURL=bim-engine-sdk.es.js.map
|
//# sourceMappingURL=bim-engine-sdk.es.js.map
|
||||||
|
|||||||
2
dist/bim-engine-sdk.es.js.map
vendored
2
dist/bim-engine-sdk.es.js.map
vendored
File diff suppressed because one or more lines are too long
8
dist/bim-engine-sdk.umd.js
vendored
8
dist/bim-engine-sdk.umd.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bim-engine-sdk.umd.js.map
vendored
2
dist/bim-engine-sdk.umd.js.map
vendored
File diff suppressed because one or more lines are too long
94
dist/index.d.ts
vendored
94
dist/index.d.ts
vendored
@@ -1,7 +1,101 @@
|
|||||||
export declare class BimEngine {
|
export declare class BimEngine {
|
||||||
private container;
|
private container;
|
||||||
|
private optBtnGroups;
|
||||||
constructor(container: HTMLElement | string);
|
constructor(container: HTMLElement | string);
|
||||||
private init;
|
private init;
|
||||||
|
private initOptBtnGroups;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮配置接口(用于外部定义按钮)
|
||||||
|
*/
|
||||||
|
declare interface ButtonConfig {
|
||||||
|
id: string;
|
||||||
|
type: ButtonType;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
keepActive?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick?: (button: OptButton) => void;
|
||||||
|
children?: ButtonConfig[];
|
||||||
|
groupId?: string;
|
||||||
|
parentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮组接口
|
||||||
|
*/
|
||||||
|
export declare interface ButtonGroup {
|
||||||
|
id: string;
|
||||||
|
buttons: OptButton[];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type ButtonType = 'button' | 'menu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击事件载荷
|
||||||
|
*/
|
||||||
|
export declare interface ClickPayload {
|
||||||
|
button: OptButton;
|
||||||
|
action: 'activate' | 'deactivate' | 'trigger';
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class OptBtnGroups {
|
||||||
|
private container;
|
||||||
|
private options;
|
||||||
|
private groups;
|
||||||
|
private activeBtnIds;
|
||||||
|
private btnRefs;
|
||||||
|
private dropdownElement;
|
||||||
|
private hoverTimeout;
|
||||||
|
private readonly DEFAULT_ICON;
|
||||||
|
constructor(options: OptBtnGroupsOptions);
|
||||||
|
private initContainer;
|
||||||
|
/**
|
||||||
|
* 添加按钮组
|
||||||
|
* @param groupId 组ID
|
||||||
|
* @param beforeGroupId 在哪个组之前插入(可选),不传则插入到最后
|
||||||
|
*/
|
||||||
|
addGroup(groupId: string, beforeGroupId?: string): void;
|
||||||
|
/**
|
||||||
|
* 添加按钮
|
||||||
|
* @param config 按钮配置(必须包含 groupId,可选包含 parentId)
|
||||||
|
*/
|
||||||
|
addButton(config: ButtonConfig): void;
|
||||||
|
private findButton;
|
||||||
|
init(): Promise<void>;
|
||||||
|
render(): void;
|
||||||
|
private renderGroup;
|
||||||
|
private renderButton;
|
||||||
|
private handleClick;
|
||||||
|
private handleSubClick;
|
||||||
|
private handleMouseEnter;
|
||||||
|
private handleMouseLeave;
|
||||||
|
private showDropdown;
|
||||||
|
private renderDropdownItem;
|
||||||
|
private closeDropdown;
|
||||||
|
private updateButtonState;
|
||||||
|
private getIcon;
|
||||||
|
private isVisible;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OptBtnGroups 配置选项
|
||||||
|
*/
|
||||||
|
export declare interface OptBtnGroupsOptions {
|
||||||
|
container: HTMLElement | string;
|
||||||
|
showLabel?: boolean;
|
||||||
|
visibility?: Record<string, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作按钮接口(内部使用,继承配置)
|
||||||
|
*/
|
||||||
|
export declare interface OptButton extends ButtonConfig {
|
||||||
|
children?: OptButton[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export { }
|
export { }
|
||||||
|
|||||||
160
docs/OptBtnGroups.md
Normal file
160
docs/OptBtnGroups.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# OptBtnGroups 组件使用文档
|
||||||
|
|
||||||
|
## 📋 组件简介
|
||||||
|
|
||||||
|
`OptBtnGroups` 是一个纯 TypeScript 实现的操作按钮组组件,使用原生 DOM API 开发,无任何框架依赖。
|
||||||
|
|
||||||
|
## 🎯 核心特性
|
||||||
|
|
||||||
|
- ✅ 纯 TypeScript + 原生 DOM API
|
||||||
|
- ✅ 多组分隔,独立背景块
|
||||||
|
- ✅ 二级下拉菜单
|
||||||
|
- ✅ 多选激活状态
|
||||||
|
- ✅ 可控的标签显示
|
||||||
|
- ✅ 按钮显隐控制
|
||||||
|
- ✅ SVG 图标支持
|
||||||
|
- ✅ 响应式横向滚动
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 安装/引入
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- UMD 方式 -->
|
||||||
|
<script src="./dist/bim-engine-sdk.umd.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const { OptBtnGroups } = window.BimEngineSDK;
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ES Module 方式
|
||||||
|
import { OptBtnGroups } from 'bim-engine-sdk';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 基础使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const optBtnGroups = new OptBtnGroups({
|
||||||
|
container: 'btn-container', // 容器 ID 或 HTMLElement
|
||||||
|
showLabel: true, // 显示文字标签
|
||||||
|
onClick: (payload) => {
|
||||||
|
console.log('点击了按钮:', payload.button.label);
|
||||||
|
console.log('操作类型:', payload.action);
|
||||||
|
if (payload.isActive !== undefined) {
|
||||||
|
console.log('激活状态:', payload.isActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 控制显隐
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new OptBtnGroups({
|
||||||
|
container: 'btn-container',
|
||||||
|
visibility: {
|
||||||
|
'1-1': true, // 显示
|
||||||
|
'3-2-1': false // 隐藏
|
||||||
|
},
|
||||||
|
onClick: (payload) => console.log(payload)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 API 文档
|
||||||
|
|
||||||
|
### 构造函数参数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface OptBtnGroupsOptions {
|
||||||
|
container: HTMLElement | string; // 必需:容器
|
||||||
|
showLabel?: boolean; // 可选:显示标签(默认 true)
|
||||||
|
visibility?: Record<string, boolean>; // 可选:显隐控制
|
||||||
|
onClick?: (payload: ClickPayload) => void; // 可选:点击回调
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 点击事件载荷
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ClickPayload {
|
||||||
|
button: OptButton; // 被点击的按钮
|
||||||
|
action: 'trigger' | 'activate' | 'deactivate'; // 操作类型
|
||||||
|
isActive?: boolean; // 激活状态(仅 keepActive 按钮有值)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `trigger`: 触发型操作(无状态保持)
|
||||||
|
- `activate`: 激活按钮
|
||||||
|
- `deactivate`: 取消激活
|
||||||
|
|
||||||
|
### 方法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 销毁组件
|
||||||
|
optBtnGroups.destroy();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 二次开发
|
||||||
|
|
||||||
|
### 修改内置按钮
|
||||||
|
|
||||||
|
编辑 `src/OptBtnGroups.ts` 中的 `defaultGroups`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private readonly defaultGroups: ButtonGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'my-group',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 'home',
|
||||||
|
label: '首页',
|
||||||
|
icon: '<svg viewBox="0 0 24 24">...</svg>',
|
||||||
|
keepActive: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改样式
|
||||||
|
|
||||||
|
编辑 `src/OptBtnGroups.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.opt-btn {
|
||||||
|
width: 50px; /* 按钮宽度 */
|
||||||
|
min-height: 50px; /* 最小高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn.active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15); /* 激活背景 */
|
||||||
|
border-bottom: 2px solid #fff; /* 底部指示条 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 开发模式
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 示例
|
||||||
|
|
||||||
|
查看 `demo-optbtn.html` 获取完整示例。
|
||||||
|
|
||||||
|
## 🤝 贡献
|
||||||
|
|
||||||
|
修改组件时请遵循:
|
||||||
|
1. 保持中文注释
|
||||||
|
2. 使用原生 DOM API(无框架依赖)
|
||||||
|
3. TypeScript 严格模式
|
||||||
|
4. 测试多种使用场景
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export class BimEngine {
|
|
||||||
private container: HTMLElement;
|
|
||||||
|
|
||||||
constructor(container: HTMLElement | string) {
|
|
||||||
const el = typeof container === 'string' ? document.getElementById(container) : container;
|
|
||||||
if (!el) throw new Error('Container not found');
|
|
||||||
this.container = el;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
this.container.innerHTML = `
|
|
||||||
<div style="font-family: sans-serif; color: #333;">
|
|
||||||
<h1>BimEngine</h1>
|
|
||||||
<p>这是一个纯 TypeScript 组件入口。</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
src/bim-engine.css
Normal file
26
src/bim-engine.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.bim-engine-wrapper {
|
||||||
|
position: relative;
|
||||||
|
/* 添加相对定位作为参照物 */
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: sans-serif;
|
||||||
|
color: #333;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #e16969;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 确保 padding 不会撑大容器 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ... (中间代码不变) ... */
|
||||||
|
|
||||||
|
/* 操作按钮组容器 */
|
||||||
|
.bim-engine-opt-btn-container {
|
||||||
|
position: absolute;
|
||||||
|
/* 改为绝对定位 */
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
69
src/bim-engine.ts
Normal file
69
src/bim-engine.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import './bim-engine.css';
|
||||||
|
import { OptBtnGroups } from './toolbar';
|
||||||
|
|
||||||
|
export class BimEngine {
|
||||||
|
private container: HTMLElement;
|
||||||
|
private optBtnGroups: OptBtnGroups | null = null;
|
||||||
|
|
||||||
|
constructor(container: HTMLElement | string) {
|
||||||
|
const el = typeof container === 'string' ? document.getElementById(container) : container;
|
||||||
|
if (!el) throw new Error('Container not found');
|
||||||
|
this.container = el;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
// 1. 清空容器可能存在的旧内容
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
|
||||||
|
// 2. 创建外层容器 div
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'bim-engine-wrapper';
|
||||||
|
|
||||||
|
// 3. 创建标题 h1
|
||||||
|
const title = document.createElement('h1');
|
||||||
|
title.textContent = 'BimEngine';
|
||||||
|
title.className = 'bim-engine-title';
|
||||||
|
|
||||||
|
// 4. 创建段落 p
|
||||||
|
const desc = document.createElement('p');
|
||||||
|
desc.textContent = '这是一个使用BIM-ENGINE。';
|
||||||
|
desc.className = 'bim-engine-desc';
|
||||||
|
|
||||||
|
// 6. 创建操作按钮组容器
|
||||||
|
const btnGroupContainer = document.createElement('div');
|
||||||
|
btnGroupContainer.id = 'opt-btn-groups';
|
||||||
|
btnGroupContainer.className = 'bim-engine-opt-btn-container';
|
||||||
|
|
||||||
|
// 7. 组装元素
|
||||||
|
wrapper.appendChild(title);
|
||||||
|
wrapper.appendChild(desc);
|
||||||
|
wrapper.appendChild(btnGroupContainer); // 将按钮组放入 wrapper 中
|
||||||
|
|
||||||
|
// 8. 挂载到主容器
|
||||||
|
this.container.appendChild(wrapper);
|
||||||
|
|
||||||
|
// 9. 初始化操作按钮组
|
||||||
|
this.initOptBtnGroups(btnGroupContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initOptBtnGroups(container: HTMLElement) {
|
||||||
|
this.optBtnGroups = new OptBtnGroups({
|
||||||
|
container,
|
||||||
|
showLabel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化并加载默认按钮
|
||||||
|
this.optBtnGroups.init().catch(err => {
|
||||||
|
console.error('Failed to initialize OptBtnGroups:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
if (this.optBtnGroups) {
|
||||||
|
this.optBtnGroups.destroy();
|
||||||
|
this.optBtnGroups = null;
|
||||||
|
}
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
import { BimEngine } from './BimEngine';
|
import { BimEngine } from './bim-engine';
|
||||||
|
export { OptBtnGroups } from './toolbar';
|
||||||
|
export type { OptButton, ButtonGroup, OptBtnGroupsOptions, ClickPayload } from './toolbar/index.type';
|
||||||
export { BimEngine };
|
export { BimEngine };
|
||||||
|
|||||||
16
src/toolbar/buttons/home/index.ts
Normal file
16
src/toolbar/buttons/home/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ButtonConfig } from '../../index.type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页按钮配置
|
||||||
|
*/
|
||||||
|
export const homeButton: ButtonConfig = {
|
||||||
|
id: 'home',
|
||||||
|
groupId: 'group-1',
|
||||||
|
type: 'button',
|
||||||
|
label: '首页',
|
||||||
|
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 21V9l8-6l8 6v12h-6v-7h-4v7z"/></svg>',
|
||||||
|
keepActive: true,
|
||||||
|
onClick: (button) => {
|
||||||
|
console.log('首页按钮被点击:', button.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
16
src/toolbar/buttons/info/index.ts
Normal file
16
src/toolbar/buttons/info/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ButtonConfig } from '../../index.type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位按钮配置
|
||||||
|
*/
|
||||||
|
export const infoButton: ButtonConfig = {
|
||||||
|
id: 'info',
|
||||||
|
groupId: 'group-2',
|
||||||
|
type: 'button',
|
||||||
|
label: '信息',
|
||||||
|
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
|
||||||
|
keepActive: false,
|
||||||
|
onClick: (button) => {
|
||||||
|
console.log('信息按钮被点击:', button.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
16
src/toolbar/buttons/location/index.ts
Normal file
16
src/toolbar/buttons/location/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ButtonConfig } from '../../index.type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位按钮配置
|
||||||
|
*/
|
||||||
|
export const locationButton: ButtonConfig = {
|
||||||
|
id: 'location',
|
||||||
|
groupId: 'group-1',
|
||||||
|
type: 'button',
|
||||||
|
label: '定位',
|
||||||
|
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 13h2v-2.75h2V13h2V8.25l-3-2l-3 2zm3 9q-4.025-3.425-6.012-6.362T4 10.2q0-3.75 2.413-5.975T12 2t5.588 2.225T20 10.2q0 2.5-1.987 5.438T12 22"/></svg>',
|
||||||
|
keepActive: false,
|
||||||
|
onClick: (button) => {
|
||||||
|
console.log('定位按钮被点击:', button.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
16
src/toolbar/buttons/setting/index.ts
Normal file
16
src/toolbar/buttons/setting/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ButtonConfig } from '../../index.type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位按钮配置
|
||||||
|
*/
|
||||||
|
export const settingButton: ButtonConfig = {
|
||||||
|
id: 'setting',
|
||||||
|
groupId: 'group-2',
|
||||||
|
type: 'button',
|
||||||
|
label: '设置',
|
||||||
|
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="m9.25 22l-.4-3.2q-.325-.125-.612-.3t-.563-.375L4.7 19.375l-2.75-4.75l2.575-1.95Q4.5 12.5 4.5 12.338v-.675q0-.163.025-.338L1.95 9.375l2.75-4.75l2.975 1.25q.275-.2.575-.375t.6-.3l.4-3.2h5.5l.4 3.2q.325.125.613.3t.562.375l2.975-1.25l2.75 4.75l-2.575 1.95q.025.175.025.338v.674q0 .163-.05.338l2.575 1.95l-2.75 4.75l-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2zM11 20h1.975l.35-2.65q.775-.2 1.438-.587t1.212-.938l2.475 1.025l.975-1.7l-2.15-1.625q.125-.35.175-.737T17.5 12t-.05-.787t-.175-.738l2.15-1.625l-.975-1.7l-2.475 1.05q-.55-.575-1.212-.962t-1.438-.588L13 4h-1.975l-.35 2.65q-.775.2-1.437.588t-1.213.937L5.55 7.15l-.975 1.7l2.15 1.6q-.125.375-.175.75t-.05.8q0 .4.05.775t.175.75l-2.15 1.625l.975 1.7l2.475-1.05q.55.575 1.213.963t1.437.587zm1.05-4.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5M12 12"/></svg>',
|
||||||
|
keepActive: false,
|
||||||
|
onClick: (button) => {
|
||||||
|
console.log('设置按钮被点击:', button.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
13
src/toolbar/buttons/walk/walk-bird/index.ts
Normal file
13
src/toolbar/buttons/walk/walk-bird/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { ButtonConfig } from '../../../index.type';
|
||||||
|
|
||||||
|
export const walkBirdButton: ButtonConfig = {
|
||||||
|
id: 'walk-bird',
|
||||||
|
groupId: 'group-1',
|
||||||
|
parentId: 'walk',
|
||||||
|
type: 'button',
|
||||||
|
label: '鸟瞰漫游',
|
||||||
|
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
|
||||||
|
onClick: (button) => {
|
||||||
|
console.log('鸟瞰漫游被点击:', button.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
16
src/toolbar/buttons/walk/walk-menu/index.ts
Normal file
16
src/toolbar/buttons/walk/walk-menu/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ButtonConfig } from '../../../index.type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 漫游菜单按钮配置
|
||||||
|
*/
|
||||||
|
export const walkMenuButton: ButtonConfig = {
|
||||||
|
id: 'walk',
|
||||||
|
groupId: 'group-1',
|
||||||
|
type: 'menu',
|
||||||
|
label: '漫游',
|
||||||
|
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
|
||||||
|
keepActive: true,
|
||||||
|
onClick: (button) => {
|
||||||
|
console.log('漫游按钮被点击:', button.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
13
src/toolbar/buttons/walk/walk-person/index.ts
Normal file
13
src/toolbar/buttons/walk/walk-person/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { ButtonConfig } from '../../../index.type';
|
||||||
|
|
||||||
|
export const walkPersonButton: ButtonConfig = {
|
||||||
|
id: 'walk-person',
|
||||||
|
groupId: 'group-1',
|
||||||
|
parentId: 'walk',
|
||||||
|
type: 'button',
|
||||||
|
label: '人视漫游',
|
||||||
|
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
|
||||||
|
onClick: (button) => {
|
||||||
|
console.log('人视漫游被点击:', button.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
181
src/toolbar/index.css
Normal file
181
src/toolbar/index.css
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
/* 容器样式 */
|
||||||
|
.toolbar-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
/* 容器本身不需要背景 */
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox 隐藏滚动条 */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* IE 10+ 隐藏滚动条 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-container::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
/* Chrome/Safari 隐藏滚动条 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮组样式 */
|
||||||
|
.opt-btn-group {
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: rgba(17, 17, 17, 0.88);
|
||||||
|
/* 每个组独立的背景 */
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-divider {
|
||||||
|
margin-right: 16px;
|
||||||
|
/* 增加右边距来分隔组 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮包装器 */
|
||||||
|
.opt-btn-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.opt-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 50px;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ccc;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
/* 默认透明底边 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn:hover {
|
||||||
|
background-color: #444;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn.active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
/* 白色半透明背景 */
|
||||||
|
color: #fff;
|
||||||
|
border-bottom: 2px solid #fff;
|
||||||
|
/* 纯白色底部横条 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标样式 */
|
||||||
|
.opt-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
/* 防止图标被压缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文字标签样式 */
|
||||||
|
.opt-btn-label {
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉箭头样式 */
|
||||||
|
.opt-btn-arrow {
|
||||||
|
font-size: 8px;
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 箭头旋转 (菜单展开时) */
|
||||||
|
.opt-btn-arrow.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 无标签模式调整 */
|
||||||
|
.opt-btn.no-label .opt-btn-arrow {
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉菜单样式 */
|
||||||
|
.opt-btn-dropdown {
|
||||||
|
position: fixed;
|
||||||
|
/* 固定定位 */
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
/* 水平居中并向上移动 */
|
||||||
|
background-color: rgba(17, 17, 17, 0.88);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
min-width: 50px;
|
||||||
|
z-index: 9999;
|
||||||
|
/* 高层级 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉菜单项样式 */
|
||||||
|
.opt-btn-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* 垂直布局 */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #b3b4b4;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 50px;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn-dropdown-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
/* 移除最后一项的分隔线 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn-dropdown-item:hover {
|
||||||
|
background-color: #444;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn-dropdown-item .opt-btn-icon.small {
|
||||||
|
width: 30px;
|
||||||
|
/* 与主按钮图标大小一致 */
|
||||||
|
height: 30px;
|
||||||
|
margin-right: 0;
|
||||||
|
/* 移除右边距 */
|
||||||
|
margin-bottom: 4px;
|
||||||
|
/* 添加下边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-btn-dropdown-item span {
|
||||||
|
font-size: 10px;
|
||||||
|
/* 较小的文字 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 无标签模式调整 */
|
||||||
|
.opt-btn.no-label .opt-btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
387
src/toolbar/index.ts
Normal file
387
src/toolbar/index.ts
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
import './index.css';
|
||||||
|
import type {
|
||||||
|
OptButton,
|
||||||
|
ButtonGroup,
|
||||||
|
OptBtnGroupsOptions,
|
||||||
|
ButtonConfig
|
||||||
|
} from './index.type';
|
||||||
|
|
||||||
|
export class OptBtnGroups {
|
||||||
|
private container: HTMLElement;
|
||||||
|
private options: OptBtnGroupsOptions;
|
||||||
|
// 改用 Array 存储 Group,方便控制顺序
|
||||||
|
private groups: ButtonGroup[] = [];
|
||||||
|
private activeBtnIds: Set<string> = new Set();
|
||||||
|
private btnRefs: Map<string, HTMLElement> = new Map();
|
||||||
|
private dropdownElement: HTMLElement | null = null;
|
||||||
|
private hoverTimeout: number | null = null;
|
||||||
|
|
||||||
|
private readonly DEFAULT_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
|
||||||
|
|
||||||
|
constructor(options: OptBtnGroupsOptions) {
|
||||||
|
const el = typeof options.container === 'string'
|
||||||
|
? document.getElementById(options.container)
|
||||||
|
: options.container;
|
||||||
|
|
||||||
|
if (!el) throw new Error('Container not found');
|
||||||
|
|
||||||
|
this.container = el;
|
||||||
|
this.options = {
|
||||||
|
showLabel: true,
|
||||||
|
visibility: {},
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
this.initContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initContainer(): void {
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加按钮组
|
||||||
|
* @param groupId 组ID
|
||||||
|
* @param beforeGroupId 在哪个组之前插入(可选),不传则插入到最后
|
||||||
|
*/
|
||||||
|
public addGroup(groupId: string, beforeGroupId?: string): void {
|
||||||
|
if (this.groups.some(g => g.id === groupId)) {
|
||||||
|
console.warn('Group ' + groupId + ' already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGroup: ButtonGroup = { id: groupId, buttons: [] };
|
||||||
|
|
||||||
|
if (beforeGroupId) {
|
||||||
|
const index = this.groups.findIndex(g => g.id === beforeGroupId);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.groups.splice(index, 0, newGroup);
|
||||||
|
} else {
|
||||||
|
console.warn(`Target group ${beforeGroupId} not found, appending ${groupId} to end.`);
|
||||||
|
this.groups.push(newGroup);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.groups.push(newGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加按钮
|
||||||
|
* @param config 按钮配置(必须包含 groupId,可选包含 parentId)
|
||||||
|
*/
|
||||||
|
public addButton(config: ButtonConfig): void {
|
||||||
|
const { groupId, parentId } = config;
|
||||||
|
|
||||||
|
if (!groupId) {
|
||||||
|
throw new Error(`Button ${config.id} config must contain 'groupId'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = this.groups.find(g => g.id === groupId);
|
||||||
|
if (!group) {
|
||||||
|
throw new Error(`Group ${groupId} not found. Please call addGroup first.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const button: OptButton = {
|
||||||
|
...config,
|
||||||
|
children: config.children || []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parentId) {
|
||||||
|
// Add as sub-button
|
||||||
|
const parentBtn = this.findButton(group.buttons, parentId);
|
||||||
|
if (!parentBtn) {
|
||||||
|
throw new Error(`Parent button ${parentId} not found in group ${groupId}`);
|
||||||
|
}
|
||||||
|
if (!parentBtn.children) {
|
||||||
|
parentBtn.children = [];
|
||||||
|
}
|
||||||
|
parentBtn.children.push(button);
|
||||||
|
} else {
|
||||||
|
// Add as main button
|
||||||
|
group.buttons.push(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findButton(buttons: OptButton[], id: string): OptButton | undefined {
|
||||||
|
for (const btn of buttons) {
|
||||||
|
if (btn.id === id) return btn;
|
||||||
|
if (btn.children) {
|
||||||
|
const found = this.findButton(btn.children, id);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
const { homeButton } = await import('./buttons/home');
|
||||||
|
const { locationButton } = await import('./buttons/location');
|
||||||
|
const { walkMenuButton } = await import('./buttons/walk/walk-menu');
|
||||||
|
const { walkPersonButton } = await import('./buttons/walk/walk-person');
|
||||||
|
const { walkBirdButton } = await import('./buttons/walk/walk-bird');
|
||||||
|
const { settingButton } = await import('./buttons/setting');
|
||||||
|
const { infoButton } = await import('./buttons/info');
|
||||||
|
|
||||||
|
// 添加组1
|
||||||
|
this.addGroup('group-1');
|
||||||
|
this.addButton(homeButton);
|
||||||
|
this.addButton(walkMenuButton);
|
||||||
|
this.addButton(walkPersonButton);
|
||||||
|
this.addButton(walkBirdButton);
|
||||||
|
this.addButton(locationButton);
|
||||||
|
this.addGroup('group-2');
|
||||||
|
this.addButton(settingButton);
|
||||||
|
this.addButton(infoButton);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): void {
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
this.btnRefs.clear();
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'toolbar-container';
|
||||||
|
|
||||||
|
// 直接遍历数组,顺序由 addGroup 控制
|
||||||
|
this.groups.forEach((group, index) => {
|
||||||
|
const groupElement = this.renderGroup(group, index, this.groups.length);
|
||||||
|
wrapper.appendChild(groupElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGroup(group: ButtonGroup, index: number, total: number): HTMLElement {
|
||||||
|
const groupEl = document.createElement('div');
|
||||||
|
groupEl.className = 'opt-btn-group';
|
||||||
|
|
||||||
|
if (index < total - 1) {
|
||||||
|
groupEl.classList.add('has-divider');
|
||||||
|
}
|
||||||
|
|
||||||
|
group.buttons.forEach(button => {
|
||||||
|
if (this.isVisible(button.id)) {
|
||||||
|
const btnWrapper = this.renderButton(button);
|
||||||
|
groupEl.appendChild(btnWrapper);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderButton(button: OptButton): HTMLElement {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'opt-btn-wrapper';
|
||||||
|
|
||||||
|
const btnEl = document.createElement('div');
|
||||||
|
btnEl.className = 'opt-btn';
|
||||||
|
|
||||||
|
if (this.activeBtnIds.has(button.id)) {
|
||||||
|
btnEl.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button.disabled) {
|
||||||
|
btnEl.classList.add('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.showLabel) {
|
||||||
|
btnEl.classList.add('no-label');
|
||||||
|
if (button.label) {
|
||||||
|
btnEl.title = button.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.className = 'opt-btn-icon';
|
||||||
|
icon.innerHTML = this.getIcon(button.icon);
|
||||||
|
btnEl.appendChild(icon);
|
||||||
|
|
||||||
|
if (this.options.showLabel && button.label) {
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'opt-btn-label';
|
||||||
|
label.textContent = button.label;
|
||||||
|
btnEl.appendChild(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button.children && button.children.length > 0) {
|
||||||
|
const arrow = document.createElement('span');
|
||||||
|
arrow.className = 'opt-btn-arrow';
|
||||||
|
arrow.textContent = '▼';
|
||||||
|
btnEl.appendChild(arrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
btnEl.addEventListener('click', () => this.handleClick(button));
|
||||||
|
btnEl.addEventListener('mouseenter', () => this.handleMouseEnter(button, btnEl));
|
||||||
|
btnEl.addEventListener('mouseleave', () => this.handleMouseLeave());
|
||||||
|
|
||||||
|
this.btnRefs.set(button.id, btnEl);
|
||||||
|
|
||||||
|
wrapper.appendChild(btnEl);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClick(button: OptButton): void {
|
||||||
|
if (button.disabled) return;
|
||||||
|
|
||||||
|
if (!button.children || button.children.length === 0) {
|
||||||
|
if (button.keepActive) {
|
||||||
|
const wasActive = this.activeBtnIds.has(button.id);
|
||||||
|
if (wasActive) {
|
||||||
|
this.activeBtnIds.delete(button.id);
|
||||||
|
} else {
|
||||||
|
this.activeBtnIds.add(button.id);
|
||||||
|
}
|
||||||
|
this.updateButtonState(button.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeDropdown();
|
||||||
|
|
||||||
|
if (button.onClick) {
|
||||||
|
button.onClick(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSubClick(button: OptButton): void {
|
||||||
|
if (button.keepActive) {
|
||||||
|
const wasActive = this.activeBtnIds.has(button.id);
|
||||||
|
if (wasActive) {
|
||||||
|
this.activeBtnIds.delete(button.id);
|
||||||
|
} else {
|
||||||
|
this.activeBtnIds.add(button.id);
|
||||||
|
}
|
||||||
|
this.updateButtonState(button.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeDropdown();
|
||||||
|
|
||||||
|
if (button.onClick) {
|
||||||
|
button.onClick(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseEnter(button: OptButton, btnEl: HTMLElement): void {
|
||||||
|
if (this.hoverTimeout) {
|
||||||
|
clearTimeout(this.hoverTimeout);
|
||||||
|
this.hoverTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button.children && button.children.length > 0) {
|
||||||
|
this.showDropdown(button, btnEl);
|
||||||
|
|
||||||
|
const arrow = btnEl.querySelector('.opt-btn-arrow');
|
||||||
|
if (arrow) {
|
||||||
|
arrow.classList.add('rotated');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.closeDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseLeave(): void {
|
||||||
|
this.hoverTimeout = window.setTimeout(() => {
|
||||||
|
this.closeDropdown();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showDropdown(button: OptButton, btnEl: HTMLElement): void {
|
||||||
|
this.closeDropdown();
|
||||||
|
|
||||||
|
if (!button.children) return;
|
||||||
|
|
||||||
|
const dropdown = document.createElement('div');
|
||||||
|
dropdown.className = 'opt-btn-dropdown';
|
||||||
|
|
||||||
|
const rect = btnEl.getBoundingClientRect();
|
||||||
|
const centerX = rect.left + rect.width / 2;
|
||||||
|
|
||||||
|
dropdown.style.top = rect.top - 8 + 'px';
|
||||||
|
dropdown.style.left = centerX + 'px';
|
||||||
|
|
||||||
|
button.children.forEach(subBtn => {
|
||||||
|
if (this.isVisible(subBtn.id)) {
|
||||||
|
const item = this.renderDropdownItem(subBtn);
|
||||||
|
dropdown.appendChild(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.addEventListener('mouseenter', () => {
|
||||||
|
if (this.hoverTimeout) {
|
||||||
|
clearTimeout(this.hoverTimeout);
|
||||||
|
this.hoverTimeout = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.addEventListener('mouseleave', () => this.handleMouseLeave());
|
||||||
|
|
||||||
|
document.body.appendChild(dropdown);
|
||||||
|
this.dropdownElement = dropdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDropdownItem(button: OptButton): HTMLElement {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'opt-btn-dropdown-item';
|
||||||
|
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.className = 'opt-btn-icon small';
|
||||||
|
icon.innerHTML = this.getIcon(button.icon);
|
||||||
|
item.appendChild(icon);
|
||||||
|
|
||||||
|
if (this.options.showLabel) {
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.textContent = button.label;
|
||||||
|
item.appendChild(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.handleSubClick(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeDropdown(): void {
|
||||||
|
if (this.dropdownElement) {
|
||||||
|
this.dropdownElement.remove();
|
||||||
|
this.dropdownElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.btnRefs.forEach(btnEl => {
|
||||||
|
const arrow = btnEl.querySelector('.opt-btn-arrow');
|
||||||
|
if (arrow) {
|
||||||
|
arrow.classList.remove('rotated');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateButtonState(buttonId: string): void {
|
||||||
|
const btnEl = this.btnRefs.get(buttonId);
|
||||||
|
if (btnEl) {
|
||||||
|
if (this.activeBtnIds.has(buttonId)) {
|
||||||
|
btnEl.classList.add('active');
|
||||||
|
} else {
|
||||||
|
btnEl.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIcon(icon?: string): string {
|
||||||
|
return icon || this.DEFAULT_ICON;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isVisible(id: string): boolean {
|
||||||
|
return this.options.visibility?.[id] !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this.closeDropdown();
|
||||||
|
if (this.hoverTimeout) {
|
||||||
|
clearTimeout(this.hoverTimeout);
|
||||||
|
}
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
this.btnRefs.clear();
|
||||||
|
this.activeBtnIds.clear();
|
||||||
|
this.groups = []; // 清空数组
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/toolbar/index.type.ts
Normal file
51
src/toolbar/index.type.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export type ButtonType = 'button' | 'menu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮配置接口(用于外部定义按钮)
|
||||||
|
*/
|
||||||
|
export interface ButtonConfig {
|
||||||
|
id: string; // 唯一标识
|
||||||
|
type: ButtonType; // 按钮类型
|
||||||
|
label: string; // 按钮文字
|
||||||
|
icon?: string; // SVG 图标(内联 SVG 字符串)
|
||||||
|
keepActive?: boolean; // 是否保持激活状态(默认 false)
|
||||||
|
disabled?: boolean; // 是否禁用
|
||||||
|
onClick?: (button: OptButton) => void; // 点击回调
|
||||||
|
children?: ButtonConfig[]; // 子按钮配置(可选,用于菜单按钮)
|
||||||
|
|
||||||
|
groupId?: string; // 所属组ID
|
||||||
|
parentId?: string; // 父按钮ID(如果是子按钮)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作按钮接口(内部使用,继承配置)
|
||||||
|
*/
|
||||||
|
export interface OptButton extends ButtonConfig {
|
||||||
|
children?: OptButton[]; // 内部使用的子按钮列表
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮组接口
|
||||||
|
*/
|
||||||
|
export interface ButtonGroup {
|
||||||
|
id: string;
|
||||||
|
buttons: OptButton[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OptBtnGroups 配置选项
|
||||||
|
*/
|
||||||
|
export interface OptBtnGroupsOptions {
|
||||||
|
container: HTMLElement | string;
|
||||||
|
showLabel?: boolean;
|
||||||
|
visibility?: Record<string, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击事件载荷
|
||||||
|
*/
|
||||||
|
export interface ClickPayload {
|
||||||
|
button: OptButton;
|
||||||
|
action: 'activate' | 'deactivate' | 'trigger';
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export default defineConfig(({ command }) => {
|
|||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/index.ts'),
|
entry: resolve(__dirname, 'src/index.ts'),
|
||||||
name: 'BimEngineSDK',
|
name: 'LyzBimEngineSDK',
|
||||||
fileName: (format) => `bim-engine-sdk.${format}.js`,
|
fileName: (format) => `bim-engine-sdk.${format}.js`,
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|||||||
Reference in New Issue
Block a user