├── .flake8 ├── .github └── workflows │ └── release.yml ├── .gitignore ├── DEVELOP.md ├── LICENSE ├── README.md ├── docs ├── api.md ├── imgs │ ├── android.png │ ├── harmony.png │ ├── ios.png │ └── show.gif └── treeData │ ├── android.json │ ├── harmony.json │ └── ios.json ├── poetry.lock ├── pyproject.toml └── uiviewer ├── __init__.py ├── __main__.py ├── _device.py ├── _logger.py ├── _models.py ├── _utils.py ├── _version.py ├── cli.py ├── parser ├── __init.py ├── android_hierarchy.py ├── harmony_hierarchy.py ├── ios_hierarchy.py ├── utils.py └── xpath_lite.py ├── routers ├── __init__.py └── api.py └── static ├── cdn ├── cdn.jsdelivr.net │ └── npm │ │ └── vue@2 ├── code.jquery.com │ └── jquery-3.6.0.min.js └── unpkg.com │ ├── element-theme-dark@1.0.2 │ └── lib │ │ ├── fonts │ │ ├── element-icons.ttf │ │ └── element-icons.woff │ │ └── index.css │ └── element-ui │ └── lib │ └── index.js ├── css └── style.css ├── favicon.ico ├── index.html └── js ├── api.js ├── config.js ├── index.js └── utils.js /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 150 3 | ignore = 4 | E501 5 | F401 6 | E402 7 | W292 8 | F403 9 | F821 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # on: 4 | # release: 5 | # types: [created] 6 | on: 7 | push: 8 | tags: 9 | - '*.*.*' 10 | 11 | jobs: 12 | build-n-publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel twine poetry 26 | poetry lock --no-update 27 | poetry install 28 | 29 | - name: Get the version from the tag 30 | id: get_version 31 | run: echo "::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}" 32 | 33 | - name: Update version in pyproject.toml 34 | run: | 35 | version=${{ steps.get_version.outputs.VERSION }} 36 | poetry version $version 37 | 38 | - name: Build a binary wheel and a source tarball 39 | run: poetry build 40 | 41 | - name: Publish distribution 📦 to PyPI 42 | uses: pypa/gh-action-pypi-publish@release/v1 43 | with: 44 | password: ${{ secrets.PYPI_API_TOKEN }} 45 | packages-dir: dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | share/python-wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .nox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | *.py,cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | cover/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | db.sqlite3-journal 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | .pybuilder/ 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | # For a library or package, you might want to ignore these files since the code is 85 | # intended to run in multiple environments; otherwise, check them in: 86 | # .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # poetry 96 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 97 | # This is especially recommended for binary packages to ensure reproducibility, and is more 98 | # commonly ignored for libraries. 99 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 100 | #poetry.lock 101 | 102 | # pdm 103 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 104 | #pdm.lock 105 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 106 | # in version control. 107 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 108 | .pdm.toml 109 | .pdm-python 110 | .pdm-build/ 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | .DS_Store 162 | prompt.txt 163 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | 2 | # Tech Stack 3 | 4 | - python3 5 | - html/css/js 6 | - fastapi 7 | - vue 8 | - element-ui 9 | 10 | 11 | # Build 12 | ``` 13 | pip3 install poetry 14 | 15 | git clone git@github.com:codematrixer/ui-viewer.git 16 | cd ui-viewer 17 | 18 | poetry lock --no-update 19 | poetry install 20 | poetry build 21 | ``` 22 | 23 | # Run 24 | ``` 25 | poetry run python3 -m uiviewer 26 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 matrixer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ui-viewer 2 | [![github actions](https://github.com/codematrixer/ui-viewer/actions/workflows/release.yml/badge.svg)](https://github.com/codematrixer/ui-viewer/actions) 3 | [![pypi version](https://img.shields.io/pypi/v/uiviewer.svg)](https://pypi.python.org/pypi/uiviewer) 4 | ![python](https://img.shields.io/pypi/pyversions/uiviewer.svg) 5 | 6 | UI hierarchy inspector for Mobile App, supporting `Android`, `iOS`, and `HarmonyOS NEXT`. 7 | 8 | Its features include: 9 | 10 | - visualize the UI hierarchy via screenshot and tree structure. 11 | - view element properties 12 | - auto generate XPath or XPathLite 13 | - auto generate coordinate percentages. 14 | - and more… 15 | 16 | 17 | This project is developed using FastAPI and Vue. It starts locally and displays UI hierarchy through web browser. 18 | 19 | ![show](https://i.ibb.co/Phfm9Q1/show.gif) 20 | 21 | # Installation 22 | - python3.8+ 23 | 24 | ```shell 25 | pip3 install -U uiviewer 26 | ``` 27 | 28 | # Run 29 | Run the following command on the terminal. (default port `8000`) 30 | 31 | ```shell 32 | uiviewer 33 | # or 34 | python3 -m uiviewer 35 | 36 | INFO: Started server process [46814] 37 | INFO: Waiting for application startup. 38 | INFO: Application startup complete. 39 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 40 | INFO: 127.0.0.1:55080 - "GET / HTTP/1.1" 307 Temporary Redirect 41 | INFO: 127.0.0.1:55080 - "GET /static/index.html HTTP/1.1" 200 OK 42 | INFO: 127.0.0.1:55080 - "GET /static/css/style.css HTTP/1.1" 200 OK 43 | INFO: 127.0.0.1:55080 - "GET /static/js/index.js HTTP/1.1" 200 OK 44 | INFO: 127.0.0.1:55080 - "GET /static/js/api.js HTTP/1.1" 200 OK 45 | INFO: 127.0.0.1:55082 - "GET /static/js/utils.js HTTP/1.1" 200 OK 46 | INFO: 127.0.0.1:55082 - "GET /static/js/config.js HTTP/1.1" 200 OK 47 | INFO: 127.0.0.1:55082 - "GET /version HTTP/1.1" 200 OK 48 | ``` 49 | And then open the browser to [http://localhost:8000](http://localhost:8000) 50 | 51 | You can also customize port to start the service. 52 | ```shell 53 | uiviewer -p 54 | # or 55 | python3 -m uiviewer -p 56 | 57 | ``` 58 | 59 | # Environment 60 | If you need to connect to a remote HDC Server or ADB server for remote device debugging, you must set the required environment variables before starting uiviewer. 61 | 62 | HarmonyOS 63 | ```bash 64 | export HDC_SERVER_HOST=127.0.0.1 # Replace with the remote host 65 | export HDC_SERVER_PORT=8710 66 | ``` 67 | 68 | Android 69 | ```bash 70 | export ANDROID_ADB_SERVER_HOST=127.0.0.1 # Replace with the remote host 71 | export ANDROID_ADB_SERVER_PORT=5037 72 | ``` 73 | 74 | If you want to remove Environment Variables, To unset the environment variables: 75 | ```bash 76 | unset HDC_SERVER_HOST 77 | unset HDC_SERVER_PORT 78 | 79 | unset ANDROID_ADB_SERVER_HOST 80 | unset ANDROID_ADB_SERVER_PORT 81 | ``` 82 | 83 | 84 | # Tips 85 | - If you are using a virtual environment, please make sure to activate it before running the command. 86 | 87 | - On iOS, please ensure that WDA is successfully started and wda port forwarding is successful in advance. 88 | - First, Use `xcode` or `tidevice` or `go-ios` to launch wda. 89 | ``` 90 | tidevice xctest -B 91 | ``` 92 | - Second, Use `tidevice` or `iproxy` to forward the wda port,and keep it running. 93 | ``` 94 | tidevice relay 8100 8100 95 | ``` 96 | - And then, To ensure the success of the browser to access `http://localhost:8100/status`, return like this: 97 | ``` 98 | { 99 | "value": { 100 | "build": { 101 | "productBundleIdentifier": "com.facebook.WebDriverAgentRunner", 102 | "time": "Mar 25 2024 15:17:30" 103 | }, 104 | ... 105 | "state": "success", 106 | "ready": true 107 | }, 108 | "sessionId": null 109 | } 110 | ``` 111 | - Finally, Input the **`wdaUrl`** in the web page, such as `http://localhost:8100` 112 | 113 | - On iOS,WDA can easily freeze when dumping high UI hierarchy. You can reduce the **`maxDepth`** on the web page. The default is 30. 114 | 115 | # Preview 116 | - HarmonyOS 117 | ![harmony](https://i.ibb.co/82BrJ1H/harmony.png) 118 | 119 | - Android 120 | ![android](https://i.ibb.co/RySs497/android.png) 121 | 122 | - iOS 123 | ![ios](https://i.ibb.co/VVWtTS3/ios.png) 124 | 125 | 126 | # Relevant 127 | - https://github.com/codematrixer/hmdriver2 128 | - https://github.com/openatx/uiautomator2 129 | - https://github.com/openatx/facebook-wda 130 | - https://github.com/alibaba/web-editor 131 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codematrixer/ui-viewer/8e439370ed53f820a5637904df72a25812ff154a/docs/api.md -------------------------------------------------------------------------------- /docs/imgs/android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codematrixer/ui-viewer/8e439370ed53f820a5637904df72a25812ff154a/docs/imgs/android.png -------------------------------------------------------------------------------- /docs/imgs/harmony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codematrixer/ui-viewer/8e439370ed53f820a5637904df72a25812ff154a/docs/imgs/harmony.png -------------------------------------------------------------------------------- /docs/imgs/ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codematrixer/ui-viewer/8e439370ed53f820a5637904df72a25812ff154a/docs/imgs/ios.png -------------------------------------------------------------------------------- /docs/imgs/show.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codematrixer/ui-viewer/8e439370ed53f820a5637904df72a25812ff154a/docs/imgs/show.gif -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "uiviewer" 3 | version = "1.0.1" 4 | description = "UI hierarchy visualization tool, supporting Android, iOS, HarmonyOS NEXT." 5 | authors = ["codematrixer "] 6 | license = "MIT" 7 | readme = "README.md" 8 | include = ["*/static/*"] 9 | 10 | [tool.poetry.scripts] 11 | uiviewer = "uiviewer.cli:main" 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.8" 15 | uvicorn = "^0.15.0" 16 | fastapi = "^0.68.0" 17 | aiofiles = "^23.1.0" 18 | uiautomator2 = "^3.0.0" 19 | facebook-wda = "^1.0.5" 20 | tidevice = "^0.12.10" 21 | hmdriver2 = "^1.4.0" 22 | 23 | [tool.poetry.extras] 24 | 25 | [tool.poetry.group.dev.dependencies] 26 | pytest = "^8.3.2" 27 | 28 | [build-system] 29 | requires = ["poetry-core>=1.0.0"] 30 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /uiviewer/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /uiviewer/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import webbrowser 5 | import uvicorn 6 | import threading 7 | 8 | from fastapi import FastAPI, Request, HTTPException 9 | from fastapi.staticfiles import StaticFiles 10 | from fastapi.responses import JSONResponse 11 | 12 | from uiviewer.routers import api 13 | from uiviewer._models import ApiResponse 14 | 15 | 16 | app = FastAPI() 17 | 18 | 19 | current_dir = os.path.dirname(os.path.abspath(__file__)) 20 | static_dir = os.path.join(current_dir, "static") 21 | 22 | app.mount("/static", StaticFiles(directory=static_dir), name="static") 23 | 24 | app.include_router(api.router) 25 | 26 | 27 | @app.exception_handler(Exception) 28 | def global_exception_handler(request: Request, exc: Exception): 29 | return JSONResponse( 30 | status_code=500, 31 | content=ApiResponse(success=False, message=str(exc)).dict() 32 | ) 33 | 34 | 35 | @app.exception_handler(HTTPException) 36 | def http_exception_handler(request: Request, exc: HTTPException): 37 | return JSONResponse( 38 | status_code=exc.status_code, 39 | content=ApiResponse(success=False, message=exc.detail).dict() 40 | ) 41 | 42 | 43 | def open_browser(port): 44 | webbrowser.open_new(f"http://127.0.0.1:{port}") 45 | 46 | 47 | def run(port=8000): 48 | timer = threading.Timer(1.0, open_browser, args=[port]) 49 | timer.daemon = True 50 | timer.start() 51 | 52 | uvicorn.run(app, host="127.0.0.1", port=port) 53 | 54 | 55 | if __name__ == "__main__": 56 | run() -------------------------------------------------------------------------------- /uiviewer/_device.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import abc 4 | import os 5 | import traceback 6 | import tempfile 7 | from typing import List, Dict, Union, Tuple 8 | from functools import cached_property # python3.8+ 9 | 10 | from PIL import Image 11 | from requests import request 12 | import tidevice 13 | import adbutils 14 | import wda 15 | import uiautomator2 as u2 16 | from hmdriver2 import hdc 17 | from fastapi import HTTPException 18 | 19 | from uiviewer._logger import logger 20 | from uiviewer._utils import file2base64, image2base64 21 | from uiviewer._models import Platform, BaseHierarchy 22 | from uiviewer.parser import android_hierarchy, ios_hierarchy, harmony_hierarchy 23 | 24 | 25 | def list_serials(platform: str) -> List[str]: 26 | devices = [] 27 | if platform == Platform.ANDROID: 28 | raws = adbutils.AdbClient().device_list() 29 | devices = [item.serial for item in raws] 30 | elif platform == Platform.IOS: 31 | raw = tidevice.Usbmux().device_list() 32 | devices = [d.udid for d in raw] 33 | else: 34 | devices = hdc.list_devices() 35 | 36 | return devices 37 | 38 | 39 | class DeviceMeta(metaclass=abc.ABCMeta): 40 | 41 | @abc.abstractmethod 42 | def take_screenshot(self) -> str: 43 | pass 44 | 45 | def dump_hierarchy(self) -> Dict: 46 | pass 47 | 48 | 49 | class HarmonyDevice(DeviceMeta): 50 | def __init__(self, serial: str): 51 | self.serial = serial 52 | self.hdc = hdc.HdcWrapper(serial) 53 | 54 | @cached_property 55 | def _display_size(self) -> Tuple: 56 | return self.hdc.display_size() 57 | 58 | def take_screenshot(self) -> str: 59 | temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png") 60 | try: 61 | # adapt windows 62 | temp_file.close() 63 | path = temp_file.name 64 | self.hdc.screenshot(path) 65 | return file2base64(path) 66 | finally: 67 | if os.path.exists(path): 68 | os.remove(path) 69 | 70 | def dump_hierarchy(self) -> BaseHierarchy: 71 | packageName, pageName = self.hdc.current_app() 72 | raw: Dict = self.hdc.dump_hierarchy() 73 | hierarchy: Dict = harmony_hierarchy.convert_harmony_hierarchy(raw) 74 | return BaseHierarchy( 75 | jsonHierarchy=hierarchy, 76 | activityName=pageName, 77 | packageName=packageName, 78 | windowSize=self._display_size, 79 | scale=1 80 | ) 81 | 82 | 83 | class AndroidDevice(DeviceMeta): 84 | def __init__(self, serial: str): 85 | self.serial = serial 86 | self.d: u2.Device = u2.connect(serial) 87 | 88 | @cached_property 89 | def _window_size(self) -> Tuple: 90 | return self.d.window_size() 91 | 92 | def take_screenshot(self) -> str: 93 | img: Image.Image = self.d.screenshot() 94 | return image2base64(img) 95 | 96 | def dump_hierarchy(self) -> BaseHierarchy: 97 | current = self.d.app_current() 98 | page_xml = self.d.dump_hierarchy() 99 | page_json = android_hierarchy.convert_android_hierarchy(page_xml) 100 | return BaseHierarchy( 101 | jsonHierarchy=page_json, 102 | activityName=current['activity'], 103 | packageName=current['package'], 104 | windowSize=self._window_size, 105 | scale=1 106 | ) 107 | 108 | 109 | class IosDevice(DeviceMeta): 110 | def __init__(self, udid: str, wda_url: str, max_depth: int) -> None: 111 | self.udid = udid 112 | self.wda_url = wda_url 113 | self._max_depth = max_depth 114 | self.client = wda.Client(wda_url) 115 | 116 | @property 117 | def max_depth(self) -> int: 118 | return int(self._max_depth) if self._max_depth else 30 119 | 120 | @cached_property 121 | def scale(self) -> int: 122 | return self.client.scale 123 | 124 | @cached_property 125 | def _window_size(self) -> Tuple: 126 | return self.client.window_size() 127 | 128 | def _check_wda_health(self) -> bool: 129 | resp = request("GET", f"{self.wda_url}/status", timeout=5).json() 130 | state = resp.get("value", {}).get("state") 131 | return state == "success" 132 | 133 | def take_screenshot(self) -> str: 134 | img: Image.Image = self.client.screenshot() 135 | return image2base64(img) 136 | 137 | def _current_bundle_id(self) -> str: 138 | resp = request("GET", f"{self.wda_url}/wda/activeAppInfo", timeout=10).json() 139 | bundleId = resp.get("value", {}).get("bundleId", None) 140 | return bundleId 141 | 142 | def dump_hierarchy(self) -> BaseHierarchy: 143 | self.client.appium_settings({"snapshotMaxDepth": self.max_depth}) 144 | data: Dict = self.client.source(format="json") 145 | hierarchy: Dict = ios_hierarchy.convert_ios_hierarchy(data, self.scale) 146 | return BaseHierarchy( 147 | jsonHierarchy=hierarchy, 148 | activityName=None, 149 | packageName=self._current_bundle_id(), 150 | windowSize=self._window_size, 151 | scale=self.scale 152 | ) 153 | 154 | 155 | def get_device(platform: str, serial: str, wda_url: str, max_depth: int) -> Union[HarmonyDevice, AndroidDevice, IosDevice]: 156 | if platform == Platform.HARMONY: 157 | return HarmonyDevice(serial) 158 | elif platform == Platform.ANDROID: 159 | return AndroidDevice(serial) 160 | else: 161 | return IosDevice(serial, wda_url, max_depth) 162 | 163 | 164 | # Global cache for devices 165 | cached_devices = {} 166 | 167 | 168 | def init_device(platform: str, serial: str, wda_url: str, max_depth: int): 169 | 170 | if serial not in list_serials(platform): 171 | logger.error(f"Device<{serial}> not found") 172 | raise HTTPException(status_code=500, detail=f"Device<{serial}> not found") 173 | 174 | try: 175 | device: Union[HarmonyDevice, AndroidDevice] = get_device(platform, serial, wda_url, max_depth) 176 | cached_devices[(platform, serial)] = device 177 | 178 | if isinstance(device, IosDevice): 179 | return device._check_wda_health() 180 | except Exception as e: 181 | logger.error(traceback.format_exc()) 182 | raise HTTPException(status_code=500, detail=str(e)) 183 | 184 | return True -------------------------------------------------------------------------------- /uiviewer/_logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | 5 | formatter = logging.Formatter('[%(asctime)s] %(filename)15s[line:%(lineno)4d] \ 6 | [%(levelname)s] %(message)s', 7 | datefmt='%Y-%m-%d %H:%M:%S') 8 | 9 | logger = logging.getLogger('hmdriver2') 10 | logger.setLevel(logging.DEBUG) 11 | 12 | console_handler = logging.StreamHandler() 13 | console_handler.setLevel(logging.DEBUG) 14 | console_handler.setFormatter(formatter) 15 | 16 | logger.addHandler(console_handler) 17 | 18 | 19 | __all__ = ['logger'] 20 | -------------------------------------------------------------------------------- /uiviewer/_models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import enum 4 | 5 | from pydantic import BaseModel 6 | from typing import Any, Union, Dict, Tuple, Optional 7 | 8 | 9 | class Platform(str, enum.Enum): 10 | ANDROID = "android" 11 | IOS = "ios" 12 | HARMONY = "harmony" 13 | 14 | 15 | class ApiResponse(BaseModel): 16 | success: bool = True 17 | data: Any = None 18 | message: Union[str] = None 19 | 20 | @classmethod 21 | def doSuccess(cls, data): 22 | return ApiResponse(success=True, data=data, message=None) 23 | 24 | @classmethod 25 | def doError(cls, message): 26 | return ApiResponse(success=False, data=None, message=message) 27 | 28 | 29 | class BaseHierarchy(BaseModel): 30 | jsonHierarchy: Optional[Dict] = None 31 | windowSize: Tuple[int, int] 32 | scale: int = 1 33 | activityName: Optional[str] = None 34 | packageName: Optional[str] = None 35 | 36 | 37 | class XPathLiteRequest(BaseModel): 38 | tree_data: Dict[str, Any] 39 | node_id: str -------------------------------------------------------------------------------- /uiviewer/_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import base64 4 | import json 5 | from typing import Dict 6 | from PIL import Image 7 | from io import BytesIO 8 | 9 | from uiviewer._logger import logger 10 | 11 | 12 | def file2base64(path: str) -> str: 13 | with open(path, "rb") as file: 14 | base64_encoded = base64.b64encode(file.read()) 15 | return base64_encoded.decode('utf-8') 16 | 17 | 18 | def image2base64(image: Image.Image, format: str = "PNG") -> str: 19 | """ 20 | PIL Image to base64 string 21 | """ 22 | buffered = BytesIO() 23 | image.save(buffered, format=format) 24 | return base64.b64encode(buffered.getvalue()).decode('utf-8') 25 | 26 | 27 | def str2json(s: str) -> Dict: 28 | try: 29 | json_obj = json.loads(s) 30 | return json_obj 31 | except json.JSONDecodeError as e: 32 | logger.error(f"Invalid JSON data: {e}") 33 | return {} -------------------------------------------------------------------------------- /uiviewer/_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import importlib.metadata 4 | 5 | __version__ = importlib.metadata.version('uiviewer') -------------------------------------------------------------------------------- /uiviewer/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import argparse 4 | from uiviewer.__main__ import run 5 | 6 | 7 | def main(): 8 | parser = argparse.ArgumentParser(description="My CLI Tool") 9 | parser.add_argument('-p', '--port', type=int, default=8000, help='local listen port for uiviewer') 10 | args = parser.parse_args() 11 | run(port=args.port) 12 | 13 | 14 | if __name__ == "__main__": 15 | main() -------------------------------------------------------------------------------- /uiviewer/parser/__init.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /uiviewer/parser/android_hierarchy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import xml.dom.minidom 4 | import uuid 5 | from typing import Dict 6 | 7 | from uiviewer.parser.utils import parse_bounds, safe_xmlstr, str2bool, str2int, convstr 8 | 9 | __alias = { 10 | 'class': '_type', 11 | 'resource-id': 'resourceId', 12 | 'content-desc': 'description', 13 | 'long-clickable': 'longClickable', 14 | 'bounds': 'rect', 15 | } 16 | 17 | __parsers = { 18 | '_type': safe_xmlstr, # node className 19 | # Android 20 | 'rect': parse_bounds, 21 | 'text': convstr, 22 | 'resourceId': convstr, 23 | 'package': convstr, 24 | 'checkable': str2bool, 25 | 'scrollable': str2bool, 26 | 'focused': str2bool, 27 | 'clickable': str2bool, 28 | 'selected': str2bool, 29 | 'longClickable': str2bool, 30 | 'focusable': str2bool, 31 | 'password': str2bool, 32 | 'index': int, 33 | 'description': convstr, 34 | # iOS 35 | 'name': convstr, 36 | 'label': convstr, 37 | 'x': str2int, 38 | 'y': str2int, 39 | 'width': str2int, 40 | 'height': str2int, 41 | # iOS && Android 42 | 'enabled': str2bool, 43 | } 44 | 45 | 46 | def _parse_node_attributes(node): 47 | attributes = {} 48 | for key, value in node.attributes.items(): 49 | key = __alias.get(key, key) 50 | parser = __parsers.get(key) 51 | if value is None: 52 | attributes[key] = None 53 | elif parser: 54 | attributes[key] = parser(value) 55 | return attributes 56 | 57 | 58 | def _parse_uiautomator_node(node): 59 | attributes = _parse_node_attributes(node) 60 | if 'bounds' in attributes: 61 | lx, ly, rx, ry = map(int, attributes.pop('bounds')) 62 | attributes['rect'] = dict(x=lx, y=ly, width=rx - lx, height=ry - ly) 63 | return attributes 64 | 65 | 66 | def convert_android_hierarchy(page_xml: str) -> Dict: 67 | dom = xml.dom.minidom.parseString(page_xml) 68 | root = dom.documentElement 69 | 70 | def __travel(node, parent_id=""): 71 | if node.attributes is None: 72 | return 73 | json_node = _parse_uiautomator_node(node) 74 | json_node['_id'] = str(uuid.uuid4()) 75 | json_node['_parentId'] = parent_id 76 | json_node['xpath'] = "" 77 | json_node.pop("package", None) 78 | if node.childNodes: 79 | children = [] 80 | for n in node.childNodes: 81 | child = __travel(n, json_node['_id']) 82 | if child: 83 | children.append(child) 84 | json_node['children'] = children 85 | 86 | # Sort the keys 87 | keys_order = ['xpath', '_type', 'resourceId', 'text', 'description'] 88 | sorted_node = {k: json_node[k] for k in keys_order if k in json_node} 89 | sorted_node.update({k: json_node[k] for k in json_node if k not in keys_order}) 90 | 91 | return sorted_node 92 | 93 | return __travel(root) -------------------------------------------------------------------------------- /uiviewer/parser/harmony_hierarchy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import uuid 4 | from typing import Dict 5 | 6 | 7 | def convert_harmony_hierarchy(data: Dict) -> Dict: 8 | ret = {"_id": str(uuid.uuid4()), "children": [], "_parentId": ""} 9 | 10 | def __travel(node_a, parent_id=""): 11 | node_b = { 12 | "index": 0, 13 | "text": "", 14 | "id": "", 15 | "_type": "", 16 | "description": "", 17 | "checkable": False, 18 | "clickable": False, 19 | "enabled": False, 20 | "focusable": False, 21 | "focused": False, 22 | "scrollable": False, 23 | "longClickable": False, 24 | "password": False, 25 | "selected": False, 26 | "rect": { 27 | "x": 0, 28 | "y": 0, 29 | "width": 0, 30 | "height": 0 31 | }, 32 | "_id": str(uuid.uuid4()), 33 | "_parentId": parent_id, 34 | "xpath": "" 35 | } 36 | 37 | attributes = node_a.get("attributes", {}) 38 | node_b["xpath"] = attributes.get("xpath", "") 39 | node_b["_type"] = attributes.get("type", "") 40 | node_b["id"] = attributes.get("id", "") 41 | node_b["description"] = attributes.get("description", "") 42 | node_b["text"] = attributes.get("text", "") 43 | node_b["checkable"] = attributes.get("checkable", "").lower() == "true" 44 | node_b["clickable"] = attributes.get("clickable", "").lower() == "true" 45 | node_b["enabled"] = attributes.get("enabled", "").lower() == "true" 46 | node_b["focusable"] = attributes.get("focusable", "").lower() == "true" 47 | node_b["focused"] = attributes.get("focused", "").lower() == "true" 48 | node_b["scrollable"] = attributes.get("scrollable", "").lower() == "true" 49 | node_b["longClickable"] = attributes.get("longClickable", "").lower() == "true" 50 | bounds = attributes.get("bounds", "") 51 | bounds = bounds.strip("[]").split("][") 52 | node_b["rect"]["x"] = int(bounds[0].split(",")[0]) 53 | node_b["rect"]["y"] = int(bounds[0].split(",")[1]) 54 | node_b["rect"]["width"] = int(bounds[1].split(",")[0]) - int(bounds[0].split(",")[0]) 55 | node_b["rect"]["height"] = int(bounds[1].split(",")[1]) - int(bounds[0].split(",")[1]) 56 | 57 | children = node_a.get("children", []) 58 | if children: 59 | node_b["children"] = [__travel(child, node_b["_id"]) for child in children] 60 | 61 | return node_b 62 | 63 | # Recursively convert children of a to match b's structure 64 | ret["children"] = [__travel(child, ret["_id"]) for child in data.get("children", [])] 65 | 66 | return ret -------------------------------------------------------------------------------- /uiviewer/parser/ios_hierarchy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import uuid 4 | from typing import Dict 5 | 6 | 7 | def convert_ios_hierarchy(data: Dict, scale: int) -> Dict: 8 | 9 | def __travel(node, parent_id=""): 10 | node['_id'] = str(uuid.uuid4()) 11 | node['_parentId'] = parent_id 12 | node['_type'] = node.pop('type', "null") 13 | node['id'] = node.pop('rawIdentifier', "null") 14 | node['xpath'] = "" 15 | if 'rect' in node: 16 | rect = node['rect'] 17 | node['rect'] = {k: v * scale for k, v in rect.items()} 18 | 19 | # Recursively process children nodes 20 | if 'children' in node: 21 | node['children'] = [__travel(child, node['_id']) for child in node['children']] 22 | 23 | # Sort the keys 24 | keys_order = ['xpath', '_type', 'label', 'name', 'id', 'value'] 25 | sorted_node = {k: node[k] for k in keys_order if k in node} 26 | sorted_node.update({k: node[k] for k in node if k not in keys_order}) 27 | 28 | return sorted_node 29 | 30 | return __travel(data) 31 | -------------------------------------------------------------------------------- /uiviewer/parser/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | 5 | 6 | def parse_bounds(text): 7 | m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', text) 8 | if m is None: 9 | return None 10 | (lx, ly, rx, ry) = map(int, m.groups()) 11 | return dict(x=lx, y=ly, width=rx - lx, height=ry - ly) 12 | 13 | 14 | def safe_xmlstr(s): 15 | return s.replace("$", "-") 16 | 17 | 18 | def str2bool(v): 19 | return v.lower() in ("yes", "true", "t", "1") 20 | 21 | 22 | def str2int(v): 23 | return int(v) 24 | 25 | 26 | def convstr(v): 27 | return v -------------------------------------------------------------------------------- /uiviewer/parser/xpath_lite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from typing import Dict 4 | 5 | 6 | class XPathLiteGenerator: 7 | def __init__(self, platform: str, treedata: Dict): 8 | """ 9 | Initializes the XPathLiteGenerator class. 10 | 11 | Args: 12 | platform (str): The platform type (e.g., 'ios', 'android'). 13 | treedata (Dict): The JSON tree structure data. 14 | 15 | Returns: 16 | None 17 | """ 18 | self.platform = platform 19 | self.treedata = treedata 20 | self.node_map = self._build_node_map(treedata) 21 | 22 | def _build_node_map(self, node: Dict, node_map: Dict[str, Dict] = None) -> Dict[str, Dict]: 23 | """ 24 | Builds a node map for quick lookup by node _id. 25 | 26 | Args: 27 | node (Dict): The current node. 28 | node_map (Dict[str, Dict], optional): The node map. Default is an empty dictionary. 29 | 30 | Returns: 31 | Dict[str, Dict]: The node map. 32 | """ 33 | if node_map is None: 34 | node_map = {} 35 | node_map[node["_id"]] = node 36 | if "children" in node: 37 | for child in node["children"]: 38 | self._build_node_map(child, node_map) 39 | return node_map 40 | 41 | def _find_node_by_id(self, target_id: str) -> Dict: 42 | """ 43 | Finds a node by its ID. 44 | 45 | Args: 46 | target_id (str): The target node ID. 47 | 48 | Returns: 49 | Dict: The target node. 50 | """ 51 | return self.node_map.get(target_id) 52 | 53 | def _get_value(self, node: Dict) -> str: 54 | """ 55 | Gets the specific attribute value of a node to generate part of the XPath expression. 56 | 57 | Args: 58 | node (Dict): The current node. 59 | 60 | Returns: 61 | str: Part of the XPath expression. 62 | """ 63 | if node.get("resourceId"): 64 | return f'//*[@resource-id="{node["resourceId"]}"]' 65 | elif node.get("text"): 66 | return f'//*[@text="{node["text"]}"]' 67 | elif node.get("description"): 68 | return f'//*[@content-desc="{node["description"]}"]' 69 | elif node.get("label"): 70 | return f'//*[@label="{node["label"]}"]' 71 | elif node.get("name"): 72 | return f'//*[@name="{node["name"]}"]' 73 | elif node.get("id"): # harmonyos, ios 74 | return f'//*[@id="{node["id"]}"]' 75 | return None 76 | 77 | def _build_xpath(self, node: Dict, path: str, found_value: bool = False) -> str: 78 | """ 79 | Recursively builds the XPath expression. 80 | 81 | Args: 82 | node (Dict): The current node. 83 | path (str): The current path. 84 | found_value (bool, optional): Whether a specific attribute value has been found. Default is False. 85 | 86 | Returns: 87 | str: The complete XPath expression. 88 | """ 89 | if not node: 90 | return path 91 | value = self._get_value(node) 92 | if value: 93 | found_value = True 94 | return value + path 95 | if self.platform == 'ios' and not (node.get("lable") or node.get("name")): 96 | # If the platform is iOS and the node does not have lable, name, build from root 97 | return self._build_from_root(node, path) 98 | 99 | parent_node = self._find_node_by_id(node["_parentId"]) 100 | if parent_node: 101 | siblings = parent_node.get("children", []) 102 | index = 1 103 | for sibling in siblings: 104 | if sibling["_type"] == node["_type"]: 105 | if sibling["_id"] == node["_id"]: 106 | break 107 | index += 1 108 | path = f'/{node["_type"]}[{index}]' + path 109 | return self._build_xpath(parent_node, path, found_value) 110 | return path 111 | 112 | def _build_from_root(self, node: Dict, path: str) -> str: 113 | """ 114 | Builds the XPath expression from the root node. 115 | 116 | Args: 117 | node (Dict): The current node. 118 | path (str): The current path. 119 | 120 | Returns: 121 | str: The complete XPath expression. 122 | """ 123 | if "_type" in node: 124 | parent_node = self._find_node_by_id(node["_parentId"]) 125 | if parent_node: 126 | siblings = parent_node.get("children", []) 127 | index = 1 128 | for sibling in siblings: 129 | if sibling["_type"] == node["_type"]: 130 | if sibling["_id"] == node["_id"]: 131 | break 132 | index += 1 133 | path = f'/{node["_type"]}[{index}]' + path 134 | return self._build_from_root(parent_node, path) 135 | return '//' + path.lstrip('/') 136 | 137 | def get_xpathLite(self, target_id: str) -> str: 138 | """ 139 | Gets the XPathLite path for the target node. 140 | 141 | Args: 142 | target_id (str): The target node ID. 143 | 144 | Returns: 145 | str: The XPathLite path. 146 | """ 147 | target_node = self._find_node_by_id(target_id) 148 | if not target_node: 149 | return None 150 | 151 | xpath_lite = self._build_xpath(target_node, "") 152 | if not xpath_lite.startswith('//*[@'): 153 | xpath_lite = self._build_from_root(target_node, "") 154 | 155 | return xpath_lite -------------------------------------------------------------------------------- /uiviewer/routers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /uiviewer/routers/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from typing import Union, Dict, Any 4 | 5 | from fastapi import APIRouter, Query 6 | from fastapi.responses import RedirectResponse 7 | 8 | from uiviewer._device import ( 9 | list_serials, 10 | init_device, 11 | cached_devices, 12 | AndroidDevice, 13 | IosDevice, 14 | HarmonyDevice 15 | ) 16 | from uiviewer._version import __version__ 17 | from uiviewer._models import ApiResponse, XPathLiteRequest 18 | from uiviewer.parser.xpath_lite import XPathLiteGenerator 19 | 20 | 21 | router = APIRouter() 22 | 23 | 24 | @router.get("/") 25 | def root(): 26 | return RedirectResponse(url="/static/index.html") 27 | 28 | 29 | @router.get("/health") 30 | def health(): 31 | return "ok" 32 | 33 | 34 | @router.get("/version", response_model=ApiResponse) 35 | def get_version(): 36 | return ApiResponse.doSuccess(__version__) 37 | 38 | 39 | @router.get("/{platform}/serials", response_model=ApiResponse) 40 | def get_serials(platform: str): 41 | serials = list_serials(platform) 42 | return ApiResponse.doSuccess(serials) 43 | 44 | 45 | @router.post("/{platform}/{serial}/connect", response_model=ApiResponse) 46 | def connect( 47 | platform: str, 48 | serial: str, 49 | wdaUrl: Union[str, None] = Query(None), 50 | maxDepth: Union[int, None] = Query(None) 51 | ): 52 | ret = init_device(platform, serial, wdaUrl, maxDepth) 53 | return ApiResponse.doSuccess(ret) 54 | 55 | 56 | @router.get("/{platform}/{serial}/screenshot", response_model=ApiResponse) 57 | def screenshot(platform: str, serial: str): 58 | device: Union[AndroidDevice, IosDevice, HarmonyDevice] = cached_devices.get((platform, serial)) 59 | data = device.take_screenshot() 60 | return ApiResponse.doSuccess(data) 61 | 62 | 63 | @router.get("/{platform}/{serial}/hierarchy", response_model=ApiResponse) 64 | def dump_hierarchy(platform: str, serial: str): 65 | device: Union[AndroidDevice, IosDevice, HarmonyDevice] = cached_devices.get((platform, serial)) 66 | data = device.dump_hierarchy() 67 | return ApiResponse.doSuccess(data) 68 | 69 | 70 | @router.post("/{platform}/hierarchy/xpathLite", response_model=ApiResponse) 71 | async def fetch_xpathLite(platform: str, request: XPathLiteRequest): 72 | tree_data = request.tree_data 73 | node_id = request.node_id 74 | generator = XPathLiteGenerator(platform, tree_data) 75 | xpath = generator.get_xpathLite(node_id) 76 | return ApiResponse.doSuccess(xpath) -------------------------------------------------------------------------------- /uiviewer/static/cdn/cdn.jsdelivr.net/npm/vue@2: -------------------------------------------------------------------------------- 1 | /*! 2 | * Vue.js v2.7.16 3 | * (c) 2014-2023 Evan You 4 | * Released under the MIT License. 5 | */ 6 | /*! 7 | * Vue.js v2.7.16 8 | * (c) 2014-2023 Evan You 9 | * Released under the MIT License. 10 | */ 11 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Vue=e()}(this,(function(){"use strict";var t=Object.freeze({}),e=Array.isArray;function n(t){return null==t}function r(t){return null!=t}function o(t){return!0===t}function i(t){return"string"==typeof t||"number"==typeof t||"symbol"==typeof t||"boolean"==typeof t}function a(t){return"function"==typeof t}function s(t){return null!==t&&"object"==typeof t}var c=Object.prototype.toString;function u(t){return"[object Object]"===c.call(t)}function l(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function f(t){return r(t)&&"function"==typeof t.then&&"function"==typeof t.catch}function d(t){return null==t?"":Array.isArray(t)||u(t)&&t.toString===c?JSON.stringify(t,p,2):String(t)}function p(t,e){return e&&e.__v_isRef?e.value:e}function v(t){var e=parseFloat(t);return isNaN(e)?t:e}function h(t,e){for(var n=Object.create(null),r=t.split(","),o=0;o-1)return t.splice(r,1)}}var _=Object.prototype.hasOwnProperty;function b(t,e){return _.call(t,e)}function $(t){var e=Object.create(null);return function(n){return e[n]||(e[n]=t(n))}}var w=/-(\w)/g,x=$((function(t){return t.replace(w,(function(t,e){return e?e.toUpperCase():""}))})),C=$((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),k=/\B([A-Z])/g,S=$((function(t){return t.replace(k,"-$1").toLowerCase()}));var O=Function.prototype.bind?function(t,e){return t.bind(e)}:function(t,e){function n(n){var r=arguments.length;return r?r>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n};function T(t,e){e=e||0;for(var n=t.length-e,r=new Array(n);n--;)r[n]=t[n+e];return r}function A(t,e){for(var n in e)t[n]=e[n];return t}function j(t){for(var e={},n=0;n0,X=W&&W.indexOf("edge/")>0;W&&W.indexOf("android");var Y=W&&/iphone|ipad|ipod|ios/.test(W);W&&/chrome\/\d+/.test(W),W&&/phantomjs/.test(W);var Q,tt=W&&W.match(/firefox\/(\d+)/),et={}.watch,nt=!1;if(q)try{var rt={};Object.defineProperty(rt,"passive",{get:function(){nt=!0}}),window.addEventListener("test-passive",null,rt)}catch(t){}var ot=function(){return void 0===Q&&(Q=!q&&"undefined"!=typeof global&&(global.process&&"server"===global.process.env.VUE_ENV)),Q},it=q&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function at(t){return"function"==typeof t&&/native code/.test(t.toString())}var st,ct="undefined"!=typeof Symbol&&at(Symbol)&&"undefined"!=typeof Reflect&&at(Reflect.ownKeys);st="undefined"!=typeof Set&&at(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var ut=null;function lt(t){void 0===t&&(t=null),t||ut&&ut._scope.off(),ut=t,t&&t._scope.on()}var ft=function(){function t(t,e,n,r,o,i,a,s){this.tag=t,this.data=e,this.children=n,this.text=r,this.elm=o,this.ns=void 0,this.context=i,this.fnContext=void 0,this.fnOptions=void 0,this.fnScopeId=void 0,this.key=e&&e.key,this.componentOptions=a,this.componentInstance=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1,this.isOnce=!1,this.asyncFactory=s,this.asyncMeta=void 0,this.isAsyncPlaceholder=!1}return Object.defineProperty(t.prototype,"child",{get:function(){return this.componentInstance},enumerable:!1,configurable:!0}),t}(),dt=function(t){void 0===t&&(t="");var e=new ft;return e.text=t,e.isComment=!0,e};function pt(t){return new ft(void 0,void 0,void 0,String(t))}function vt(t){var e=new ft(t.tag,t.data,t.children&&t.children.slice(),t.text,t.elm,t.context,t.componentOptions,t.asyncFactory);return e.ns=t.ns,e.isStatic=t.isStatic,e.key=t.key,e.isComment=t.isComment,e.fnContext=t.fnContext,e.fnOptions=t.fnOptions,e.fnScopeId=t.fnScopeId,e.asyncMeta=t.asyncMeta,e.isCloned=!0,e}"function"==typeof SuppressedError&&SuppressedError;var ht=0,mt=[],gt=function(){for(var t=0;t0&&(ne((c=re(c,"".concat(a||"","_").concat(s)))[0])&&ne(l)&&(f[u]=pt(l.text+c[0].text),c.shift()),f.push.apply(f,c)):i(c)?ne(l)?f[u]=pt(l.text+c):""!==c&&f.push(pt(c)):ne(c)&&ne(l)?f[u]=pt(l.text+c.text):(o(t._isVList)&&r(c.tag)&&n(c.key)&&r(a)&&(c.key="__vlist".concat(a,"_").concat(s,"__")),f.push(c)));return f}var oe=1,ie=2;function ae(t,n,c,u,l,f){return(e(c)||i(c))&&(l=u,u=c,c=void 0),o(f)&&(l=ie),function(t,n,o,i,c){if(r(o)&&r(o.__ob__))return dt();r(o)&&r(o.is)&&(n=o.is);if(!n)return dt();e(i)&&a(i[0])&&((o=o||{}).scopedSlots={default:i[0]},i.length=0);c===ie?i=ee(i):c===oe&&(i=function(t){for(var n=0;n0,s=n?!!n.$stable:!a,c=n&&n.$key;if(n){if(n._normalized)return n._normalized;if(s&&o&&o!==t&&c===o.$key&&!a&&!o.$hasNormal)return o;for(var u in i={},n)n[u]&&"$"!==u[0]&&(i[u]=Oe(e,r,u,n[u]))}else i={};for(var l in r)l in i||(i[l]=Te(r,l));return n&&Object.isExtensible(n)&&(n._normalized=i),V(i,"$stable",s),V(i,"$key",c),V(i,"$hasNormal",a),i}function Oe(t,n,r,o){var i=function(){var n=ut;lt(t);var r=arguments.length?o.apply(null,arguments):o({}),i=(r=r&&"object"==typeof r&&!e(r)?[r]:ee(r))&&r[0];return lt(n),r&&(!i||1===r.length&&i.isComment&&!ke(i))?void 0:r};return o.proxy&&Object.defineProperty(n,r,{get:i,enumerable:!0,configurable:!0}),i}function Te(t,e){return function(){return t[e]}}function Ae(e){return{get attrs(){if(!e._attrsProxy){var n=e._attrsProxy={};V(n,"_v_attr_proxy",!0),je(n,e.$attrs,t,e,"$attrs")}return e._attrsProxy},get listeners(){e._listenersProxy||je(e._listenersProxy={},e.$listeners,t,e,"$listeners");return e._listenersProxy},get slots(){return function(t){t._slotsProxy||Ne(t._slotsProxy={},t.$scopedSlots);return t._slotsProxy}(e)},emit:O(e.$emit,e),expose:function(t){t&&Object.keys(t).forEach((function(n){return zt(e,t,n)}))}}}function je(t,e,n,r,o){var i=!1;for(var a in e)a in t?e[a]!==n[a]&&(i=!0):(i=!0,Ee(t,a,r,o));for(var a in t)a in e||(i=!0,delete t[a]);return i}function Ee(t,e,n,r){Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){return n[r][e]}})}function Ne(t,e){for(var n in e)t[n]=e[n];for(var n in t)n in e||delete t[n]}function Pe(){var t=ut;return t._setupContext||(t._setupContext=Ae(t))}var De,Me,Ie=null;function Le(t,e){return(t.__esModule||ct&&"Module"===t[Symbol.toStringTag])&&(t=t.default),s(t)?e.extend(t):t}function Re(t){if(e(t))for(var n=0;ndocument.createEvent("Event").timeStamp&&(on=function(){return an.now()})}var sn=function(t,e){if(t.post){if(!e.post)return 1}else if(e.post)return-1;return t.id-e.id};function cn(){var t,e;for(rn=on(),en=!0,Xe.sort(sn),nn=0;nnnn&&Xe[n].id>t.id;)n--;Xe.splice(n+1,0,t)}else Xe.push(t);tn||(tn=!0,En(cn))}}var ln="watcher",fn="".concat(ln," callback"),dn="".concat(ln," getter"),pn="".concat(ln," cleanup");function vn(t,e){return mn(t,null,{flush:"post"})}var hn={};function mn(n,r,o){var i=void 0===o?t:o,s=i.immediate,c=i.deep,u=i.flush,l=void 0===u?"pre":u;i.onTrack,i.onTrigger;var f,d,p=ut,v=function(t,e,n){void 0===n&&(n=null);var r=_n(t,null,n,p,e);return c&&r&&r.__ob__&&r.__ob__.dep.depend(),r},h=!1,m=!1;if(Bt(n)?(f=function(){return n.value},h=Rt(n)):Lt(n)?(f=function(){return n.__ob__.dep.depend(),n},c=!0):e(n)?(m=!0,h=n.some((function(t){return Lt(t)||Rt(t)})),f=function(){return n.map((function(t){return Bt(t)?t.value:Lt(t)?(t.__ob__.dep.depend(),Wn(t)):a(t)?v(t,dn):void 0}))}):f=a(n)?r?function(){return v(n,dn)}:function(){if(!p||!p._isDestroyed)return d&&d(),v(n,ln,[y])}:E,r&&c){var g=f;f=function(){return Wn(g())}}var y=function(t){d=_.onStop=function(){v(t,pn)}};if(ot())return y=E,r?s&&v(r,fn,[f(),m?[]:void 0,y]):f(),E;var _=new Xn(ut,f,E,{lazy:!0});_.noRecurse=!r;var b=m?[]:hn;return _.run=function(){if(_.active)if(r){var t=_.get();(c||h||(m?t.some((function(t,e){return L(t,b[e])})):L(t,b)))&&(d&&d(),v(r,fn,[t,b===hn?void 0:b,y]),b=t)}else _.get()},"sync"===l?_.update=_.run:"post"===l?(_.post=!0,_.update=function(){return un(_)}):_.update=function(){if(p&&p===ut&&!p._isMounted){var t=p._preWatchers||(p._preWatchers=[]);t.indexOf(_)<0&&t.push(_)}else un(_)},r?s?_.run():b=_.get():"post"===l&&p?p.$once("hook:mounted",(function(){return _.get()})):_.get(),function(){_.teardown()}}function gn(t){var e=t._provided,n=t.$parent&&t.$parent._provided;return n===e?t._provided=Object.create(n):e}function yn(t,e,n){bt();try{if(e)for(var r=e;r=r.$parent;){var o=r.$options.errorCaptured;if(o)for(var i=0;i1)return n&&a(e)?e.call(r):e}},h:function(t,e,n){return ae(ut,t,e,n,2,!0)},getCurrentInstance:function(){return ut&&{proxy:ut}},useSlots:function(){return Pe().slots},useAttrs:function(){return Pe().attrs},useListeners:function(){return Pe().listeners},mergeDefaults:function(t,n){var r=e(t)?t.reduce((function(t,e){return t[e]={},t}),{}):t;for(var o in n){var i=r[o];i?e(i)||a(i)?r[o]={type:i,default:n[o]}:i.default=n[o]:null===i&&(r[o]={default:n[o]})}return r},nextTick:En,set:Nt,del:Pt,useCssModule:function(e){return t},useCssVars:function(t){if(q){var e=ut;e&&vn((function(){var n=e.$el,r=t(e,e._setupProxy);if(n&&1===n.nodeType){var o=n.style;for(var i in r)o.setProperty("--".concat(i),r[i])}}))}},defineAsyncComponent:function(t){a(t)&&(t={loader:t});var e=t.loader,n=t.loadingComponent,r=t.errorComponent,o=t.delay,i=void 0===o?200:o,s=t.timeout;t.suspensible;var c=t.onError,u=null,l=0,f=function(){var t;return u||(t=u=e().catch((function(t){if(t=t instanceof Error?t:new Error(String(t)),c)return new Promise((function(e,n){c(t,(function(){return e((l++,u=null,f()))}),(function(){return n(t)}),l+1)}));throw t})).then((function(e){return t!==u&&u?u:(e&&(e.__esModule||"Module"===e[Symbol.toStringTag])&&(e=e.default),e)})))};return function(){return{component:f(),delay:i,timeout:s,error:r,loading:n}}},onBeforeMount:Pn,onMounted:Dn,onBeforeUpdate:Mn,onUpdated:In,onBeforeUnmount:Ln,onUnmounted:Rn,onActivated:Fn,onDeactivated:Hn,onServerPrefetch:Bn,onRenderTracked:Un,onRenderTriggered:zn,onErrorCaptured:function(t,e){void 0===e&&(e=ut),Vn(t,e)}}),qn=new st;function Wn(t){return Zn(t,qn),qn.clear(),t}function Zn(t,n){var r,o,i=e(t);if(!(!i&&!s(t)||t.__v_skip||Object.isFrozen(t)||t instanceof ft)){if(t.__ob__){var a=t.__ob__.dep.id;if(n.has(a))return;n.add(a)}if(i)for(r=t.length;r--;)Zn(t[r],n);else if(Bt(t))Zn(t.value,n);else for(r=(o=Object.keys(t)).length;r--;)Zn(t[o[r]],n)}}var Gn=0,Xn=function(){function t(t,e,n,r,o){!function(t,e){void 0===e&&(e=Me),e&&e.active&&e.effects.push(t)}(this,Me&&!Me._vm?Me:t?t._scope:void 0),(this.vm=t)&&o&&(t._watcher=this),r?(this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.before=r.before):this.deep=this.user=this.lazy=this.sync=!1,this.cb=n,this.id=++Gn,this.active=!0,this.post=!1,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new st,this.newDepIds=new st,this.expression="",a(e)?this.getter=e:(this.getter=function(t){if(!K.test(t)){var e=t.split(".");return function(t){for(var n=0;n-1)if(i&&!b(o,"default"))s=!1;else if(""===s||s===S(t)){var u=jr(String,o.type);(u<0||c-1:"string"==typeof t?t.split(",").indexOf(n)>-1:(r=t,"[object RegExp]"===c.call(r)&&t.test(n));var r}function Mr(t,e){var n=t.cache,r=t.keys,o=t._vnode,i=t.$vnode;for(var a in n){var s=n[a];if(s){var c=s.name;c&&!e(c)&&Ir(n,a,r,o)}}i.componentOptions.children=void 0}function Ir(t,e,n,r){var o=t[e];!o||r&&o.tag===r.tag||o.componentInstance.$destroy(),t[e]=null,y(n,e)}!function(e){e.prototype._init=function(e){var n=this;n._uid=sr++,n._isVue=!0,n.__v_skip=!0,n._scope=new ze(!0),n._scope.parent=void 0,n._scope._vm=!0,e&&e._isComponent?function(t,e){var n=t.$options=Object.create(t.constructor.options),r=e._parentVnode;n.parent=e.parent,n._parentVnode=r;var o=r.componentOptions;n.propsData=o.propsData,n._parentListeners=o.listeners,n._renderChildren=o.children,n._componentTag=o.tag,e.render&&(n.render=e.render,n.staticRenderFns=e.staticRenderFns)}(n,e):n.$options=Cr(cr(n.constructor),e||{},n),n._renderProxy=n,n._self=n,function(t){var e=t.$options,n=e.parent;if(n&&!e.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(t)}t.$parent=n,t.$root=n?n.$root:t,t.$children=[],t.$refs={},t._provided=n?n._provided:Object.create(null),t._watcher=null,t._inactive=null,t._directInactive=!1,t._isMounted=!1,t._isDestroyed=!1,t._isBeingDestroyed=!1}(n),function(t){t._events=Object.create(null),t._hasHookEvent=!1;var e=t.$options._parentListeners;e&&Ue(t,e)}(n),function(e){e._vnode=null,e._staticTrees=null;var n=e.$options,r=e.$vnode=n._parentVnode,o=r&&r.context;e.$slots=xe(n._renderChildren,o),e.$scopedSlots=r?Se(e.$parent,r.data.scopedSlots,e.$slots):t,e._c=function(t,n,r,o){return ae(e,t,n,r,o,!1)},e.$createElement=function(t,n,r,o){return ae(e,t,n,r,o,!0)};var i=r&&r.data;Et(e,"$attrs",i&&i.attrs||t,null,!0),Et(e,"$listeners",n._parentListeners||t,null,!0)}(n),Ge(n,"beforeCreate",void 0,!1),function(t){var e=ar(t.$options.inject,t);e&&(Ot(!1),Object.keys(e).forEach((function(n){Et(t,n,e[n])})),Ot(!0))}(n),tr(n),function(t){var e=t.$options.provide;if(e){var n=a(e)?e.call(t):e;if(!s(n))return;for(var r=gn(t),o=ct?Reflect.ownKeys(n):Object.keys(n),i=0;i1?T(n):n;for(var r=T(arguments,1),o='event handler for "'.concat(t,'"'),i=0,a=n.length;iparseInt(this.max)&&Ir(e,n[0],n,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)Ir(this.cache,t,this.keys)},mounted:function(){var t=this;this.cacheVNode(),this.$watch("include",(function(e){Mr(t,(function(t){return Dr(e,t)}))})),this.$watch("exclude",(function(e){Mr(t,(function(t){return!Dr(e,t)}))}))},updated:function(){this.cacheVNode()},render:function(){var t=this.$slots.default,e=Re(t),n=e&&e.componentOptions;if(n){var r=Pr(n),o=this.include,i=this.exclude;if(o&&(!r||!Dr(o,r))||i&&r&&Dr(i,r))return e;var a=this.cache,s=this.keys,c=null==e.key?n.Ctor.cid+(n.tag?"::".concat(n.tag):""):e.key;a[c]?(e.componentInstance=a[c].componentInstance,y(s,c),s.push(c)):(this.vnodeToCache=e,this.keyToCache=c),e.data.keepAlive=!0}return e||t&&t[0]}},Fr={KeepAlive:Rr};!function(t){var e={get:function(){return B}};Object.defineProperty(t,"config",e),t.util={warn:gr,extend:A,mergeOptions:Cr,defineReactive:Et},t.set=Nt,t.delete=Pt,t.nextTick=En,t.observable=function(t){return jt(t),t},t.options=Object.create(null),F.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,A(t.options.components,Fr),function(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=T(arguments,1);return n.unshift(this),a(t.install)?t.install.apply(t,n):a(t)&&t.apply(null,n),e.push(t),this}}(t),function(t){t.mixin=function(t){return this.options=Cr(this.options,t),this}}(t),Nr(t),function(t){F.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&u(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&a(n)&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}(t)}(Er),Object.defineProperty(Er.prototype,"$isServer",{get:ot}),Object.defineProperty(Er.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Er,"FunctionalRenderContext",{value:ur}),Er.version=Kn;var Hr=h("style,class"),Br=h("input,textarea,option,select,progress"),Ur=function(t,e,n){return"value"===n&&Br(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},zr=h("contenteditable,draggable,spellcheck"),Vr=h("events,caret,typing,plaintext-only"),Kr=function(t,e){return Gr(e)||"false"===e?"false":"contenteditable"===t&&Vr(e)?e:"true"},Jr=h("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),qr="http://www.w3.org/1999/xlink",Wr=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Zr=function(t){return Wr(t)?t.slice(6,t.length):""},Gr=function(t){return null==t||!1===t};function Xr(t){for(var e=t.data,n=t,o=t;r(o.componentInstance);)(o=o.componentInstance._vnode)&&o.data&&(e=Yr(o.data,e));for(;r(n=n.parent);)n&&n.data&&(e=Yr(e,n.data));return function(t,e){if(r(t)||r(e))return Qr(t,to(e));return""}(e.staticClass,e.class)}function Yr(t,e){return{staticClass:Qr(t.staticClass,e.staticClass),class:r(t.class)?[t.class,e.class]:e.class}}function Qr(t,e){return t?e?t+" "+e:t:e||""}function to(t){return Array.isArray(t)?function(t){for(var e,n="",o=0,i=t.length;o-1?Oo(t,e,n):Jr(e)?Gr(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):zr(e)?t.setAttribute(e,Kr(e,n)):Wr(e)?Gr(n)?t.removeAttributeNS(qr,Zr(e)):t.setAttributeNS(qr,e,n):Oo(t,e,n)}function Oo(t,e,n){if(Gr(n))t.removeAttribute(e);else{if(Z&&!G&&"TEXTAREA"===t.tagName&&"placeholder"===e&&""!==n&&!t.__ieph){var r=function(e){e.stopImmediatePropagation(),t.removeEventListener("input",r)};t.addEventListener("input",r),t.__ieph=!0}t.setAttribute(e,n)}}var To={create:ko,update:ko};function Ao(t,e){var o=e.elm,i=e.data,a=t.data;if(!(n(i.staticClass)&&n(i.class)&&(n(a)||n(a.staticClass)&&n(a.class)))){var s=Xr(e),c=o._transitionClasses;r(c)&&(s=Qr(s,to(c))),s!==o._prevClass&&(o.setAttribute("class",s),o._prevClass=s)}}var jo,Eo,No,Po,Do,Mo,Io={create:Ao,update:Ao},Lo=/[\w).+\-_$\]]/;function Ro(t){var e,n,r,o,i,a=!1,s=!1,c=!1,u=!1,l=0,f=0,d=0,p=0;for(r=0;r=0&&" "===(h=t.charAt(v));v--);h&&Lo.test(h)||(u=!0)}}else void 0===o?(p=r+1,o=t.slice(0,r).trim()):m();function m(){(i||(i=[])).push(t.slice(p,r).trim()),p=r+1}if(void 0===o?o=t.slice(0,r).trim():0!==p&&m(),i)for(r=0;r-1?{exp:t.slice(0,Po),key:'"'+t.slice(Po+1)+'"'}:{exp:t,key:null};Eo=t,Po=Do=Mo=0;for(;!ei();)ni(No=ti())?oi(No):91===No&&ri(No);return{exp:t.slice(0,Do),key:t.slice(Do+1,Mo)}}(t);return null===n.key?"".concat(t,"=").concat(e):"$set(".concat(n.exp,", ").concat(n.key,", ").concat(e,")")}function ti(){return Eo.charCodeAt(++Po)}function ei(){return Po>=jo}function ni(t){return 34===t||39===t}function ri(t){var e=1;for(Do=Po;!ei();)if(ni(t=ti()))oi(t);else if(91===t&&e++,93===t&&e--,0===e){Mo=Po;break}}function oi(t){for(var e=t;!ei()&&(t=ti())!==e;);}var ii,ai="__r",si="__c";function ci(t,e,n){var r=ii;return function o(){null!==e.apply(null,arguments)&&fi(t,o,n,r)}}var ui=xn&&!(tt&&Number(tt[1])<=53);function li(t,e,n,r){if(ui){var o=rn,i=e;e=i._wrapper=function(t){if(t.target===t.currentTarget||t.timeStamp>=o||t.timeStamp<=0||t.target.ownerDocument!==document)return i.apply(this,arguments)}}ii.addEventListener(t,e,nt?{capture:n,passive:r}:n)}function fi(t,e,n,r){(r||ii).removeEventListener(t,e._wrapper||e,n)}function di(t,e){if(!n(t.data.on)||!n(e.data.on)){var o=e.data.on||{},i=t.data.on||{};ii=e.elm||t.elm,function(t){if(r(t[ai])){var e=Z?"change":"input";t[e]=[].concat(t[ai],t[e]||[]),delete t[ai]}r(t[si])&&(t.change=[].concat(t[si],t.change||[]),delete t[si])}(o),Yt(o,i,li,fi,ci,e.context),ii=void 0}}var pi,vi={create:di,update:di,destroy:function(t){return di(t,vo)}};function hi(t,e){if(!n(t.data.domProps)||!n(e.data.domProps)){var i,a,s=e.elm,c=t.data.domProps||{},u=e.data.domProps||{};for(i in(r(u.__ob__)||o(u._v_attr_proxy))&&(u=e.data.domProps=A({},u)),c)i in u||(s[i]="");for(i in u){if(a=u[i],"textContent"===i||"innerHTML"===i){if(e.children&&(e.children.length=0),a===c[i])continue;1===s.childNodes.length&&s.removeChild(s.childNodes[0])}if("value"===i&&"PROGRESS"!==s.tagName){s._value=a;var l=n(a)?"":String(a);mi(s,l)&&(s.value=l)}else if("innerHTML"===i&&ro(s.tagName)&&n(s.innerHTML)){(pi=pi||document.createElement("div")).innerHTML="".concat(a,"");for(var f=pi.firstChild;s.firstChild;)s.removeChild(s.firstChild);for(;f.firstChild;)s.appendChild(f.firstChild)}else if(a!==c[i])try{s[i]=a}catch(t){}}}}function mi(t,e){return!t.composing&&("OPTION"===t.tagName||function(t,e){var n=!0;try{n=document.activeElement!==t}catch(t){}return n&&t.value!==e}(t,e)||function(t,e){var n=t.value,o=t._vModifiers;if(r(o)){if(o.number)return v(n)!==v(e);if(o.trim)return n.trim()!==e.trim()}return n!==e}(t,e))}var gi={create:hi,update:hi},yi=$((function(t){var e={},n=/:(.+)/;return t.split(/;(?![^(]*\))/g).forEach((function(t){if(t){var r=t.split(n);r.length>1&&(e[r[0].trim()]=r[1].trim())}})),e}));function _i(t){var e=bi(t.style);return t.staticStyle?A(t.staticStyle,e):e}function bi(t){return Array.isArray(t)?j(t):"string"==typeof t?yi(t):t}var $i,wi=/^--/,xi=/\s*!important$/,Ci=function(t,e,n){if(wi.test(e))t.style.setProperty(e,n);else if(xi.test(n))t.style.setProperty(S(e),n.replace(xi,""),"important");else{var r=Si(e);if(Array.isArray(n))for(var o=0,i=n.length;o-1?e.split(Ai).forEach((function(e){return t.classList.add(e)})):t.classList.add(e);else{var n=" ".concat(t.getAttribute("class")||""," ");n.indexOf(" "+e+" ")<0&&t.setAttribute("class",(n+e).trim())}}function Ei(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(Ai).forEach((function(e){return t.classList.remove(e)})):t.classList.remove(e),t.classList.length||t.removeAttribute("class");else{for(var n=" ".concat(t.getAttribute("class")||""," "),r=" "+e+" ";n.indexOf(r)>=0;)n=n.replace(r," ");(n=n.trim())?t.setAttribute("class",n):t.removeAttribute("class")}}function Ni(t){if(t){if("object"==typeof t){var e={};return!1!==t.css&&A(e,Pi(t.name||"v")),A(e,t),e}return"string"==typeof t?Pi(t):void 0}}var Pi=$((function(t){return{enterClass:"".concat(t,"-enter"),enterToClass:"".concat(t,"-enter-to"),enterActiveClass:"".concat(t,"-enter-active"),leaveClass:"".concat(t,"-leave"),leaveToClass:"".concat(t,"-leave-to"),leaveActiveClass:"".concat(t,"-leave-active")}})),Di=q&&!G,Mi="transition",Ii="animation",Li="transition",Ri="transitionend",Fi="animation",Hi="animationend";Di&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Li="WebkitTransition",Ri="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Fi="WebkitAnimation",Hi="webkitAnimationEnd"));var Bi=q?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function Ui(t){Bi((function(){Bi(t)}))}function zi(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),ji(t,e))}function Vi(t,e){t._transitionClasses&&y(t._transitionClasses,e),Ei(t,e)}function Ki(t,e,n){var r=qi(t,e),o=r.type,i=r.timeout,a=r.propCount;if(!o)return n();var s=o===Mi?Ri:Hi,c=0,u=function(){t.removeEventListener(s,l),n()},l=function(e){e.target===t&&++c>=a&&u()};setTimeout((function(){c0&&(n=Mi,l=a,f=i.length):e===Ii?u>0&&(n=Ii,l=u,f=c.length):f=(n=(l=Math.max(a,u))>0?a>u?Mi:Ii:null)?n===Mi?i.length:c.length:0,{type:n,timeout:l,propCount:f,hasTransform:n===Mi&&Ji.test(r[Li+"Property"])}}function Wi(t,e){for(;t.length1}function ta(t,e){!0!==e.data.show&&Gi(e)}var ea=function(t){var a,s,c={},u=t.modules,l=t.nodeOps;for(a=0;av?b(t,n(o[g+1])?null:o[g+1].elm,o,p,g,i):p>g&&w(e,f,v)}(f,h,m,i,u):r(m)?(r(t.text)&&l.setTextContent(f,""),b(f,null,m,0,m.length-1,i)):r(h)?w(h,0,h.length-1):r(t.text)&&l.setTextContent(f,""):t.text!==e.text&&l.setTextContent(f,e.text),r(v)&&r(p=v.hook)&&r(p=p.postpatch)&&p(t,e)}}}function S(t,e,n){if(o(n)&&r(t.parent))t.parent.data.pendingInsert=e;else for(var i=0;i-1,a.selected!==i&&(a.selected=i);else if(D(aa(a),r))return void(t.selectedIndex!==s&&(t.selectedIndex=s));o||(t.selectedIndex=-1)}}function ia(t,e){return e.every((function(e){return!D(e,t)}))}function aa(t){return"_value"in t?t._value:t.value}function sa(t){t.target.composing=!0}function ca(t){t.target.composing&&(t.target.composing=!1,ua(t.target,"input"))}function ua(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function la(t){return!t.componentInstance||t.data&&t.data.transition?t:la(t.componentInstance._vnode)}var fa={bind:function(t,e,n){var r=e.value,o=(n=la(n)).data&&n.data.transition,i=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;r&&o?(n.data.show=!0,Gi(n,(function(){t.style.display=i}))):t.style.display=r?i:"none"},update:function(t,e,n){var r=e.value;!r!=!e.oldValue&&((n=la(n)).data&&n.data.transition?(n.data.show=!0,r?Gi(n,(function(){t.style.display=t.__vOriginalDisplay})):Xi(n,(function(){t.style.display="none"}))):t.style.display=r?t.__vOriginalDisplay:"none")},unbind:function(t,e,n,r,o){o||(t.style.display=t.__vOriginalDisplay)}},da={model:na,show:fa},pa={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function va(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?va(Re(e.children)):t}function ha(t){var e={},n=t.$options;for(var r in n.propsData)e[r]=t[r];var o=n._parentListeners;for(var r in o)e[x(r)]=o[r];return e}function ma(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}var ga=function(t){return t.tag||ke(t)},ya=function(t){return"show"===t.name},_a={name:"transition",props:pa,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(ga)).length){var r=this.mode,o=n[0];if(function(t){for(;t=t.parent;)if(t.data.transition)return!0}(this.$vnode))return o;var a=va(o);if(!a)return o;if(this._leaving)return ma(t,o);var s="__transition-".concat(this._uid,"-");a.key=null==a.key?a.isComment?s+"comment":s+a.tag:i(a.key)?0===String(a.key).indexOf(s)?a.key:s+a.key:a.key;var c=(a.data||(a.data={})).transition=ha(this),u=this._vnode,l=va(u);if(a.data.directives&&a.data.directives.some(ya)&&(a.data.show=!0),l&&l.data&&!function(t,e){return e.key===t.key&&e.tag===t.tag}(a,l)&&!ke(l)&&(!l.componentInstance||!l.componentInstance._vnode.isComment)){var f=l.data.transition=A({},c);if("out-in"===r)return this._leaving=!0,Qt(f,"afterLeave",(function(){e._leaving=!1,e.$forceUpdate()})),ma(t,o);if("in-out"===r){if(ke(a))return u;var d,p=function(){d()};Qt(c,"afterEnter",p),Qt(c,"enterCancelled",p),Qt(f,"delayLeave",(function(t){d=t}))}}return o}}},ba=A({tag:String,moveClass:String},pa);delete ba.mode;var $a={props:ba,beforeMount:function(){var t=this,e=this._update;this._update=function(n,r){var o=Je(t);t.__patch__(t._vnode,t.kept,!1,!0),t._vnode=t.kept,o(),e.call(t,n,r)}},render:function(t){for(var e=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,o=this.$slots.default||[],i=this.children=[],a=ha(this),s=0;s-1?ao[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:ao[t]=/HTMLUnknownElement/.test(e.toString())},A(Er.options.directives,da),A(Er.options.components,ka),Er.prototype.__patch__=q?ea:E,Er.prototype.$mount=function(t,e){return function(t,e,n){var r;t.$el=e,t.$options.render||(t.$options.render=dt),Ge(t,"beforeMount"),r=function(){t._update(t._render(),n)},new Xn(t,r,E,{before:function(){t._isMounted&&!t._isDestroyed&&Ge(t,"beforeUpdate")}},!0),n=!1;var o=t._preWatchers;if(o)for(var i=0;i\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,La=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Ra="[a-zA-Z_][\\-\\.0-9_a-zA-Z".concat(U.source,"]*"),Fa="((?:".concat(Ra,"\\:)?").concat(Ra,")"),Ha=new RegExp("^<".concat(Fa)),Ba=/^\s*(\/?)>/,Ua=new RegExp("^<\\/".concat(Fa,"[^>]*>")),za=/^]+>/i,Va=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},Za=/&(?:lt|gt|quot|amp|#39);/g,Ga=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,Xa=h("pre,textarea",!0),Ya=function(t,e){return t&&Xa(t)&&"\n"===e[0]};function Qa(t,e){var n=e?Ga:Za;return t.replace(n,(function(t){return Wa[t]}))}function ts(t,e){for(var n,r,o=[],i=e.expectHTML,a=e.isUnaryTag||N,s=e.canBeLeftOpenTag||N,c=0,u=function(){if(n=t,r&&Ja(r)){var u=0,d=r.toLowerCase(),p=qa[d]||(qa[d]=new RegExp("([\\s\\S]*?)(]*>)","i"));w=t.replace(p,(function(t,n,r){return u=r.length,Ja(d)||"noscript"===d||(n=n.replace(//g,"$1").replace(//g,"$1")),Ya(d,n)&&(n=n.slice(1)),e.chars&&e.chars(n),""}));c+=t.length-w.length,t=w,f(d,c-u,c)}else{var v=t.indexOf("<");if(0===v){if(Va.test(t)){var h=t.indexOf("--\x3e");if(h>=0)return e.shouldKeepComment&&e.comment&&e.comment(t.substring(4,h),c,c+h+3),l(h+3),"continue"}if(Ka.test(t)){var m=t.indexOf("]>");if(m>=0)return l(m+2),"continue"}var g=t.match(za);if(g)return l(g[0].length),"continue";var y=t.match(Ua);if(y){var _=c;return l(y[0].length),f(y[1],_,c),"continue"}var b=function(){var e=t.match(Ha);if(e){var n={tagName:e[1],attrs:[],start:c};l(e[0].length);for(var r=void 0,o=void 0;!(r=t.match(Ba))&&(o=t.match(La)||t.match(Ia));)o.start=c,l(o[0].length),o.end=c,n.attrs.push(o);if(r)return n.unarySlash=r[1],l(r[0].length),n.end=c,n}}();if(b)return function(t){var n=t.tagName,c=t.unarySlash;i&&("p"===r&&Ma(n)&&f(r),s(n)&&r===n&&f(n));for(var u=a(n)||!!c,l=t.attrs.length,d=new Array(l),p=0;p=0){for(w=t.slice(v);!(Ua.test(w)||Ha.test(w)||Va.test(w)||Ka.test(w)||(x=w.indexOf("<",1))<0);)v+=x,w=t.slice(v);$=t.substring(0,v)}v<0&&($=t),$&&l($.length),e.chars&&$&&e.chars($,c-$.length,c)}if(t===n)return e.chars&&e.chars(t),"break"};t;){if("break"===u())break}function l(e){c+=e,t=t.substring(e)}function f(t,n,i){var a,s;if(null==n&&(n=c),null==i&&(i=c),t)for(s=t.toLowerCase(),a=o.length-1;a>=0&&o[a].lowerCasedTag!==s;a--);else a=0;if(a>=0){for(var u=o.length-1;u>=a;u--)e.end&&e.end(o[u].tag,n,i);o.length=a,r=a&&o[a-1].tag}else"br"===s?e.start&&e.start(t,[],!0,n,i):"p"===s&&(e.start&&e.start(t,[],!1,n,i),e.end&&e.end(t,n,i))}f()}var es,ns,rs,os,is,as,ss,cs,us=/^@|^v-on:/,ls=/^v-|^@|^:|^#/,fs=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,ds=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,ps=/^\(|\)$/g,vs=/^\[.*\]$/,hs=/:(.*)$/,ms=/^:|^\.|^v-bind:/,gs=/\.[^.\]]+(?=[^\]]*$)/g,ys=/^v-slot(:|$)|^#/,_s=/[\r\n]/,bs=/[ \f\t\r\n]+/g,$s=$(Na),ws="_empty_";function xs(t,e,n){return{type:1,tag:t,attrsList:e,attrsMap:js(e),rawAttrsMap:{},parent:n,children:[]}}function Cs(t,e){es=e.warn||Ho,as=e.isPreTag||N,ss=e.mustUseProp||N,cs=e.getTagNamespace||N,e.isReservedTag,rs=Bo(e.modules,"transformNode"),os=Bo(e.modules,"preTransformNode"),is=Bo(e.modules,"postTransformNode"),ns=e.delimiters;var n,r,o=[],i=!1!==e.preserveWhitespace,a=e.whitespace,s=!1,c=!1;function u(t){if(l(t),s||t.processed||(t=ks(t,e)),o.length||t===n||n.if&&(t.elseif||t.else)&&Os(n,{exp:t.elseif,block:t}),r&&!t.forbidden)if(t.elseif||t.else)a=t,u=function(t){for(var e=t.length;e--;){if(1===t[e].type)return t[e];t.pop()}}(r.children),u&&u.if&&Os(u,{exp:a.elseif,block:a});else{if(t.slotScope){var i=t.slotTarget||'"default"';(r.scopedSlots||(r.scopedSlots={}))[i]=t}r.children.push(t),t.parent=r}var a,u;t.children=t.children.filter((function(t){return!t.slotScope})),l(t),t.pre&&(s=!1),as(t.tag)&&(c=!1);for(var f=0;fc&&(s.push(i=t.slice(c,o)),a.push(JSON.stringify(i)));var u=Ro(r[1].trim());a.push("_s(".concat(u,")")),s.push({"@binding":u}),c=o+r[0].length}return c-1")+("true"===i?":(".concat(e,")"):":_q(".concat(e,",").concat(i,")"))),qo(t,"change","var $$a=".concat(e,",")+"$$el=$event.target,"+"$$c=$$el.checked?(".concat(i,"):(").concat(a,");")+"if(Array.isArray($$a)){"+"var $$v=".concat(r?"_n("+o+")":o,",")+"$$i=_i($$a,$$v);"+"if($$el.checked){$$i<0&&(".concat(Qo(e,"$$a.concat([$$v])"),")}")+"else{$$i>-1&&(".concat(Qo(e,"$$a.slice(0,$$i).concat($$a.slice($$i+1))"),")}")+"}else{".concat(Qo(e,"$$c"),"}"),null,!0)}(t,r,o);else if("input"===i&&"radio"===a)!function(t,e,n){var r=n&&n.number,o=Wo(t,"value")||"null";o=r?"_n(".concat(o,")"):o,Uo(t,"checked","_q(".concat(e,",").concat(o,")")),qo(t,"change",Qo(e,o),null,!0)}(t,r,o);else if("input"===i||"textarea"===i)!function(t,e,n){var r=t.attrsMap.type,o=n||{},i=o.lazy,a=o.number,s=o.trim,c=!i&&"range"!==r,u=i?"change":"range"===r?ai:"input",l="$event.target.value";s&&(l="$event.target.value.trim()");a&&(l="_n(".concat(l,")"));var f=Qo(e,l);c&&(f="if($event.target.composing)return;".concat(f));Uo(t,"value","(".concat(e,")")),qo(t,u,f,null,!0),(s||a)&&qo(t,"blur","$forceUpdate()")}(t,r,o);else if(!B.isReservedTag(i))return Yo(t,r,o),!1;return!0},text:function(t,e){e.value&&Uo(t,"textContent","_s(".concat(e.value,")"),e)},html:function(t,e){e.value&&Uo(t,"innerHTML","_s(".concat(e.value,")"),e)}},Rs={expectHTML:!0,modules:Ds,directives:Ls,isPreTag:function(t){return"pre"===t},isUnaryTag:Pa,mustUseProp:Ur,canBeLeftOpenTag:Da,isReservedTag:oo,getTagNamespace:io,staticKeys:function(t){return t.reduce((function(t,e){return t.concat(e.staticKeys||[])}),[]).join(",")}(Ds)},Fs=$((function(t){return h("type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap"+(t?","+t:""))}));function Hs(t,e){t&&(Ms=Fs(e.staticKeys||""),Is=e.isReservedTag||N,Bs(t),Us(t,!1))}function Bs(t){if(t.static=function(t){if(2===t.type)return!1;if(3===t.type)return!0;return!(!t.pre&&(t.hasBindings||t.if||t.for||m(t.tag)||!Is(t.tag)||function(t){for(;t.parent;){if("template"!==(t=t.parent).tag)return!1;if(t.for)return!0}return!1}(t)||!Object.keys(t).every(Ms)))}(t),1===t.type){if(!Is(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var e=0,n=t.children.length;e|^function(?:\s+[\w$]+)?\s*\(/,Vs=/\([^)]*?\);*$/,Ks=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,Js={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},qs={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},Ws=function(t){return"if(".concat(t,")return null;")},Zs={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:Ws("$event.target !== $event.currentTarget"),ctrl:Ws("!$event.ctrlKey"),shift:Ws("!$event.shiftKey"),alt:Ws("!$event.altKey"),meta:Ws("!$event.metaKey"),left:Ws("'button' in $event && $event.button !== 0"),middle:Ws("'button' in $event && $event.button !== 1"),right:Ws("'button' in $event && $event.button !== 2")};function Gs(t,e){var n=e?"nativeOn:":"on:",r="",o="";for(var i in t){var a=Xs(t[i]);t[i]&&t[i].dynamic?o+="".concat(i,",").concat(a,","):r+='"'.concat(i,'":').concat(a,",")}return r="{".concat(r.slice(0,-1),"}"),o?n+"_d(".concat(r,",[").concat(o.slice(0,-1),"])"):n+r}function Xs(t){if(!t)return"function(){}";if(Array.isArray(t))return"[".concat(t.map((function(t){return Xs(t)})).join(","),"]");var e=Ks.test(t.value),n=zs.test(t.value),r=Ks.test(t.value.replace(Vs,""));if(t.modifiers){var o="",i="",a=[],s=function(e){if(Zs[e])i+=Zs[e],Js[e]&&a.push(e);else if("exact"===e){var n=t.modifiers;i+=Ws(["ctrl","shift","alt","meta"].filter((function(t){return!n[t]})).map((function(t){return"$event.".concat(t,"Key")})).join("||"))}else a.push(e)};for(var c in t.modifiers)s(c);a.length&&(o+=function(t){return"if(!$event.type.indexOf('key')&&"+"".concat(t.map(Ys).join("&&"),")return null;")}(a)),i&&(o+=i);var u=e?"return ".concat(t.value,".apply(null, arguments)"):n?"return (".concat(t.value,").apply(null, arguments)"):r?"return ".concat(t.value):t.value;return"function($event){".concat(o).concat(u,"}")}return e||n?t.value:"function($event){".concat(r?"return ".concat(t.value):t.value,"}")}function Ys(t){var e=parseInt(t,10);if(e)return"$event.keyCode!==".concat(e);var n=Js[t],r=qs[t];return"_k($event.keyCode,"+"".concat(JSON.stringify(t),",")+"".concat(JSON.stringify(n),",")+"$event.key,"+"".concat(JSON.stringify(r))+")"}var Qs={on:function(t,e){t.wrapListeners=function(t){return"_g(".concat(t,",").concat(e.value,")")}},bind:function(t,e){t.wrapData=function(n){return"_b(".concat(n,",'").concat(t.tag,"',").concat(e.value,",").concat(e.modifiers&&e.modifiers.prop?"true":"false").concat(e.modifiers&&e.modifiers.sync?",true":"",")")}},cloak:E},tc=function(t){this.options=t,this.warn=t.warn||Ho,this.transforms=Bo(t.modules,"transformCode"),this.dataGenFns=Bo(t.modules,"genData"),this.directives=A(A({},Qs),t.directives);var e=t.isReservedTag||N;this.maybeComponent=function(t){return!!t.component||!e(t.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function ec(t,e){var n=new tc(e),r=t?"script"===t.tag?"null":nc(t,n):'_c("div")';return{render:"with(this){return ".concat(r,"}"),staticRenderFns:n.staticRenderFns}}function nc(t,e){if(t.parent&&(t.pre=t.pre||t.parent.pre),t.staticRoot&&!t.staticProcessed)return rc(t,e);if(t.once&&!t.onceProcessed)return oc(t,e);if(t.for&&!t.forProcessed)return sc(t,e);if(t.if&&!t.ifProcessed)return ic(t,e);if("template"!==t.tag||t.slotTarget||e.pre){if("slot"===t.tag)return function(t,e){var n=t.slotName||'"default"',r=fc(t,e),o="_t(".concat(n).concat(r?",function(){return ".concat(r,"}"):""),i=t.attrs||t.dynamicAttrs?vc((t.attrs||[]).concat(t.dynamicAttrs||[]).map((function(t){return{name:x(t.name),value:t.value,dynamic:t.dynamic}}))):null,a=t.attrsMap["v-bind"];!i&&!a||r||(o+=",null");i&&(o+=",".concat(i));a&&(o+="".concat(i?"":",null",",").concat(a));return o+")"}(t,e);var n=void 0;if(t.component)n=function(t,e,n){var r=e.inlineTemplate?null:fc(e,n,!0);return"_c(".concat(t,",").concat(cc(e,n)).concat(r?",".concat(r):"",")")}(t.component,t,e);else{var r=void 0,o=e.maybeComponent(t);(!t.plain||t.pre&&o)&&(r=cc(t,e));var i=void 0,a=e.options.bindings;o&&a&&!1!==a.__isScriptSetup&&(i=function(t,e){var n=x(e),r=C(n),o=function(o){return t[e]===o?e:t[n]===o?n:t[r]===o?r:void 0},i=o("setup-const")||o("setup-reactive-const");if(i)return i;var a=o("setup-let")||o("setup-ref")||o("setup-maybe-ref");if(a)return a}(a,t.tag)),i||(i="'".concat(t.tag,"'"));var s=t.inlineTemplate?null:fc(t,e,!0);n="_c(".concat(i).concat(r?",".concat(r):"").concat(s?",".concat(s):"",")")}for(var c=0;c>>0}(a)):"",")")}(t,t.scopedSlots,e),",")),t.model&&(n+="model:{value:".concat(t.model.value,",callback:").concat(t.model.callback,",expression:").concat(t.model.expression,"},")),t.inlineTemplate){var i=function(t,e){var n=t.children[0];if(n&&1===n.type){var r=ec(n,e.options);return"inlineTemplate:{render:function(){".concat(r.render,"},staticRenderFns:[").concat(r.staticRenderFns.map((function(t){return"function(){".concat(t,"}")})).join(","),"]}")}}(t,e);i&&(n+="".concat(i,","))}return n=n.replace(/,$/,"")+"}",t.dynamicAttrs&&(n="_b(".concat(n,',"').concat(t.tag,'",').concat(vc(t.dynamicAttrs),")")),t.wrapData&&(n=t.wrapData(n)),t.wrapListeners&&(n=t.wrapListeners(n)),n}function uc(t){return 1===t.type&&("slot"===t.tag||t.children.some(uc))}function lc(t,e){var n=t.attrsMap["slot-scope"];if(t.if&&!t.ifProcessed&&!n)return ic(t,e,lc,"null");if(t.for&&!t.forProcessed)return sc(t,e,lc);var r=t.slotScope===ws?"":String(t.slotScope),o="function(".concat(r,"){")+"return ".concat("template"===t.tag?t.if&&n?"(".concat(t.if,")?").concat(fc(t,e)||"undefined",":undefined"):fc(t,e)||"undefined":nc(t,e),"}"),i=r?"":",proxy:true";return"{key:".concat(t.slotTarget||'"default"',",fn:").concat(o).concat(i,"}")}function fc(t,e,n,r,o){var i=t.children;if(i.length){var a=i[0];if(1===i.length&&a.for&&"template"!==a.tag&&"slot"!==a.tag){var s=n?e.maybeComponent(a)?",1":",0":"";return"".concat((r||nc)(a,e)).concat(s)}var c=n?function(t,e){for(var n=0,r=0;r':'
',_c.innerHTML.indexOf(" ")>0}var xc=!!q&&wc(!1),Cc=!!q&&wc(!0),kc=$((function(t){var e=co(t);return e&&e.innerHTML})),Sc=Er.prototype.$mount;return Er.prototype.$mount=function(t,e){if((t=t&&co(t))===document.body||t===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=kc(r));else{if(!r.nodeType)return this;r=r.innerHTML}else t&&(r=function(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}(t));if(r){var o=$c(r,{outputSourceRange:!1,shouldDecodeNewlines:xc,shouldDecodeNewlinesForHref:Cc,delimiters:n.delimiters,comments:n.comments},this),i=o.render,a=o.staticRenderFns;n.render=i,n.staticRenderFns=a}}return Sc.call(this,t,e)},Er.compile=$c,A(Er,Jn),Er.effect=function(t,e){var n=new Xn(ut,t,E,{sync:!0});e&&(n.update=function(){e((function(){return n.run()}))})},Er})); -------------------------------------------------------------------------------- /uiviewer/static/cdn/unpkg.com/element-theme-dark@1.0.2/lib/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codematrixer/ui-viewer/8e439370ed53f820a5637904df72a25812ff154a/uiviewer/static/cdn/unpkg.com/element-theme-dark@1.0.2/lib/fonts/element-icons.ttf -------------------------------------------------------------------------------- /uiviewer/static/cdn/unpkg.com/element-theme-dark@1.0.2/lib/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codematrixer/ui-viewer/8e439370ed53f820a5637904df72a25812ff154a/uiviewer/static/cdn/unpkg.com/element-theme-dark@1.0.2/lib/fonts/element-icons.woff -------------------------------------------------------------------------------- /uiviewer/static/css/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | margin: 0; 4 | } 5 | .el-button { 6 | background-color: #3679E3 !important; 7 | border-color: #3679E3 !important; 8 | color: white !important; 9 | } 10 | .el-button:hover { 11 | background-color: #3966C3 !important; 12 | border-color: #3966C3 !important; 13 | color: white !important; 14 | } 15 | 16 | .el-select .el-input__inner:focus { 17 | border-color: #3966C3 !important; 18 | } 19 | .el-select .el-input__inner::placeholder { 20 | color: #a5a2a2; 21 | } 22 | .el-select-dropdown, 23 | .el-select .el-input__inner, 24 | .el-select-dropdown .el-select-dropdown__item { 25 | background-color: #212933 !important; 26 | color: #FDFDFD !important; 27 | border-color: #212933 !important; 28 | } 29 | 30 | .custom-input .el-input__inner { 31 | border-color: #2D3744; 32 | } 33 | .custom-input .el-input__inner::placeholder { 34 | color: #a5a2a2; 35 | } 36 | .custom-input .el-input__inner:focus, 37 | .custom-input .el-input__inner:hover { 38 | border-color: #3966C3; 39 | } 40 | 41 | 42 | #app { 43 | display: flex; 44 | flex-direction: column; 45 | height: 100%; 46 | width: 100%; 47 | } 48 | .header { 49 | height: 65px; 50 | display: flex; 51 | align-items: center; 52 | padding: 0 20px; 53 | background-color: #222222; 54 | border-bottom: #333844 1px solid; 55 | color: #fff; 56 | } 57 | .main { 58 | display: flex; 59 | width: 100%; 60 | height: calc(100% - 65px); 61 | } 62 | 63 | .left, .center, .right { 64 | display: flex; 65 | flex-direction: column; 66 | } 67 | .left { 68 | width: 25%; 69 | background-color: #212933; 70 | border-right: #333844 1px solid; 71 | justify-content: center; 72 | align-items: center; 73 | position: relative; 74 | } 75 | #screenshotCanvas, #hierarchyCanvas { 76 | position: absolute; 77 | top: 0; 78 | left: 0; 79 | width: 100%; 80 | height: 100%; 81 | } 82 | .center { 83 | width: 30%; 84 | background-color: #212933; 85 | } 86 | 87 | .right { 88 | /* flex: 1; */ 89 | width: 45%; 90 | padding-right: 12px; 91 | background-color: #212933; 92 | } 93 | 94 | .divider { 95 | width: 1.5px; 96 | background-color: #333844; 97 | cursor: ew-resize; 98 | height: 100%; 99 | transition: background-color 0.3s; 100 | } 101 | .divider-hover, .divider-dragging { 102 | background-color: #3679E3; 103 | width: 3px; 104 | } 105 | 106 | .region-title { 107 | font-weight: bold; 108 | margin-left: 15px; 109 | margin-bottom: 10px; 110 | } 111 | 112 | .custom-table { 113 | width: 100%; 114 | border-bottom: none !important; 115 | overflow: auto; 116 | } 117 | 118 | .custom-tree { 119 | overflow: auto; 120 | white-space: nowrap; 121 | } 122 | 123 | .custom-table .el-table__row { 124 | margin-bottom: 0; 125 | } 126 | 127 | .custom-table .el-table__cell { 128 | padding: 5px; 129 | border-right: 1px solid #333844; 130 | border-bottom: 1px solid #333844; 131 | } 132 | 133 | .custom-table .el-table__body-wrapper { 134 | border-bottom: none !important; 135 | } 136 | 137 | .custom-table .el-table__row:last-child .el-table__cell { 138 | border-bottom: none !important; 139 | } 140 | 141 | .custom-table .el-table__header-wrapper, 142 | .custom-table .el-table__body-wrapper { 143 | width: 100% !important; 144 | } 145 | 146 | .custom-table .el-table__header, 147 | .custom-table .el-table__body { 148 | width: 100% !important; 149 | } 150 | 151 | .attr-button { 152 | font-size: 12px; 153 | padding: 2px 3px; 154 | margin-left: 5px; 155 | } 156 | 157 | code { 158 | background-color: #2D3740; 159 | padding: 3px 5px; 160 | border-radius: 4px; 161 | font-family: monospace; 162 | } 163 | 164 | .custom-link { 165 | color: #BBBBBB; 166 | text-decoration: none !important; 167 | } 168 | .custom-link:hover { 169 | color: white; 170 | text-decoration: underline; 171 | } 172 | 173 | .loading { 174 | position: relative; 175 | width: 30px; 176 | height: 30px; 177 | border: 2px solid #000; 178 | border-top-color: rgba(0, 0, 0, 0.2); 179 | border-right-color: rgba(0, 0, 0, 0.2); 180 | border-bottom-color: rgba(0, 0, 0, 0.2); 181 | border-radius: 100%; 182 | 183 | animation: circle infinite 0.75s linear; 184 | } 185 | 186 | @keyframes circle { 187 | 0% { 188 | transform: rotate(0); 189 | } 190 | 100% { 191 | transform: rotate(360deg); 192 | } 193 | } -------------------------------------------------------------------------------- /uiviewer/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codematrixer/ui-viewer/8e439370ed53f820a5637904df72a25812ff154a/uiviewer/static/favicon.ico -------------------------------------------------------------------------------- /uiviewer/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UI Viewer 7 | 8 | 9 | 10 | 11 | 12 | 13 |
18 | 19 |
20 |
21 | UI Viewer 22 | {{version}} 23 |
24 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 44 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 57 | 62 | 63 | 64 | 65 | 66 | 70 | 71 | {{ isConnected ? 'Connected' : 'Connect' }} 72 | 73 | 74 | 78 | 81 | Dump Hierarchy 82 | 83 | 84 | 85 | 89 | 92 | 93 | Dump Hierarchy 94 | 95 | 96 |
97 | GitHub 102 | 103 | 104 |
105 |
106 |
107 | 108 | 109 |
110 |
111 |

