From 86266646533e2345ff214884c8d3103d085159c8 Mon Sep 17 00:00:00 2001 From: youmetme <321640253@qq.com> Date: Thu, 3 Oct 2024 18:23:21 +0800 Subject: [PATCH] init --- .gitignore | 1 + .python-version | 1 + .vscode/settings.json | 7 ++ README.md | 5 + api/api.py | 1 + app.py | 12 ++ core/__init__.py | 0 core/objects/backup_object.py | 67 +++++++++++ core/objects/blob_object.py | 70 +++++++++++ core/objects/tree_object.py | 31 +++++ core/plan/Plan.py | 8 ++ core/plan/PlanMinHeap.py | 15 +++ core/plan/__init__.py | 0 core/target/__init__.py | 0 core/target/targetBase.py | 25 ++++ core/timer/__init__.py | 0 dockerfile | 0 docs/开发指南.md | 0 makefile | 0 pyproject.toml | 11 ++ script/build.sh | 0 test.py | 4 + tests/core/objects/test_back_object.py | 0 tests/core/objects/test_blob_object.py | 17 +++ tests/core/objects/test_tree_object.py | 0 utils/log.py | 6 + uv.lock | 156 +++++++++++++++++++++++++ web/.gitignore | 30 +++++ web/.prettierrc.js | 22 ++++ web/.vscode/extensions.json | 3 + web/README.md | 33 ++++++ web/bun.lockb | Bin 0 -> 67950 bytes web/env.d.ts | 1 + web/index.html | 13 +++ web/package.json | 29 +++++ web/public/favicon.ico | Bin 0 -> 4286 bytes web/src/App.vue | 21 ++++ web/src/assets/base.css | 86 ++++++++++++++ web/src/assets/logo.svg | 1 + web/src/assets/main.css | 35 ++++++ web/src/main.ts | 6 + web/tsconfig.app.json | 14 +++ web/tsconfig.json | 11 ++ web/tsconfig.node.json | 19 +++ web/vite.config.ts | 16 +++ 45 files changed, 777 insertions(+) create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 api/api.py create mode 100644 app.py create mode 100644 core/__init__.py create mode 100644 core/objects/backup_object.py create mode 100644 core/objects/blob_object.py create mode 100644 core/objects/tree_object.py create mode 100644 core/plan/Plan.py create mode 100644 core/plan/PlanMinHeap.py create mode 100644 core/plan/__init__.py create mode 100644 core/target/__init__.py create mode 100644 core/target/targetBase.py create mode 100644 core/timer/__init__.py create mode 100644 dockerfile create mode 100644 docs/开发指南.md create mode 100644 makefile create mode 100644 pyproject.toml create mode 100644 script/build.sh create mode 100644 test.py create mode 100644 tests/core/objects/test_back_object.py create mode 100644 tests/core/objects/test_blob_object.py create mode 100644 tests/core/objects/test_tree_object.py create mode 100644 utils/log.py create mode 100644 uv.lock create mode 100644 web/.gitignore create mode 100644 web/.prettierrc.js create mode 100644 web/.vscode/extensions.json create mode 100644 web/README.md create mode 100755 web/bun.lockb create mode 100644 web/env.d.ts create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/public/favicon.ico create mode 100644 web/src/App.vue create mode 100644 web/src/assets/base.css create mode 100644 web/src/assets/logo.svg create mode 100644 web/src/assets/main.css create mode 100644 web/src/main.ts create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a592909 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ + +# 千手 +一款计划备份工具 + + diff --git a/api/api.py b/api/api.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/api.py @@ -0,0 +1 @@ + diff --git a/app.py b/app.py new file mode 100644 index 0000000..ef0fa1c --- /dev/null +++ b/app.py @@ -0,0 +1,12 @@ +from flask import Flask + +app = Flask(__name__, static_folder="./web/dist/", static_url_path="") + + +@app.route("/") +def main(): + return app.send_static_file("index.html") + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/objects/backup_object.py b/core/objects/backup_object.py new file mode 100644 index 0000000..e322e05 --- /dev/null +++ b/core/objects/backup_object.py @@ -0,0 +1,67 @@ +""" +******************************************** +* @Date: 2024 09 27 +* @Description: BackupObject备份对象 +******************************************** +""" + +import os +from typing import List, Union +from .tree_object import TreeObject +from .blob_object import BlobObject + + +class BackupObject(ObjectBase): + """ + 备份对象,负责管理存储对象(tree、blob)对象、备份信息、恢复等操作 + """ + + backup_name: str + backup_time_lately: str + backup_time_create: str + backup_size: int + backup_version_number: int + backup_tree: List[Union[TreeObject, BlobObject]] + new_backup_flag: bool + + def __init__(self, backup_name: str, backup_base_path: str): + self.backup_name = backup_name + + backup_path = os.path.join(backup_base_path, backup_name) + if not os.path.exists(backup_path): + self.new_backup_flag = True + else: + self.new_backup_flag = False + pass #TODO 读取备份信息 + + + def createNewBackup(self, backup_dirs: List[str]): + """ + 创建新的备份 + + :return + """ + for backup_dir in backup_dirs: + if os.path.isdir(backup_dir): + self.backup_tree.append(TreeObject(backup_dir)) + else: + self.backup_tree.append(BlobObject(backup_dir)) + + def backup(self): + pass + + def recover(self, recover_path: str): + """ + 恢复到指定备份 + + :return + """ + pass + + def getBackupTree(self): + """ + 获取备份树 + + :return + """ + pass diff --git a/core/objects/blob_object.py b/core/objects/blob_object.py new file mode 100644 index 0000000..7deb68e --- /dev/null +++ b/core/objects/blob_object.py @@ -0,0 +1,70 @@ +""" +******************************************** +* @Date: 2024 09 27 +* @Description: BlobObject存储对象 +******************************************** +""" + +import hashlib +import os +import zlib + + +class BlobObject: + object_id: str + __buff_size: int = 8192 + + def __init__(self, file_path: str) -> None: + self.file_path = file_path + self.object_id = self.contentSha1() + + def writeBlob(self, base_path) -> None: + Folder = base_path + "/" + self.__getFolderName() + if not os.path.exists(Folder): + os.makedirs(Folder) + self.__compressFile( + self.file_path, + base_path + "/" + self.__getFolderName() + "/" + self.__getFileName(), + ) + + def __compressFile(self, file_path, save_path) -> None: + compresser = zlib.compressobj(9) + write_file = open(save_path, "wb") + with open(file_path, "rb") as f: + while True: + data = f.read(self.__buff_size) + if not data: + break + compressedData = compresser.compress(data) + write_file.write(compressedData) + write_file.write(compresser.flush()) + write_file.close() + + def __getFolderName(self) -> str: + return self.object_id[:2] + + def __getFileName(self) -> str: + return self.object_id[2:] + + def getBlobAbsPath(self) -> str: + """ + 获取blob的绝对路径,此相对路径是基于存储路径的,不是相对当前文件的路径 + + :return 相对路径,格式为xx/xxxx... + """ + return self.__getFolderName() + "/" + self.__getFileName() + + def contentSha1(self) -> str: + """ + 计算文件内容的sha1值 + + :return sha1的hex值 + """ + with open(self.file_path, "rb") as f: + sha1 = hashlib.sha1() + while True: + data = f.read(self.__buff_size) + if not data: + break + sha1.update(data) + return sha1.hexdigest() diff --git a/core/objects/tree_object.py b/core/objects/tree_object.py new file mode 100644 index 0000000..cc40aa4 --- /dev/null +++ b/core/objects/tree_object.py @@ -0,0 +1,31 @@ +""" +******************************************** +* @Date: 2024 09 27 +* @Description: BackObject树对象,标记多个存储存储对象和树对象,为备份对象提供目标 +******************************************** +""" + +from typing import Dict, List +from .blob_object import BlobObject +import os + + +class TreeObject: + __children: List[BlobObject] = [] # 备份树节点列表,节点为BlobObject备份存储对象 + __file_map: Dict[str, str] = {} # 备份文件路径到blob对象的映射 + + def __init__(self, tree_path: str): + self.tree_path = tree_path + self.__loadChildren() + + def __loadChildren(self): + for root, dirs, files in os.walk(self.tree_path): + for name in files: + blob_path = os.path.join(root, name) + blob = BlobObject(blob_path) + self.__children.append(blob) + self.__file_map[blob_path] = blob.object_id + + def writeBlobs(self, base_path: str): + for child in self.__children: + child.writeBlob(base_path) diff --git a/core/plan/Plan.py b/core/plan/Plan.py new file mode 100644 index 0000000..9338e2c --- /dev/null +++ b/core/plan/Plan.py @@ -0,0 +1,8 @@ +class Plan: + __priority_factor: float # 优先级因子 + + def __init__(self): + pass + + def getPriorityFactor(self) -> float: + return self.__priority_factor diff --git a/core/plan/PlanMinHeap.py b/core/plan/PlanMinHeap.py new file mode 100644 index 0000000..4ba93e4 --- /dev/null +++ b/core/plan/PlanMinHeap.py @@ -0,0 +1,15 @@ +from typing import List +from .Plan import Plan + + +class PlanPriorityMinHeap: + """ + 计划优先小顶堆,按照优先级存储多个计划 + """ + + plans: List[Plan] + def __init__(self): + self.plans = [] + + def addPlan(self, plan: Plan): + pass diff --git a/core/plan/__init__.py b/core/plan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/target/__init__.py b/core/target/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/target/targetBase.py b/core/target/targetBase.py new file mode 100644 index 0000000..c68f1ea --- /dev/null +++ b/core/target/targetBase.py @@ -0,0 +1,25 @@ +""" +******************************************** +* @Date: 2024 09 27 +* @Description: TargetBase存储后端的抽象类 +******************************************** +""" + +from abc import ABC, abstractmethod +from core.objects.back_object import BackObject + + +class TargetBase(ABC): + @abstractmethod + def test(self) -> bool: + """ + 测试目标连通性 + + :param + :return + """ + pass + + @abstractmethod + def back(self, back_object: BackObject): + pass diff --git a/core/timer/__init__.py b/core/timer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/docs/开发指南.md b/docs/开发指南.md new file mode 100644 index 0000000..e69de29 diff --git a/makefile b/makefile new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0634ffb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "thousandhands" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "flask>=3.0.3", + "pytest>=8.3.3", +] + diff --git a/script/build.sh b/script/build.sh new file mode 100644 index 0000000..e69de29 diff --git a/test.py b/test.py new file mode 100644 index 0000000..5681f79 --- /dev/null +++ b/test.py @@ -0,0 +1,4 @@ +import pytest + +if __name__ == "__main__": + pytest.main(["tests/", "-v"]) diff --git a/tests/core/objects/test_back_object.py b/tests/core/objects/test_back_object.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/objects/test_blob_object.py b/tests/core/objects/test_blob_object.py new file mode 100644 index 0000000..61ee7a9 --- /dev/null +++ b/tests/core/objects/test_blob_object.py @@ -0,0 +1,17 @@ +from core.objects.blob_object import BlobObject +import hashlib +import tempfile + + +class Test_Blob: + + def test_newBlob(self): + newBlob = BlobObject("README.md") + assert ( + newBlob.object_id + == hashlib.sha1(open("README.md", "rb").read()).hexdigest() + ) + def test_writeBlob(self): + newBlob = BlobObject("README.md") + with tempfile.TemporaryDirectory() as tmpdirname: + newBlob.writeBlob(tmpdirname) \ No newline at end of file diff --git a/tests/core/objects/test_tree_object.py b/tests/core/objects/test_tree_object.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/log.py b/utils/log.py new file mode 100644 index 0000000..4392010 --- /dev/null +++ b/utils/log.py @@ -0,0 +1,6 @@ +import logging + + +class Logger: + def __init__(self, name): + self.logger = logging.getLogger(name) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..6a74674 --- /dev/null +++ b/uv.lock @@ -0,0 +1,156 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "blinker" +version = "1.8.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/57/a6a1721eff09598fb01f3c7cda070c1b6a0f12d63c83236edf79a440abcc/blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83", size = 23161 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", size = 9456 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "flask" +version = "3.0.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/e1/d104c83026f8d35dfd2c261df7d64738341067526406b40190bc063e829a/flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842", size = 676315 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", size = 101735 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "thousandhands" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "flask", specifier = ">=3.0.3" }, + { name = "pytest", specifier = ">=8.3.3" }, +] + +[[package]] +name = "werkzeug" +version = "3.0.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/e2/6dbcaab07560909ff8f654d3a2e5a60552d937c909455211b1b36d7101dc/werkzeug-3.0.4.tar.gz", hash = "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306", size = 803966 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl", hash = "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", size = 227554 }, +] diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..8ee54e8 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/web/.prettierrc.js b/web/.prettierrc.js new file mode 100644 index 0000000..299a8f7 --- /dev/null +++ b/web/.prettierrc.js @@ -0,0 +1,22 @@ +module.exports = { + printWidth: 200, //单行长度 + tabWidth: 4, //缩进长度 + useTabs: false, //使用空格代替tab缩进 + semi: false, //句末使用分号 + singleQuote: true, //使用单引号 + quoteProps: 'as-needed', //仅在必需时为对象的key添加引号 + jsxSingleQuote: true, // jsx中使用单引号 + trailingComma: 'all', //多行时尽可能打印尾随逗号 + bracketSpacing: true, //在对象前后添加空格-eg: { foo: bar } + jsxBracketSameLine: true, //多属性html标签的‘>’折行放置 + arrowParens: 'always', //单参数箭头函数参数周围使用圆括号-eg: (x) => x + requirePragma: false, //无需顶部注释即可格式化 + insertPragma: false, //在已被preitter格式化的文件顶部加上标注 + proseWrap: 'preserve', //不知道怎么翻译 + htmlWhitespaceSensitivity: 'ignore', //对HTML全局空白不敏感 + vueIndentScriptAndStyle: false, //不对vue中的script及style标签缩进 + endOfLine: 'lf', //结束行形式 + embeddedLanguageFormatting: 'auto', //对引用代码进行格式化 + }; + + \ No newline at end of file diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..13a91a0 --- /dev/null +++ b/web/README.md @@ -0,0 +1,33 @@ +# web + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +bun install +``` + +### Compile and Hot-Reload for Development + +```sh +bun dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +bun build +``` diff --git a/web/bun.lockb b/web/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..a4e760bacfe288349250241ed599824739be15c1 GIT binary patch literal 67950 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� zQdzP-C9|!&b=*`&>OTD-QAPV6`^u_+pywkI-%bLufGfn|WHL!uNw2b|Q|pJT)22}^r+?xyG%rwzuceoB;N=Xe%=n@%6DYxwqc z*7orj{k(HacYv&aJ$*0Pl3-5NgVOY-M<()n&wM2EEUCWM5pnR*1pe2ucNq2j^kI7! z2=3NzP!#J-cc{H8JFiTw`YuNpEAO!-f*&tGCKx$A!9w2wgyewRo;*7DwL zNpH*-^}aQVdn&FnAMkH~73?K3ojkAV62XK2#6oW-zSaLR#=zvmPIrW^I#RdRZyDDP zcQI3-`_&R=zL@H{`sW_CKZF`*%hWyCtB*@9>`@5I5TH70E5UlB+gEkYi&1(1UY%Mh zXVS^uxN%+eEnWPr_@bZlZpFcNZE$bav;2k0q1Vq2i?OHDt`)CdNS>|yuhADCZLvTrDRVj2^uy`{T-eA7hwd$F?Y2D=D%gQZn_ zAWIeAx(I{Gp+-tS^YLuSDuKF^xr0{hk9dsfZv=2GpP|#SAwAet8@VSXvf#VcgtVo6 z!S;F7_W6SrQrC6mE9$hH+dJjU%-2rzBS6%&Ybsz zUJcd^$3O2BsB#Gt53OEtD$UbSy!3i(QS@l=aa;-t+SWunkDYOYy2p^ZB!tNYX{A@M zFq@uOJ+?TmB^Yh{{z6@Sq^`F{^0F=k&fd40g4snM={js@ z8rt1dKIaoTNHATL{V1dIA_=pvh@ParW`Kp{mA!`ei{tn$$1n3A6zWuX;z|y`GaUoI zwF0aGQuhJNd^hK;^eN_nj+E2p4N1+PEiDIJCk?PkIU7%Aj9037l1E3}c_^hDPLOZ6rdfnd;Kf5uzSQiNzFWH6=MP5zLD$z| zimmPAxT z?u6oW#6`TyQx*taW29~(m5SS!NEt~i+gr{t{syFmr>yUko*i7*Io9+&JSxd)gWem< zPu9sHF1^HWhG71y@x~~Z()Cx8#xnJKbL$Mq=VT^G-4A8`ulQ}`3!0|Q8|AXw$FihF z8)$LdgZAsXek-p#WAiEJ$*o(#+@J2K?i<^ecq>`!!}}mrLxY3lx-XK3zA7C<_-l&P zt$D265$EmB`}|8%|NHaU?;iEPx1Y59KK@Jw_brR6wN`4LN81$5xsz6xmim)yJcb^@hYlhS)@$Wm!Mgbs_9C2nN14=KSIO0OGxN8jBqEuS z&l!#*b-!`EyvJH7L4ujTvB&L-M1-XI>b=!{E!Rx@EWbJzuc&^1m*VJm`a4;IZQv9#iXalb#{WHs+VjA)Ya zy!ZWrEII=5!TYa?B05=o6pj1xT@_znxa4wpJ!fxwxY3!V0OIrCl{#VA8?ZHx^Ivy5g07aY$>@Lo>=Bmyd8&2OBtu9&|9G_ZXNy z5x`<}@S#L90locXS zq&Zlw8i`*hw*8LWb{uSxx@=l=XMDP2ZNvx=jC=3w(+97p2aR)=^pQ>F`d>PHmZ#MJ2 zVq}&$kTmtNss>%glgDQklfB-KK9J?)Yrf)hx$S=H*8{Q<#Pf_&Ur%}KdIvr}^#Gx3 zkJPnoEI8{M#al*Y%g<+WSd4>j*o}6jBtVdaYSQoggdCmdsh;H%Bywh+)zNR}_FONy z^j-I1mJC3Pg14eJfcrz*Z*JE1)ng%joN zYPU8yzA*4@^$Fd){oP9cx2{AlPq;Owt$ryIVUbY!!gIxS%~`;7`?_UY7yZ69l*ewJ z)jP@5K9Xm>?~c)Vle%2DwTOZ4W!qc5~LZpiRWstE@R(-6^gr*Rgm3zPGd zl)s&3i!zrAt_mi*IE#UmGo^Y!ZZ5r4O@*o~`OL>mM0`&obu+Ak^{K2)J4h4ua4wv{ zroqe06mol`w|6=3l`B;$4NgY}dCHpoV&B!}rKDl%yBK&nj>go@*FSXqb>KVSm)0n!4-0)f zTb$#8C+C)i2g7DobjO>|#LCk+*KK?hcB;{zU6~`oUhF~Wx+8TTmDJ*vEZ&H3QE-dl z?Ve7aaIel~ckqEs4q-}%;$ zEo8lYv$Qa)Tl@94)78rvLcPfK%oC|AHWi~*dMh#KD_hQWMPa9_+63Lg%lLa!scFDd z{((1Z+*t3#2&qjKJTVs;n8)JnaRewQsdM&_HGF!+6d?2X3&P)1NL^)Bog+7^N;4Yd zNLDVtJn?jp1k?Q6n4b-E`m6caGgzqtEamQ35AuC%{eJHasxEN!g*cat7Eh0_VLwM| z;mYAPgsvA-m;B*v@6X?rt0?Ws6v(;mYjRz_s+F?1*fext{6!{47@^a%FZYv`YSK>^ zG}$tbu+^{GU39srUb>b(g(c=TKR<0VCTPRlBF9PHh;A zPHZxrk@KE)0+~ip>}4OP_ji0)9+WNeo?|$L`k-=?rM~9u(YN;ci3nXEr0$%-^4qW3 zS$zf6+zRG>N^(TSq@RzF3veWtjgIQdUB6g;@Lja<)I(9hnIDG=o?Q_leE8-_sc8;% zo-{Ti@xUGAb4XvLZp1|O(kqV7l~MXjhX=JU zy=m^_^7p7?k7qPXj!PX5%aRxS(9jvPE@&Vq_4;&Vc)-EE%?RBnZHDt^E&YWHCc$p>`m;8b==OL%;pnEgH=d# zSCXG1bc2w(X|MXbyJSYVhvX*fJ{2rV9GmkfBMKlIvpz0F(f;Y4ecBCd_D&Vd1p*ZYX`r$)GvVnGk zDyzY#g+jDXyA~z9Q7GVHN840XhpGDgFV&&2YI|Wht!?hPZ)HDT(DE*Zl1fPOKQ%CB3`?m zVEk{%uj5bLJ4Suc$a1qgjym9ubEyr0&M* z_4x#I!BUn%&iQ+q?nWI68`PWQOdr1(JfH2@cb3*-pTgn>!EsMg)}(Wrd!r^|4h-p^ zp3ai_aeX#3{w5m(LN^?#J4qu%RNLbH#ja%v!|)kHaq`y#O+GoEQn3%~rKq1r`g6Dl z?IRFlxr7x)b0(Eq+=MZcG*K#yMz!dfjEeTkgFJ+81XB0CjM0jPEoRSJYp7~J`(DGe zNKek<G|Br@W!-~R^W?RbCc#Fu1xW8hT)g#Cv%h~ zN}pHGKFQ>K_uj{MJd8Dq-6NQkEL*E03<*z+%b1lU$P~G#nw7wB%HAcu?t~MS= zQTtVXRADE@{6)>2&<|fcqgCl^oJ;EX@$bC%LA6sOuM4A)y1{(JH0OIRGkvc*hcX%8o#&4Hq~+4yrq2n4~OA*+Xr8~ zA}TfztaQl2<(!yL$?;qH?c4X|w%6Z_NL>wIL6N}hsgnk(Dn0KEsZvM2sB-VOQK1;P zM_E6rJ8?n!X5U)*Hv-Z91Y`$p`K)}673!pZ$N2aq;e#xO>DF#Mguj=Nx)(`%SaV4X z6cy*5^+-LQaGmYB&A523p|!?o*56JcT;PDL%b@rEuFq1eM<>{3-Z#E<-_T8c71r%C z81L@;wCw{zHyWvnT`8_@p>OUzpZt(*g!`^tDf8E2uEv1kg9i>Is9(NwdN8aC>pSQ7 z(*4y5V_5CNW;Um?$=sdJ6{?Fi5{h<=yCQTiBXyMmHTiEjq-D0zQH?HR*eJ2kSmW)_ z3w~H>f8MKPWTe>coS~8S!L`Dy*P8cS=6F~Z7;Z5UE7daLvN)?(vC<=tZ!t*SD}g6p zi#gj4Iu^fm*(>?MYQ?vN&Yq^>^EU!6o@2HC;Dy&G?4DXSTGmE3nv*4Yf`dO&Zs+k6 z$9}hc{aRebaSy`ZSfuX!{LKS2$J>Lx4v(4@H8iV)ySGr5;0e|_U}dI=;-{UWo2@Rg zcp1?pSVp5g^e*zCPj#lLX7b&uvX5zh1gOxwK(PcE>j#X zO7;{~!%t)>o!x8o&eSU531w}ON$EjO?S$!$D7+i)6z7YBCnfVc#MH4@p=Wa8lzFqt_Us;}2@9^a4)f1ySxa|ktRlBviSKu?nOfIdRY}CX* z!|o9s!7m}QCaB(jAK`C2QrFQ?&Q~#?-K1>%=-u@G+c%k>=zOS`d^&af`owG6r%BnC zbXj5>mnSJF{6AlbaM7VkJAD896|qNb5jHg3AHw)AAaoOux@Jc zmvSYJB%U@uCr4f(tZnPWQJ2;zu@W&!UO|~D^yrw{MWN&NPv+uu^OmM=&zDbrcj7?k zUPbDbs$0$*R*S2MS{=DUqH>W;DhU;Tn`7WsrG;y$D@k!&B{mo1*?8eozOQX)%LF{U z9Sjf1u}d*~KOY9|fs9*kkeDu40B3mwZtj>G(q2xC@mt((cwblvZ zk!a$~^07VF@!{Lo+993-D^nQ-j@v3zO=^Ct79g{ zH(D^%zN;IaHNQf3*N8E@sw|x&>3&x>^Ty%GgR6(i`FnZhVUan9x$M z-Hp&qLh3fZOuF~R*TOE!JVp1YfGu}Iz2$^W+DNrd#k>#W%_Xf!Tcu@G%XFKc8m&t= z2ey-vKX0PTnudp4tlM#&2O;wa-DITh^@66am_fM?-u0uk>slX_8(-xZw?D*iqF^|E z_GZOdrpu>->m~Kr<~xQ5<^|6DcT#oYWpQ=W6DwMtu#@EWwy9XE_=?WwZtio9b;H7&=!NqNHD zBQD+pdEK3Y)D>eb%}w7MFjihx#&$5O^2S$QrmF;*{kRg0DL)Ku>rnCpD9;zx(&K33L+2;T zYFkj41iE`BuS@mHytvdLp`g@}>yV$KvQOjvRJwr`?OT~8yiNKFF+_ZCAay^UQHgy< z7F(d@cEd%i_)bYD@jAD*q-l5(N69z+?^mZU*{k+k^&IV-lu3;2@*Q958#Q+`3tq-^ zYJNjtwns1+p__)(&6}C_G8(3HJtae(Y<9w)%Tn7aGG{F6(mYWyeU7KTk`$?VG5w_& ztd+P3`b&fiHso3MyqHdNcfRm5n6HiRN8Ts6iPTMNlb0f@a<19i=QYaIE?LGwMK;ws zJDnJmf2DmSNnMBjbsr7?(u?KQ3-4bgjVsLz)67_7+6yyqEXXj_MiiXidepLg+)PL6 z>fIaDAH99=d#y&62L8&4cqbooTxGrVm4xS!FNK_|JGHBPp8A^qXu0Y?a#-acwxhuZ zk%_|Cq;8qDn_4PG7|8bqZy|Nh^-F1H%N;W{K7QvF<&;A3-~(JyHF>+HleYfTpEjdD zhToMIfAAhxPSp5GY(YnH{%lRF&aK{z&xa!jIQAWU#)pV+22wYRfqN)fe2728Z|!TZ ziy*mhbjGB1gZHMJgB@oA2Oc)J&(t2DZzBVd4gm&}ouAd;Fjoker$#h1lzmwId}L7w zp__@+C3sRTrZ@5QA$Gfs`XjpJwT}@>zMV_0oiAGF%HzD2Kc}}DB|97RJRyz8!xTE8 z7+*WJQA?d(H{p_g{h+4uG30ZRETnFsB4#V=>GAb4YE_fHaW8r(JHHf2Ar3JJg9X4}K#_$vq@> zfIMnmZ~n;h+{s+!5pRwJCK1C60zqq+5xTiZU90^DjvLZPuh1>krBU%qUtax|Q&in) zX+TI~HG0hR&K;rS9}nEqbNYZ)rdk=1(LxxziCYme6|s?6&gYmJ8A#cK&u*@O~Ypx!=K#*1eUf*gel9CzKCk?si7b3qdy>{Msw9xuc z-3y-vM|0Vn?dQ_l`^9afu3?+{$FAAfiJo>>N%!L%jaT@m7e{e4EHCS44PR}OGwc2dH<{c zsavYiVUXr3v0xcTUdJM@J=J!{oXnAxcZLLyZmH*1(L!PfS9cfo%%;`z_q35cZed+g zu4&Z|ZU@9+OgzUPHPoLpM;<5DqRpv;v;Q(AR@u~fvgSq z`~ORVU5Uin{*CAViG2cx4ix&g$L(;pe#=l|P2Z^d-yx7kODx4xe%^gnA8)iy2!T?Am9(Z5AcNBP&|vAb4&v;Q|n0L~2?I}b1T3{i(^`*+^|H}>PV&%Yz^ z-x&dTt^ff}A^xu=xUYIT+xvJqIG|L(KWBkMX6Vm02Dz+)Ti6Yd_4 zgxhXzY;GsG1*PCS5lwIdB=q*dofqR9G7_c3;^dMtOLvU0l>Kk>p?EmgzZ5NIRF^|ETaH`@rC)6 z00$7CVIGV#EdUJw>p)L{XD&frYVT17HQ<0DwGL&IJI+75WC} z4V*ua^LI&RryiUWQUH=@aI{;e%djJOj zM*wF47XY}Ay8^fYK&}UXCxADAKLF%HEx4aU4&3K`0pOTG%`*Tn4}OMYfWPw@_5s~r zYmkTRXAsy%_7lbg_ALzH9Dp?dx<0ypuy3IN!2pmO0sz~)0&of7A^?ox1%OC^2!JSn z^8j%GF#yp3mjPk{@&IxHvH&sxk^rs&qygLjNCij%xDJpEkO*)UAOQf53pyWiAU6Zx z7C<_{O#s+`SPy>A0m$9~%;N@twh-WOCHMo<{U_U&P}*Er%)<9DIOvM~aeNf|mxC^- zN(#cEcZg48IX8hAAHz~ukY9*jbZd_x0wwVRW-;tFG7>u_qWp(w0EZlu4DM^=(C&VC z3zQt<7vw**J?n`J@j@8t^fMQj-N0(8_Nw63_J(gFfl|xM(7#YPy%C(U&`B@K$dj>&yvX8iKnd(=%7UR`+2@J8 zKTG~KIusaiF2HmT;6L}x6igN9+d_q(krjDo$)9Tf+Eda$`t#Rnf8Eo+=D_c=g>whv z{(69AOR(La9+-j|0p|{hqVj7JnYqIt*5draV2Dr-?mo^w&JNy0PHWDhg}qsz1k6G> z4;*(mO(EK29wT*@pk!<0z?!pD!qcDa_3UKjaZn<(J*)oOw^vx(bM(NzDNW7`s*Pw5 z#89l*TSuf&w$?m-4jLEi?GjP`!zfu$!VJ8g(hb1UGEBLPE)hrBfD+hKd5R)u>N2CV zpal8`y#DK+{&j!)aJGF*x}%YeFW#~W)CBoOz@94P>EZ2T=k1L;Zu9iIf#CE!&Q z**f7N>mUgRP$YsPh)QPz0*H{P42m0@h#Tt7y`7}tZgk(91u#az0YMZP#U~&N%76^| z;mD%E6BI`Q5fFED@QEUVhz=?!D$IYXs=NAbbaVB4-{YH^+f}E|U#Cu;I(2Hxz;sSB z78W<=uksdk`Lr_g>g6{{OI!sCjgso(U)8#4SAHTXsck1UT56t;Fu_qh>sv<3Uf;iZ ze}l}rBJ>-g3B`E7V9$EI?}4w40-_3nir(!J0^H;UVrGcdAq;q3LYAB;292w zcsh7S4t@8!QHM8wCeYB*?wEW?w!b|#dBo-txY?{s12Qm-Ug*TC@W7KJlw3^md=uL1`HIDV*SuLhYpoK`Sx*1DFWpjP#Q&c z`uy9zZy+f{1kVed8;&aY>b^51B_t@lzZ*aJ^sLEmOUi?w&@40Uq2K1M?v#B%QkH|# z36!61f2^*@ye%grmUDn(f{;mn8}6$IyDLk z#YmeP4|#OpSAkb0<$h4wfHJKAj*HvpW-peMCj`%#$Bq>a+fuAb$_t>h2G9Jn3)Z}| z-+POsYyySGM8V~A?pWQZCHmBq;A2rwi?##y&wsA!4N3X8sOQy;Ls!C+mP*QLXY%%F zFI~E%N29=(lF}9wYR|_L%Lf+D-t~&4Tnq|Hu<)6>(TAFJ-YF^lL7|rZeV{fYshHBZn8199b(VYe6BYr!-tvc}MmoHIlMTp#8P)!A_&6&zT}Adj#4m zduJ{=qwSSPCFKXfv*}2EhY3%Q_^YHeff-UgO|tu6{9@+ATP5XeP)N4oYqzdiFl)s) zNx2CFfxOtt_l8_s|IoR&G7pP+t{}q)3ZZTMV8Fq9_KiOu6s%;(`+F-xV>F*fuh=kk z)Y&EZh~qH>78Av%mPKi${NTil9-p<_f>i;0FKAfv0*z*w!jh*uPnek9j`4KQgWsta zlor{UlZ>|atN?}9Oh8K=^QD+iF%|-Te=P*aXtTb{u2#*JjzBAssVvmIz;iMM2~ST|_ntiG(JSnFYz6%;t7`o-!uGY5Qh>}IM5jle(i z{dO!Gn*lu$_D3J`CayjI=JWoyEpHE@Z9JFj+LyDe`KoINQ0zjnCF!6%#84aT-E-vT z)#EvZmKYh{P_Vo#RFl!})jRe)+_jM6QN#@tsixLwIHd$hc`%)%kc~w`UE#*z{OYIq z9)5n^uFaUUSnL9x-h#5e+uU7!N1Tah1MR`gjol(pXx^Uxm$g?c`R#P94w;RDC!p#T zDt>?XTdR2uW|hxiJl)BvhJ%Nqjjsc*&An;g&^JNBnwsXhGTq}=LjeiIY6WB&*y0UzTfctmaLwYpul#BQVt3x&iaNo72WVw zqow}@g~Cx#)`3E37v*2|>rc1l9A-RhWuzHOg+El5Q9Y^r%e=wSo}iFy;91?7*VF9r z&vs2}{CRg!C?p1DKo>qv-}=*a@7}WV+Y5QD%;tKJ5gCjAuG(|;I|bKI9|ax+(WC=i zjT%~cZAQ!U|9nHqBi;Dwkm(??7bt)vwCoGGrjho) z_0XXTzZ468Os!1R_ndP-^`X>H!X&wmr{z>?PkzqD+^(OUSF-5df?P$$*C}Q|*jsY1Ox;}pG>TmwT=0((zumQc z1Z3lj&}4fx!n^#jdQ0ki*$kN4UniY0*h5rJX%U? znddex-2GI^!RMG=voV*t7q)?iJl|JKrnY?l{YK_G7-*^OI;?OXe|`R|3(wlJF$eX~ z{wPM)ctIIb=U?~9^Dis|r5DBmyvtuep{S;F_uZ|Y7`cdekWV}*A_qud{`G||mron@ z#;ud;7!Mouvw+qHXv@_Rb9VIFu@)5Jk(LMvn&su(ebu1hd>5w|NpM{rkNx{jUHD1S zxj)lh8u6f>(V(0KwCgS|nswyd?`bbhP^f>DF>38jWBu{>hF#i}QJ6+sgO5CHY3on_ z`;zrd?gE9}GIFacHE$*QXKUcWqfZS!Ok;s4XsKqXHPmxUx{cg5yw!mZB+n1vYXP)o z`$y)MJo&&eRu4mK3frc7p4)i%^;Mlm()>gFy+At?6pCGX?tW_25T$c-hQ>6S4@wp& zo}nMl+q0{#1QhZ};JFPHngP$MY*@Um<`?Wnlpu#zLy6m^o+=sADL=P{tt4STBkJ>KW~cNrQa zC@NekWL(;`eanmT&e<+0FMvWVJ>Ky3!FO#4P84X!eMQuBZT2+}E;$jR-iA$))LTJm z1)lt}+#w&mIjJ+_VPh^awnQ&5O1x}Y)}D8~Y?2bMQAtVc1vr29Zw+)uUaWlRwk~?B z0L4g>8pIm_OJJ3h7{n>fhA3?@^2rlY;grJ~g1t9(%g=1Y+y-UTg>Y@T5;Oa$J%Y(m6I7n4)sf-4yJ=b`bv$~qBoyupBbF{_Pkj;X`G69 z=NwRo_oK$s_l=x9eF2bCiU*QHM8dQ<9alx zgn>{v^Yst)4ySG3+l_1Pnz6opj~;K^hUhe+KEA8z=VjYZJa$yjH!c0{m%Ca_nbA~u z#Bq6jkA0E-!oD1&v0$xVky*TK(4aMI27;ILV4ilx(+|Rmh(7+u0cW0jVD-|UkUj(H zp^rQo+>v{0uja4<8tZ!w{k-;~6^E9V)Z>PLc{&M@i|LiTa_?PQ?H&~*S`gydG&zFq~`nDARC2cYW)vpGyo2YHy zO54E8JY}jsKa9OwXcv8|s!FxTKL&fYp{VXvm4GL#jCSgHu65SSq-^#f>u?Y^rXpgu zSFF$yx{xf%0VPIupv0sV3O0(|polX}0uY}HJH#a3ynJ#~OkoOFSe~N=Tq2` zX)zlhyZ{@H4oxhz6$KPyNkpnt{b7tRbR}G{kW+99QWfs=`8<)3u4q1LWx1y682RN` zhbv({1a3OUbr2?zfka9`VVW;+p?PN!fq#+)kRp0u3(eV5Vrz=gldK{pn`8HOgH0~b z8r5)NK(r_2pa^&>G_U9-2UUwhBtVHTg`XUgJYmfn@`n5&-KoE1LBg@PFuo|D1QOXc zKM2nAI+`^v2qgZ2X5qNlg?{X<2Dd$_Ago2~#>RT-XkGJFsMV_9FWdpSAupyruY;~m z!ioC0Yq7Ahsr1d>d^q2m6C>6d67=fanw6oKG}xi!XqJ1$kii0Ejqe?PiJ zl2?Ey87!~8^pX?}-k_^|3@K``HucYxxB0K`8K zNYh0P!V$|?9;26va-tMYY^yb_QT0G^k2sPi=+i@*FDVT?-HSLy2^dx0I99(FjMfNp zMKF32Y9MO(M+F zL+oVe$W9wPR$&r7Ha~56*-;W&=A{kGHEUUi#Ev8U&I;m8G`^ZR^6#w7HeTW@vXRoR zByPycD&h!fS0EjWbQ`jkWYj;PDwsZZP|0eth-p_8?@MJZ@szZyuoOc+3$9025l{I; zsz~BO*yKenG3^St32_{n4`dtSDQQ<>(|2(Xi64`lHn@Ejm)Q2)Q-Ef%m)Ow*nAqB* zWs0?^6aVKT4%2`r#W2dEn%{@e8~1b>N~uZoXf;ZhW>J|VW?cNs|k)NoM&7~ zHh0A3*`*Gu5o(@23ww>h_&g&T3>h({Y!+|9OBWlL1$YM|NMwLLa6B;EEaKE~NYNG$ zi{qqiof;Aw^%fNO0jG{-=@CfQcN(p;^%0P4sWh;p2>?na(m>*di2eeQ^_?~jGhGXb z2SKN<@tDD3p_!3Jx_E2=T4*elc00{6Zo!!uX(I|>VgZVD+IV6VaaE={ia4`5ifpO0 zr2>}KLVTxzV{_v`vZa0pmf1oRjiuATldA`aCKKr+nS$A8zBJIxNQ94aTk3aUnWC9! z>FUhJ52e{s>BwdV9E4Lf9q_px7Sn1rtxjG9OeAc(?{ao7pryWK13ohCRcCJ%g#0Q`Ff^w;;&YYGh zf!F#5x4V-#%Ob&TePbV$Zl0m;Qo0`F*UGSWnTNk?6S1f1S_9cKorz;xJfL~?kej`K zSrB!~1lBKN^OjCi-2hqTfMb2L%~dx>#V?$Lm4C3kW40-azuMzhbexzu-3yTgNqH6* z4z{S*p*wV?KiMn-%(fI7;BF|0yZc;f20)gkWqVPQ$;Qc)!>ns;RK;mF8{H9)F~*^VaixJ8m&1UW2{<6dv=|OcG#6iJ ztJvRgfMBoLz{h^!m0aG$Fu)Qpm`2?QYQClem-z`7M3;ootuSAltb6oY^hu;L7}5i{ zVl+-=DF^AzS1iCaKP9dMZ)`GoP9aJ{d0;2x z0iMnQokxfH7zrTeCz>d(WH_iQNpJ@kTc{`99d0Aj5#odi{PI42*~NeSl3y!us>l{m zq6QX5Vs?k;aMMIHY_#7+B^2=}(w5b0CqXu2Pa=aU9KFVRB{H`nMkkkku z6@DgcUaR19VVG`$g%v-}Tj6aS8Xr>@Rm7yF8I3lSNN5a9$N|nO2Po^CU5#*~OZFy) zgrK)Ea7gZqEau)MxMR|6M7i-6$rQ=J67|ah1#@(9@0?E;7iw@8jpK=CGiljHRE7T1gi@wP zVBCsI_qf>|al0Gk4TP>;s5L2{&~Z{4lgUk6PbxBMI4NDWoJ>YMpmM%xAqoOre8J<0 z`OZNXb~^#PJqZlqYaB3aW(NpcibcfCNMST(2>a=NegNSdEqxUB(28HrlWB*l7!N98 zzwlDn9&llP?BN!;*e~O9<&DB$7#BOA?1B#FVvx^5qPhzg#qlU4+BSHUOBP zupM_1xXojL-Iiib#f^>IUupoto`h29$}dfI-iYqck7&VKcI&s)UMe5!(4fIyfRy_~ z9u%rjqFO#46Trz`QfkzU8;2XjVchS=WTG0S^vD@&_EIyC&7bs+4gcv~B<|?&w_Gs^ z@Oq5epjWXjk_&fV8NrqZdzXeH;sX;d?w9*$3{XT!??|lhu)0!J{V3|txrq!=PhzGF z<{j{CvH-}ILRYz4Y{s??Ah8*cL@WRuB#Bq6L{bez^0eM^GZbTBBnAWMOzW2X7T8Nji3h)TIuLlc%gjakb| z8r2#uuEvz$SW>ZU`TO zLE}00J|8lG7DH z@~I0j)-(p4xeJ!!W<_9FGbAQ(=*>-oFcPd+lY}51P{Jf=C(wmP5*}g#g|Dz_PG$>i zSDRqmPAPl?0E^=2Z@m`hF;^bNzMWTdcamT$3M4xxW;(p;b=6I#9+)IW1wy2v0)>R) NK?!@p`v35M{sUlV^40(V literal 0 HcmV?d00001 diff --git a/web/env.d.ts b/web/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..a888544 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..cbbdff1 --- /dev/null +++ b/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build --force" + }, + "dependencies": { + "vue": "^3.4.29" + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.14.5", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/tsconfig": "^0.5.1", + "element-plus": "^2.8.4", + "npm-run-all2": "^6.2.0", + "typescript": "~5.4.0", + "unplugin-auto-import": "^0.18.3", + "unplugin-vue-components": "^0.27.4", + "vite": "^5.3.1", + "vue-tsc": "^2.0.21" + } +} diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..36272c3 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/web/src/assets/base.css b/web/src/assets/base.css new file mode 100644 index 0000000..8816868 --- /dev/null +++ b/web/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/web/src/assets/logo.svg b/web/src/assets/logo.svg new file mode 100644 index 0000000..7565660 --- /dev/null +++ b/web/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/web/src/assets/main.css b/web/src/assets/main.css new file mode 100644 index 0000000..36fb845 --- /dev/null +++ b/web/src/assets/main.css @@ -0,0 +1,35 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..0ac3a5f --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,6 @@ +import './assets/main.css' + +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..e14c754 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..66b5e57 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..f094063 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..5c45e1d --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,16 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +})