Compare commits

..
16 Commits
Author SHA1 Message Date
feie9454 f569b17214 Merge branch 'main' of https://git.xn--876a.net/feie9454/circuit-virtual-lab-neo 2025-09-23 12:57:16 +08:00
feie9454 07df9c9209 实物电路上传识别,PWA支持 2025-09-23 12:57:14 +08:00
feie9454 5ae7e85eb7 评论与评分系统 2025-09-19 22:59:35 +08:00
feie9454 b9abbd09b6 优化模型列表和详情页 2025-09-19 22:07:18 +08:00
feie9454 61f76c338b 修复电表renderF安全策略 2025-09-13 16:11:05 +08:00
feie9454 a94ce15731 模型上传 2025-09-13 13:36:55 +08:00
feie9454 5176724172 实现登录注册与模型大厅、预览视图 2025-09-13 12:56:18 +08:00
feie9454 2d08c9f2d0 fmt 2025-09-13 09:54:54 +08:00
feie9454 bf7f5ec32e add text box 2025-09-13 01:37:58 +08:00
feie9454 c4a7d0d5a2 移动端适配,开关改善select 2025-09-12 15:57:55 +08:00
feie9454 2fd050ec6d 修复电阻箱模拟 2025-09-12 11:40:42 +08:00
feie9454 5159b22b6f 添加电阻箱 2025-09-12 11:38:52 +08:00
feie9454 9a7b4effba 增加滑动变阻器和可调电压 2025-09-12 11:17:29 +08:00
feie9454 09e6ff720e 0909eve 2025-09-12 09:30:56 +08:00
feie9454 11f4fec721 0911-noon 2025-09-11 19:22:18 +08:00
feie9454 29e674a81b 0911-m 2025-09-11 10:19:03 +08:00
52 changed files with 4175 additions and 1509 deletions
+2
View File
@@ -0,0 +1,2 @@
- 添加图像识别
- 添加分享大厅
+330
View File
@@ -4,10 +4,20 @@
"": { "": {
"name": "circuit-virtual-lab", "name": "circuit-virtual-lab",
"dependencies": { "dependencies": {
"@zumer/snapdom": "^1.9.11",
"katex": "^0.16.22",
"markdown-it": "^14.1.0",
"markdown-it-katex": "^2.0.3",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-math": "^5.2.1",
"mermaid": "^11.12.0",
"openai": "^5.20.0", "openai": "^5.20.0",
"vite-plugin-singlefile": "^2.3.0",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "4",
}, },
"devDependencies": { "devDependencies": {
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
@@ -17,6 +27,10 @@
}, },
}, },
"packages": { "packages": {
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
"@antfu/utils": ["@antfu/utils@9.2.0", "", {}, "sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
@@ -25,6 +39,18 @@
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.1", "", {}, "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="],
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="],
"@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="],
"@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="],
"@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="],
"@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
@@ -77,8 +103,14 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/utils": ["@iconify/utils@3.0.2", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@antfu/utils": "^9.2.0", "@iconify/types": "^2.0.0", "debug": "^4.4.1", "globals": "^15.15.0", "kolorist": "^1.8.0", "local-pkg": "^1.1.1", "mlly": "^1.7.4" } }, "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@mermaid-js/parser": ["@mermaid-js/parser@0.6.2", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.29", "", {}, "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.29", "", {}, "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.1", "", { "os": "android", "cpu": "arm" }, "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.1", "", { "os": "android", "cpu": "arm" }, "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag=="],
@@ -123,8 +155,80 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.1", "", { "os": "win32", "cpu": "x64" }, "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.1", "", { "os": "win32", "cpu": "x64" }, "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA=="],
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
"@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
"@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
"@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
"@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
"@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
"@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
"@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
"@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
"@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
"@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
"@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
"@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
"@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
"@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
"@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
"@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.29" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw=="], "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.29" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw=="],
"@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="], "@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="],
@@ -143,6 +247,8 @@
"@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="],
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@vue/language-core": ["@vue/language-core@3.0.6", "", { "dependencies": { "@volar/language-core": "2.4.23", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^2.0.5", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-e2RRzYWm+qGm8apUHW1wA5RQxzNhkqbbKdbKhiDUcmMrNAZGyM8aTiL3UrTqkaFI5s7wJRGGrp4u3jgusuBp2A=="], "@vue/language-core": ["@vue/language-core@3.0.6", "", { "dependencies": { "@volar/language-core": "2.4.23", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^2.0.5", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-e2RRzYWm+qGm8apUHW1wA5RQxzNhkqbbKdbKhiDUcmMrNAZGyM8aTiL3UrTqkaFI5s7wJRGGrp4u3jgusuBp2A=="],
"@vue/reactivity": ["@vue/reactivity@3.5.21", "", { "dependencies": { "@vue/shared": "3.5.21" } }, "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA=="], "@vue/reactivity": ["@vue/reactivity@3.5.21", "", { "dependencies": { "@vue/shared": "3.5.21" } }, "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA=="],
@@ -157,54 +263,278 @@
"@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="], "@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="],
"@zumer/snapdom": ["@zumer/snapdom@1.9.11", "", {}, "sha512-7yEDRCz9AaksPaKEZxH6BlIZ2tpNN0/gI/8coCYgRIYE1ZIDQ8f5KsMPADtupcdj4t3et97+BP9AQYifGOvYdw=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"alien-signals": ["alien-signals@2.0.7", "", {}, "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg=="], "alien-signals": ["alien-signals@2.0.7", "", {}, "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="],
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="],
"cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
"cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
"d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
"d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
"d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
"d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
"d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
"d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
"d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
"d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
"d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
"d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
"d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
"d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
"d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
"d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
"dagre-d3-es": ["dagre-d3-es@7.0.11", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw=="],
"dayjs": ["dayjs@1.11.18", "", {}, "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="],
"de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
"dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="], "esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"katex": ["katex@0.16.22", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg=="],
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
"langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="],
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
"markdown-it-katex": ["markdown-it-katex@2.0.3", "", { "dependencies": { "katex": "^0.6.0" } }, "sha512-nUkkMtRWeg7OpdflamflE/Ho/pWl64Lk9wNBKOmaj33XkQdumhXAIYhI0WO03GeiycPCsxbmX536V5NEXpC3Ng=="],
"markdown-it-link-attributes": ["markdown-it-link-attributes@4.0.1", "", {}, "sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ=="],
"markdown-it-math": ["markdown-it-math@5.2.1", "", { "peerDependencies": { "mathup": "^1.0.0", "temml": "^0.11.0" }, "optionalPeers": ["mathup", "temml"] }, "sha512-0YJczaqvBxgOt13oKj/4HYs/34Y9FsHiTktsIBaWYqrE+k1JxmMSTxCcTezTVul0YjmhmCBDehK65vWxjzRbdg=="],
"marked": ["marked@16.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w=="],
"match-at": ["match-at@0.1.1", "", {}, "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q=="],
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"mermaid": ["mermaid@11.12.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.2", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.11", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"openai": ["openai@5.20.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-Bmc2zLM/YWgFrDpXr9hwXqGGDdMmMpE9+qoZPsaHpn0Y/Qk1Vu26hNqXo7+nHdli+sLsXINvS1f8kR3NKhGKmA=="], "openai": ["openai@5.20.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-Bmc2zLM/YWgFrDpXr9hwXqGGDdMmMpE9+qoZPsaHpn0Y/Qk1Vu26hNqXo7+nHdli+sLsXINvS1f8kR3NKhGKmA=="],
"package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
"rollup": ["rollup@4.50.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.1", "@rollup/rollup-android-arm64": "4.50.1", "@rollup/rollup-darwin-arm64": "4.50.1", "@rollup/rollup-darwin-x64": "4.50.1", "@rollup/rollup-freebsd-arm64": "4.50.1", "@rollup/rollup-freebsd-x64": "4.50.1", "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", "@rollup/rollup-linux-arm-musleabihf": "4.50.1", "@rollup/rollup-linux-arm64-gnu": "4.50.1", "@rollup/rollup-linux-arm64-musl": "4.50.1", "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", "@rollup/rollup-linux-ppc64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-musl": "4.50.1", "@rollup/rollup-linux-s390x-gnu": "4.50.1", "@rollup/rollup-linux-x64-gnu": "4.50.1", "@rollup/rollup-linux-x64-musl": "4.50.1", "@rollup/rollup-openharmony-arm64": "4.50.1", "@rollup/rollup-win32-arm64-msvc": "4.50.1", "@rollup/rollup-win32-ia32-msvc": "4.50.1", "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA=="], "rollup": ["rollup@4.50.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.1", "@rollup/rollup-android-arm64": "4.50.1", "@rollup/rollup-darwin-arm64": "4.50.1", "@rollup/rollup-darwin-x64": "4.50.1", "@rollup/rollup-freebsd-arm64": "4.50.1", "@rollup/rollup-freebsd-x64": "4.50.1", "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", "@rollup/rollup-linux-arm-musleabihf": "4.50.1", "@rollup/rollup-linux-arm64-gnu": "4.50.1", "@rollup/rollup-linux-arm64-musl": "4.50.1", "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", "@rollup/rollup-linux-ppc64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-musl": "4.50.1", "@rollup/rollup-linux-s390x-gnu": "4.50.1", "@rollup/rollup-linux-x64-gnu": "4.50.1", "@rollup/rollup-linux-x64-musl": "4.50.1", "@rollup/rollup-openharmony-arm64": "4.50.1", "@rollup/rollup-win32-arm64-msvc": "4.50.1", "@rollup/rollup-win32-ia32-msvc": "4.50.1", "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA=="],
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="], "vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="],
"vite-plugin-singlefile": ["vite-plugin-singlefile@2.3.0", "", { "dependencies": { "micromatch": "^4.0.8" }, "peerDependencies": { "rollup": "^4.44.1", "vite": "^5.4.11 || ^6.0.0 || ^7.0.0" } }, "sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"vue": ["vue@3.5.21", "", { "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", "@vue/runtime-dom": "3.5.21", "@vue/server-renderer": "3.5.21", "@vue/shared": "3.5.21" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA=="], "vue": ["vue@3.5.21", "", { "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", "@vue/runtime-dom": "3.5.21", "@vue/server-renderer": "3.5.21", "@vue/shared": "3.5.21" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA=="],
"vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="],
"vue-tsc": ["vue-tsc@3.0.6", "", { "dependencies": { "@volar/typescript": "2.4.23", "@vue/language-core": "3.0.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-Tbs8Whd43R2e2nxez4WXPvvdjGbW24rOSgRhLOHXzWiT4pcP4G7KeWh0YCn18rF4bVwv7tggLLZ6MJnO6jXPBg=="], "vue-tsc": ["vue-tsc@3.0.6", "", { "dependencies": { "@volar/typescript": "2.4.23", "@vue/language-core": "3.0.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-Tbs8Whd43R2e2nxez4WXPvvdjGbW24rOSgRhLOHXzWiT4pcP4G7KeWh0YCn18rF4bVwv7tggLLZ6MJnO6jXPBg=="],
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
"langium/vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="],
"markdown-it-katex/katex": ["katex@0.6.0", "", { "dependencies": { "match-at": "^0.1.0" }, "bin": { "katex": "cli.js" } }, "sha512-rS4mY3SvHYg5LtQV6RBcK0if7ur6plyEukAOV+jGGPqFImuzu8fHL6M752iBmRGoUyF0bhZbAPoezehn7xYksA=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
} }
} }
+5 -3
View File
@@ -2,9 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="manifest" href="/manifest.webmanifest" />
<title>Vite + Vue + TS</title> <meta name="theme-color" content="#ffffff" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>AI 电路实验室</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+11 -1
View File
@@ -9,10 +9,20 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@zumer/snapdom": "^1.9.11",
"katex": "^0.16.22",
"markdown-it": "^14.1.0",
"markdown-it-katex": "^2.0.3",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-math": "^5.2.1",
"mermaid": "^11.12.0",
"openai": "^5.20.0", "openai": "^5.20.0",
"vue": "^3.5.18" "vite-plugin-singlefile": "^2.3.0",
"vue": "^3.5.18",
"vue-router": "4"
}, },
"devDependencies": { "devDependencies": {
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

+18
View File
@@ -0,0 +1,18 @@
{
"name": "AI 电路实验室",
"short_name": "AI电路实验室",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"description": "一个可安装的 PWA:在浏览器中体验交互式电路虚拟实验室。",
"icons": [
{
"src": "/icon.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
+61
View File
@@ -0,0 +1,61 @@
// Basic service worker for PWA installability and future caching
// Generated by Copilot — you can extend with runtime caching if needed.
const CACHE_NAME = 'cvl-cache-v1';
const APP_SHELL = [
'/',
'/index.html',
'/icon.png',
// Vite will handle asset hashing; we keep shell minimal to avoid stale caches.
];
self.addEventListener('install', (event) => {
// Skip waiting so updates activate faster.
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)).catch(() => void 0)
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.map((k) => (k === CACHE_NAME ? undefined : caches.delete(k)))
)
)
);
// Take control of uncontrolled clients as soon as possible.
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const { request } = event;
// Only handle GET requests and same-origin.
if (request.method !== 'GET' || new URL(request.url).origin !== self.location.origin) {
return;
}
// Network-first for HTML navigations; cache-first for others.
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then((resp) => {
const copy = resp.clone();
caches.open(CACHE_NAME).then((cache) => cache.put('/', copy)).catch(() => void 0);
return resp;
})
.catch(() => caches.match('/') || caches.match('/index.html'))
);
} else {
event.respondWith(
caches.match(request).then((cached) =>
cached ||
fetch(request).then((resp) => {
const copy = resp.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy)).catch(() => void 0);
return resp;
}).catch(() => cached)
)
);
}
});
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+71 -1350
View File
File diff suppressed because it is too large Load Diff
+192
View File
@@ -0,0 +1,192 @@
// Lightweight API client for Basic Auth backend
// Base URL can be configured via Vite env VITE_API_BASE, defaults to '/api'
export type User = {
id: string
username: string
}
export type Me = User & {
avatarUrl?: string
}
export type ModelSummary = {
id: string
title: string
brief?: string
authorId: string
authorName?: string
createdAt?: string
updatedAt?: string
avgRating?: number | null
ratingCount?: number
}
export type ModelDetail = ModelSummary & {
model: any
desc: string
}
export type CommentItem = {
id: string
content: string
authorId: string
authorName?: string
createdAt: string
updatedAt?: string
}
export type ListModelsQuery = {
q?: string
authorId?: string
page?: number
pageSize?: number
sort?: 'new' | 'old'
}
const API_BASE: string = (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_API_BASE) || '/api'
function buildAuthHeader(basicToken?: string): Record<string, string> {
if (!basicToken) return {}
return { Authorization: `Basic ${basicToken}` }
}
async function handle<T>(res: Response): Promise<T> {
if (!res.ok) {
let msg = res.status + ' ' + res.statusText
try {
const data = await res.json()
if (data && data.message) msg = data.message
} catch {}
throw new Error(msg)
}
const ct = res.headers.get('content-type') || ''
if (ct.includes('application/json')) return (await res.json()) as T
return (await res.text()) as unknown as T
}
export const api = {
base: API_BASE,
// Users
async getUser(id: string) {
const res = await fetch(`${API_BASE}/users/${encodeURIComponent(id)}`)
return handle<User>(res)
},
async register(username: string, password: string) {
const res = await fetch(`${API_BASE}/users/register`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username, password })
})
return handle<User>(res)
},
async me(basicToken?: string) {
const res = await fetch(`${API_BASE}/users/me`, {
headers: { ...buildAuthHeader(basicToken) },
})
return handle<Me>(res)
},
async updateMe(payload: { username?: string; avatarBase64?: string; avatarMime?: string }, basicToken?: string) {
const res = await fetch(`${API_BASE}/users/me`, {
method: 'PUT',
headers: { 'content-type': 'application/json', ...buildAuthHeader(basicToken) },
body: JSON.stringify(payload)
})
return handle<Me>(res)
},
avatarUrl(userId: string) {
return `${API_BASE}/users/${encodeURIComponent(userId)}/avatar`
},
// Models
async createModel(
payload: { title: string; desc?: string; model: any; previewBase64?: string; previewMime?: string },
basicToken?: string
) {
const res = await fetch(`${API_BASE}/models`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...buildAuthHeader(basicToken) },
body: JSON.stringify(payload)
})
return handle<ModelDetail>(res)
},
async updateModel(id: string, payload: Partial<{ title: string; desc: string; model: any; previewBase64: string; previewMime: string }>, basicToken?: string) {
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(id)}`, {
method: 'PUT',
headers: { 'content-type': 'application/json', ...buildAuthHeader(basicToken) },
body: JSON.stringify(payload)
})
return handle<ModelDetail>(res)
},
async deleteModel(id: string, basicToken?: string) {
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: { ...buildAuthHeader(basicToken) },
})
return handle<{ ok: true }>(res)
},
async getModel(id: string) {
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(id)}`)
return handle<ModelDetail>(res)
},
async listModels(query: ListModelsQuery = {}) {
const usp = new URLSearchParams()
if (query.q) usp.set('q', query.q)
if (query.authorId) usp.set('authorId', query.authorId)
if (query.page) usp.set('page', String(query.page))
if (query.pageSize) usp.set('pageSize', String(query.pageSize))
if (query.sort) usp.set('sort', query.sort)
const res = await fetch(`${API_BASE}/models${usp.toString() ? '?' + usp.toString() : ''}`)
return handle<{ items: ModelSummary[]; total?: number; page?: number; pageSize?: number }>(res)
},
async downloadModelJson(id: string) {
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(id)}/model.json`)
return handle<any>(res)
},
previewUrl(id: string) {
return `${API_BASE}/models/${encodeURIComponent(id)}/preview`
},
// Ratings
async setRating(id: string, value: number, basicToken?: string) {
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(id)}/rating`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...buildAuthHeader(basicToken) },
body: JSON.stringify({ value })
})
return handle<{ id: string; value: number }>(res)
},
async getMyRating(id: string, basicToken?: string) {
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(id)}/my-rating`, {
headers: { ...buildAuthHeader(basicToken) }
})
return handle<{ myRating: number | null }>(res)
},
// Comments
async listComments(id: string, page = 1, pageSize = 20) {
const usp = new URLSearchParams({ page: String(page), pageSize: String(pageSize) })
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(id)}/comments?${usp}`)
return handle<{ items: CommentItem[]; total: number; page: number; pageSize: number }>(res)
},
async addComment(id: string, content: string, basicToken?: string) {
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(id)}/comments`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...buildAuthHeader(basicToken) },
body: JSON.stringify({ content })
})
return handle<{ id: string }>(res)
},
async deleteComment(id: string, commentId: string, basicToken?: string) {
const res = await fetch(`${API_BASE}/models/${encodeURIComponent(id)}/comments/${encodeURIComponent(commentId)}`, {
method: 'DELETE',
headers: { ...buildAuthHeader(basicToken) }
})
return handle<void>(res)
},
}
export type BasicCredential = { username: string; password: string }
export function encodeBasic({ username, password }: BasicCredential) {
return btoa(`${username}:${password}`)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

+25 -3
View File
@@ -11,6 +11,7 @@ const props = defineProps<{
// 向父组件发出事件:可选,用于保存/载入 AI 偏好设置 // 向父组件发出事件:可选,用于保存/载入 AI 偏好设置
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'error', err: string): void (e: 'error', err: string): void
(e: 'close'): void
}>() }>()
let client: OpenAI = new OpenAI({ let client: OpenAI = new OpenAI({
@@ -39,9 +40,11 @@ const sending = ref(false)
function summarizeState(s: any): string { function summarizeState(s: any): string {
try { try {
if (!s) return '无状态' if (!s) return '无状态'
const full = JSON.stringify(s) const circuit = {instances: s.instances}
console.log(circuit);
const full = JSON.stringify(circuit)
// 将完整电路状态作为隐藏的 system 上下文传递 // 将完整电路状态作为隐藏的 system 上下文传递
return `电路状态: ${full}` return `电路状态: ${full}\n这是一个单臂直流电桥测电阻电路。`
} catch (e) { } catch (e) {
return '状态序列化失败' return '状态序列化失败'
} }
@@ -78,11 +81,17 @@ async function ask() {
messages: [persona, stateMsg, ...recent] messages: [persona, stateMsg, ...recent]
}) })
// 注意:不要直接持有原始对象并修改其属性(那样不会触发响应式)。
// 先 push,再通过数组中的“代理对象”按索引去修改,才能保证 UI 实时刷新。
const assistantMsg: ChatMessage = { role: 'assistant', content: '' } const assistantMsg: ChatMessage = { role: 'assistant', content: '' }
messages.value.push(assistantMsg) messages.value.push(assistantMsg)
const idx = messages.value.length - 1
for await (const part of stream as any) { for await (const part of stream as any) {
const delta: string = part?.choices?.[0]?.delta?.content ?? '' const delta: string = part?.choices?.[0]?.delta?.content ?? ''
if (delta) assistantMsg.content += delta if (delta) {
// 通过响应式代理更新内容,触发视图更新
messages.value[idx].content += delta
}
await nextTick(); scrollToBottom() await nextTick(); scrollToBottom()
} }
} catch (err: any) { } catch (err: any) {
@@ -109,6 +118,7 @@ onMounted(() => nextTick(scrollToBottom))
<div class="ai-panel"> <div class="ai-panel">
<header class="ai-header"> <header class="ai-header">
<div class="title">AI 助手</div> <div class="title">AI 助手</div>
<button class="close-btn" title="关闭" @click="emit('close')">×</button>
</header> </header>
<div class="chat-list" ref="listRef"> <div class="chat-list" ref="listRef">
@@ -152,6 +162,18 @@ onMounted(() => nextTick(scrollToBottom))
color: #111827; color: #111827;
} }
.close-btn {
padding: 4px 8px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
color: #111827;
cursor: pointer;
font-weight: 700;
line-height: 1;
}
.close-btn:hover { background: #f3f4f6; }
.key-box { .key-box {
flex: 1; flex: 1;
display: flex; display: flex;
+147
View File
@@ -0,0 +1,147 @@
<script setup lang="ts">
import { useAuth } from '../store/auth'
import { api } from '../api/client'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps<{ makeSave: () => any }>()
const auth = useAuth()
const router = useRouter()
// 面板状态
const showPanel = ref(false)
const title = ref('')
const desc = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
function openUpload() {
if (!auth.state.basicToken) {
// 未登录则跳转登录页,并带上重定向回当前页
const redirect = router.currentRoute.value.fullPath
router.push({ path: '/login', query: { redirect } })
return
}
// 打开面板
error.value = null
showPanel.value = true
}
function closePanel() {
if (loading.value) return
showPanel.value = false
}
async function submitUpload() {
if (!auth.state.basicToken) {
// 理论上不会到这里,兜底
const redirect = router.currentRoute.value.fullPath
router.push({ path: '/login', query: { redirect } })
return
}
error.value = null
loading.value = true
try {
const model = props.makeSave()
await api.createModel({ title: title.value.trim() || '未命名模型', desc: desc.value.trim(), model }, auth.state.basicToken || undefined)
showPanel.value = false
alert('上传成功,前往“模型广场”查看。')
} catch (e: any) {
error.value = e?.message || String(e)
} finally {
loading.value = false
}
}
</script>
<template>
<button class="btn" @click="openUpload">上传模型</button>
<!-- 上传信息面板 -->
<div v-if="showPanel" class="upload-overlay" @click.self="closePanel">
<div class="upload-panel">
<div class="panel-header">
<h3>上传模型</h3>
<button class="icon-btn" title="关闭" @click="closePanel">✕</button>
</div>
<div class="panel-body">
<label class="field">
<span class="label">标题</span>
<input class="input" v-model="title" :disabled="loading" placeholder="请输入模型标题" />
</label>
<label class="field">
<span class="label">描述</span>
<textarea class="textarea" v-model="desc" :disabled="loading" placeholder="必填:介绍一下你的模型"></textarea>
</label>
<p v-if="error" class="error">{{ error }}</p>
</div>
<div class="panel-footer">
<button class="btn secondary" @click="closePanel" :disabled="loading">取消</button>
<button class="btn primary" @click="submitUpload" :disabled="!title || !desc || loading">
{{ loading ? '正在上传…' : '确认上传' }}
</button>
</div>
</div>
</div>
</template>
<style scoped>
.upload-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.upload-panel {
width: min(520px, 92vw);
background: #fff;
border-radius: 10px;
box-shadow: 0 12px 30px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid #eee;
}
.panel-header h3 { margin: 0; font-size: 16px; }
.icon-btn {
border: none;
background: transparent;
font-size: 16px;
cursor: pointer;
color: #666;
}
.icon-btn:hover { color: #000; }
.panel-body { padding: 16px; }
.field { display: block; margin-bottom: 12px; }
.field .label { display: block; font-size: 12px; color: #666; margin-bottom: 6px; }
.input, .textarea {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.textarea { min-height: 100px; resize: vertical; }
.error { color: #c00; font-size: 12px; margin: 8px 0 0; }
.panel-footer {
padding: 12px 16px;
display: flex;
gap: 10px;
justify-content: flex-end;
border-top: 1px solid #eee;
}
.btn.primary {
background: #3b82f6;
color: white;
}
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
</style>
+14
View File
@@ -0,0 +1,14 @@
import { computed } from 'vue'
import { useRoute } from 'vue-router'
// Detects preview mode from the URL query: ?preview=true
// Usage: const { isPreview } = usePreview()
export function usePreview() {
const route = useRoute()
const isPreview = computed(() => {
const v = route.query.preview
if (Array.isArray(v)) return v.includes('true')
return String(v ?? '').toLowerCase() === 'true'
})
return { isPreview }
}
-146
View File
@@ -1,146 +0,0 @@
// 显式导入所有电路元件图片,并定义其基本属性
import battery from './circuit-ele/battery.png'
import capacitor from './circuit-ele/capacitor.png'
import inductor from './circuit-ele/inductor.png'
import lightBulb from './circuit-ele/light_bulb.png'
import lightBulbGrow from './circuit-ele/light_bulb_grow.png'
import resistor from './circuit-ele/resistor.png'
import switchOff from './circuit-ele/switch_off.png'
import switchOn from './circuit-ele/switch_on.png'
import meter from './circuit-ele/meter.png'
export type ConnectionPoint = {
// 以百分比(0..1)定义在图片盒子内的位置
x: number
y: number
name?: string
}
export type CircuitElement = {
key: string
name: string
url: string
defaultSize?: number // 渲染尺寸(像素),可选
connectionPoints: ConnectionPoint[]
propertySchemas?: PropertySchema[]
stateImages?: { on: string; off: string }
// 预设:一键应用一组属性值
preset?: Preset[]
}
export type PropertySchema = {
key: string
label: string
type: 'number' | 'text'
unit?: string
default: number | string
}
// 预设定义:展示名称 + 要覆盖的属性值
export type Preset = {
name: string
propertyValues: Record<string, number | string>
}
export const elements: CircuitElement[] = [
{
key: 'battery',
name: '电池',
url: battery,
defaultSize: 110,
connectionPoints: [
{ x: 0.05, y: 0.5, name: 'A' }, // 左侧
{ x: 0.95, y: 0.5, name: 'B' }, // 右侧
],
propertySchemas: [
{ key: 'voltage', label: '电压', type: 'number', unit: 'V', default: 5 },
],
},
{
key: 'switch',
name: '开关',
url: switchOff,
defaultSize: 130,
connectionPoints: [
{ x: 0.05, y: 0.7, name: 'A' },
{ x: 0.83, y: 0.7, name: 'B' },
],
// 点击元件切换 on/off 显示
stateImages: { on: switchOn, off: switchOff },
// 开关没有额外可编辑属性
},
{
key: 'light_bulb',
name: '灯泡',
url: lightBulb,
defaultSize: 80,
connectionPoints: [
{ x: 0.5, y: 1, name: 'A' },
{ x: 0.72, y: 0.8, name: 'B' },
],
propertySchemas: [
{ key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 20 },
],
// 当电流超过阈值时在前端根据 state 切换图片
stateImages: { on: lightBulbGrow, off: lightBulb },
},
{
key: 'capacitor',
name: '电容',
url: capacitor,
defaultSize: 120,
connectionPoints: [
{ x: 0.6, y: 0.96, name: 'A' },
{ x: 0.4, y: 0.9, name: 'B' },
],
propertySchemas: [
{ key: 'capacitance', label: '电容', type: 'number', unit: 'F', default: 0.000001 },
],
},
{
key: 'inductor',
name: '电感',
url: inductor,
defaultSize: 100,
connectionPoints: [
{ x: 0.05, y: 0.5, name: 'A' },
{ x: 0.95, y: 0.5, name: 'B' },
],
propertySchemas: [
{ key: 'inductance', label: '电感', type: 'number', unit: 'H', default: 0.001 },
],
},
{
key: 'resistor',
name: '电阻',
url: resistor,
defaultSize: 110,
connectionPoints: [
{ x: 0.05, y: 0.5, name: 'A' },
{ x: 0.95, y: 0.5, name: 'B' },
],
propertySchemas: [
{ key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 5 },
],
},
{
key: 'meter',
name: '表',
url: meter,
defaultSize: 150,
connectionPoints: [
{ x: 0.25, y: 1, name: 'A' },
{ x: 0.45, y: 1, name: 'B' },
],
preset:[{
name: '电流表', propertyValues: { resistance: 1, renderFunc: '(i) => `${(i * 1000).toFixed(2)} mA`' }
},{
name: '电压表', propertyValues: { resistance: 1000000, renderFunc: '(i, r) => `${(i * r).toFixed(2)} V`' }
}],
propertySchemas: [
{ key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 1 },
{ key: 'renderFunc', label: '显示函数', type: 'text', default: '(i) => `${(i * 1000).toFixed(2)} mA`' },
],
},
]
+25 -1
View File
@@ -1,5 +1,29 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import './style.css' import './style.css'
import { createRouter, createWebHistory } from 'vue-router'
createApp(App).mount('#app') const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('./views/ModelList.vue') },
{ path: '/models/:id', component: () => import('./views/ModelDetail.vue') },
{ path: '/lab', component: () => import('./views/VirtualLab.vue') },
{ path: '/me', component: () => import('./views/Profile.vue') },
{ path: '/login', component: () => import('./views/Login.vue') },
]
})
createApp(App).use(router).mount('#app')
// Register Service Worker for PWA (only in production or localhost)
if ('serviceWorker' in navigator) {
const isLocalhost = /^(localhost|127\.0\.0\.1|\[::1\])$/.test(location.hostname);
if (import.meta.env.PROD || isLocalhost) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.catch((err) => console.warn('[SW] registration failed:', err));
});
}
}
+80
View File
@@ -0,0 +1,80 @@
import { reactive, readonly } from 'vue'
import { api, encodeBasic, type Me } from '../api/client'
type State = {
me: Me | null
basicToken: string | null
loading: boolean
error: string | null
}
const KEY = 'cvln_basic_token'
const state = reactive<State>({
me: null,
basicToken: localStorage.getItem(KEY),
loading: false,
error: null,
})
async function refreshMe() {
if (!state.basicToken) { state.me = null; return }
state.loading = true; state.error = null
try {
state.me = await api.me(state.basicToken)
} catch (e: any) {
state.error = e?.message || String(e)
// token 失效则清理
state.basicToken = null
localStorage.removeItem(KEY)
state.me = null
} finally { state.loading = false }
}
async function login(username: string, password: string) {
state.loading = true; state.error = null
try {
const token = encodeBasic({ username, password })
state.basicToken = token
localStorage.setItem(KEY, token)
await refreshMe()
return true
} catch (e: any) {
state.error = e?.message || String(e)
logout()
return false
} finally { state.loading = false }
}
async function register(username: string, password: string) {
state.loading = true; state.error = null
try {
await api.register(username, password)
// 注册后直接登录
return await login(username, password)
} catch (e: any) {
state.error = e?.message || String(e)
return false
} finally { state.loading = false }
}
function logout() {
state.basicToken = null
localStorage.removeItem(KEY)
state.me = null
}
export function useAuth() {
return {
state: readonly(state),
login,
register,
logout,
refreshMe,
}
}
// try fetch me on module load
if (state.basicToken) {
refreshMe()
}
+18
View File
@@ -10,3 +10,21 @@ body{
font-family: "meter-font"; font-family: "meter-font";
src: url('./assets/Jersey20-Regular.ttf') format('truetype'); src: url('./assets/Jersey20-Regular.ttf') format('truetype');
} }
.btn {
padding: 6px 10px;
border: 1px solid #e5e7eb;
border-radius: 6px;
background: #fff;
color: #111827;
cursor: pointer;
}
.btn:hover {
background: #f3f4f6;
}
*{
box-sizing: border-box;
}
+11
View File
@@ -0,0 +1,11 @@
declare module 'markdown-it-katex' {
import type MarkdownIt from 'markdown-it'
const plugin: (md: MarkdownIt, ...args: any[]) => void
export default plugin
}
declare module 'markdown-it-link-attributes' {
import type MarkdownIt from 'markdown-it'
const plugin: (md: MarkdownIt, options?: { attrs?: Record<string, string> }) => void
export default plugin
}
+63
View File
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuth } from '../store/auth'
const auth = useAuth()
const router = useRouter()
const route = useRoute()
const mode = ref<'login' | 'register'>('login')
const username = ref('')
const password = ref('')
async function submit() {
if (!username.value || !password.value) return alert('请输入用户名和密码')
const ok = mode.value === 'login'
? await auth.login(username.value, password.value)
: await auth.register(username.value, password.value)
if (ok) {
const to = (route.query.redirect as string) || '/'
router.replace(to)
}
}
</script>
<template>
<div class="auth-page">
<div class="card">
<h1>{{ mode === 'login' ? '登录' : '注册' }}</h1>
<label>
<span>用户名</span>
<input v-model="username" placeholder="用户名" />
</label>
<label>
<span>密码</span>
<input v-model="password" type="password" placeholder="密码" />
</label>
<button class="btn" @click="submit" :disabled="auth.state.loading">
{{ auth.state.loading ? '提交中…' : (mode === 'login' ? '登录' : '注册并登录') }}
</button>
<p class="tip">
<button class="link" @click="mode = mode === 'login' ? 'register' : 'login'">
{{ mode === 'login' ? '没有账户?去注册' : '已有账户?去登录' }}
</button>
</p>
<p v-if="auth.state.error" class="error">{{ auth.state.error }}</p>
<p class="seed">种子用户:admin / admin123</p>
</div>
</div>
</template>
<style scoped>
.auth-page{height:100%;display:flex;align-items:center;justify-content:center;background:#f9fafb}
.card{width:320px;display:flex;flex-direction:column;gap:10px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px}
label{display:flex;flex-direction:column;gap:6px}
input{padding:8px 10px;border:1px solid #e5e7eb;border-radius:8px}
.btn{padding:8px 10px;border:1px solid #e5e7eb;border-radius:8px;background:#111827;color:#fff}
.link{border:none;background:none;color:#2563eb;cursor:pointer}
.tip{margin:4px 0 0 0}
.error{color:#b91c1c}
.seed{color:#6b7280;font-size:12px}
</style>
+539
View File
@@ -0,0 +1,539 @@
<script setup lang="ts">
import { onMounted, reactive, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { api, type ModelDetail as MD, type User, type CommentItem } from '../api/client'
import MarkdownIt from 'markdown-it'
import mk from 'markdown-it-katex'
import { useAuth } from '../store/auth'
// Mermaid is optional; we will lazy-init when needed
import mermaid from 'mermaid'
const route = useRoute()
const router = useRouter()
type State = {
model: MD | null
loading: boolean
error: string | null
author: User | null
mdHtml: string
comments: CommentItem[]
commentsPage: number
commentsTotal: number
myRating: number | null
ratingBusy: boolean
commentInput: string
commentBusy: boolean
}
const st = reactive<State>({ model: null, loading: false, error: null, author: null, mdHtml: '', comments: [], commentsPage: 1, commentsTotal: 0, myRating: null, ratingBusy: false, commentInput: '', commentBusy: false })
const id = computed(() => String(route.params.id))
const { state: auth } = useAuth()
// markdown renderer with KaTeX and link attrs
const md = new MarkdownIt({ html: false, linkify: true, typographer: true })
.use(mk)
function renderMarkdown(input?: string) {
const text = input || ''
// Basic mermaid code block passthrough; we'll render after mount
const html = md.render(text)
st.mdHtml = html
}
async function load() {
st.loading = true; st.error = null
try {
const data = await api.getModel(id.value)
st.model = data
renderMarkdown(data.desc)
// fetch author for avatar/username
if (data.authorId) {
try { st.author = await api.getUser(data.authorId) } catch { }
}
// load rating
try {
if (auth.basicToken) {
const r = await api.getMyRating(id.value, auth.basicToken)
st.myRating = r.myRating
} else st.myRating = null
} catch { }
// load comments
try {
const { items, total } = await api.listComments(id.value, 1, 20)
st.comments = items
st.commentsTotal = total
st.commentsPage = 1
} catch { }
} catch (e: any) {
st.error = e?.message || String(e)
} finally { st.loading = false }
}
async function downloadJson() {
try {
const content = await api.downloadModelJson(id.value)
const blob = new Blob([JSON.stringify(content, null, 2)], { type: 'application/json;charset=utf-8' })
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = (st.model?.title || 'model') + '.json'
a.click()
URL.revokeObjectURL(a.href)
} catch (e: any) { alert(e?.message || String(e)) }
}
async function importToLab() {
try {
const content = await api.downloadModelJson(id.value)
sessionStorage.setItem('cvln_import_json', JSON.stringify(content))
router.push('/lab')
} catch (e: any) { alert(e?.message || String(e)) }
}
function goBack() {
router.back()
}
async function setRating(value: number) {
if (!auth.basicToken) { alert('请先登录后评分'); return }
if (value < 1 || value > 5) return
st.ratingBusy = true
try {
await api.setRating(id.value, value, auth.basicToken)
st.myRating = value
// optimistic update avg/count if available
if (st.model) {
const prevCount = st.model.ratingCount || 0
const prevAvg = st.model.avgRating || 0
const hadMy = st.myRating !== null
const newCount = hadMy ? prevCount : prevCount + 1
const newAvg = hadMy
? prevCount > 0 ? (prevAvg * prevCount - (st.myRating ?? 0) + value) / prevCount : value
: (prevAvg * prevCount + value) / newCount
st.model = { ...st.model, ratingCount: newCount, avgRating: newAvg }
}
} catch (e: any) { alert(e?.message || String(e)) }
finally { st.ratingBusy = false }
}
async function addComment() {
if (!auth.basicToken) { alert('请先登录后评论'); return }
const text = st.commentInput.trim()
if (!text) return
st.commentBusy = true
try {
await api.addComment(id.value, text, auth.basicToken)
st.commentInput = ''
const { items, total } = await api.listComments(id.value, 1, 20)
st.comments = items
st.commentsTotal = total
st.commentsPage = 1
} catch (e: any) { alert(e?.message || String(e)) }
finally { st.commentBusy = false }
}
onMounted(async () => {
await load()
// Mermaid init after content mounted
try {
mermaid.initialize({ startOnLoad: false, theme: 'neutral' })
// find all <code class="language-mermaid"> blocks inside .md-body
requestAnimationFrame(async () => {
const container = document.querySelector('.md-body') as HTMLElement | null
if (!container) return
const blocks = Array.from(container.querySelectorAll('pre code.language-mermaid')) as HTMLElement[]
let idx = 0
for (const code of blocks) {
const graph = code.textContent || ''
const id = 'mmd-' + idSafe(idx++)
const wrap = document.createElement('div')
wrap.className = 'mmd-wrap'
const { svg } = await mermaid.render(id, graph)
wrap.innerHTML = svg
const pre = code.closest('pre')
if (pre && pre.parentElement) pre.parentElement.replaceChild(wrap, pre)
}
})
} catch { }
})
function idSafe(n: number) { return String(n).replace(/[^a-zA-Z0-9_-]/g, '') }
watch(() => st.model?.desc, (v) => renderMarkdown(v))
</script>
<template>
<div class="detail-page">
<div class="back-btn-container">
<button class="back-btn" @click="goBack">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
返回
</button>
</div>
<div v-if="st.loading">加载中…</div>
<div v-else-if="st.error" class="error">{{ st.error }}</div>
<template v-else-if="st.model">
<div class="head card">
<img class="preview" :src="api.previewUrl(st.model.id)" alt="preview" />
<div class="meta">
<div>
<h1 class="title">{{ st.model.title }}</h1>
<div class="author-row">
<img class="avatar" :src="api.avatarUrl(st.model.authorId)"
alt="avatar" />
<div class="author">
<div class="name">{{ st.author?.username || st.model.authorName
||
'未知用户' }}</div>
<div class="dates">
<span>创建于 {{ new Date(st.model.createdAt ||
'').toLocaleString()
}}</span>
<span v-if="st.model.updatedAt"> · 更新于 {{ new
Date(st.model.updatedAt).toLocaleString() }}</span>
</div>
</div>
</div>
<div class="rate-box">
<div class="label">我的评分:</div>
<div class="stars" :class="{ busy: st.ratingBusy }">
<button v-for="n in 5" :key="n" class="star"
:class="{ on: (st.myRating || 0) >= n }"
:disabled="st.ratingBusy" @click="setRating(n)">★</button>
</div>
</div>
</div>
<div class="ops">
<button class="btn" @click="downloadJson">下载 JSON</button>
<button class="btn primary" @click="importToLab">导入到实验室</button>
</div>
</div>
</div>
<div v-if="st.model.desc" class="desc-card card">
<h2>介绍:</h2>
<div class="md-body" v-html="st.mdHtml"></div>
</div>
<div class="grid">
<div class="card stat">
<div class="label">元件数</div>
<div class="value">{{ Array.isArray(st.model.model?.instances) ?
st.model.model.instances.length : 0 }}</div>
</div>
<div class="card stat">
<div class="label">版本</div>
<div class="value">{{ st.model.model?.version ?? '-' }}</div>
</div>
<div class="card stat">
<div class="label">世界尺寸</div>
<div class="value">{{ st.model.model?.world?.worldSize ?? '-' }}</div>
</div>
<div class="card stat">
<div class="label">平均评分</div>
<div class="value">{{ (st.model.avgRating ?? 0).toFixed(1) }} ★ <span
class="count">({{ st.model.ratingCount || 0 }})</span></div>
</div>
</div>
<div class="card comments">
<div v-if="st.comments.length === 0" class="empty">还没有评论,来当第一位评论者吧。
</div>
<ul class="list">
<li v-for="c in st.comments" :key="c.id" class="comment-item">
<img class="avatar small" :src="api.avatarUrl(c.authorId)"
alt="avatar" />
<div class="body">
<div class="meta-line">
<span class="name">{{ c.authorName || '用户' }}</span>
<span class="time">{{ new Date(c.createdAt).toLocaleString()
}}</span>
</div>
<div class="content">{{ c.content }}</div>
</div>
</li>
</ul>
<h2>评论</h2>
<div class="add" v-if="auth.me">
<textarea v-model="st.commentInput" placeholder="写下你的看法…"
rows="3"></textarea>
<button class="btn primary"
:disabled="st.commentBusy || !st.commentInput.trim()"
@click="addComment">发表评论</button>
</div>
</div>
</template>
</div>
</template>
<style>
.katex-html {
display: none;
}
</style>
<style scoped>
.detail-page {
padding: 16px;
height: 100%;
overflow: auto;
box-sizing: border-box;
background: #fafafa
}
.back-btn-container {
margin-bottom: 16px
}
.back-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
color: #374151
}
.back-btn:hover {
background: #f9fafb;
border-color: #d1d5db
}
.back-btn svg {
width: 16px;
height: 16px
}
.card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 12px
}
.head {
display: flex;
gap: 12px;
align-items: stretch;
}
.preview {
width: 320px;
height: 320px;
object-fit: cover;
background: #f3f4f6;
border-radius: 8px;
border: 1px solid #e5e7eb
}
.meta {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.title {
margin: 0 0 6px 0
}
.author-row {
display: flex;
align-items: center;
gap: 10px;
margin: 6px 0 8px
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: #f3f4f6;
border: 1px solid #e5e7eb
}
.avatar.small {
width: 28px;
height: 28px
}
.author .name {
font-weight: 600
}
.author .dates {
color: #6b7280;
font-size: 12px
}
.ops {
display: flex;
gap: 8px;
margin-top: 8px;
align-self: end;
}
.btn {
padding: 6px 10px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
cursor: pointer
}
.btn.primary {
background: #111827;
color: #fff;
border-color: #111827
}
.md-body {
line-height: 1.75
}
.md-body :deep(h1),
.md-body :deep(h2),
.md-body :deep(h3) {
margin: 12px 0 6px
}
.md-body :deep(p) {
margin: 8px 0
}
.md-body :deep(code) {
background: #f3f4f6;
padding: 2px 4px;
border-radius: 4px
}
.md-body :deep(pre) {
background: #0b1020;
color: #e5e7eb;
padding: 10px;
border-radius: 8px;
overflow: auto
}
.desc-card {
margin-top: 12px
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin-top: 12px
}
.stat .label {
color: #6b7280
}
.stat .value {
font-size: 20px;
font-weight: 700;
margin-top: 4px
}
.error {
color: #b91c1c
}
.mmd-wrap {
display: block;
overflow: auto
}
.rate-box {
margin-top: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.rate-box .stars {
display: inline-flex;
gap: 6px
}
.rate-box .star {
font-size: 20px;
background: transparent;
border: none;
cursor: pointer;
color: #9ca3af
}
.rate-box .star.on {
color: #f59e0b
}
.rate-box .stars.busy {
opacity: .6;
pointer-events: none
}
.comments {
margin-top: 12px
}
.comments .add {
display: flex;
flex-direction: column;
gap: 8px
}
.comments textarea {
width: 100%;
box-sizing: border-box;
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 8px
}
.comments .list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px
}
.comments .comment-item {
display: flex;
gap: 8px
}
.comments .comment-item .meta-line {
color: #6b7280;
font-size: 12px
}
.comments .comment-item .name {
font-weight: 600;
color: #111827;
margin-right: 6px
}
.comments .empty {
color: #6b7280;
font-size: 14px
}
</style>
+181
View File
@@ -0,0 +1,181 @@
<script setup lang="ts">
import { onMounted, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { api, type ModelSummary } from '../api/client'
const router = useRouter()
const st = reactive({
q: '',
sort: 'new' as 'new' | 'old',
page: 1,
pageSize: 20,
total: 0,
items: [] as ModelSummary[],
loading: false,
error: '' as string | null,
})
async function load() {
st.loading = true; st.error = null
try {
const res = await api.listModels({ q: st.q || undefined, page: st.page, pageSize: st.pageSize, sort: st.sort })
st.items = res.items || []
st.total = res.total || st.items.length
} catch (e: any) {
st.error = e?.message || String(e)
} finally { st.loading = false }
}
function goDetail(id: string) { router.push(`/models/${id}`) }
onMounted(load)
</script>
<template>
<div class="list-page">
<div class="toolbar">
<input v-model="st.q" placeholder="搜索标题/描述"
@keydown.enter="st.page = 1; load()" />
<select v-model="st.sort" @change="st.page = 1; load()">
<option value="new">最新</option>
<option value="old">最早</option>
</select>
<button class="btn" @click="st.page = 1; load()"
:disabled="st.loading">搜索</button>
</div>
<div v-if="st.error" class="error">{{ st.error }}</div>
<div v-else class="grid">
<div v-for="m in st.items" :key="m.id" class="card"
@click="goDetail(m.id)">
<img :src="api.previewUrl(m.id)" alt="preview" />
<div class="meta">
<h3>{{ m.title }}
<span v-if="m.authorId == 'cmfhpkfwi0000vaqgog4cx5lr'" class="official-badge">官方</span>
</h3>
<p>{{ m.brief }}</p>
</div>
</div>
</div>
<div class="pager" v-if="st.total > st.pageSize">
<button :disabled="st.page <= 1" @click="st.page--; load()">上一页</button>
<span>第 {{ st.page }} 页</span>
<button :disabled="st.page * st.pageSize >= st.total"
@click="st.page++; load()">下一页</button>
</div>
</div>
</template>
<style scoped>
.list-page {
padding: 12px;
height: 100%;
box-sizing: border-box;
overflow: auto
}
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px
}
input,
select {
padding: 6px 8px;
border: 1px solid #e5e7eb;
border-radius: 8px
}
.btn {
padding: 6px 10px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px
}
.card {
border: 1px solid #e5e7eb;
border-radius: 10px;
overflow: hidden;
cursor: pointer;
background: #fff;
display: flex;
flex-direction: column
}
.card img {
width: 100%;
aspect-ratio: 1;
object-fit: contain;
background: white
}
.meta {
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.meta h3 {
margin: 0;
font-size: 14px;
display: flex;
align-items: center;
justify-content: space-between;
}
.official-badge {
background: linear-gradient(135deg, #ff6b6b, #feca57);
color: white;
font-size: 10px;
font-weight: 600;
padding: 2px 6px;
border-radius: 12px;
position: relative;
overflow: hidden;
}
.official-badge::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
.meta p {
margin: 0;
color: #6b7280;
font-size: 12px
}
.pager {
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
margin-top: 12px
}
.error {
color: #b91c1c
}
</style>
+84
View File
@@ -0,0 +1,84 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { useAuth } from '../store/auth'
import { api } from '../api/client'
const auth = useAuth()
const me = computed(() => auth.state.me)
type St = {
uploading: boolean
error: string | null
success: string | null
}
const st = reactive<St>({ uploading: false, error: null, success: null })
async function onPickFile(ev: Event) {
const input = ev.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
st.error = null; st.success = null
if (!/^image\//.test(file.type)) { st.error = '请选择图片文件'; return }
if (file.size > 2 * 1024 * 1024) { st.error = '图片大小不能超过 2MB'; return }
const reader = new FileReader()
st.uploading = true
reader.onload = async () => {
try {
const base64 = (reader.result as string).split(',')[1]
await api.updateMe({ avatarBase64: base64, avatarMime: file.type }, auth.state.basicToken || undefined)
await auth.refreshMe()
st.success = '头像已更新'
} catch (e: any) {
st.error = e?.message || String(e)
} finally { st.uploading = false }
}
reader.onerror = () => { st.error = '读取文件失败'; st.uploading = false }
reader.readAsDataURL(file)
}
</script>
<template>
<div class="profile-page">
<div class="card">
<h1>个人信息</h1>
<div v-if="!me">请先登录。</div>
<template v-else>
<div class="row">
<img class="avatar" :src="api.avatarUrl(me.id)" alt="avatar" />
<div class="meta">
<div class="name">{{ me.username }}</div>
<div class="id">ID: {{ me.id }}</div>
</div>
</div>
<div class="ops">
<label class="btn" :class="{ disabled: st.uploading }" :aria-disabled="st.uploading">
<input type="file" accept="image/*" @change="onPickFile" hidden :disabled="st.uploading" />
上传/更换头像
</label>
</div>
<div class="tips">支持常见图片格式,建议正方形,大小不超过 2MB。</div>
<div class="msg">
<span class="error" v-if="st.error">{{ st.error }}</span>
<span class="ok" v-else-if="st.success">{{ st.success }}</span>
</div>
</template>
</div>
</div>
</template>
<style scoped>
.profile-page{padding:16px;box-sizing:border-box}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px}
.row{display:flex;align-items:center;gap:12px}
.avatar{width:80px;height:80px;border-radius:50%;background:#f3f4f6;border:1px solid #e5e7eb;object-fit:cover}
.meta .name{font-size:18px;font-weight:700}
.meta .id{color:#6b7280;font-size:12px}
.ops{margin-top:12px}
.btn{display:inline-block;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;cursor:pointer}
.btn.disabled{opacity:0.6;pointer-events:none}
.tips{color:#6b7280;font-size:12px;margin-top:8px}
.msg{margin-top:8px}
.error{color:#b91c1c}
.ok{color:#065f46}
</style>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#f3f4f6"/>
</linearGradient>
</defs>
<rect x="14" y="14" width="100" height="100" rx="12" ry="12" fill="url(#bg)" stroke="#111827" stroke-width="4"/>
<text x="50%" y="54%" dominant-baseline="middle" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="72" fill="#111827">T</text>
</svg>

After

Width:  |  Height:  |  Size: 554 B

+37
View File
@@ -0,0 +1,37 @@
import os
import subprocess
def convert_png_to_webp():
png_files = []
# 递归遍历当前目录及所有子目录
for root, dirs, files in os.walk('.'):
for file in files:
if file.lower().endswith('.png'):
png_files.append(os.path.join(root, file))
if not png_files:
print("当前目录及子目录下没有找到PNG文件。")
return
print(f"找到 {len(png_files)} 个PNG文件,开始转换...")
# 遍历并转换每个PNG文件
for png_file in png_files:
# 构建输出文件名(替换扩展名为.webp)
webp_file = os.path.splitext(png_file)[0] + '.webp'
# 构建ffmpeg命令,设置压缩质量为80%(可以根据需要调整)
cmd = ['ffmpeg', '-i', png_file, '-c:v', 'libwebp', '-quality', '80', webp_file]
try:
print(f"正在转换: {png_file} -> {webp_file}")
subprocess.run(cmd, check=True)
print(f"成功转换: {webp_file}")
except subprocess.CalledProcessError as e:
print(f"转换失败: {png_file}, 错误: {e}")
print("转换完成!")
if __name__ == "__main__":
convert_png_to_webp()
+240
View File
@@ -0,0 +1,240 @@
// 显式导入所有电路元件图片,并定义其基本属性
import battery from './circuit-ele/battery.webp'
import powerSupply from './circuit-ele/power_supply.webp'
import capacitor from './circuit-ele/capacitor.webp'
import inductor from './circuit-ele/inductor.webp'
import lightBulb from './circuit-ele/light_bulb.webp'
import lightBulbGrow from './circuit-ele/light_bulb_grow.webp'
import resistor from './circuit-ele/resistor.webp'
import resistanceBox from './circuit-ele/resistance_box.webp'
import switchOff from './circuit-ele/switch_off.webp'
import switchOn from './circuit-ele/switch_on.webp'
import meter from './circuit-ele/meter.webp'
import slidingRheostat from './circuit-ele/sliding_rheostat.webp'
import slidingRheostatPin from './circuit-ele/sliding_rheostat_pin.webp'
import textBox from './circuit-ele/text_box.svg'
export type ConnectionPoint = {
// 以百分比(0..1)定义在图片盒子内的位置
x: number
y: number
name?: string
}
export type CircuitElement = {
key: string
name: string
url: string
defaultSize?: number // 渲染尺寸(像素),可选
connectionPoints: ConnectionPoint[]
propertySchemas?: PropertySchema[]
stateImages?: { on: string; off: string }
// 预设:一键应用一组属性值
preset?: Preset[]
// 可选:叠加图片(例如滑动变阻器的滑块)
pinUrl?: string
}
export type PropertySchema =
| {
key: string
label: string
type: 'number'
unit?: string
default: number
}
| {
key: string
label: string
type: 'text'
default: string
}
| {
key: string
label: string
type: 'color'
default: string
}
| {
key: string
label: string
type: 'select'
options: Array<{ label?: string; value: string }> | string[]
default: string
}
// 预设定义:展示名称 + 要覆盖的属性值
export type Preset = {
name: string
propertyValues: Record<string, number | string>
}
export const elements: CircuitElement[] = [
{
key: 'text_box',
name: '文本框',
url: textBox,
defaultSize: 0,
// 文本框不参与电路连接,无端点
connectionPoints: [],
propertySchemas: [
{ key: 'text', label: '文本', type: 'text', default: '双击编辑' },
{ key: 'fontSize', label: '字体大小', type: 'number', unit: 'px', default: 24 },
{ key: 'color', label: '颜色', type: 'color', default: '#111827' },
// 可选:是否粗体/对齐方式等,后续再加
],
},
{
key: 'battery',
name: '电池',
url: battery,
defaultSize: 110,
connectionPoints: [
{ x: 0.95, y: 0.5, name: '正极' }, // 右侧
{ x: 0.05, y: 0.5, name: '负极' }, // 左侧
],
propertySchemas: [
{ key: 'voltage', label: '电压', type: 'number', unit: 'V', default: 3 },
{ key: 'internalResistance', label: '内阻', type: 'number', unit: 'Ω', default: 0.5 },
],
},
{
key: 'power_supply',
name: '电源',
url: powerSupply,
defaultSize: 180,
connectionPoints: [
{ x: 0.8, y: 1, name: '正极' },
{ x: 0.2, y: 1, name: '负极' },
],
propertySchemas: [
{ key: 'voltage', label: '电压', type: 'number', unit: 'V', default: 5 },
{ key: 'internalResistance', label: '内阻', type: 'number', unit: 'Ω', default: 0.1 },
],
},
{
key: 'switch',
name: '开关',
url: switchOff,
defaultSize: 130,
connectionPoints: [
{ x: 0.05, y: 0.7, name: 'A' },
{ x: 0.95, y: 0.7, name: 'B' },
],
// 点击元件切换 on/off 显示
stateImages: { on: switchOn, off: switchOff },
propertySchemas: [
{ key: 'state', label: '状态', type: 'select', options: ['off', 'on'], default: 'off' },
],
},
{
key: 'light_bulb',
name: '灯泡',
url: lightBulb,
defaultSize: 80,
connectionPoints: [
{ x: 0.5, y: 1, name: 'A' },
{ x: 0.72, y: 0.8, name: 'B' },
],
propertySchemas: [
{ key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 20 },
],
// 当电流超过阈值时在前端根据 state 切换图片
stateImages: { on: lightBulbGrow, off: lightBulb },
},
{
key: 'sliding_rheostat',
name: '滑动变阻器',
url: slidingRheostat,
pinUrl: slidingRheostatPin,
defaultSize: 180,
connectionPoints: [
{ x: 0.15, y: 0.28, name: '上部左侧' },
{ x: 0.85, y: 0.28, name: '上部右侧' },
{ x: 0.15, y: 0.76, name: '下部左侧' },
{ x: 0.85, y: 0.76, name: '下部右侧' },
],
propertySchemas: [
{ key: 'maxResistance', label: '最大电阻', type: 'number', unit: 'Ω', default: 100 },
{ key: 'position', label: '滑块位置', type: 'number', default: 0.5 },
],
},
{
key: 'resistor',
name: '电阻',
url: resistor,
defaultSize: 110,
connectionPoints: [
{ x: 0.05, y: 0.5, name: 'A' },
{ x: 0.95, y: 0.5, name: 'B' },
],
propertySchemas: [
{ key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 5 },
],
},
{
key: 'resistance_box',
name: '电阻箱',
url: resistanceBox,
defaultSize: 180,
connectionPoints: [
{ x: 0.1, y: 1, name: 'A' },
{ x: 0.9, y: 1, name: 'B' },
],
propertySchemas: [
{ key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 100 },
],
},
{
key: 'meter',
name: '表',
url: meter,
defaultSize: 150,
connectionPoints: [
{ x: 0.25, y: 1, name: '正极' },
{ x: 0.45, y: 1, name: '负极' },
],
preset: [{
name: '微安电流表', propertyValues: { resistance: 0.05, dispNum: 1000000, dispFig: 2, dispUnit: 'uA' }
}, {
name: '毫安电流表', propertyValues: { resistance: 0.5, dispNum: 1000, dispFig: 1, dispUnit: 'mA' }
}, {
name: '电流表', propertyValues: { resistance: 0.1, dispNum: 1, dispFig: 2, dispUnit: 'A' }
}, {
name: '电压表', propertyValues: { resistance: 10000000, dispNum: 10000000, dispFig: 2, dispUnit: 'V' }
}],
propertySchemas: [
{ key: 'resistance', label: '电阻', type: 'number', unit: 'Ω', default: 0.1 },
{ key: 'dispNum', label: '显示数值倍率', type: 'number', default: 1 },
{ key: 'dispFig', label: '有效数字位数', type: 'number', default: 2 },
{ key: 'dispUnit', label: '单位', type: 'text', default: 'A' },
],
},
{
key: 'capacitor',
name: '电容',
url: capacitor,
defaultSize: 120,
connectionPoints: [
{ x: 0.4, y: 0.9, name: '正极' },
{ x: 0.6, y: 0.96, name: '负极' },
],
propertySchemas: [
{ key: 'capacitance', label: '电容', type: 'number', unit: 'F', default: 0.000001 },
],
},
{
key: 'inductor',
name: '电感',
url: inductor,
defaultSize: 100,
connectionPoints: [
{ x: 0.05, y: 0.5, name: 'A' },
{ x: 0.95, y: 0.5, name: 'B' },
],
propertySchemas: [
{ key: 'inductance', label: '电感', type: 'number', unit: 'H', default: 0.001 },
],
},
]
+19
View File
@@ -0,0 +1,19 @@
export function formatValue(v: number | null, unit: string) {
if (v == null || !Number.isFinite(v)) return '—'
// 简易工程计数法
const absv = Math.abs(v)
const units = [
{ s: 1e-9, u: 'n' },
{ s: 1e-6, u: 'µ' },
{ s: 1e-3, u: 'm' },
{ s: 1, u: '' },
{ s: 1e3, u: 'k' },
{ s: 1e6, u: 'M' },
]
let u = ''
let val = v
for (let i = units.length - 1; i >= 0; i--) {
if (absv >= units[i].s) { u = units[i].u; val = v / units[i].s; break }
}
return `${val.toFixed(3)} ${u}${unit}`
}
+6
View File
@@ -1 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
-2
View File
@@ -15,8 +15,6 @@
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
+8 -1
View File
@@ -1,7 +1,14 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { viteSingleFile } from 'vite-plugin-singlefile'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue(), /* viteSingleFile() */],
server: {
host: '0.0.0.0',
proxy: {
'/api': 'http://localhost:3000',
}
}
}) })