Selected Element Info

112 | 116 | 117 | 118 | 125 | 126 | 127 |
128 |
134 |
135 | 136 |
137 |

UI hierarchy 138 |

139 | 144 | 145 | 155 | 156 |
157 |
158 |
159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /uiviewer/static/js/api.js: -------------------------------------------------------------------------------- 1 | import { API_HOST } from './config.js'; 2 | 3 | async function checkResponse(response) { 4 | if (response.status === 500) { 5 | throw new Error('Server error: 500'); 6 | } 7 | return response.json(); 8 | } 9 | 10 | export async function getVersion() { 11 | const response = await fetch(`${API_HOST}version`); 12 | return checkResponse(response); 13 | } 14 | 15 | export async function listDevices(platform) { 16 | const response = await fetch(`${API_HOST}${platform}/serials`); 17 | return checkResponse(response); 18 | } 19 | 20 | export async function connectDevice(platform, serial, wdaUrl, maxDepth) { 21 | let url = `${API_HOST}${platform}/${serial}/connect`; 22 | 23 | if (platform === 'ios') { 24 | const queryParams = []; 25 | if (wdaUrl) { 26 | queryParams.push(`wdaUrl=${encodeURIComponent(wdaUrl)}`); 27 | } 28 | if (maxDepth) { 29 | queryParams.push(`maxDepth=${encodeURIComponent(maxDepth)}`); 30 | } 31 | 32 | if (queryParams.length > 0) { 33 | url += `?${queryParams.join('&')}`; 34 | } 35 | } 36 | 37 | const response = await fetch(url, { 38 | method: 'POST' 39 | }); 40 | 41 | return checkResponse(response); 42 | } 43 | 44 | export async function fetchScreenshot(platform, serial) { 45 | const response = await fetch(`${API_HOST}${platform}/${serial}/screenshot`); 46 | return checkResponse(response); 47 | } 48 | 49 | export async function fetchHierarchy(platform, serial) { 50 | const response = await fetch(`${API_HOST}${platform}/${serial}/hierarchy`); 51 | return checkResponse(response); 52 | } 53 | 54 | export async function fetchXpathLite(platform, treeData, nodeId) { 55 | const response = await fetch(`${API_HOST}${platform}/hierarchy/xpathLite`, { 56 | method: 'POST', 57 | headers: { 58 | 'Content-Type': 'application/json' 59 | }, 60 | body: JSON.stringify({ 61 | tree_data: treeData, 62 | node_id: nodeId 63 | }) 64 | }); 65 | 66 | return checkResponse(response); 67 | } -------------------------------------------------------------------------------- /uiviewer/static/js/config.js: -------------------------------------------------------------------------------- 1 | export const API_HOST = '/'; 2 | -------------------------------------------------------------------------------- /uiviewer/static/js/index.js: -------------------------------------------------------------------------------- 1 | import { saveToLocalStorage, getFromLocalStorage, copyToClipboard } from './utils.js'; 2 | import { getVersion, listDevices, connectDevice, fetchScreenshot, fetchHierarchy, fetchXpathLite } from './api.js'; 3 | 4 | 5 | new Vue({ 6 | el: '#app', 7 | data() { 8 | return { 9 | version: "", 10 | platform: getFromLocalStorage('platform', 'harmony'), 11 | serial: "", 12 | devices: [], 13 | isConnected: false, 14 | isConnecting: false, 15 | isDumping: false, 16 | wdaUrl: getFromLocalStorage('wdaUrl', ''), 17 | snapshotMaxDepth: getFromLocalStorage('snapshotMaxDepth', 30), 18 | 19 | packageName: getFromLocalStorage('packageName', ''), 20 | activityName: getFromLocalStorage('activityName', ''), 21 | displaySize: getFromLocalStorage('displaySize', [0, 0]), 22 | scale: getFromLocalStorage('scale', 1), 23 | screenshotTransform: {scale: 1, offsetX: 0, offsetY: 0}, 24 | jsonHierarchy: {}, 25 | xpathLite: "//", 26 | mouseClickCoordinatesPercent: null, 27 | hoveredNode: null, 28 | selectedNode: null, 29 | 30 | treeData: [], 31 | defaultTreeProps: { 32 | children: 'children', 33 | label: this.getTreeLabel 34 | }, 35 | nodeFilterText: '', 36 | centerWidth: 500, 37 | isDividerHovered: false, 38 | isDragging: false 39 | }; 40 | }, 41 | computed: { 42 | selectedNodeDetails() { 43 | const isHarmony = this.platform === 'harmony'; 44 | const defaultDetails = this.getDefaultNodeDetails(this.platform); 45 | 46 | if (!this.selectedNode) { 47 | return defaultDetails; 48 | } 49 | 50 | const nodeDetails = Object.entries(this.selectedNode) 51 | .filter(([key]) => !['children', '_id', '_parentId', 'frame'].includes(key)) 52 | .map(([key, value]) => ({ 53 | key: key === '_type' ? (isHarmony ? 'type' : 'className') : key, 54 | value 55 | })); 56 | 57 | return [...defaultDetails, ...nodeDetails]; 58 | } 59 | }, 60 | watch: { 61 | platform(newVal) { 62 | saveToLocalStorage('platform', newVal); 63 | }, 64 | wdaUrl(newVal) { 65 | saveToLocalStorage('wdaUrl', newVal); 66 | }, 67 | snapshotMaxDepth(newVal) { 68 | saveToLocalStorage('snapshotMaxDepth', newVal); 69 | }, 70 | nodeFilterText(val) { 71 | this.$refs.treeRef.filter(val); 72 | } 73 | }, 74 | created() { 75 | this.fetchVersion(); 76 | }, 77 | mounted() { 78 | this.loadCachedScreenshot(); 79 | const canvas = this.$el.querySelector('#hierarchyCanvas'); 80 | canvas.addEventListener('mousemove', this.onMouseMove); 81 | canvas.addEventListener('click', this.onMouseClick); 82 | canvas.addEventListener('mouseleave', this.onMouseLeave); 83 | 84 | // 设置Canvas的尺寸和分辨率 85 | this.setupCanvasResolution('#screenshotCanvas'); 86 | this.setupCanvasResolution('#hierarchyCanvas'); 87 | }, 88 | methods: { 89 | initPlatform() { 90 | this.serial = '' 91 | this.isConnected = false 92 | this.selectedNode = null 93 | this.treeData = [] 94 | }, 95 | async fetchVersion() { 96 | try { 97 | const response = await getVersion(); 98 | this.version = response.data; 99 | } catch (err) { 100 | console.error(err); 101 | } 102 | }, 103 | async listDevice() { 104 | try { 105 | const response = await listDevices(this.platform); 106 | this.devices = response.data; 107 | } catch (err) { 108 | this.$message({ showClose: true, message: `Error: ${err.message}`, type: 'error' }); 109 | } 110 | }, 111 | async connectDevice() { 112 | this.isConnecting = true; 113 | try { 114 | if (!this.serial) { 115 | throw new Error('Please select device first'); 116 | } 117 | if (this.platform === 'ios' && !this.wdaUrl) { 118 | throw new Error('Please input wdaUrl first'); 119 | } 120 | 121 | const response = await connectDevice(this.platform, this.serial, this.wdaUrl, this.snapshotMaxDepth); 122 | if (response.success) { 123 | this.isConnected = true; 124 | await this.screenshotAndDumpHierarchy(); 125 | } else { 126 | throw new Error(response.message); 127 | } 128 | } catch (err) { 129 | this.$message({ showClose: true, message: `Error: ${err.message}`, type: 'error' }); 130 | } finally { 131 | this.isConnecting = false; 132 | } 133 | }, 134 | async screenshotAndDumpHierarchy() { 135 | this.isDumping = true; 136 | try { 137 | await this.fetchScreenshot(); 138 | await this.fetchHierarchy(); 139 | } catch (err) { 140 | this.$message({ showClose: true, message: `Error: ${err.message}`, type: 'error' }); 141 | } finally { 142 | this.isDumping = false; 143 | } 144 | }, 145 | async fetchScreenshot() { 146 | try { 147 | const response = await fetchScreenshot(this.platform, this.serial); 148 | if (response.success) { 149 | const base64Data = response.data; 150 | this.renderScreenshot(base64Data); 151 | saveToLocalStorage('cachedScreenshot', base64Data); 152 | } else { 153 | throw new Error(response.message); 154 | } 155 | } catch (error) { 156 | console.error(error); 157 | } 158 | }, 159 | async fetchHierarchy() { 160 | try { 161 | const response = await fetchHierarchy(this.platform, this.serial); 162 | if (response.success) { 163 | const ret = response.data; 164 | this.packageName = ret.packageName; 165 | this.activityName = ret.activityName; 166 | this.displaySize = ret.windowSize; 167 | this.scale = ret.scale; 168 | this.jsonHierarchy = ret.jsonHierarchy; 169 | this.treeData = [ret.jsonHierarchy]; 170 | 171 | saveToLocalStorage('packageName', ret.packageName); 172 | saveToLocalStorage('activityName', ret.activityName); 173 | saveToLocalStorage('displaySize', ret.windowSize); 174 | saveToLocalStorage('scale', ret.scale); 175 | 176 | this.hoveredNode = null; 177 | this.selectedNode = null; 178 | 179 | this.renderHierarchy(); 180 | } else { 181 | throw new Error(response.message); 182 | } 183 | } catch (error) { 184 | console.error(error); 185 | } 186 | }, 187 | renderHierarchy() { 188 | const canvas = this.$el.querySelector('#hierarchyCanvas'); 189 | const ctx = canvas.getContext('2d'); 190 | ctx.clearRect(0, 0, canvas.width, canvas.height); 191 | 192 | const { scale, offsetX, offsetY } = this.screenshotTransform; 193 | ctx.setLineDash([2, 6]); 194 | 195 | const drawNode = (node) => { 196 | if (node.rect) { 197 | const { x, y, width, height } = node.rect; 198 | ctx.strokeStyle = 'red'; 199 | ctx.lineWidth = 0.8; 200 | ctx.strokeRect(x * scale + offsetX, y * scale + offsetY, width * scale, height * scale); 201 | } 202 | if (node.children) { 203 | node.children.forEach(drawNode); 204 | } 205 | }; 206 | 207 | drawNode(this.jsonHierarchy); 208 | 209 | if (this.hoveredNode) { 210 | const { x, y, width, height } = this.hoveredNode.rect; 211 | ctx.setLineDash([]); 212 | ctx.globalAlpha = 0.6; 213 | ctx.fillStyle = '#3679E3'; 214 | ctx.fillRect(x * scale + offsetX, y * scale + offsetY, width * scale, height * scale); 215 | ctx.globalAlpha = 1.0; 216 | } 217 | 218 | if (this.selectedNode) { 219 | const { x, y, width, height } = this.selectedNode.rect; 220 | ctx.setLineDash([]); 221 | ctx.strokeStyle = 'red'; 222 | ctx.lineWidth = 2; 223 | ctx.strokeRect(x * scale + offsetX, y * scale + offsetY, width * scale, height * scale); 224 | } 225 | }, 226 | async fetchXpathLite(nodeId) { 227 | try { 228 | const response = await fetchXpathLite(this.platform,this.jsonHierarchy, nodeId); 229 | if (response.success) { 230 | this.xpathLite = response.data; 231 | } else { 232 | throw new Error(response.message); 233 | } 234 | } catch (error) { 235 | console.error(error); 236 | } 237 | }, 238 | loadCachedScreenshot() { 239 | const cachedScreenshot = getFromLocalStorage('cachedScreenshot', null); 240 | if (cachedScreenshot) { 241 | this.renderScreenshot(cachedScreenshot); 242 | } 243 | }, 244 | 245 | // 解决在高分辨率屏幕上,Canvas绘制的内容可能会显得模糊。这是因为Canvas的默认分辨率与屏幕的物理像素密度不匹配 246 | setupCanvasResolution(selector) { 247 | const canvas = this.$el.querySelector(selector); 248 | const dpr = window.devicePixelRatio || 1; 249 | const rect = canvas.getBoundingClientRect(); 250 | canvas.width = rect.width * dpr; 251 | canvas.height = rect.height * dpr; 252 | const ctx = canvas.getContext('2d'); 253 | ctx.scale(dpr, dpr); 254 | }, 255 | renderScreenshot(base64Data) { 256 | const img = new Image(); 257 | img.src = `data:image/png;base64,${base64Data}`; 258 | img.onload = () => { 259 | const canvas = this.$el.querySelector('#screenshotCanvas'); 260 | const ctx = canvas.getContext('2d'); 261 | 262 | const { clientWidth: canvasWidth, clientHeight: canvasHeight } = canvas; 263 | 264 | this.setupCanvasResolution('#screenshotCanvas'); 265 | 266 | const { width: imgWidth, height: imgHeight } = img; 267 | const scale = Math.min(canvasWidth / imgWidth, canvasHeight / imgHeight); 268 | const x = (canvasWidth - imgWidth * scale) / 2; 269 | const y = (canvasHeight - imgHeight * scale) / 2; 270 | 271 | this.screenshotTransform = { scale, offsetX: x, offsetY: y }; 272 | 273 | ctx.clearRect(0, 0, canvasWidth, canvasHeight); 274 | ctx.drawImage(img, x, y, imgWidth * scale, imgHeight * scale); 275 | 276 | this.setupCanvasResolution('#hierarchyCanvas'); 277 | }; 278 | }, 279 | findSmallestNode(node, mouseX, mouseY, scale, offsetX, offsetY) { 280 | let smallestNode = null; 281 | 282 | const checkNode = (node) => { 283 | if (node.rect) { 284 | const { x, y, width, height } = node.rect; 285 | const scaledX = x * scale + offsetX; 286 | const scaledY = y * scale + offsetY; 287 | const scaledWidth = width * scale; 288 | const scaledHeight = height * scale; 289 | 290 | if (mouseX >= scaledX && mouseY >= scaledY && mouseX <= scaledX + scaledWidth && mouseY <= scaledY + scaledHeight) { 291 | if (!smallestNode || (width * height < smallestNode.rect.width * smallestNode.rect.height)) { 292 | smallestNode = node; 293 | } 294 | } 295 | } 296 | if (node.children) { 297 | node.children.forEach(checkNode); 298 | } 299 | }; 300 | 301 | checkNode(node); 302 | return smallestNode; 303 | }, 304 | getDefaultNodeDetails(platform) { 305 | const commonDetails = [ 306 | { key: 'displaySize', value: this.displaySize }, 307 | {key: 'scale', value: this.scale }, 308 | { key: '点击坐标 %', value: this.mouseClickCoordinatesPercent } 309 | ]; 310 | 311 | switch (platform) { 312 | case 'ios': 313 | return [ 314 | { key: 'bundleId', value: this.packageName }, 315 | ...commonDetails 316 | ]; 317 | case 'android': 318 | return [ 319 | { key: 'packageName', value: this.packageName }, 320 | { key: 'activityName', value: this.activityName }, 321 | ...commonDetails 322 | ]; 323 | case 'harmony': 324 | return [ 325 | { key: 'packageName', value: this.packageName }, 326 | { key: 'pageName', value: this.activityName }, 327 | ...commonDetails 328 | ]; 329 | default: 330 | return commonDetails; 331 | } 332 | }, 333 | onMouseMove(event) { 334 | const canvas = this.$el.querySelector('#hierarchyCanvas'); 335 | const rect = canvas.getBoundingClientRect(); 336 | const mouseX = event.clientX - rect.left; 337 | const mouseY = event.clientY - rect.top; 338 | 339 | const { scale, offsetX, offsetY } = this.screenshotTransform; 340 | 341 | const hoveredNode = this.findSmallestNode(this.jsonHierarchy, mouseX, mouseY, scale, offsetX, offsetY); 342 | if (hoveredNode !== this.hoveredNode) { 343 | this.hoveredNode = hoveredNode; 344 | this.renderHierarchy(); 345 | } 346 | }, 347 | async onMouseClick(event) { 348 | const canvas = this.$el.querySelector('#hierarchyCanvas'); 349 | const rect = canvas.getBoundingClientRect(); 350 | const mouseX = event.clientX - rect.left; 351 | const mouseY = event.clientY - rect.top; 352 | 353 | const { scale, offsetX, offsetY } = this.screenshotTransform; 354 | 355 | const percentX = (mouseX / canvas.width); 356 | const percentY = (mouseY / canvas.height); 357 | 358 | this.mouseClickCoordinatesPercent = `(${percentX.toFixed(2)}, ${percentY.toFixed(2)})`; 359 | 360 | const selectedNode = this.findSmallestNode(this.jsonHierarchy, mouseX, mouseY, scale, offsetX, offsetY); 361 | if (selectedNode !== this.selectedNode) { 362 | this.selectedNode = selectedNode ? selectedNode : null; 363 | 364 | await this.fetchXpathLite(selectedNode._id) 365 | this.selectedNode && (this.selectedNode.xpath = this.xpathLite); 366 | 367 | this.renderHierarchy(); 368 | 369 | } else { 370 | // 保证每次点击重新计算`selectedNodeDetails`,更新点击坐标 371 | this.selectedNode = { ...this.selectedNode }; 372 | } 373 | }, 374 | onMouseLeave() { 375 | if (this.hoveredNode) { 376 | this.hoveredNode = null; 377 | this.renderHierarchy(); 378 | } 379 | }, 380 | async handleTreeNodeClick(node) { 381 | this.selectedNode = node; 382 | 383 | await this.fetchXpathLite(node._id) 384 | this.selectedNode && (this.selectedNode.xpath = this.xpathLite); 385 | 386 | this.renderHierarchy(); 387 | }, 388 | filterNode(value, data) { 389 | if (!value) return true; 390 | if (!data) return false; 391 | const { _type, resourceId, lable, text, id } = data; 392 | const filterMap = { 393 | android: [_type, resourceId, text], 394 | ios: [_type, lable], 395 | harmony: [_type, text, id] 396 | }; 397 | const fieldsToFilter = filterMap[this.platform]; 398 | const isFieldMatch = fieldsToFilter.some(field => field && field.indexOf(value) !== -1); 399 | const label = this.getTreeLabel(data); 400 | const isLabelMatch = label && label.indexOf(value) !== -1; 401 | return isFieldMatch || isLabelMatch; 402 | }, 403 | getTreeLabel(node) { 404 | const { _type="", resourceId="", label="", text="", id="" } = node; 405 | const labelMap = { 406 | android: resourceId || text, 407 | ios: label, 408 | harmony: text || id 409 | }; 410 | return `${_type} - ${labelMap[this.platform] || ''}`; 411 | }, 412 | copyToClipboard(value) { 413 | const success = copyToClipboard(value); 414 | this.$message({ showClose: true, message: success ? "复制成功" : "复制失败", type: success ? 'success' : 'error' }); 415 | }, 416 | startDrag(event) { 417 | this.isDragging = true; 418 | document.addEventListener('mousemove', this.onDrag); 419 | document.addEventListener('mouseup', this.stopDrag); 420 | }, 421 | onDrag(event) { 422 | this.centerWidth = event.clientX - this.$el.querySelector('.left').offsetWidth; 423 | }, 424 | stopDrag() { 425 | this.isDragging = false; 426 | document.removeEventListener('mousemove', this.onDrag); 427 | document.removeEventListener('mouseup', this.stopDrag); 428 | }, 429 | hoverDivider() { 430 | this.isDividerHovered = true; 431 | }, 432 | leaveDivider() { 433 | this.isDividerHovered = false; 434 | } 435 | } 436 | }); -------------------------------------------------------------------------------- /uiviewer/static/js/utils.js: -------------------------------------------------------------------------------- 1 | export function saveToLocalStorage(key, value) { 2 | localStorage.setItem(key, value); 3 | } 4 | 5 | export function getFromLocalStorage(key, defaultValue) { 6 | return localStorage.getItem(key) || defaultValue; 7 | } 8 | 9 | export function copyToClipboard(value) { 10 | if (typeof value === 'object') { 11 | value = JSON.stringify(value, null, 2); 12 | } 13 | 14 | if (value === null || value === undefined || value === '') { 15 | value = ''; 16 | } 17 | 18 | const textarea = document.createElement('textarea'); 19 | textarea.value = value; 20 | document.body.appendChild(textarea); 21 | textarea.select(); 22 | try { 23 | document.execCommand('copy'); 24 | return true; 25 | } catch (err) { 26 | return false; 27 | } finally { 28 | document.body.removeChild(textarea); 29 | } 30 | } --------------------------------------------------------------------------------