feat(组件): 添加虚拟滚动优化应用网格性能

为AppGrid组件添加vue-virtual-scroller实现虚拟滚动功能,当应用数量超过50个时自动启用
更新package.json添加vue-virtual-scroller依赖
添加vue-virtual-scroller的类型声明
优化网格布局响应式处理,根据窗口宽度动态调整列数
This commit is contained in:
2026-03-29 19:43:53 +08:00
parent f382e6d75d
commit 0dedd0faf0
4 changed files with 192 additions and 29 deletions

73
package-lock.json generated
View File

@@ -1,18 +1,19 @@
{ {
"name": "spark-store", "name": "spark-store",
"version": "4.9.9alpha4", "version": "5.0.0beta1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "spark-store", "name": "spark-store",
"version": "4.9.9alpha4", "version": "5.0.0beta1",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"axios": "^1.13.2", "axios": "^1.13.2",
"pino": "^10.3.0", "pino": "^10.3.0",
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18",
"vue-virtual-scroller": "^2.0.0-beta.8"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "^1.51.4", "@dotenvx/dotenvx": "^1.51.4",
@@ -114,7 +115,6 @@
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -123,7 +123,6 @@
"version": "7.28.5", "version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -132,7 +131,6 @@
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
"dev": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.29.0" "@babel/types": "^7.29.0"
}, },
@@ -156,7 +154,6 @@
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true,
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5" "@babel/helper-validator-identifier": "^7.28.5"
@@ -3089,7 +3086,6 @@
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
"integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
"dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.0", "@babel/parser": "^7.29.0",
"@vue/shared": "3.5.30", "@vue/shared": "3.5.30",
@@ -3102,7 +3098,6 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"dev": true,
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
}, },
@@ -3113,14 +3108,12 @@
"node_modules/@vue/compiler-core/node_modules/estree-walker": { "node_modules/@vue/compiler-core/node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
"dev": true
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
"integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
"dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.5.30", "@vue/compiler-core": "3.5.30",
"@vue/shared": "3.5.30" "@vue/shared": "3.5.30"
@@ -3130,7 +3123,6 @@
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
"integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
"dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.0", "@babel/parser": "^7.29.0",
"@vue/compiler-core": "3.5.30", "@vue/compiler-core": "3.5.30",
@@ -3146,14 +3138,12 @@
"node_modules/@vue/compiler-sfc/node_modules/estree-walker": { "node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
"dev": true
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
"integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
"dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.30", "@vue/compiler-dom": "3.5.30",
"@vue/shared": "3.5.30" "@vue/shared": "3.5.30"
@@ -3178,7 +3168,6 @@
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
"integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
"dev": true,
"dependencies": { "dependencies": {
"@vue/shared": "3.5.30" "@vue/shared": "3.5.30"
} }
@@ -3187,7 +3176,6 @@
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
"integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
"dev": true,
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.30", "@vue/reactivity": "3.5.30",
"@vue/shared": "3.5.30" "@vue/shared": "3.5.30"
@@ -3197,7 +3185,6 @@
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
"integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
"dev": true,
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.30", "@vue/reactivity": "3.5.30",
"@vue/runtime-core": "3.5.30", "@vue/runtime-core": "3.5.30",
@@ -3209,7 +3196,6 @@
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
"integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
"dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.5.30", "@vue/compiler-ssr": "3.5.30",
"@vue/shared": "3.5.30" "@vue/shared": "3.5.30"
@@ -3221,8 +3207,7 @@
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
"integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="
"dev": true
}, },
"node_modules/@vue/test-utils": { "node_modules/@vue/test-utils": {
"version": "2.4.6", "version": "2.4.6",
@@ -4442,8 +4427,7 @@
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
"dev": true
}, },
"node_modules/data-urls": { "node_modules/data-urls": {
"version": "5.0.0", "version": "5.0.0",
@@ -7961,6 +7945,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/mitt": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz",
"integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==",
"license": "MIT"
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -10073,7 +10063,7 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "devOptional": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -11514,7 +11504,6 @@
"version": "3.5.30", "version": "3.5.30",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
"dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.30", "@vue/compiler-dom": "3.5.30",
"@vue/compiler-sfc": "3.5.30", "@vue/compiler-sfc": "3.5.30",
@@ -11561,6 +11550,24 @@
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
} }
}, },
"node_modules/vue-observe-visibility": {
"version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz",
"integrity": "sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-resize": {
"version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz",
"integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-tsc": { "node_modules/vue-tsc": {
"version": "3.2.5", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz",
@@ -11577,6 +11584,20 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/vue-virtual-scroller": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-beta.8.tgz",
"integrity": "sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==",
"license": "MIT",
"dependencies": {
"mitt": "^2.1.0",
"vue-observe-visibility": "^2.0.0-alpha.1",
"vue-resize": "^2.0.0-alpha.1"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/w3c-xmlserializer": { "node_modules/w3c-xmlserializer": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",

View File

@@ -75,6 +75,7 @@
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"axios": "^1.13.2", "axios": "^1.13.2",
"pino": "^10.3.0", "pino": "^10.3.0",
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18",
"vue-virtual-scroller": "^2.0.0-beta.8"
} }
} }

View File

@@ -16,8 +16,10 @@
试试其他关键词或检查拼写是否正确 试试其他关键词或检查拼写是否正确
</p> </p>
</div> </div>
<!-- 应用数量较少时使用普通网格 -->
<div <div
v-else-if="!loading" v-else-if="!loading && apps.length <= 50"
class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4" class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
> >
<AppCard <AppCard
@@ -28,6 +30,28 @@
@open-detail="$emit('open-detail', app)" @open-detail="$emit('open-detail', app)"
/> />
</div> </div>
<!-- 应用数量较多时使用虚拟滚动 -->
<RecycleScroller
v-else-if="!loading"
class="scroller"
:items="gridRows"
:item-size="itemHeight"
key-field="id"
v-slot="{ item }"
>
<div class="grid-row grid gap-x-4 gap-y-8" :class="gridColumnsClass">
<AppCard
v-for="app in item.apps"
:key="app.pkgname"
:app="app"
:show-origin="storeFilter === 'both'"
@open-detail="$emit('open-detail', app)"
/>
</div>
</RecycleScroller>
<!-- 加载骨架屏 -->
<div v-else class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> <div v-else class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<div <div
v-for="n in 8" v-for="n in 8"
@@ -53,10 +77,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from "vue";
import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import AppCard from "./AppCard.vue"; import AppCard from "./AppCard.vue";
import type { App } from "../global/typedefinition"; import type { App } from "../global/typedefinition";
defineProps<{ const props = defineProps<{
apps: App[]; apps: App[];
loading: boolean; loading: boolean;
storeFilter?: "spark" | "apm" | "both"; storeFilter?: "spark" | "apm" | "both";
@@ -65,4 +92,95 @@ defineProps<{
defineEmits<{ defineEmits<{
(e: "open-detail", app: App): void; (e: "open-detail", app: App): void;
}>(); }>();
// 当前列数
const columns = ref(4);
// 根据窗口宽度更新列数
const updateColumns = () => {
const width = window.innerWidth;
if (width >= 1536) columns.value = 4;
else if (width >= 1280) columns.value = 3;
else if (width >= 640) columns.value = 2;
else columns.value = 1;
};
onMounted(() => {
updateColumns();
window.addEventListener("resize", updateColumns);
});
onUnmounted(() => {
window.removeEventListener("resize", updateColumns);
});
// 网格列数类名
const gridColumnsClass = computed(() => {
const map: Record<number, string> = {
1: "grid-cols-1",
2: "grid-cols-2",
3: "grid-cols-3",
4: "grid-cols-4",
};
return map[columns.value] || "grid-cols-4";
});
// 每个应用卡片的高度(包括 gap
// 卡片实际高度约 96px + 垂直间距 32px (gap-y-8)
const itemHeight = 128;
// 将应用列表分组为行
const gridRows = computed(() => {
const rows: { id: number; apps: App[] }[] = [];
for (let i = 0; i < props.apps.length; i += columns.value) {
rows.push({
id: i,
apps: props.apps.slice(i, i + columns.value),
});
}
return rows;
});
</script> </script>
<style scoped>
.scroller {
height: calc(100vh - 140px); /* 调整高度 */
overflow-y: auto;
padding: 0; /* 移除内边距 */
margin: -24px -16px; /* 抵消父容器的 px-4 py-6 */
}
@media (min-width: 1024px) {
.scroller {
margin: -24px -40px; /* 抵消父容器的 lg:px-10 */
}
}
.grid-row {
padding: 12px 16px;
box-sizing: border-box;
min-height: 128px; /* 确保最小高度 */
}
/* 确保 RecycleScroller 的样式正确 */
:deep(.vue-recycle-scroller) {
position: relative;
}
:deep(.vue-recycle-scroller__item-wrapper) {
flex: 1;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
}
:deep(.vue-recycle-scroller__item-view) {
position: absolute;
top: 0;
left: 0;
width: 100%;
box-sizing: border-box;
}
</style>

23
src/vite-env.d.ts vendored
View File

@@ -21,3 +21,26 @@ declare interface IpcChannels {
} }
declare const __APP_VERSION__: string; declare const __APP_VERSION__: string;
// vue-virtual-scroller type declarations
declare module "vue-virtual-scroller" {
import { DefineComponent } from "vue";
export const RecycleScroller: DefineComponent<{
items: any[];
itemSize: number;
keyField?: string;
direction?: "vertical" | "horizontal";
buffer?: number;
}>;
export const DynamicScroller: DefineComponent<{
items: any[];
minItemSize: number;
keyField?: string;
direction?: "vertical" | "horizontal";
buffer?: number;
}>;
export const DynamicScrollerItem: DefineComponent<{}>;
}