diff --git a/package.json b/package.json index 9bc60d22..710b0000 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.37.2", "globals": "^15.11.0", + "jsdom": "^25.0.1", "postcss": "^8.4.47", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d65bffbd..16db3b1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: globals: specifier: ^15.11.0 version: 15.11.0 + jsdom: + specifier: ^25.0.1 + version: 25.0.1 postcss: specifier: ^8.4.47 version: 8.4.47 @@ -134,7 +137,7 @@ importers: version: 4.2.0(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8) vitest: specifier: ^2.1.4 - version: 2.1.4 + version: 2.1.4(jsdom@25.0.1) packages: @@ -932,6 +935,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1000,6 +1007,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -1096,6 +1106,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1124,9 +1138,17 @@ packages: engines: {node: '>=4'} hasBin: true + cssstyle@4.1.0: + resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==} + engines: {node: '>=18'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -1148,6 +1170,9 @@ packages: supports-color: optional: true + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1163,6 +1188,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1366,6 +1395,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -1468,12 +1501,28 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + i18next@23.16.2: resolution: {integrity: sha512-dFyxwLXxEQK32f6tITBMaRht25mZPJhQ0WbC0p3bO2mWBal9lABTMqSka5k+GLSRWLzeJBKDpH7BeIA9TZI7Jg==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1563,6 +1612,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -1622,6 +1674,15 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -1705,6 +1766,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1744,6 +1813,9 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + nwsapi@2.2.13: + resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1799,6 +1871,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2056,6 +2131,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2067,6 +2145,13 @@ packages: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -2177,6 +2262,9 @@ packages: svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.9.2: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2220,6 +2308,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.61: + resolution: {integrity: sha512-In7VffkDWUPgwa+c9picLUxvb0RltVwTkSgMNFgvlGSWveCzGBemBqTsgJCL4EDFWZ6WH0fKTsot6yNhzy3ZzQ==} + + tldts@6.1.61: + resolution: {integrity: sha512-rv8LUyez4Ygkopqn+M6OLItAOT9FF3REpPQDkdMx5ix8w4qkuE7Vo2o/vw1nxKQYmJDV8JpAMJQr1b+lTKf0FA==} + hasBin: true + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -2232,6 +2327,14 @@ packages: resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} engines: {node: '>=10'} + tough-cookie@5.0.0: + resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} + engines: {node: '>=16'} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -2370,6 +2473,26 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -2407,6 +2530,25 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3177,6 +3319,12 @@ snapshots: acorn@8.13.0: {} + agent-base@7.1.1: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -3267,6 +3415,8 @@ snapshots: assertion-error@2.0.1: {} + asynckit@0.4.0: {} + autoprefixer@10.4.20(postcss@8.4.47): dependencies: browserslist: 4.24.0 @@ -3372,6 +3522,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@4.1.1: {} concat-map@0.0.1: {} @@ -3395,8 +3549,17 @@ snapshots: cssesc@3.0.0: {} + cssstyle@4.1.0: + dependencies: + rrweb-cssom: 0.7.1 + csstype@3.1.3: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + data-view-buffer@1.0.1: dependencies: call-bind: 1.0.7 @@ -3419,6 +3582,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.4.3: {} + deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -3435,6 +3600,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -3747,6 +3914,12 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fraction.js@4.3.7: {} fsevents@2.3.3: @@ -3841,14 +4014,36 @@ snapshots: dependencies: function-bind: 1.1.2 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.1 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.5: + dependencies: + agent-base: 7.1.1 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + i18next@23.16.2: dependencies: '@babel/runtime': 7.25.7 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} import-fresh@3.3.0: @@ -3928,6 +4123,8 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -3988,6 +4185,34 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@25.0.1: + dependencies: + cssstyle: 4.1.0 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.13 + parse5: 7.2.1 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.0.2: {} json-buffer@3.0.1: {} @@ -4059,6 +4284,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -4092,6 +4323,8 @@ snapshots: normalize-range@0.1.2: {} + nwsapi@2.2.13: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -4156,6 +4389,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@7.2.1: + dependencies: + entities: 4.5.0 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -4349,6 +4586,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.24.0 fsevents: 2.3.3 + rrweb-cssom@0.7.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4366,6 +4605,12 @@ snapshots: es-errors: 1.3.0 is-regex: 1.1.4 + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -4501,6 +4746,8 @@ snapshots: svg-parser@2.0.4: {} + symbol-tree@3.2.4: {} + synckit@0.9.2: dependencies: '@pkgr/core': 0.1.1 @@ -4560,6 +4807,12 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@6.1.61: {} + + tldts@6.1.61: + dependencies: + tldts-core: 6.1.61 + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -4568,6 +4821,14 @@ snapshots: tosource@2.0.0-alpha.3: {} + tough-cookie@5.0.0: + dependencies: + tldts: 6.1.61 + + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@1.3.0(typescript@5.6.3): dependencies: typescript: 5.6.3 @@ -4684,7 +4945,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - vitest@2.1.4: + vitest@2.1.4(jsdom@25.0.1): dependencies: '@vitest/expect': 2.1.4 '@vitest/mocker': 2.1.4(vite@5.4.8) @@ -4706,6 +4967,8 @@ snapshots: vite: 5.4.8 vite-node: 2.1.4 why-is-node-running: 2.3.0 + optionalDependencies: + jsdom: 25.0.1 transitivePeerDependencies: - less - lightningcss @@ -4719,6 +4982,23 @@ snapshots: void-elements@3.1.0: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.0.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4 @@ -4780,6 +5060,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + ws@8.18.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + yallist@3.1.1: {} yaml@2.5.1: {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ae4a0a10..d74a4e97 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -254,6 +254,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -2942,6 +2948,7 @@ dependencies = [ name = "project-graph" version = "0.1.0" dependencies = [ + "base64 0.13.1", "serde", "serde_json", "tauri", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f4c00dfc..01b15976 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,3 +26,4 @@ tauri-plugin-store = "2.0.0-rc" tauri-plugin-http = "2" tauri-plugin-gamepad = "0.0.4" +base64 = "0.13" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ce6fafb3..52453d9a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,8 +1,11 @@ +use std::fs::File; use std::io::Read; use std::io::Write; +use base64::{decode, encode}; use std::env; -use tauri::Manager; +use std::fs::read; // 引入 read 函数用于读取文件 +use tauri::Manager; // 引入 base64 编码函数 // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command #[tauri::command] @@ -58,6 +61,34 @@ fn check_json_exist(path: String) -> bool { // window.open_devtools(); // } +#[tauri::command] +fn convert_image_to_base64(image_path: String) -> Result { + match read(&image_path) { + Ok(image_data) => { + let base64_str = encode(&image_data); + Ok(base64_str) + } + Err(e) => Err(format!("无法读取文件: {}, {}", e, image_path)), + } +} + +/// 将base64编码字符串保存为图片文件 +#[tauri::command] +fn save_base64_to_image(base64_str: &str, file_name: &str) -> Result<(), String> { + // 进行解码 + match decode(base64_str) { + Ok(image_data) => { + // 创建文件并写入数据 + let mut file = File::create(file_name).map_err(|e| format!("无法创建文件: {}", e))?; + file.write_all(&image_data) + .map_err(|e| format!("无法写入文件: {}", e))?; + Ok(()) + } + Err(e) => Err(format!("解码失败: {}", e)), + } +} + + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { println!("程序运行了!"); @@ -85,6 +116,8 @@ pub fn run() { set_env_value, open_json_by_path, save_json_by_path, + convert_image_to_base64, + save_base64_to_image, check_json_exist // open_dev_tools ]) .run(tauri::generate_context!()) diff --git a/src/__tests__/unit/pathString.test.tsx b/src/__tests__/unit/pathString.test.tsx new file mode 100644 index 00000000..378698f6 --- /dev/null +++ b/src/__tests__/unit/pathString.test.tsx @@ -0,0 +1,52 @@ +import { describe, it, expect } from "vitest"; +import { PathString } from "../../utils/pathString"; + +describe("测试路径函数", () => { + it("absolute2file", () => { + expect( + PathString.absolute2file("C:\\Users\\Administrator\\Desktop\\test.txt"), + ).toBe("test"); + + expect( + PathString.absolute2file("C:\\Users\\Administrator\\Desktop\\test.json"), + ).toBe("test"); + + expect( + PathString.absolute2file( + "C:\\Users\\Administrator\\Desktop\\test.testing.json", + ), + ).toBe("test.testing"); + + expect( + PathString.absolute2file( + "C:\\Users\\Administrator\\Desktop\\a.b.c.d.json", + ), + ).toBe("a.b.c.d"); + }); + + it("测试获取文件所在文件夹的路径", () => { + expect( + PathString.dirPath("C:\\Users\\Administrator\\Desktop\\test.json"), + ).toBe("C:\\Users\\Administrator\\Desktop"); + + expect( + PathString.dirPath("C:\\Users\\Administrator\\Desktop\\test.txt"), + ).toBe("C:\\Users\\Administrator\\Desktop"); + + expect( + PathString.dirPath("C:\\Users\\Administrator\\Desktop\\file"), + ).toBe("C:\\Users\\Administrator\\Desktop"); + + expect( + PathString.dirPath("D:\\test.json"), + ).toBe("D:"); + + expect( + PathString.dirPath("user/test.json"), + ).toBe("user"); + + // expect( + // PathString.dirPath("/user/test.json"), + // ).toBe("/user"); + }); +}); diff --git a/src/core/RecentFileManager.tsx b/src/core/RecentFileManager.tsx index 33af2e3d..7dd6b7f0 100644 --- a/src/core/RecentFileManager.tsx +++ b/src/core/RecentFileManager.tsx @@ -12,6 +12,7 @@ import { Serialized } from "../types/node"; import { StageHistoryManager } from "./stage/stageManager/StageHistoryManager"; import { Section } from "./stageObject/entity/Section"; import { ConnectPoint } from "./stageObject/entity/ConnectPoint"; +import { ImageNode } from "./stageObject/entity/ImageNode"; /** * 管理最近打开的文件列表 @@ -90,7 +91,7 @@ export namespace RecentFileManager { export async function validAndRefreshRecentFiles() { const recentFiles = await getRecentFiles(); const recentFilesValid: RecentFile[] = []; - + // 是否存在文件丢失情况 let isFileLost = false; @@ -106,8 +107,6 @@ export namespace RecentFileManager { console.log("文件不存在,删除记录", file.path); isFileLost = true; } - - } catch (e) { console.error("无法检测文件是否存在:", file.path); console.error(e); @@ -177,6 +176,8 @@ export namespace RecentFileManager { StageManager.addSection(new Section(entity)); } else if (entity.type === "core:connect_point") { StageManager.addConnectPoint(new ConnectPoint(entity)); + } else if (entity.type === "core:image_node") { + StageManager.addImageNode(new ImageNode(entity)); } else { console.warn("加载文件时,出现未知的实体类型:" + entity); } diff --git a/src/core/controller/concrete/ControllerCopy.tsx b/src/core/controller/concrete/ControllerCopy.tsx index 55307044..f6c43a5c 100644 --- a/src/core/controller/concrete/ControllerCopy.tsx +++ b/src/core/controller/concrete/ControllerCopy.tsx @@ -10,6 +10,9 @@ import { Controller } from "../Controller"; import { ControllerClass } from "../ControllerClass"; import { v4 as uuidv4 } from "uuid"; import { Entity } from "../../stageObject/StageObject"; +import { ImageNode } from "../../stageObject/entity/ImageNode"; +import { invoke } from "@tauri-apps/api/core"; +import { PathString } from "../../../utils/pathString"; /** * 关于复制相关的功能 @@ -134,22 +137,34 @@ async function readClipboardItems(mouseLocation: Vector) { try { navigator.clipboard.read().then(async (items) => { for (const item of items) { - if ( - item.types.includes("image/png") || - item.types.includes("image/jpeg") - ) { + if (item.types.includes("image/png")) { const blob = await item.getType(item.types[0]); // 获取 Blob 对象 const base64String = await convertBlobToBase64(blob); // 转换为 Base64 字符串 - console.log("Base64 String:", base64String); + const imageUUID = uuidv4(); + const folder = PathString.dirPath(Stage.Path.getFilePath()); + const imagePath = `${folder}${Stage.Path.getSep()}${imageUUID}.png`; - // 显示图像 - // const url = `data:${blob.type};base64,${base64String}`; - // const imageElement = document.getElementById( - // "clipboardImage", - // ) as HTMLImageElement; - // imageElement.src = url; - // imageElement.style.display = "block"; - break; + invoke("save_base64_to_image", { + base64Str: base64String, + fileName: imagePath, + }) + .then(() => { + console.log("save image to file success"); + + // 要延迟一下,等待保存完毕 + setTimeout(() => { + const imageNode = new ImageNode({ + uuid: imageUUID, + location: [mouseLocation.x, mouseLocation.y], + path: `${imageUUID}.png`, + }); + // imageNode.setBase64StringForced(base64String); + StageManager.addImageNode(imageNode); + }, 100); + }) + .catch((error) => { + console.error("save image to file error", error); + }); } if (item.types.includes("text/plain")) { const blob = await item.getType("text/plain"); // 获取文本内容 diff --git a/src/core/controller/concrete/ControllerNodeMove.tsx b/src/core/controller/concrete/ControllerNodeMove.tsx index 65ebf97c..7544e5b9 100644 --- a/src/core/controller/concrete/ControllerNodeMove.tsx +++ b/src/core/controller/concrete/ControllerNodeMove.tsx @@ -20,46 +20,17 @@ ControllerNodeMove.mousedown = (event: MouseEvent) => { const pressWorldLocation = Renderer.transformView2World( new Vector(event.clientX, event.clientY), ); - const isHaveNodeSelected = StageManager.getTextNodes().some( - (node) => node.isSelected, - ); - const isHaveSectionSelected = StageManager.getSections().some( - (section) => section.isSelected, - ); ControllerNodeMove.lastMoveLocation = pressWorldLocation.clone(); - const clickedNode = StageManager.findTextNodeByLocation(pressWorldLocation); - const clickedSection = StageManager.findSectionByLocation(pressWorldLocation); - const clickedConnectPoint = - StageManager.findConnectPointByLocation(pressWorldLocation); - - if (clickedSection !== null) { - Controller.isMovingEntity = true; - if (isHaveSectionSelected && !clickedSection.isSelected) { - StageManager.getSections().forEach((section) => { - section.isSelected = false; - }); - } - clickedSection.isSelected = true; - } - - if (clickedConnectPoint !== null) { - Controller.isMovingEntity = true; - if (clickedConnectPoint && !clickedConnectPoint.isSelected) { - StageManager.getConnectPoints().forEach((point) => { - point.isSelected = false; - }); - } - clickedConnectPoint.isSelected = true; - } - - if (clickedNode !== null) { + const clickedEntity = + StageManager.findConnectableEntityByLocation(pressWorldLocation); + if (clickedEntity !== null) { Controller.isMovingEntity = true; - if (isHaveNodeSelected && !clickedNode.isSelected) { - StageManager.getTextNodes().forEach((node) => { - node.isSelected = false; + if (clickedEntity && !clickedEntity.isSelected) { + StageManager.getEntities().forEach((entity) => { + entity.isSelected = false; }); + clickedEntity.isSelected = true; } - clickedNode.isSelected = true; // 同时清空所有边的选中状态 StageManager.getAssociations().forEach((edge) => { edge.isSelected = false; @@ -77,6 +48,7 @@ ControllerNodeMove.mousemove = (event: MouseEvent) => { const worldLocation = Renderer.transformView2World( new Vector(event.clientX, event.clientY), ); + console.log(worldLocation.toString()); const diffLocation = worldLocation.subtract( ControllerNodeMove.lastMoveLocation, ); @@ -93,6 +65,7 @@ ControllerNodeMove.mousemove = (event: MouseEvent) => { } StageManager.moveSections(diffLocation); StageManager.moveConnectPoints(diffLocation); + StageManager.moveImageNodes(diffLocation); ControllerNodeMove.lastMoveLocation = worldLocation.clone(); } diff --git a/src/core/render/canvas2d/ImageRenderer.tsx b/src/core/render/canvas2d/ImageRenderer.tsx new file mode 100644 index 00000000..1e06628c --- /dev/null +++ b/src/core/render/canvas2d/ImageRenderer.tsx @@ -0,0 +1,66 @@ +import { StringDict } from "../../dataStruct/StringDict"; +import { Vector } from "../../dataStruct/Vector"; +import { Camera } from "../../stage/Camera"; +import { Canvas } from "../../stage/Canvas"; + +/** + * 图片渲染器 + */ +export namespace ImageRenderer { + // 有待改成长字符串缓存字典,并测试性能对比 + const imageBase64Cache: StringDict = StringDict.create(); + + export function getImageSizeByBase64(imageBase64: string): Vector { + if (imageBase64Cache.hasId(imageBase64)) { + const imageElement = imageBase64Cache.getById(imageBase64); + if (imageElement) { + return new Vector(imageElement.width, imageElement.height); + } + } + return new Vector(0, 0); + } + + /** + * 根据base64编码字符串来渲染出图片 + * @param imageBase64 base64编码的图片字符串,不包含前缀data:image/png;base64, + * @param location + */ + export function renderPngBase64FromLeftTop( + imageBase64: string, + location: Vector, + ) { + if (imageBase64Cache.hasId(imageBase64)) { + const imageElement = imageBase64Cache.getById(imageBase64); + if (imageElement) { + Canvas.ctx.drawImage( + imageElement, + location.x, + location.y, + imageElement.width * Camera.currentScale, + imageElement.height * Camera.currentScale, + ); + } + } else { + // 字典中没有,则创建,并缓存 + const imageElement = new Image(); + imageElement.src = `data:image/png;base64,${imageBase64}`; + imageElement.onload = () => { + imageBase64Cache.setById(imageBase64, imageElement); + // 调整碰撞箱大小 + }; + } + } + + export function renderImageElement( + imageElement: HTMLImageElement, + location: Vector, + ) { + Canvas.ctx.drawImage( + imageElement, + location.x, + location.y, + (imageElement.width / (window.devicePixelRatio || 1)) * Camera.currentScale, + (imageElement.height / (window.devicePixelRatio || 1)) * Camera.currentScale, + ); + } +} diff --git a/src/core/render/canvas2d/RenderUtils.tsx b/src/core/render/canvas2d/RenderUtils.tsx index 95f87e48..e0fae702 100644 --- a/src/core/render/canvas2d/RenderUtils.tsx +++ b/src/core/render/canvas2d/RenderUtils.tsx @@ -5,6 +5,7 @@ import { Vector } from "../../dataStruct/Vector"; import { CubicBezierCurve, SymmetryCurve } from "../../dataStruct/shape/Curve"; import { Camera } from "../../stage/Camera"; + /** * 一些基础的渲染图形 * 注意:这些渲染的参数都是View坐标系下的。 @@ -402,6 +403,7 @@ export namespace RenderUtils { height, ); } + /** * 绘制一个像素点 * @param location @@ -455,4 +457,7 @@ export namespace RenderUtils { Canvas.ctx.fillStyle = color.toString(); Canvas.ctx.fill(); } + + + } diff --git a/src/core/render/canvas2d/entityRenderer/EntityRenderer.tsx b/src/core/render/canvas2d/entityRenderer/EntityRenderer.tsx index 13a5d6e8..6bfb44c8 100644 --- a/src/core/render/canvas2d/entityRenderer/EntityRenderer.tsx +++ b/src/core/render/canvas2d/entityRenderer/EntityRenderer.tsx @@ -11,6 +11,8 @@ import { ConnectPoint } from "../../../stageObject/entity/ConnectPoint"; import { replaceTextWhenProtect } from "../../../../utils/font"; import { Random } from "../../../algorithm/random"; import { StageStyleManager } from "../../../stageStyle/StageStyleManager"; +import { ImageNode } from "../../../stageObject/entity/ImageNode"; +import { ImageRenderer } from "../ImageRenderer"; /** * 处理节点相关的绘制 @@ -198,4 +200,45 @@ export namespace EntityRenderer { 2 * Camera.currentScale, ); } + + export function renderImageNode(imageNode: ImageNode) { + if (imageNode.isSelected) { + // 在外面增加一个框 + CollisionBoxRenderer.render( + imageNode.collisionBox, + StageStyleManager.currentStyle.CollideBoxSelectedColor, + ); + } + // 节点身体矩形 + RenderUtils.renderRect( + new Rectangle( + Renderer.transformWorld2View(imageNode.rectangle.location), + imageNode.rectangle.size.multiply(Camera.currentScale), + ), + Color.Transparent, + StageStyleManager.currentStyle.StageObjectBorderColor, + 2 * Camera.currentScale, + Renderer.NODE_ROUNDED_RADIUS * Camera.currentScale, + ); + if (imageNode.state === "loading") { + RenderUtils.renderTextFromCenter( + "loading...", + Renderer.transformWorld2View(imageNode.rectangle.center), + 20 * Camera.currentScale, + Color.White, + ); + } else if (imageNode.state === "success") { + ImageRenderer.renderImageElement( + imageNode.imageElement, + Renderer.transformWorld2View(imageNode.rectangle.location), + ); + } else if (imageNode.state === "error") { + RenderUtils.renderTextFromCenter( + "Error", + Renderer.transformWorld2View(imageNode.rectangle.center), + 20 * Camera.currentScale, + Color.Red, + ); + } + } } diff --git a/src/core/render/canvas2d/renderer.tsx b/src/core/render/canvas2d/renderer.tsx index deea2bba..efe9f1d9 100644 --- a/src/core/render/canvas2d/renderer.tsx +++ b/src/core/render/canvas2d/renderer.tsx @@ -326,6 +326,12 @@ export namespace Renderer { } EntityRenderer.renderConnectPoint(connectPoint); } + for (const imageNode of StageManager.getImageNodes()) { + if (!viewRectangle.isCollideWith(imageNode.rectangle)) { + continue; + } + EntityRenderer.renderImageNode(imageNode); + } } export function renderEdges(viewRectangle: Rectangle) { @@ -520,7 +526,8 @@ export namespace Renderer { `历史: ${StageHistoryManager.statusText()}`, `fps: ${(1 / deltaTime).toFixed()}`, `delta: ${deltaTime.toFixed(2)}`, - `Controller.isViewMoveByClickMiddle: ${Controller.isViewMoveByClickMiddle}` + `Controller.isViewMoveByClickMiddle: ${Controller.isViewMoveByClickMiddle}`, + `path: ${Stage.Path.getFilePath()}` ]; for (const [k, v] of Object.entries(timings)) { detailsData.push(`time:${k}: ${v.toFixed(2)}`); diff --git a/src/core/stage/Camera.tsx b/src/core/stage/Camera.tsx index 27f08d4c..9b6100f4 100644 --- a/src/core/stage/Camera.tsx +++ b/src/core/stage/Camera.tsx @@ -145,9 +145,9 @@ export namespace Camera { Settings.watch("moveAmplitude", (value) => { moveAmplitude = value; }); - Settings.watch("moveFriction", value => { + Settings.watch("moveFriction", (value) => { frictionCoefficient = value; - }) + }); } /** @@ -182,4 +182,9 @@ export namespace Camera { ); Camera.targetScale = Camera.currentScale; } + + export function resetScale() { + Camera.currentScale = 1; + Camera.targetScale = 1; + } } diff --git a/src/core/stage/Stage.tsx b/src/core/stage/Stage.tsx index 23d9d005..88fbb812 100644 --- a/src/core/stage/Stage.tsx +++ b/src/core/stage/Stage.tsx @@ -12,6 +12,7 @@ import { Controller } from "../controller/Controller"; import { StageManager } from "./stageManager/StageManager"; import { PointDashEffect } from "../effect/concrete/PointDashEffect"; import { ControllerGamepad } from "../controller/ControllerGamepad"; +import { family } from "@tauri-apps/plugin-os"; /** * 舞台对象 @@ -23,6 +24,48 @@ import { ControllerGamepad } from "../controller/ControllerGamepad"; * 但这个里面主要存一些动态的属性,以及特效交互等信息 */ export namespace Stage { + /** + * 此Path存在的意义为摆脱状态管理只能在组件函数中的限制 + */ + export namespace Path { + let currentPath = "Project Graph"; + export const draftName = "Project Graph"; + + export function getSep(): string { + const fam = family(); + if (fam === "windows") { + return "\\"; + } else { + return "/"; + } + } + + /** + * 是否是草稿 + * @returns + */ + export function isDraft() { + return currentPath === "Project Graph"; + } + + /** + * 此函数唯一的调用:只能在app.tsx的useEffect检测函数中调用 + * 为了同步状态管理中的路径。 + * @param path + */ + export function setPathInEffect(path: string) { + currentPath = path; + } + + /** + * 提供一个函数供外部调用,获取当前路径 + * @returns + */ + export function getFilePath() { + return currentPath; + } + } + export let effects: Effect[] = []; /** * 是否正在框选 diff --git a/src/core/stage/StageDumper.tsx b/src/core/stage/StageDumper.tsx index 72015d28..6dd3167f 100644 --- a/src/core/stage/StageDumper.tsx +++ b/src/core/stage/StageDumper.tsx @@ -1,6 +1,7 @@ import { Serialized } from "../../types/node"; import { Edge } from "../stageObject/association/Edge"; import { ConnectPoint } from "../stageObject/entity/ConnectPoint"; +import { ImageNode } from "../stageObject/entity/ImageNode"; import { Section } from "../stageObject/entity/Section"; import { TextNode } from "../stageObject/entity/TextNode"; import { Entity } from "../stageObject/StageObject"; @@ -46,6 +47,19 @@ export namespace StageDumper { }; } + export function dumpImageNode(imageNode: ImageNode): Serialized.ImageNode { + return { + location: [ + imageNode.rectangle.location.x, + imageNode.rectangle.location.y, + ], + size: [imageNode.rectangle.size.x, imageNode.rectangle.size.y], + path: imageNode.path, + uuid: imageNode.uuid, + type: "core:image_node", + }; + } + export function dumpSection(section: Section): Serialized.Section { return { location: [section.rectangle.location.x, section.rectangle.location.y], @@ -69,6 +83,7 @@ export namespace StageDumper { | Serialized.Section | Serialized.Node | Serialized.ConnectPoint + | Serialized.ImageNode )[] = StageManager.getTextNodes().map((node) => dumpTextNode(node)); nodes.push( @@ -79,6 +94,9 @@ export namespace StageDumper { dumpConnectPoint(connectPoint), ), ); + nodes.push( + ...StageManager.getImageNodes().map((node) => dumpImageNode(node)), + ); return { version: latestVersion, diff --git a/src/core/stage/StageLoader.tsx b/src/core/stage/StageLoader.tsx index d1fcaedb..355a57e6 100644 --- a/src/core/stage/StageLoader.tsx +++ b/src/core/stage/StageLoader.tsx @@ -165,4 +165,5 @@ export namespace StageLoader { data.version = 9; return data; } + // 增加了ImageNode } diff --git a/src/core/stage/StageSaveManager.tsx b/src/core/stage/StageSaveManager.tsx index 04db637d..1c3d3732 100644 --- a/src/core/stage/StageSaveManager.tsx +++ b/src/core/stage/StageSaveManager.tsx @@ -39,6 +39,38 @@ export namespace StageSaveManager { }); } + /** + * + * without path 意思是不需要传入path,直接使用当前的path + * @param data + * @param successCallback + * @param errorCallback + */ + export function saveHandleWithoutCurrentPath( + data: Serialized.File, + successCallback: () => void, + errorCallback: (err: any) => void, + ) { + if (Stage.Path.isDraft()) { + errorCallback("当前文档的状态为草稿,请您先保存为文件"); + return; + } + invoke("save_json_by_path", { + path: Stage.Path.getFilePath(), + content: JSON.stringify(data, null, 2), + }) + .then((res) => { + console.log(res); + Stage.effects.push(ViewFlashEffect.SaveFile()); + StageHistoryManager.reset(data); // 重置历史 + successCallback(); + isCurrentSaved = true; + }) + .catch((err) => { + errorCallback(err); + }); + } + export function saveSvgHandle( path: string, string: string, diff --git a/src/core/stage/stageManager/StageHistoryManager.tsx b/src/core/stage/stageManager/StageHistoryManager.tsx index f9cdcb06..1efb003b 100644 --- a/src/core/stage/stageManager/StageHistoryManager.tsx +++ b/src/core/stage/stageManager/StageHistoryManager.tsx @@ -43,8 +43,8 @@ export namespace StageHistoryManager { }); } - export function reset(file: Serialized.File) { - historyList = [file]; + export function reset(serializedFile: Serialized.File) { + historyList = [serializedFile]; currentIndex = 0; StageSaveManager.setIsCurrentSaved(true); } diff --git a/src/core/stage/stageManager/StageManager.tsx b/src/core/stage/stageManager/StageManager.tsx index 3dfaf115..d9902b62 100644 --- a/src/core/stage/stageManager/StageManager.tsx +++ b/src/core/stage/stageManager/StageManager.tsx @@ -29,6 +29,7 @@ import { StageSectionPackManager } from "./concreteMethods/StageSectionPackManag import { StageNodeTextTransfer } from "./concreteMethods/StageNodeTextTransfer"; import { ConnectPoint } from "../../stageObject/entity/ConnectPoint"; import { StageGeneratorAI } from "./concreteMethods/StageGeneratorAI"; +import { ImageNode } from "../../stageObject/entity/ImageNode"; // littlefean:应该改成类,实例化的对象绑定到舞台上。这成单例模式了 // 开发过程中会造成多开 @@ -57,6 +58,9 @@ export namespace StageManager { export function getSections(): Section[] { return entities.valuesToArray().filter((node) => node instanceof Section); } + export function getImageNodes(): ImageNode[] { + return entities.valuesToArray().filter((node) => node instanceof ImageNode); + } export function getConnectPoints(): ConnectPoint[] { return entities .valuesToArray() @@ -89,6 +93,9 @@ export namespace StageManager { export function deleteOneTextNode(node: TextNode) { entities.deleteValue(node); } + export function deleteOneImage(node: ImageNode) { + entities.deleteValue(node); + } export function deleteOneSection(section: Section) { entities.deleteValue(section); } @@ -119,6 +126,9 @@ export namespace StageManager { export function addTextNode(node: TextNode) { entities.addValue(node, node.uuid); } + export function addImageNode(node: ImageNode) { + entities.addValue(node, node.uuid); + } export function addSection(section: Section) { entities.addValue(section, section.uuid); } @@ -316,6 +326,15 @@ export namespace StageManager { return null; } + export function findImageNodeByLocation(location: Vector): ImageNode | null { + for (const node of getImageNodes()) { + if (node.collisionBox.isPointInCollisionBox(location)) { + return node; + } + } + return null; + } + export function findConnectableEntityByLocation( location: Vector, ): ConnectableEntity | null { @@ -417,6 +436,9 @@ export namespace StageManager { export function moveConnectPoints(delta: Vector) { StageEntityMoveManager.moveConnectPoints(delta); // 连续过程,不记录历史,只在结束时记录 } + export function moveImageNodes(delta: Vector) { + StageEntityMoveManager.moveImageNodes(delta); // 连续过程,不记录历史,只在结束时记录 + } export function moveNodesWithChildren(delta: Vector) { StageEntityMoveManager.moveNodesWithChildren(delta); // 连续过程,不记录历史,只在结束时记录 } diff --git a/src/core/stage/stageManager/concreteMethods/StageDeleteManager.tsx b/src/core/stage/stageManager/concreteMethods/StageDeleteManager.tsx index 77ca2e66..db5294e5 100644 --- a/src/core/stage/stageManager/concreteMethods/StageDeleteManager.tsx +++ b/src/core/stage/stageManager/concreteMethods/StageDeleteManager.tsx @@ -3,6 +3,7 @@ import { ProgressNumber } from "../../../dataStruct/ProgressNumber"; import { ExplodeAshEffect } from "../../../effect/concrete/ExplodeDashEffect"; import { Edge } from "../../../stageObject/association/Edge"; import { ConnectPoint } from "../../../stageObject/entity/ConnectPoint"; +import { ImageNode } from "../../../stageObject/entity/ImageNode"; import { Section } from "../../../stageObject/entity/Section"; import { TextNode } from "../../../stageObject/entity/TextNode"; import { Entity } from "../../../stageObject/StageObject"; @@ -21,6 +22,8 @@ export namespace StageDeleteManager { deleteSection(entity); } else if (entity instanceof ConnectPoint) { deleteConnectPoint(entity); + } else if (entity instanceof ImageNode) { + deleteImageNode(entity); } } StageManager.updateReferences(); @@ -44,7 +47,20 @@ export namespace StageDeleteManager { // 再删除自己 StageManager.deleteOneSection(entity); } - + function deleteImageNode(entity: ImageNode) { + if (StageManager.getImageNodes().includes(entity)) { + StageManager.deleteOneImage(entity); + Stage.effects.push( + new ExplodeAshEffect( + new ProgressNumber(0, 30), + entity.collisionBox.getRectangle(), + Color.White, + ), + ); + // 删除所有相关的边 + deleteEntityAfterClearEdges(entity); + } + } function deleteConnectPoint(entity: ConnectPoint) { // 先判断这个node是否在nodes里 if (StageManager.getConnectPoints().includes(entity)) { diff --git a/src/core/stage/stageManager/concreteMethods/StageEntityMoveManager.tsx b/src/core/stage/stageManager/concreteMethods/StageEntityMoveManager.tsx index be5e7128..08a6e3eb 100644 --- a/src/core/stage/stageManager/concreteMethods/StageEntityMoveManager.tsx +++ b/src/core/stage/stageManager/concreteMethods/StageEntityMoveManager.tsx @@ -60,13 +60,19 @@ export namespace StageEntityMoveManager { } } export function moveConnectPoints(delta: Vector) { - console.log("moveConnectPoints"); for (const point of StageManager.getConnectPoints()) { if (point.isSelected) { moveEntityUtils(point, delta); } } } + export function moveImageNodes(delta: Vector) { + for (const node of StageManager.getImageNodes()) { + if (node.isSelected) { + moveEntityUtils(node, delta); + } + } + } export function moveNodesWithChildren(delta: Vector) { for (const node of StageManager.getTextNodes()) { diff --git a/src/core/stageObject/entity/ImageNode.tsx b/src/core/stageObject/entity/ImageNode.tsx new file mode 100644 index 00000000..1ab7eb7d --- /dev/null +++ b/src/core/stageObject/entity/ImageNode.tsx @@ -0,0 +1,164 @@ +import { invoke } from "@tauri-apps/api/core"; +import { Serialized } from "../../../types/node"; +import { Rectangle } from "../../dataStruct/shape/Rectangle"; +import { Vector } from "../../dataStruct/Vector"; +import { CollisionBox } from "../collisionBox/collisionBox"; +import { ConnectableEntity } from "../StageObject"; +import { Stage } from "../../stage/Stage"; +import { PathString } from "../../../utils/pathString"; + +/** + * 一个图片节点 + * 图片的路径字符串决定了这个图片是什么 + * + * 有两个转换过程: + * + * 图片路径 -> base64字符串 -> 图片Element -> 完成 + * gettingBase64 + * | + * v + * fileNotfound + * base64EncodeError + * + */ +export class ImageNode extends ConnectableEntity { + isHiddenBySectionCollapse: boolean = false; + public uuid: string; + public collisionBox: CollisionBox; + + /** + * 这里的path是相对于工程文件的相对路径 + * 例如:"example.png" + */ + public path: string; + /** + * 节点是否被选中 + */ + _isSelected: boolean = false; + + /** + * 获取节点的选中状态 + */ + public get isSelected() { + return this._isSelected; + } + + public set isSelected(value: boolean) { + this._isSelected = value; + } + + private _base64String: string = ""; + + /** + * 图片的三种状态 + */ + public state: "loading" | "success" | "error" = "loading"; + + private _imageElement: HTMLImageElement = new Image(); + + public get imageElement(): HTMLImageElement { + return this._imageElement; + } + + constructor( + { + uuid, + location = [0, 0], + size = [100, 100], + path = "", + }: Partial & { uuid: string }, + public unknown = false, + ) { + super(); + this.uuid = uuid; + this.path = path; + this.collisionBox = new CollisionBox([ + new Rectangle(new Vector(...location), new Vector(...size)), + ]); + this.state = "loading"; + // 初始化创建的时候,开始获取base64String + if (!Stage.Path.isDraft()) { + this.updateBase64StringByPath( + PathString.dirPath(Stage.Path.getFilePath()), + ); + } else { + // 一般只有在粘贴板粘贴时和初次打开文件时才调用这里 + // 所以这里只可能时初次打开文件时还是草稿的状态 + + setTimeout(() => { + this.updateBase64StringByPath( + PathString.dirPath(Stage.Path.getFilePath()), + ); + }, 1000); + } + } + + /** + * + * @param folderPath 工程文件所在路径文件夹,不加尾部斜杠 + * @returns + */ + public updateBase64StringByPath(folderPath: string) { + if (this.path === "") { + return; + } + + invoke("convert_image_to_base64", { + imagePath: `${folderPath}\\${this.path}`, + }) + .then((res) => { + // 获取base64String成功 + + this._base64String = res; + const imageElement = new Image(); + this._imageElement = imageElement; + imageElement.src = `data:image/png;base64,${this._base64String}`; + imageElement.onload = () => { + // 图片加载成功 + console.log("图片加载成功"); + + // 调整碰撞箱大小 + + this.rectangle.size = new Vector( + imageElement.width / (window.devicePixelRatio || 1), + imageElement.height / (window.devicePixelRatio || 1), + ); + this.state = "success"; + }; + imageElement.onerror = () => { + console.log("图片加载失败"); + this.state = "error"; + }; + }) + .catch((err) => { + // 获取base64String失败 + console.log("图片可能不存在?", err); + this.state = "error"; + }); + } + + /** + * 只读,获取节点的矩形 + * 若要修改节点的矩形,请使用 moveTo等 方法 + */ + public get rectangle(): Rectangle { + return this.collisionBox.shapeList[0] as Rectangle; + } + + public get geometryCenter() { + return this.rectangle.location + .clone() + .add(this.rectangle.size.clone().multiply(0.5)); + } + + move(delta: Vector): void { + const newRectangle = this.rectangle.clone(); + newRectangle.location = newRectangle.location.add(delta); + this.collisionBox.shapeList[0] = newRectangle; + } + moveTo(location: Vector): void { + const newRectangle = this.rectangle.clone(); + newRectangle.location = location.clone(); + this.collisionBox.shapeList[0] = newRectangle; + } +} diff --git a/src/locales/en.yml b/src/locales/en.yml index e6d43c13..35faa479 100644 --- a/src/locales/en.yml +++ b/src/locales/en.yml @@ -140,6 +140,7 @@ appMenu: items: resetByAll: Reset View by All Content resetBySelect: Reset View by Selected Content + resetScale: Reset the scale to the standard size more: title: More items: diff --git a/src/locales/zh-CN.yml b/src/locales/zh-CN.yml index 7e17bf31..05141a93 100644 --- a/src/locales/zh-CN.yml +++ b/src/locales/zh-CN.yml @@ -118,6 +118,7 @@ appMenu: items: resetByAll: 根据全部内容重置视野 resetBySelect: 根据选中内容重置视野 + resetScale: 将缩放重置为标准大小 more: title: 更多 items: diff --git a/src/locales/zh-TW.yml b/src/locales/zh-TW.yml index 902df868..9e5c09e1 100644 --- a/src/locales/zh-TW.yml +++ b/src/locales/zh-TW.yml @@ -117,6 +117,7 @@ appMenu: items: resetByAll: 根據全部內容重置視野 resetBySelect: 根據選中內容重置視野 + resetScale: 將縮放重置為標準大小 more: title: 更多 items: diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index ee299109..6ba67015 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -29,6 +29,7 @@ import { StageDumper } from "../core/stage/StageDumper"; import StartFilePanel from "./_start_file_panel"; import { PathString } from "../utils/pathString"; import { useTranslation } from "react-i18next"; +import { Stage } from "../core/stage/Stage"; export default function App() { const [maxmized, setMaxmized] = React.useState(false); @@ -86,11 +87,16 @@ export default function App() { }; }, []); + /** + * 监控路径变化的地方 + */ React.useEffect(() => { - if (file === "Project Graph") { - getCurrentWindow().setTitle("Project Graph"); + if (file === Stage.Path.draftName) { + getCurrentWindow().setTitle(Stage.Path.draftName); + Stage.Path.setPathInEffect(Stage.Path.draftName) } else { getCurrentWindow().setTitle(`${filename} - Project Graph`); + Stage.Path.setPathInEffect(file); } }, [file]); @@ -105,7 +111,7 @@ export default function App() { }, [maxmized]); const handleClose = () => { - if (file === "Project Graph") { + if (file === Stage.Path.draftName) { dialog.show({ title: "真的要关闭吗?", content: "您现在的新建草稿没有保存,是否要关闭项目?", diff --git a/src/pages/_app_menu.tsx b/src/pages/_app_menu.tsx index 5ca9e8c1..a965e2f6 100644 --- a/src/pages/_app_menu.tsx +++ b/src/pages/_app_menu.tsx @@ -22,6 +22,7 @@ import { Folder, FolderCog, FolderOpen, + Scaling, } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { @@ -356,6 +357,12 @@ export default function AppMenu({ > {t("view.items.resetBySelect")} + } + onClick={() => Camera.resetScale()} + > + {t("view.items.resetScale")} + } title={t("more.title")}> { console.log(StageManager.getEntities()); console.log(StageManager.getEdges()); - console.log(file); // localStorage测试 // 尽量不要用这个,端口号一变就没了 localStorage.setItem("_test", "123"); diff --git a/src/pages/_recent_files_panel.tsx b/src/pages/_recent_files_panel.tsx index 4df98db2..9ab1e5d7 100644 --- a/src/pages/_recent_files_panel.tsx +++ b/src/pages/_recent_files_panel.tsx @@ -15,6 +15,7 @@ import { RecentFileManager } from "../core/RecentFileManager"; import { useDialog } from "../utils/dialog"; import { isDesktop } from "../utils/platform"; import { StageSaveManager } from "../core/stage/StageSaveManager"; +import { Stage } from "../core/stage/Stage"; export default function RecentFilesPanel() { const [recentFiles, setRecentFiles] = React.useState< @@ -50,7 +51,7 @@ export default function RecentFilesPanel() { const onClickFile = (file: RecentFileManager.RecentFile) => { return () => { - if (currentFile === "Project Graph") { + if (currentFile === Stage.Path.draftName) { dialog.show({ title: "真的要切换吗?", content: "您现在的新建草稿没有保存,是否要切换项目?", @@ -83,7 +84,7 @@ export default function RecentFilesPanel() { const checkoutFile = (file: RecentFileManager.RecentFile) => { try { const path = file.path; - setFile(decodeURIComponent(path)); + setFile(decodeURIComponent(path)); if (isDesktop && !path.endsWith(".json")) { dialog.show({ title: "请选择一个JSON文件", diff --git a/src/pages/_start_file_panel.tsx b/src/pages/_start_file_panel.tsx index f4fac4d2..6754f82f 100644 --- a/src/pages/_start_file_panel.tsx +++ b/src/pages/_start_file_panel.tsx @@ -156,7 +156,7 @@ export default function StartFilePanel({ open = false }: { open: boolean }) { }; const checkoutFile = (path: string) => { try { - setFile(decodeURIComponent(path)); + setFile(decodeURIComponent(path)); if (isDesktop && !path.endsWith(".json")) { dialog.show({ title: "请选择一个JSON文件", diff --git a/src/pages/_toolbar.tsx b/src/pages/_toolbar.tsx index b5f3a8bc..ccf094c1 100644 --- a/src/pages/_toolbar.tsx +++ b/src/pages/_toolbar.tsx @@ -40,8 +40,6 @@ import { invoke } from "@tauri-apps/api/core"; import { ViewFlashEffect } from "../core/effect/concrete/ViewFlashEffect"; import { save as saveFileDialog } from "@tauri-apps/plugin-dialog"; import { StageSaveManager } from "../core/stage/StageSaveManager"; -import { useRecoilState } from "recoil"; -import { fileAtom } from "../state"; interface ToolbarItemProps { icon: React.ReactNode; // 定义 icon 的类型 @@ -222,7 +220,6 @@ function AlignNodePanel() { */ export default function Toolbar({ className = "" }: { className?: string }) { const popupDialog = usePopupDialog(); - const [file] = useRecoilState(fileAtom); const [isCopyClearShow, setIsCopyClearShow] = useState(false); useEffect(() => { @@ -312,8 +309,7 @@ export default function Toolbar({ className = "" }: { className?: string }) { openBrowserOrFile(); } else { // Stage.effects.push(new TextRiseEffect("请先保存文件")); - StageSaveManager.saveHandle( - file, + StageSaveManager.saveHandleWithoutCurrentPath( StageDumper.dump(), () => { openBrowserOrFile(); diff --git a/src/pages/test.tsx b/src/pages/test.tsx index efa2cda7..69a39cb2 100644 --- a/src/pages/test.tsx +++ b/src/pages/test.tsx @@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next"; import { XML } from "../utils/xml"; import { StageDumper } from "../core/stage/StageDumper"; import { fetch } from "@tauri-apps/plugin-http"; +import { invoke } from "@tauri-apps/api/core"; export default function TestPage() { const [switchValue, setSwitchValue] = React.useState(false); @@ -55,6 +56,16 @@ export default function TestPage() { + ); } + +function handleTestImageBase64() { + invoke("convert_image_to_base64", { + imagePath: "D:\\Projects\\Project-Tools\\project-graph\\src\\assets\\icon.png" + }).then((res) => { + console.log(res); + }); + +} diff --git a/src/types/node.tsx b/src/types/node.tsx index b479e991..7cc4d6c2 100644 --- a/src/types/node.tsx +++ b/src/types/node.tsx @@ -34,7 +34,11 @@ export namespace Serialized { export type ConnectPoint = Entity & { type: "core:connect_point"; } - + export type ImageNode = Entity & { + path: string; + size: Vector; + type: "core:image_node"; + } export type Edge = StageObject & { type: "core:edge"; source: string; @@ -44,7 +48,7 @@ export namespace Serialized { export type File = { version: 9; // 最新版本 src\core\stage\StageDumper.tsx latestVersion - nodes: (Node | Section | ConnectPoint)[]; + nodes: (Node | Section | ConnectPoint | ImageNode)[]; edges: Edge[]; }; } diff --git a/src/utils/pathString.tsx b/src/utils/pathString.tsx index 641c0677..f5b1f1cf 100644 --- a/src/utils/pathString.tsx +++ b/src/utils/pathString.tsx @@ -1,8 +1,15 @@ import { family } from "@tauri-apps/plugin-os"; export namespace PathString { + /** + * 将绝对路径转换为文件名 + * @param path + * @returns + */ export function absolute2file(path: string): string { const fam = family(); + // const fam = "windows"; // vitest 测试时打开此行注释 + if (fam === "windows") { path = path.replace(/\\/g, "/"); } @@ -17,4 +24,35 @@ export namespace PathString { return file; } } + + /** + * 根据文件的绝对路径,获取当前文件所在目录的路径 + * @param path 必须是一个文件的路径,不能是文件夹的路径 + * @returns + */ + export function dirPath(path: string): string { + const fam = family(); + // const fam = "windows"; // vitest 测试时打开此行注释 + + if (fam === "windows") { + path = path.replace(/\\/g, "/"); // 将反斜杠替换为正斜杠 + } + + const file = path.split("/").pop(); // 获取文件名 + if (!file) { + throw new Error("Invalid path"); + } + + let directory = path.substring(0, path.length - file.length); // 获取目录路径 + if (directory.endsWith("/")) { + directory = directory.slice(0, -1); // 如果目录路径以斜杠结尾,去掉最后的斜杠 + } + + if (fam === "windows") { + // 再换回反斜杠 + return directory.replace(/\//g, "\\"); + } + + return directory; // 返回目录路径 + } } diff --git a/vitest.config.ts b/vitest.config.ts index 5d6aac04..6c06d3d0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { // 配置测试环境 - environment: 'node', // 环境, node 或 jsdom + environment: 'jsdom', // 环境, node 或 jsdom // 全局设置 globals: true, include: ["src/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],