ali

try it

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         ali
// @namespace    http://tampermonkey.net/
// @version      2024.3.6.2
// @description  try it
// @author       You
// @match        https://ads.aligenie.com/labeltools?type=BATCHWORK&*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=aligenie.com
// @grant        none
// ==/UserScript==

(async function() {
    'use strict';

/**==================================

快捷键:
【Q】        ==>  俯视角左调
【E】        ==>  俯视角右调
【鼠标中键】 ==>  进入/退出三视图编辑模式
【空格\1】   ==>  帧数前切(选中个体时,控制在关联帧范围;三视图总览表→批量前切)
【`】        ==>  帧数后切(选中个体时,控制在关联帧范围;三视图总览表→批量后切)
【Ctrl + Y】 ==>   开启/关闭自动删除
【P】        ==>  开启/关闭快捷键自定义面板
【ESC】      ==>  删除当前对象
【Alt + `】  ==>  向后一帧(等同 Ctrl ←)
【1】        ==>  向前一帧(等同 空格)
【Alt + 1】  ==>  向前一帧(等同 Ctrl →)
【Esc】      ==>  删除当前对象
【Z】        ==>  失焦个体

便捷交互:
- 双击右键菜单删除项 可删除整个对象串
- 点击右键菜单复制项 左区域==>向前复制  右区域==>向后复制  最两侧的空白区域复制5帧
- 三视图放大模式下双击三视图切到三视图预览表 ,再双击三视图预览区切回

按钮:
- 单页重检:一键勾选单页下的重新检查按钮
- 点数筛选:隐藏点数合格的快捷属性标签
- 锁定标签:退出三视图放大模式后不会取消快捷属性的勾选

自动生效:
- 自动勾选 展示映射、自车轨迹、行车轨迹
- 三视图放大模式下自动勾选快捷属性
- 切换个体时重置成位移模式
- 个体属性面板自动保存
- 检查快捷属性标签的点数并且给出颜色提示
- 个体属性面板生成几何信息面板,并计算距离,对当前对象的快捷属性标签加入颜色提示
- 点云小视图尺寸放大

==================================**/
    if(await awaitLoad()) showMessage('工具已启用')

    let saveRecord = []
    let isDel = false
    let isFocus = false
    let isObserveToolBar = 0
    let mouseX, mouseY
    let isDelLinkAll = false
    let isHideLabel = false
    let isAttrLabelKeep = false
    let curTarget = []
    let targetDistance = -1
    let isPointViewAdjust = true


    new MutationObserver((mrs) => {
        // if(mrs[0].addedNodes[0]?.nodeName !== '#text') console.log('=======',mrs)
        mrs.some((mr) => {
			[...mr.addedNodes].some((an) => {
				// if(mrs[0].addedNodes[0]?.nodeName !== '#text') console.log(111, an)
				if(an.nodeName == '#text') return

				if(isDelLinkAll && an.className.includes('ant-modal-root') && an.innerText.includes('确认要删除对象吗')) {
					[...an.querySelectorAll('button')].find(btn => btn.innerText === '确 定')?.click()
				}

				if(an.className.includes('index_mask__uUfCW')) {
					let menu = an.children[0]
					menu.style.transition = '.1s all'

					let menuList = [...document.querySelectorAll('.index_context__ItAcv li')]
					let menu_ul = document.querySelector('.index_context__ItAcv ul')
					let focusItem = menuList.find(item => item.textContent.includes('聚焦'))

					let copyItem = menuList.find(item => item.textContent.includes('复制创建'))
					if(copyItem) {
						menu_ul.insertBefore(copyItem, menu_ul.firstChild)
						let menuRectInfo = menu.getBoundingClientRect()
						let copyItemRectInfo = copyItem.getBoundingClientRect()

						copyItem.addEventListener('click', (e) => {
							let matchStr
							console.log(e.offsetX, copyItemRectInfo.width/2 )
							if(e.offsetX >= copyItemRectInfo.width/2) {
								matchStr = e.offsetX < copyItemRectInfo.width - 20 ? '向后1帧' : '向后5帧'
							} else {
								matchStr = e.offsetX > 20 ? '向前1帧' : '向前5帧'
							}

							let item = [...document.querySelectorAll('.ant-menu-submenu.ant-menu-submenu-popup:not(.ant-menu-submenu-hidden) li')].find(item => item.innerText.includes(matchStr)).click()
							showMessage('复制:'+ matchStr)
						})

						// console.log('menuRectInfo', menuRectInfo)
						// console.log('copyItemRectInfo', copyItemRectInfo)

						menu.style.left = `${ menuRectInfo.x - (menuRectInfo.width/2) }px`;
						menu.style.top = `${ menuRectInfo.y - (copyItemRectInfo.y - mouseY + (copyItemRectInfo.height/2)) }px`
					}

					let delItem = menuList.find(item => item.textContent.includes('删除'))
					if(delItem) {
						if(copyItem) copyItem.insertAdjacentElement('afterend', delItem) //重排
						delItem.addEventListener('dblclick', (e) => {
							let item = [...document.querySelectorAll('.ant-menu-submenu.ant-menu-submenu-popup:not(.ant-menu-submenu-hidden) li')].find(item => item.innerText.includes('全部目标关联对象')).click()
							showMessage('删除全部关联对象')
							isDelLinkAll = true
							setTimeout(() => {isDelLinkAll = false} , 3000)
						})
					}
				}

				if(an.className.includes('index_TheFoldHeadBox__GXNHj') && an.textContent.includes('对象列表') && an.textContent.includes('批量质检')) {
					let header = an.querySelector('.index_title__LoGZA')
					let btn = document.createElement('button')
					btn.textContent = '单页重检'
					setElStyle(btn, {
						background: '#eee',
						padding: '5px 8px',
						borderRadius: '3px'
					})
					btn.addEventListener('click', () => {
						let allCheckBtn = [...document.querySelectorAll('use')].filter(use => use.getAttribute('xlink:href') === '#icon-icon-zhijian' && !use.parentElement.textContent.includes('批量质检'))
						allCheckBtn.some(btn => btn.parentElement.parentElement.click())
					})
					header.append(btn)
				}

				if(an.className.includes('index_view__MhyQ9')) {
					an.addEventListener('dblclick', e => {
						if(e.shiftKey) return
						an?.querySelector('.index_tracking-close__4LOgH').click()
					})
				}
				if(an.className.includes('index_tracking-view__6ChPR')) {
					// document.querySelector('.index_tracking-close__4LOgH')
					document.querySelector('.index_main-wrap__sCQxm').addEventListener('dblclick', e => {
						if(e.shiftKey) return
						an?.querySelector('.index_tracking-ball__A7uMf')?.click()
					})
				}

				if(an?.className?.includes('index_question__4Tm9w') || an?.className?.includes('index_QuestionTabs__kk6nt') && an.parentElement?.className !== 'index_trackingEditWrap__c2kEj') { //ctrl E 的面板不监视
					if(an?.className?.includes('index_question__4Tm9w')) an = an.querySelector('.index_QuestionTabs__kk6nt')
					if(isPointViewAdjust) {
						pointViewAdjust(an)
					} else {
						$('.index_control__C8NVb').style.width = '400px'
					}
					resetTrasitionPattern()
					an.style.position = 'relative'

					an.append(createGeometryPanel())

					an.style.border = '2px solid red'
					new MutationObserver((mrs) => {
						mrs.some((mr) => {
							if(mr.type === 'attributes' && mr.target.className.includes('ant-radio-wrapper ant-radio-wrapper-checked') && mr.oldValue === 'ant-radio-wrapper') {
								console.log('new select')
								document.querySelector('.index_question__4Tm9w').querySelector('.ant-btn').click()
								let time = Date.now()
								// console.log('gen'+ time)
								saveRecord.push([time, 0])
							}
						})
					}).observe(an, { childList: true, subtree: true, attributes: true, attributeOldValue: true});
					return true;
				}
				if(an?.className?.includes('labelTex_wrap__QrqM5')) {
					setElStyle(an.children[0], {
						top: '310px',
						left: '870px',
						height: '35px',
						fontSize: '22px',
						textAlign: 'center',
						lineHeight: '30px',
					})

					let curFrameNum = document.querySelector('.index_left__ioHQp .ant-pagination-simple-pager input').value
					let curItem = curTarget.items[curFrameNum-1]
					if(!curItem) return
					let {positionX: x, positionY: y} = curItem.geometry
					console.log('add Label')

					Observe(an, (mrs) => {
						mrs.some(mr => {
							console.log(mr);
							[...mr.addedNodes].some(an => {
								if(an.parentElement.className.includes('labelTex_text__9KRjf')) {
									filterPointLabel(an.parentElement, targetDistance !== -1 ? targetDistance : void 0)
								}
							})
							if(mr.type == 'characterData') {
								console.log(mr)
								console.log(mr.target.parentElement.className)
								filterPointLabel(mr.target.parentElement, targetDistance !== -1 ? targetDistance : void 0)
							}
						})
					}, {characterData: true, childList: true, subtree: true})
				}

				if(an?.className?.includes('ant-message-notice') && an.innerText === '只允许切换锚点类型') an.remove()
				if(isDel && an?.className?.includes('ant-modal-root') && an.querySelector('.ant-modal-body').innerText === '当前组内共有1个对象,删除后不可恢复,请谨慎操作!') setTimeout(() => {an.querySelector('.ant-btn.ant-btn-primary.ant-btn-sm').click()})
				if(isDel && an?.className?.includes('ant-table-row ant-table-row-level-0 common_rowBgColor__CUySE')) {
					let tdIdx;
					[...document.querySelector('.ant-table-thead').children[0].children].find((item, idx) => {
						if(item.innerText == '对象数') tdIdx = idx
					});

					let delRow = [...document.querySelectorAll('.ant-table-row')][0];
					console.log('del '+delRow.dataset.rowKey);
					console.log([...delRow.querySelectorAll('span')].at(-1));
					[...delRow.querySelectorAll('span')].at(-1).click()

					return true
				}

				if(!isObserveToolBar) {
					let toolBar = [...document.querySelectorAll('.toolBar_toolBar__q14CL')].find(item => item.innerHTML.includes('快捷属性') );
					if(toolBar) {
						['展示映射', '自车轨迹', '行车轨迹'].some((title, i) => toolBar.querySelector(`button[title="${title}"]`).click())

						Observe(toolBar, (mrs) => {
							if(!isAttrLabelKeep && mrs.find((mr) => mr.removedNodes[0]?.innerHTML.includes('自动外扩'))) return
							let btn = document.querySelector('button[title="快捷属性"]')
							console.log(btn.className)
							setTimeout(() => { !btn.className.includes('toolBar_active__PfJFI') ? btn.click() : void 0 })
						}, { childList: true, subtree: true})

						isObserveToolBar = 1
					}
				}

			});
		})
    }).observe(document, { childList: true, subtree: true, attributes: true, attributeOldValue: true})

    //调整点云小视图尺寸
    function pointViewAdjust(an) {
        let pointViewHeight = 700
        $('.index_context__orjUS.flex-column').style.height = pointViewHeight + 'px';
        $('.index_context__orjUS.flex-column').style.position = 'relative';
        $('.index_question__4Tm9w').style.height = `calc(100% - ${pointViewHeight}px)`
        $('.index_control__C8NVb').style.width = '450px'

        $('.index_context__orjUS.flex-column').style.marginTop = 0

        let imgIndicator = [...document.querySelectorAll('.index_footer__9yCgR.flex-row')].find(item => item?.parentElement?.className.includes('index_context__orjUS flex-column'));
        setElStyle(imgIndicator, {
            position: 'absolute',
            top: `${ pointViewHeight - 40}px`,
            left: '10px',
            width: 'fit-content',
            background: '#fff',
        })


        //去除tab
        an.querySelector('.ant-tabs.ant-tabs-top').remove();

        //去除一级标题
        [...an.querySelectorAll('.index_title__KB3CQ')].some(title => title.remove())

        //去除二级标题
        let titleArr = ['物体形态', '标注类别', '残影属性', '物体运动状态?', '遮挡属性'];
        [...an.querySelectorAll('.index_title__Fzt5I')].some(title => {
            if(titleArr.find(item => title.innerText.includes(item))) title.remove()
        })

        an.querySelector('.index_line__wZq6j').remove()

        let btn = an.nextElementSibling
        if(an.nextElementSibling?.innerText == '确认保存') btn.style.display = 'none'
    }

    function resetTrasitionPattern() {
        let btn = document.querySelector('.index_cube-scale-translation-tracking__2BRIw > button:first-child')
        if(btn && !btn.className.includes('index_translation-btn__+VcaE')) btn.click()
    }

    function createGeometryPanel() {
        let curFrameNum = document.querySelector('.index_left__ioHQp .ant-pagination-simple-pager input').value

        let geometryPanel = document.createElement('div')
        geometryPanel.className = 'geometry-panel'
        setElStyle(geometryPanel, {
            position: 'absolute',
            top: '75px',
            right: '5px',
            padding: '5px 10px',
            borderRadius: '5px'
        })
        let curItem = curTarget.items[curFrameNum-1]
        let {positionX: x, positionY: y} = curItem.geometry
        targetDistance = parseInt(Math.sqrt(x**2 + y**2))
        let contentObj = {
            '帧数': curItem.frameIdex+1,
            'X': x,
            'Y': y,
            '距离': targetDistance + 'm',
        }
        for(let k in contentObj) {
            let item = document.createElement('div')
            item.textContent = `${k}: ${contentObj[k]}`
            geometryPanel.append(item)
        }
        return geometryPanel
    }



    let header = $('.index_TopBar__atir2')
    let controlBar = document.createElement('div')
    let btn_hideLabel = document.createElement('button')
    let btn_keepLabel = document.createElement('button')
    let btn_pointViewAdjust = document.createElement('button')
    btn_hideLabel.textContent = '点数筛选'
    btn_keepLabel.textContent = '锁定标签'

    let local_isPointViewAdjust = localStorage.getItem('isPointViewAdjust')
    if(local_isPointViewAdjust === null) {
        btn_pointViewAdjust.textContent = '还原视图尺寸'
        localStorage.setItem('isPointViewAdjust', isPointViewAdjust)
    } else if(local_isPointViewAdjust === 'false') {
        isPointViewAdjust = false
        btn_pointViewAdjust.textContent = '调整视图尺寸'

    } else if(local_isPointViewAdjust === 'true') {
        isPointViewAdjust = true
        btn_pointViewAdjust.textContent = '还原视图尺寸'
    }
    let btnStyle = {
        marginRight: '5px',
        padding: '2px 5px',
        background: '#888',
        borderRadius: '4px'
    }
    setElStyle(new Map([[ btn_hideLabel, btnStyle ], [ btn_keepLabel, btnStyle ], [ btn_pointViewAdjust, btnStyle ], [controlBar, {width: '600px'}]]))



    btn_hideLabel.addEventListener('click', ()=> {
        isHideLabel = !isHideLabel
        let allLabel = [...document.querySelectorAll('.cube_text__q6bGx')];
        allLabel.some(isHideLabel ? (label) => { filterPointLabel(label, -1) } : (label) => { label.style.display = 'block' })
    })
    btn_keepLabel.addEventListener('click', ()=> { isAttrLabelKeep = !isAttrLabelKeep })
    btn_pointViewAdjust.addEventListener('click', ()=> {
        isPointViewAdjust = !isPointViewAdjust
        localStorage.setItem('isPointViewAdjust', isPointViewAdjust)
    })

    controlBar.append(btn_hideLabel)
    controlBar.append(btn_keepLabel)
    //controlBar.append(btn_pointViewAdjust)
    header.children[0].insertAdjacentElement('afterend', controlBar)

    Observe(document.querySelector('.index_container__M6WXK'), (mrs) => {
        mrs.some(mr => {
            [...mr.addedNodes].some(an => {
                if(an?.className?.includes('cube_wrap__TBfjR') ) {
                    filterPointLabel(an.children[0], -1)
                } else if(an.parentElement.className.includes('cube_text__q6bGx')) {
                    filterPointLabel(an.parentElement, -1)
                }
            })
            if(mr.type == 'characterData' && mr.target.parentElement.className.includes('cube_text__q6bGx')) {
                filterPointLabel(mr.target.parentElement, -1)
            }
        })
    }, {characterData: true, childList: true, subtree: true})

    function filterPointLabel(label, distance) {
        label.style.background = '#000'
        label.style.display = 'block'

        let pointCount = parseInt(label.innerText)
        if(isNaN(pointCount)) {
            return
        }
        if(distance !== -1) {
            if((distance < 90 && pointCount < 10) || (distance > 90 && pointCount < 3)) {
                     const ky = new KeyframeEffect(null, {
                         background: ['red', '#000']
                     }, {
                         duration: 500,
                         iterations: Infinity,
                         direction: 'alternate'
                     })
                     ky.target = label;
                     let ani = new Animation(ky);
                     ani.play();
            } else {
                label.style.background = '#000'
                label.getAnimations().forEach(ani => ani.cancel())
            }
        } else {
            if(pointCount >= 3 && pointCount < 10) {
                label.style.background = 'coral'
            } else if(pointCount < 3) {
                label.style.background = 'red'
            } else if(isHideLabel) {
                label.style.display = 'none'
            }
        }
    }

    // let attrMap = {
    //     '物体形态': 'body_shape',
    //     '形变体': 'soft',
    //     '刚体': 'rigid',
    //     '标注类别': 'type',
    //     '小汽车': 'Car',
    //     '货车/卡车': 'Truck',
    //     '公交车': 'Bus',
    //     '施工车': 'Construction',
    //     '三轮车': 'Tricycle',
    //     '骑二轮车的人': 'Cyclist',
    //     '行人': 'Pedestrian',
    //     '其他车辆': 'Other',
    //     '残影属性': 'blur',
    //     '分离': 'blur',
    //     '粘连': 'sticking',
    //     '无残影': 'no',
    //     ignore: 'ignore',
    //     '鬼影': 'multi-reflection',
    // }
    let attrMap = {
        soft:'形变体',
        rigid:  '刚体',

    }
    const originalXHR = window.XMLHttpRequest;
    const base = 'https://ads.aligenie.com/api/item/ItemServiceI'
    window.XMLHttpRequest = function () {
        let xhr = new originalXHR();

        xhr.addEventListener('readystatechange', function () {
            if (xhr.responseURL === `${base}/linkageSaveItems`) {
                let saveRecordItem = null
                let findRes = saveRecord.some((item) => {
                    if(item[1] == 0) {
                        saveRecordItem = item
                        return true
                    }
                })
                if(saveRecordItem && xhr.readyState === 4 && xhr.status == 200) {
                    let {retMsg, success } = JSON.parse(xhr.responseText)
                    if(retMsg === 'success' && success === true) {
                        let res = JSON.parse(xhr.responseText)
                        let labels =JSON.parse(res.retValue.itemVO).items[0].labels
                        showMessage(`修改完成(物体形态-${attrMap[labels.body_shape]})`, {type: 'success'})
                        saveRecordItem[1] = 1
                        saveRecord.some((item, idx) => {
                            if(item[0] === saveRecordItem[0]) {
                                saveRecord.splice(idx, 1)
                                console.log('删除'+ saveRecordItem[0])
                            }
                        })
                        console.log('请求', saveRecord)
                    }
                } else if(findRes && xhr.readyState === 4) {
                    console.log('请求响应失败')
                    showMessage('保存失败', {type: 'error'})

                    saveRecordItem[1] = 2
                }
            }


            if(xhr.responseURL === `${base}/getCruxItemInfo` && xhr.readyState === 4 && xhr.status == 200) {
                let res = JSON.parse(xhr.responseText)
                console.log(res)

                let items = JSON.parse(res.retValue.workResult).items
                curTarget = {
                    id: items[0].relations.instance.instance_id,
                    items: new Array(50)
                }

                items.some(item => {
                    let frameIdex = item.relations.instance.index_id
                    curTarget.items[frameIdex] = {
                        id: item.id,
                        frameIdex,
                        geometry: item.meta.geometry
                    }
                })
                console.log(curTarget)
            }


            if([`${base}/getCruxItemInfo`, `${base}/linkageSaveItems`].includes(xhr.responseURL) && xhr.readyState === 4 && xhr.status == 200) {
                let res = JSON.parse(xhr.responseText)
                console.log(res)

                let items = JSON.parse(res.retValue[xhr.responseURL === `${base}/getCruxItemInfo` ? 'workResult' : 'itemVO']).items
                curTarget = {
                    id: items[0].relations.instance.instance_id,
                    items: new Array(50)
                }

                items.some(item => {
                    let frameIdex = item.relations.instance.index_id
                    curTarget.items[frameIdex] = {
                        id: item.id,
                        frameIdex,
                        geometry: item.meta.geometry
                    }
                })

                let geometryPanel = document.querySelector('.geometry-panel')
                if(geometryPanel) {
                    showMessage('几何数据保存完成', {showTime: 600})
                    geometryPanel.remove();
                    [...document.querySelectorAll('.index_QuestionTabs__kk6nt')].find(attrPanel => attrPanel.parentElement.className === '').append(createGeometryPanel())
                    filterPointLabel(document.querySelector('.labelTex_text__9KRjf'), targetDistance)

                }
                console.log('modify', curTarget)
            }
            if(xhr.responseURL === `${base}/copyUuid` && xhr.readyState === 4 && xhr.status == 200) {
                // let res = JSON.parse(xhr.responseText)
                // let copyRes = JSON.parse(res.retValue.workResult)
                // // console.log(copyRes)
                // let distanceArr = copyRes.items.map(item => {
                //     let {positionX:x, positionY:y} = item.meta.geometry
                //     return parseInt(Math.sqrt(x**2 + y**2)) + 'm'
                // })
                // showMessage('距离:'+ distanceArr.join(','))

                let res = JSON.parse(xhr.responseText)
                if(!res.retValue) return
                // console.log('copy', res)
                JSON.parse(res.retValue.workResult).items.some(item => {
                    let frameIdex = item.relations.instance.index_id
                    curTarget.items[frameIdex] = {
                        id: item.id,
                        frameIdex,
                        geometry: item.meta.geometry
                    }
                })
                console.log('add item', curTarget)
            }
            if(xhr.responseURL === `${base}/deleteBatchItem` && xhr.readyState === 4 && xhr.status == 200) {
                let res = JSON.parse(xhr.responseText)
                // console.log('del', res)
                let id = res.retValue.uuid[0]

                curTarget.items.some((item, idx)=> {
                    if(item.id !== id) return

                    curTarget.items[idx] = void 0
                    return true
                })
                console.log('del item', curTarget)
            }
        });
        return xhr;
    };

    let div = document.createElement('div')
    document.body.appendChild(div)
    div.outerHTML = `
  <div class="setting">
    <div class="set-head">自定义快捷键</div>
    <div class="set-body">
      <div class="set-item">
        <div class="key">Q</div>
        <div class="effect">俯视角左调</div>
      </div>
    </div>
  </div>`

    let settingStyle = document.createElement('style')
    document.body.appendChild(settingStyle)
    settingStyle.innerHTML = `
    .setting {
      display: none;
      position: absolute;
      z-index: 999;
      top: 45%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 200px;
      height: 100px;
      background-color: #f1f1f1;
      box-shadow: rgba(0, 0, 0, .15) 0px 0px 5px 2px;
      border-radius: 5px;
    }
    .set-head {
      letter-spacing: 0.5px;
      height: 30px;
      line-height: 30px;
      text-align: center;
      border-bottom: #ccc 2px solid;
      font-size: 15px;
      user-select: none;
    }
    .set-body {
      padding: 5px 10px;
    }
    .set-item {
      display: flex;
      align-items: center;
    }
    .key {
      width: 70px;
      height: 23px;
      line-height: 23px;
      margin-right: 10px;
      background-color: #ccc;
      border-radius: 5px;
      font-size: 12px;
      text-align: center;
      color: #f5f4f4;
      transition: .1s all;
      user-select: none;
    }
    .effect {
      font-size: 14px;
      height: 30px;
      line-height: 30px;
      text-align: center;
    }`;

    let settingPanel = document.querySelector('.setting')
    let key = settingPanel.querySelector('.key')
    key.addEventListener('click', (e) => {
      isFocus = true
      key.style.boxShadow = 'rgba(0, 0, 0, .15) 0px 0px 2px 2px'
      key.innerText = '请按下键盘'
    })
    settingPanel.addEventListener('click', (e)=> {
      if(e.target !== key && isFocus) {
        isFocus = false
        key.style.boxShadow = null
        key.innerText = JSON.parse(localStorage.getItem('key')).keyName
      }
    })

    window.addEventListener('mousemove', (e) => {(mouseX = e.x, mouseY = e.y)})

    window.addEventListener('keydown', (e) => {
        // console.log(e.keyCode, e)

        let turnRightKey_local
        if(localStorage.getItem('key')) {
            turnRightKey_local = JSON.parse(localStorage.getItem('key')).keyCode
        }

        if(!isFocus && e.keyCode == 80) {
            let settingPanel = document.querySelector('.setting')
            settingPanel.style.display = settingPanel.style.display == 'none' ? 'block' : 'none'
            settingPanel.querySelector('.key').innerText = localStorage.getItem('key') ? JSON.parse(localStorage.getItem('key')).keyName : 'Q'
        } else if(isFocus && e.keyCode == 80) {
            showMessage('关闭失败', {type: 'warning'})
        }
        if(isFocus) {
            e.preventDefault()
            console.log(e)
            let keyName = e.key !== " " ? e.key.toUpperCase() : 'Space'.toUpperCase()
            key.innerText = keyName
            if(key.innerText !== '请按下键盘') localStorage.setItem('key', JSON.stringify({keyCode: e.keyCode, keyName}))
            settingPanel.click()
        }


        if(e.keyCode == 89 && e.ctrlKey) {
            isDel = !isDel
            showMessage(`${isDel ? '开启' : '关闭'}:列表自动删除`)
        }

        if((!isFocus && turnRightKey_local && e.keyCode == turnRightKey_local) || (!isFocus && !turnRightKey_local && e.keyCode == 81)) {
            if(e.keyCode == 81 && document.querySelector('.index_container__v2bdb.hidden')) return

            document.body.dispatchEvent(new KeyboardEvent("keydown", {
                key: "r",
                keyCode: 82,
                code: "KeyR",
                bubbles: true,
                cancelable: true
            }));
        }
        if(e.keyCode == 69) {
            document.body.dispatchEvent(new KeyboardEvent("keydown", {
                key: "t",
                keyCode: 84,
                code: "KeyT",
                bubbles: true,
                cancelable: true
            }));
        }

        if([32, 192, 49, 50, 51].includes(e.keyCode)) {
            let commonKey = {
                32: {
                    key: "ArrowRight",
                    keyCode: 39,
                    code: "ArrowRight",
                    bubbles: true,
                    cancelable: true,
                    ctrlKey: true,
                    shiftKey: document.querySelector('.index_view__MhyQ9') ? true : false,
                },
                192: {
                    key: "ArrowLeft",
                    keyCode: 37,
                    code: "ArrowLeft",
                    bubbles: true,
                    cancelable: true,
                    ctrlKey: true,
                    shiftKey: document.querySelector('.index_view__MhyQ9') ? true : false,
                },
            }
            let {32: right, 192: left} = commonKey
            let keydownMap = {
                49: right,
                32: right,
                51: right,
                192: left,
                50: left,
            }

            if(document.querySelector('.index_view__MhyQ9')) {
                document.body.dispatchEvent(new KeyboardEvent("keydown", keydownMap[e.keyCode]));
            } else if(document.querySelector('.index_container__v2bdb').children.length || document.querySelector('.index_small-container__aVxtr')){
                // if([32, 49, 192].includes(e.keyCode)) {
                //     turn( [32, 49].includes(e.keyCode) ? 'right' : 'left')
                // }
                // else if([50, 51].includes(e.keyCode)) {
                //     document.body.dispatchEvent(new KeyboardEvent("keydown", keydownMap[e.keyCode]));
                // }
                if([192, 49, 32].includes(e.keyCode) && !e.altKey) {
                    turn( [32, 49].includes(e.keyCode) ? 'right' : 'left')
                } else if([192, 49].includes(e.keyCode)) {
                    document.body.dispatchEvent(new KeyboardEvent("keydown", keydownMap[e.keyCode]));
                }


                function turn(direction) {
                    let visiblePages = [...document.querySelector('.index_trackItem__6QT87').querySelectorAll('.index_track-rect__AvEjU')];

                    if(document.querySelector('.index_absolute-center__GLVXE')) { //判断是否超出分页区
                        let curPageDom = document.querySelector('.index_absolute-center__GLVXE').parentElement
                        let curPage = document.querySelector('.index_absolute-center__GLVXE').innerText -0

                        visiblePages.some((curPage, idx) => {
                            if(curPage === curPageDom) {
                                let pages = direction === 'right' ? visiblePages.slice(idx+1) : visiblePages.slice(0, idx).reverse()
                                if(!pages.length) { //处理首页和末页
                                    let {width, left} = document.querySelector('.index_scroll-line__GHJXu').children[0].style //progress bar
                                    if((direction === 'right' && parseInt(/(.*)%/.exec(width)[1]) + parseInt(/(.*)%/.exec(left)[1]) !== 100) || (direction === 'left' && left !== '0%')) { //判断是否需要更新预览区
                                        updatePages(direction)
                                        turn(direction)
                                        return true
                                    }
                                }
                                pages.some((nextPage, nextIdx) => {
                                    if(nextPage.className.includes('index_relation__pabdZ') || nextPage.className.includes('index_key-relation__qe-c4')) {
                                        nextPage.click()
                                        return true
                                    }
                                    if(nextIdx == pages.length-1) {
                                        let {width, left} = document.querySelector('.index_scroll-line__GHJXu').children[0].style //progress bar
                                        if(parseInt(/(.*)%/.exec(width)[1]) + parseInt(/(.*)%/.exec(left)[1]) !== 100) { //判断是否需要更新预览区
                                            updatePages(direction)
                                            turn(direction)
                                            return true
                                        }
                                    }
                                })
                                return true
                            }

                            let {width, left} = document.querySelector('.index_scroll-line__GHJXu').children[0].style //progress bar
                            if(direction === 'right' && curPage === curPageDom && idx == visiblePages.length-1) { //当前页位于分页区末页
                                if(parseInt(/(.*)%/.exec(width)[1]) + parseInt(/(.*)%/.exec(left)[1]) == 100) return true

                                updatePages(direction)
                                turn(direction)
                                return true
                            } else if(direction === 'left' && curPage === curPageDom && idx == 0) { //当前页位于首页
                                if(left == '0%') return true //判断是否需要更新分页区

                                updatePages(direction)
                                turn(direction)
                                return true
                            }
                        })
                    } else {
                        let pages = direction === 'right' ? visiblePages : visiblePages.reverse()
                        // console.log('pages', pages)
                        pages.some((curPage, idx) => {
                            if(curPage.className.includes('index_relation__pabdZ') || curPage.className.includes('index_key-relation__qe-c4')) {
                                console.log(curPage)
                                curPage.click()
                                return true
                            }

                            let {width, left} = document.querySelector('.index_scroll-line__GHJXu').children[0].style //progress bar
                            if(direction === 'right' && idx == pages.length-1 && (parseInt(/(.*)%/.exec(width)[1]) + parseInt(/(.*)%/.exec(left)[1]) !== 100)) {
                                updatePages(direction)
                                turn(direction)
                                return true

                            } else if(direction === 'left' && idx == pages.length-1 && left !== '0%') { //遍历至首页 且 需要更新分页区
                                updatePages(direction)
                                turn(direction)
                                return true

                            }
                        })
                    }

                    function updatePages(direction) {
                        let viewTurnBtn = document.querySelector('.index_scroll-bar__V5IcI').querySelectorAll('.anticon')[direction === 'left' ? 0 : 1]
                        viewTurnBtn.click()
                    }
                }
            } else if(!document.querySelector('.index_container__v2bdb').children.length && !document.querySelector('.index_small-container__aVxtr')){
                document.body.dispatchEvent(new KeyboardEvent("keydown", keydownMap[e.keyCode]));
            }
        }

        if(e.keyCode == 27) {
			if(!e.isTrusted) return
			showMessage('删除当前对象')
			document.body.dispatchEvent(new KeyboardEvent("keydown", {
				key: "Backspace",
				keyCode: 8,
				code: "Backspace",
				ctrlKey: true,
				bubbles: true,
				cancelable: true
			}))
        }
		if(e.keyCode == 90) {
			document.body.dispatchEvent(new KeyboardEvent("keyup", {
				key: "Escape",
				keyCode: 27,
				code: "Escape",
				ctrlKey: false,
				bubbles: true,
				cancelable: true
			}))
		}
    });

    window.addEventListener('mousedown', (e) => {
        if(e.button == 1) {
			e.preventDefault()
            document.body.dispatchEvent(new KeyboardEvent("keydown", {
                key: "e",
                keyCode: 69,
                code: "KeyE",
                ctrlKey: true,
                shiftKey: true,
                bubbles: true,
                cancelable: true
            }))
        }
    })


    function awaitLoad() {
        return new Promise((res, rej) => {
            let observer = new MutationObserver((mrs) => {
                // console.log(mrs)
                mrs.some((mr) => {
                    [...mr.addedNodes].some((an) => {
                        let findEl = document.querySelector('.index_container__v2bdb')
                        if(an?.nodeName === 'DIV' && findEl) {
                            // console.log(an)
                            observer.disconnect();
                            res(true)
                        }
                    })
                })
            });
            observer.observe(document, { childList: true, subtree: true})
        })
    }

    /**
     * @description 展示消息框
     * @param {string} message 展示内容
     * @param {object} [config] 配置对象
     * @param {string} [config.type='default'] 内容类型(可选值:'default'、'success'、'warning'、'error')
     * @param {number} [showTime=3000] 展示时间
     * @param {string} [direction='top]' 展示的位置(可选值:'top'、'top left'、'left'、'top right'、'right'、'bottom'、'bottom left'、'bottom right')
     * @return {void}
     */
    function showMessage(message, config) { //type = 'default', showTime = 3000, direction
        let MessageWrap = document.createElement('div')
        MessageWrap.className = 'messageWrap'
        setElStyle(MessageWrap, {
            position: 'absolute',
            zIndex: '9999'
        })

        let MessageBox = document.createElement('div')
        MessageBox.innerText = message

        let closeBtn = document.createElement('div')
        closeBtn.textContent = '×'
        closeBtn.addEventListener('click', MessageBox.remove.bind(MessageBox)) //关闭消息提示

        setElStyle(MessageBox, {
            position: 'relative',
            minWidth: '200px',
            marginTop: '5px',
            padding: '6px 50px',
            lineHeight: '25px',
            backgroundColor: 'pink',
            textAlign: 'center',
            fontSize: '16px',
            borderRadius: '5px',
            transition: 'all 1s'
        })

        setElStyle(closeBtn, {
            position: 'absolute',
            top: '-3px',
            right: '3px',
            width: '15px',
            height: '15px',
            zIndex: '999',
            fontWeight: '800',
            fontSize: '15px',
            borderRadius: '5px',
            cursor: 'pointer',
            userSelect: 'none'
        })
        //控制方向
        switch(config?.direction) {
            case 'top': setElStyle(MessageWrap, {top: '1%', left: '50%', transform: 'translateX(-50%)'}); break;
            case 'top left': setElStyle(MessageWrap, {top: '1%', left: '.5%'}); break;
            case 'left': setElStyle(MessageWrap, {top: '50%', left: '1%', transform: 'translateY(-50%)'}); break;
            case 'top right': setElStyle(MessageWrap, {top: '1%', right: '.5%', }); break;
            case 'right': setElStyle(MessageWrap, {top: '50%', right: '.5%', transform: 'translateY(-50%)'}); break;
            case 'bottom': setElStyle(MessageWrap, {bottom: '1%', left: '50%', transform: 'translateX(-50%)'}); break;
            case 'bottom left': setElStyle(MessageWrap, {bottom: '1%'}); break;
            case 'bottom right': setElStyle(MessageWrap, {bottom: '1%', right: '.5%'}); break;
            default: setElStyle(MessageWrap, {top: '1%', left: '50%', transform: 'translateX(-50%)'}); break;
        }

        switch(config?.type) {
            case 'success': setElStyle(MessageBox, {border: '1.5px solid rgb(225, 243, 216)', backgroundColor: 'rgb(240, 249, 235)', color: 'rgb(103, 194, 58)'}); break;
            case 'warning': setElStyle(MessageBox, {border: '1.5px solid rgb(250, 236, 216)', backgroundColor: 'rgb(253, 246, 236)', color: 'rgb(230, 162, 60)'}); break;
            case 'error': setElStyle(MessageBox, {border: '1.5px solid rgb(253, 226, 226)', backgroundColor: 'rgb(254, 240, 240)', color: 'rgb(245, 108, 108)'}); break;
            default: setElStyle(MessageBox, {border: '1.5px solid rgba(202, 228, 255) ', backgroundColor: 'rgba(236, 245, 255)', color: 'rgb(64, 158, 255)'}); break;
        }

        MessageBox.appendChild(closeBtn)
        let oldMessageWrap = document.querySelector('.messageWrap')
        if(oldMessageWrap) {
            oldMessageWrap.appendChild(MessageBox)
        } else {
            MessageWrap.appendChild(MessageBox)
            document.body.appendChild(MessageWrap)
        }
        let ani = MessageBox.animate([
            {
                transform: "translate(0, -100%)" ,
                opacity: 0.3,
            },
            {
                transform: "translate(0, 18px)",
                opacity: 0.7,
                offset: 0.9,
            },
            {
                transform: "translate(0, 15px)",
                opacity: 1,
                offset: 1,
            },
        ], {
            duration: 300,
            fill: 'forwards',
            easing: 'ease-out',
        })

        //控制消失
        let timer = setTimeout(() => {
            ani.onfinish = () => {
                MessageBox.remove()
            }
            ani.reverse()
        }, (config?.showTime || 3000))

        //鼠标悬停时不清除,离开时重新计时
        MessageBox.addEventListener('mouseenter', () => clearTimeout(timer))
        MessageBox.addEventListener('mouseleave', () => {
            timer = setTimeout(() => {
                ani.reverse()
                ani.onfinish = () => {
                    MessageBox.remove()
                }
            }, (config?.showTime || 3000))
        })
    }


    /**
     * @description 修改元素的css样式
     * @param {Map} styleMap 样式表
     * @return {void}
     */
    function setElStyle(...args) {
        let dataType = /\[object (.*)\]/.exec(Object.prototype.toString.call(args[0]))[1]

        if (dataType === 'Map') {
            const styleMap = args[0]
            for (const [el, styleObj] of styleMap) {
                setStyleObj(el, styleObj)
            }
        } else if (/HTML.*Element/.test(dataType)) {
            const [el, styleObj] = args
            setStyleObj(el, styleObj)
        }

        function setStyleObj(el, styleObj) {
            for (let attr in styleObj) {
                if (el.style[attr] !== undefined) { //检查是否存在该CSS属性
                    el.style[attr] = styleObj[attr]
                } else {
                    //将key转为标准css属性名
                    let formatAttr = attr.replace(/[A-Z]/, (match) => `-${match.toLowerCase()}`)
                    console.error(el, `的 ${formatAttr} CSS属性设置失败!`)
                }
            }
        }
    }


    function Observe(target, callBack, options = { childList: true, subtree: true, attributes: true, attributeOldValue: true}) {
        let ob = new MutationObserver(callBack);
        ob.observe(target, options);
        return ob
    }

    function $(sel) {
        return document.querySelector(sel)
    }
/**
==日志===
2024/2/1
- 新增:个体属性面板自动保存

2024/2/3
- 新增:快捷键【Q】俯视角左调
- 新增:快捷键【E】俯视角右调
- 新增:快捷键【鼠标中键】进入/退出三视图编辑模式
- 新增:快捷键【空格】帧数前切(选中个体时,控制在关联帧范围;三视图总览表→批量前切)
- 新增:快捷键【`】帧数后切(选中个体时,控制在关联帧范围;三视图总览表→批量后切)
- 新增:快捷键【Ctrl + Y】 开启/关闭自动删除

2024/2/4
- 新增:快捷键【P】开启/关闭快捷键自定义面板

2024/2/19
- 新增:首次进入时,默认选中 展示映射、自车轨迹、行车轨迹
- 新增:放大三视图时,自动显示快捷属性
- 新增:快捷键【1】前进一帧
- 新增:快捷键【2】向后一帧(Ctrl →)
- 新增:快捷键【3】向前一帧(Ctrl ←)

2024/2/20
- 新增:批量重检
- 新增:选中个体时默认平移模式
- 新增:双击调出/关闭 三视图预览表

2024/2/22
- 调整:俯视角左调设为Q时,只在三视图放大时生效,避免与聚焦功能冲突
- 调整:取消 【2】和【3】
- 新增:【Alt + `】向后一帧(Ctrl ←)
- 新增:【Alt + `】向前一帧(Ctrl →)
- 新增:【ESC】删除当前对象
- 新增:右键菜单追踪鼠标,实现快捷复制。点击复制项的左半边 向前复制1帧,点击右半边 向后复制1帧

2024/2/23
- 新增:简化点云图右键菜单项,并调整复制和删除的位置
- 修复:缩放网页大小引起的鼠标中间默认行为触发
- 新增:双击右键菜单删除项可删除全帧对象
- 新增:复制对象后的消息框反馈和距离提示

2024/2/24
- 调整:取消右键菜单的简化操作(当右键非菜单区域时,会导致平台崩溃)
- 新增:几何属性的颜色提示
- 新增:点数筛选
- 新增:快捷属性标签锁定

2024/2/25
- 新增:个体属性面板展示几何信息
- 新增:三视图放大下的点数验证,并给出颜色提示

2024/3/1
- 新增:点云小视图尺寸调整

2024/3/6
- 新增:【Z】失焦个体
- 修复:首次进入三视图放大模式,保存失效和点云图小窗调整失效
- 调整:点数Label和几何面板位置
=========
**/
})();