diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..deed335 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env diff --git a/index.html b/index.html new file mode 100644 index 0000000..1e793d5 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ + Dart & Flutter 学习测验 (30题)

Dart & Flutter 学习测验

第 1 / 30 题 得分:0
/ 30 分
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..09907b3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2445 @@ +{ + "name": "flutter-quiz", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flutter-quiz", + "version": "1.0.0", + "dependencies": { + "@vitejs/plugin-vue": "^5.2.4", + "concurrently": "^9.2.4", + "express": "^4.19.2", + "vite": "^6.4.3", + "vue": "^3.5.40", + "vue-router": "^4.6.4" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..beb618d --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "flutter-quiz", + "version": "1.0.0", + "description": "Dart & Flutter 学习测验系统(可管理题目,支持分享)", + "main": "server.js", + "scripts": { + "dev": "concurrently -n server,web -c blue,green \"node server.js\" \"vite\"", + "build": "vite build", + "start": "node server.js" + }, + "dependencies": { + "@vitejs/plugin-vue": "^5.2.4", + "concurrently": "^9.2.4", + "express": "^4.19.2", + "vite": "^6.4.3", + "vue": "^3.5.40", + "vue-router": "^4.6.4" + }, + "allowScripts": { + "esbuild@0.25.12": true + } +} diff --git a/questions.json b/questions.json new file mode 100644 index 0000000..522cf40 --- /dev/null +++ b/questions.json @@ -0,0 +1,1666 @@ +{ + "sets": [ + { + "id": 1, + "name": "Dart & Flutter 学习测验", + "questions": [ + { + "id": 1, + "lv": 1, + "q": "Dart 中声明\"类型自动推断\"的变量,用哪个关键字?", + "opts": [ + "var", + "let", + "const", + "dynamic" + ], + "ans": 0, + "exp": "Dart 用 var 由编译器自动推断类型;let 是 JS 的;const 表示编译期常量。" + }, + { + "id": 2, + "lv": 1, + "q": "Dart 中表示 64 位整数的数据类型是?", + "opts": [ + "integer", + "int", + "number", + "long" + ], + "ans": 1, + "exp": "Dart 用 int 表示整数(64位),double 表示浮点数,两者父类是 num。" + }, + { + "id": 3, + "lv": 1, + "q": "以下哪个是 Dart 中正确的单行注释写法?", + "opts": [ + "# 注释", + "/* 注释 */", + "// 注释", + "<!-- 注释 -->" + ], + "ans": 2, + "exp": "Dart 单行注释用 //,多行用 /* */# 是 Python,#! 在 Dart 中只是 shebang 声明。" + }, + { + "id": 4, + "lv": 1, + "q": "Dart 程序的入口函数是?", + "opts": [ + "runApp()", + "main()", + "init()", + "start()" + ], + "ans": 1, + "exp": "入口是 main()。Flutter 中通常写成 void main() => runApp(MyApp());" + }, + { + "id": 5, + "lv": 1, + "q": "要在控制台输出文字,应调用哪个函数?", + "opts": [ + "console.log()", + "System.out.println()", + "echo", + "print()" + ], + "ans": 3, + "exp": "Dart 用 print() 输出到控制台,比如 print(\"Hello Dart\");" + }, + { + "id": 6, + "lv": 1, + "q": "int x = 10; 要打印出 x=10,以下哪种写法正确?", + "opts": [ + "print(\"x=\" + x);", + "print(\"x=$x\");", + "print('x=${x}');", + "以上都可以" + ], + "ans": 3, + "exp": "Dart 中 + 拼接字符串、$x 插值、${x} 表达式插值都合法,三种写法均能输出 x=10。" + }, + { + "id": 7, + "lv": 2, + "q": "以下哪个 不是 Dart 的循环写法?", + "opts": [ + "for (var i=0; i<5; i++)", + "while (ok)", + "foreach (x in list)", + "do { } while (ok)" + ], + "ans": 2, + "exp": "Dart 没有 foreach(x in list) 语法,应写 for (var x in list)list.forEach(...)。" + }, + { + "id": 8, + "lv": 2, + "q": "执行 for (var i=0; i<5; i++) print(i); 会输出?", + "opts": [ + "1 2 3 4 5", + "0 1 2 3 4", + "0 1 2 3 4 5", + "5 4 3 2 1" + ], + "ans": 1, + "exp": "i 从 0 开始,条件 i<5,所以输出 0 1 2 3 4。" + }, + { + "id": 9, + "lv": 2, + "q": "Dart 的 switch 语句中,每个 case 结束必须以什么结尾(否则编译报错)?", + "opts": [ + "end", + "return / break / throw", + "continue", + "逗号" + ], + "ans": 1, + "exp": "Dart 每个非空 case 必须以 break、return 或 throw 等结束,不能像 C 语言那样自动穿透。" + }, + { + "id": 10, + "lv": 2, + "q": "定义一个返回两数之和的函数,哪个写法正确?", + "opts": [ + "int add(int a,int b){ return a+b; }", + "add(int a,int b) => a+b;", + "void add(int a,int b){ return a+b; }", + "A 和 B 都可以" + ], + "ans": 3, + "exp": "A 是传统写法,B 是箭头函数简写(返回表达式),两者都合法;C 返回 void 却 return 值,编译错误。" + }, + { + "id": 11, + "lv": 2, + "q": "以下哪个是可选命名参数的正确声明?", + "opts": [ + "void f({String name}) {}", + "void f([String name]) {}", + "void f(String? name) {}", + "void f(String name=\"\") {}" + ], + "ans": 0, + "exp": "{} 包裹的是命名参数(调用用 f(name: x));[] 是可选位置参数;? 是空安全;最后一个是默认值写法但不属于命名参数。" + }, + { + "id": 12, + "lv": 2, + "q": "函数 void f({int a = 1}) {},以下调用哪个正确?", + "opts": [ + "f()", + "f(1)", + "f(a: 1)", + "A 和 C 都可以" + ], + "ans": 3, + "exp": "命名参数有默认值时可省略(A 合法);必须按名调用 f(a: 1)(C 合法);f(1) 用位置传参给命名参数会报错。" + }, + { + "id": 13, + "lv": 3, + "q": "Dart 中表示\"数组/列表\"的类型是?", + "opts": [ + "Array", + "ArrayList", + "List", + "Vector" + ], + "ans": 2, + "exp": "Dart 用 List(可增长数组),字面量写法 [1,2,3]。" + }, + { + "id": 14, + "lv": 3, + "q": "创建包含 1、2、3 的列表,正确的是?", + "opts": [ + "{1, 2, 3}", + "[1, 2, 3]", + "(1, 2, 3)", + "List(1,2,3)" + ], + "ans": 1, + "exp": "[] 是 List;{} 是 Set/Map;() 不是集合字面量。" + }, + { + "id": 15, + "lv": 3, + "q": "var list = [10, 20, 30]; print(list[1]); 输出?", + "opts": [ + "10", + "20", + "30", + "报错" + ], + "ans": 1, + "exp": "Dart 索引从 0 开始,list[1] 是第二个元素 20。" + }, + { + "id": 16, + "lv": 3, + "q": "对 List 每个元素做处理并返回一个新集合的方法是?", + "opts": [ + "forEach()", + "map()", + "where()", + "add()" + ], + "ans": 1, + "exp": "map() 对每个元素变换并返回 Iterable;forEach() 只遍历返回 void;where() 是过滤。" + }, + { + "id": 17, + "lv": 3, + "q": "var a=[1,2,3]; var b=[0, ...a]; 那么 b.length 是?", + "opts": [ + "3", + "4", + "7", + "报错" + ], + "ans": 1, + "exp": "...a 是扩展运算符,把 a 展开成 1,2,3,加上开头的 0,b = [0,1,2,3],长度 4。" + }, + { + "id": 18, + "lv": 3, + "q": "以下哪种集合类型保证元素不重复?", + "opts": [ + "List", + "Map", + "Set", + "Queue" + ], + "ans": 2, + "exp": "Set 存储唯一元素,重复添加会被忽略;Map 存储键值对,键也不重复。" + }, + { + "id": 19, + "lv": 4, + "q": "定义类 class Dog { String name; } 后,创建一个实例哪个正确?", + "opts": [ + "Dog d = Dog();", + "var d = new Dog;", + "Dog d = new Dog();", + "以上都可以" + ], + "ans": 3, + "exp": "Dart 2 之后 new 关键字可选,且省略括号的 Dog() 也合法,三种写法都能创建实例。" + }, + { + "id": 20, + "lv": 4, + "q": "子类继承父类使用哪个关键字?", + "opts": [ + "with", + "implements", + "extends", + "inherits" + ], + "ans": 2, + "exp": "继承用 extendsimplements 是实现接口;with 用于混入 Mixin。" + }, + { + "id": 21, + "lv": 4, + "q": "包含未实现方法、不能被直接实例化的类,用什么关键字声明?", + "opts": [ + "abstract", + "interface", + "base", + "pure" + ], + "ans": 0, + "exp": "abstract 抽象类可含未实现方法,不能直接 new,需子类实现后实例化。" + }, + { + "id": 22, + "lv": 4, + "q": "在构造函数/实例方法中,this 指向?", + "opts": [ + "当前实例", + "父类对象", + "类本身", + "静态成员" + ], + "ans": 0, + "exp": "this 指向当前实例对象,用于区分参数与成员变量,如 this.name = name。" + }, + { + "id": 23, + "lv": 4, + "q": "创建一个只能装 String 的 List,哪个写法正确?", + "opts": [ + "List<String> names = [];", + "List names = <String>[];", + "var names = <String>[];", + "以上都是" + ], + "ans": 3, + "exp": "Dart 泛型语法多样:类型标注式、泛型字面量 <String>[] 都合法,三种写法结果相同。" + }, + { + "id": 24, + "lv": 4, + "q": "抽象类 Animal 有抽象方法 void speak(),子类 Dog 必须?", + "opts": [ + "重写 speak() 提供实现", + "调用父类构造", + "删除 speak", + "什么也不用做" + ], + "ans": 0, + "exp": "子类必须用 @override 实现所有抽象方法,否则 Dog 也是抽象类无法实例化。" + }, + { + "id": 25, + "lv": 5, + "q": "Dart 空安全中,声明一个可能为 null 的字符串类型是?", + "opts": [ + "String", + "String?", + "String!", + "nullable String" + ], + "ans": 1, + "exp": "在类型后加 ? 表示可空:String? s = null;;不加 ? 的 String 保证非空。" + }, + { + "id": 26, + "lv": 5, + "q": "空值合并运算符 ?? 的作用是?", + "opts": [ + "若左边为 null 则返回右边", + "强制把 null 变为非空", + "判断对象是否相等", + "抛出空异常" + ], + "ans": 0, + "exp": "a ?? b:a 不为 null 用 a,否则用 b。常配合 ??= 设置默认值。" + }, + { + "id": 27, + "lv": 5, + "q": "使用 async/await 时,异步函数一般返回什么类型?", + "opts": [ + "Stream<T>", + "Future<T>", + "void", + "T" + ], + "ans": 1, + "exp": "async 函数返回 Future<T>,await 会挂起等待该 Future 完成后再继续执行。" + }, + { + "id": 28, + "lv": 5, + "q": "执行以下代码,输出顺序是?
Future.delayed(Duration(seconds:1), () => print(\"A\"));
print(\"B\");
", + "opts": [ + "A 后 B", + "B 后 A", + "同时输出", + "只输出 A" + ], + "ans": 1, + "exp": "Future.delayed 是异步任务,先执行同步的 print(\"B\"),1 秒后再输出 A。" + }, + { + "id": 29, + "lv": 6, + "q": "Flutter 中用于监听持续不断的数据流(如多次事件)的是?", + "opts": [ + "Future", + "Stream", + "List", + "Map" + ], + "ans": 1, + "exp": "Stream 可监听多次异步事件;Future 只代表一次性的结果。" + }, + { + "id": 30, + "lv": 6, + "q": "Flutter 中构建界面的基本单位是?", + "opts": [ + "Activity", + "View", + "Widget", + "Component" + ], + "ans": 2, + "exp": "Flutter 一切皆 Widget,UI 由 Widget 树组合而成(如 Text、Container、Column)。" + }, + { + "id": 31, + "lv": 6, + "q": "界面状态需要随数据变化而更新时,应使用哪类组件?", + "opts": [ + "StatelessWidget", + "StatefulWidget", + "Container", + "Scaffold" + ], + "ans": 1, + "exp": "有动态状态用 StatefulWidget(配合 State 里的 setState 刷新);无状态用 StatelessWidget。" + } + ] + }, + { + "id": 9, + "name": "Dart 开发实战", + "questions": [ + { + "id": 98, + "lv": 1, + "q": "开发注册页时,用户输入的年龄是字符串,需要转成 int 保存。已知 String ageText = \"18\";,正确的转换写法是?", + "opts": [ + "int(ageText)", + "toInt(ageText)", + "int.parse(ageText)", + "ageText.parseInt()" + ], + "ans": 2, + "exp": "int.parse() 把字符串解析成整数,解析失败会抛 FormatException,更稳妥的是 int.tryParse()(失败返回 null)。注意 Dart 没有 \"18\".parseInt() 这种写法,int(...) 也不是转换函数。" + }, + { + "id": 99, + "lv": 1, + "q": "项目里要定义一个\"永远不允许修改\"的 API 地址常量,以下哪种声明最合适?", + "opts": [ + "final String api = \"https://api.example.com\";", + "const String api = \"https://api.example.com\";", + "String api = \"https://api.example.com\";", + "var api = \"https://api.example.com\";" + ], + "ans": 1, + "exp": "const 是编译期常量:值在编译时就确定,且不可变,编译器可直接内联替换,还能配合 const 构造函数做实例复用。final 是运行期赋值一次后不可变。给字面量常量用 const 是更优选择。" + }, + { + "id": 100, + "lv": 1, + "q": "你在重构代码时写下:var x = 10; x = \"hello\"; 会发生什么?", + "opts": [ + "正常运行,x 变成字符串", + "编译报错:x 已被推断为 int 类型", + "运行时报错", + "x 自动变为 null" + ], + "ans": 1, + "exp": "var 声明时由初始值自动推断类型,推断后类型就固定了:x 是 int,再赋 String 在编译期直接报错。想存任意类型,应显式声明 dynamicObject?。" + }, + { + "id": 101, + "lv": 1, + "q": "给变量起名时,以下哪个不是合法的 Dart 标识符?", + "opts": [ + "_score", + "2x", + "中文名", + "name_2" + ], + "ans": 1, + "exp": "Dart 标识符不能以数字开头_score 合法(下划线开头表示库内私有,是 Dart 的约定俗成);Dart 支持 Unicode 标识符,中文变量名同样合法。" + }, + { + "id": 102, + "lv": 1, + "q": "结算功能计算人均消费:int total = 10; int people = 4; double avg = total / people; 打印 avg 得到?", + "opts": [ + "2", + "2.5", + "2.0", + "报错" + ], + "ans": 1, + "exp": "Dart 中 int 除以 int 的结果一定是 double:10 / 4 = 2.5。想得到整除结果用 ~/10 ~/ 4 == 2,取余用 %。" + }, + { + "id": 103, + "lv": 2, + "q": "写优惠逻辑:订单数 > 10 打 9 折,> 50 打 8 折。对 orders = 60,以下代码会得到什么折扣?
double d; if (orders > 10) { d = 0.9; } else if (orders > 50) { d = 0.8; }", + "opts": [ + "0.8", + "0.9", + "1.0", + "0.7" + ], + "ans": 1, + "exp": "if-else 链从上到下匹配,命中即止:60 > 10 先成立就直接取 0.9,后面的 else if 不再执行。正确写法应把范围大的条件放前面(先判断 > 50)。判断顺序是 if-else 链最经典的坑。" + }, + { + "id": 104, + "lv": 2, + "q": "接口返回 Map<String, dynamic> data,想遍历所有 key 打印出来,以下哪种写法正确?", + "opts": [ + "for (var k in data) print(k);", + "for (var k in data.keys) print(k);", + "data.keys.forEach(print);", + "B 和 C 都可以" + ], + "ans": 3, + "exp": "Map 本身不能直接 for-in 遍历,要先取 keys / values / entries。B 是标准写法;C 中 forEach(print) 利用了函数引用,同样正确。两种都行,按团队规范选择。" + }, + { + "id": 105, + "lv": 2, + "q": "写一个返回用户名的函数:String getUserName() { },忘了写 return,会怎样?", + "opts": [ + "返回 null", + "编译报错", + "运行时崩溃", + "自动返回空字符串" + ], + "ans": 1, + "exp": "空安全下,声明了非空返回类型 String 的函数必须有 return,漏写直接编译报错,把隐患消灭在编译期。如果确实允许无返回值,应把返回类型声明为 String?。" + }, + { + "id": 106, + "lv": 2, + "q": "问候函数:void greet(String name, {String greeting = \"你好\"}),调用 greet(\"小明\", \"早上好\") 会发生什么?", + "opts": [ + "正常执行,greeting 为 \"早上好\"", + "编译报错:命名参数不能按位置传参", + "greeting 变为 null", + "编译警告但能运行" + ], + "ans": 1, + "exp": "{} 包裹的是命名参数,必须写成 greet(\"小明\", greeting: \"早上好\")[] 才是可选位置参数。命名参数让调用意图清晰,是团队协作中常用的做法。" + }, + { + "id": 107, + "lv": 2, + "q": "写\"重试发送验证码\"逻辑:要求至少发送一次,失败则继续重试。哪种循环语义最贴切?", + "opts": [ + "while", + "do-while", + "for", + "repeat" + ], + "ans": 1, + "exp": "do-while 先执行循环体再判断条件,保证至少执行一次,正是\"先发一次、不行再重试\"的语义。while 先判断后执行,可能一次都不执行。" + }, + { + "id": 108, + "lv": 3, + "q": "实现\"删除待办\"功能,list.remove(3)list.removeAt(3) 的区别是?", + "opts": [ + "remove 按值删除,removeAt 按下标删除", + "两者完全相同", + "remove 删下标,removeAt 按值删除", + "remove 会删除所有等于 3 的元素" + ], + "ans": 0, + "exp": "remove(value) 删除第一个值相等的元素并返回是否成功;removeAt(index) 按下标删除并返回被删的元素,越界抛 RangeError。写代码前想清楚你要删的是\"值\"还是\"位置\"。" + }, + { + "id": 109, + "lv": 3, + "q": "解析接口数据:用 data[\"name\"] 取一个不存在的 key 时,返回什么?", + "opts": [ + "抛异常", + "null", + "空字符串", + "抛出 NoSuchMethodError" + ], + "ans": 1, + "exp": "Map 用 [] 取值时,key 不存在返回 null 而不是抛异常。所以 data[\"name\"] 可能为 null,取值后判空再使用是常规操作(配合 ?? 兜底)。" + }, + { + "id": 110, + "lv": 3, + "q": "配置列表按条件插入元素:var l = [1, 2, if (isVip) 3];isVip 为 false 时 l 是?", + "opts": [ + "[1, 2]", + "[1, 2, 3]", + "[1, 2, false]", + "编译错误" + ], + "ans": 0, + "exp": "这是 collection-if 语法:条件不成立时该元素直接不生成。Flutter 里常用于条件渲染组件列表,配合 ... 展开和 collection-for 一起使用。" + }, + { + "id": 111, + "lv": 3, + "q": "两个用户各自关注了一批标签,要找出共同关注的标签,用哪种写法?", + "opts": [ + "a & b", + "a.intersect(b)", + "a.toSet().intersection(b.toSet())", + "a.union(b)" + ], + "ans": 2, + "exp": "List 没有交集方法,要先转 Set。intersection() 返回两个集合的共同元素;union() 是并集。注意方法名是 intersection(),不是其它语言的 intersect()。" + }, + { + "id": 112, + "lv": 3, + "q": "计算购物车总价:List<double> prices = []; 空列表调用 prices.reduce((a, b) => a + b) 会?", + "opts": [ + "返回 0", + "抛出 StateError 异常", + "返回 null", + "返回空列表" + ], + "ans": 1, + "exp": "reduce 要求列表至少有一个元素,空列表直接抛 \"Bad state: No element\"。更稳的写法是 fold(0, (a, b) => a + b):带初始值,空列表返回 0。" + }, + { + "id": 113, + "lv": 4, + "q": "写日期工具类,希望 DateUtil.parse(\"2024-01-01\") 在字符串不合法时返回 null 而不是抛异常,用哪种构造函数?", + "opts": [ + "factory 构造函数", + "普通构造函数", + "命名构造函数", + "const 构造函数" + ], + "ans": 0, + "exp": "factory 构造函数可以不返回新实例——可以返回缓存实例、子类实例,甚至返回 null。它把\"按需创建/校验\"的逻辑封装在构造入口,调用方写法统一且安全。" + }, + { + "id": 114, + "lv": 4, + "q": "日志打印、权限检查这类能力要被多个互不相关的类复用,又不想强制继承关系,Dart 的推荐做法是?", + "opts": [ + "extends 继承", + "implements 实现", + "with 混入 Mixin", + "abstract 抽象类" + ], + "ans": 2, + "exp": "Mixinwith)把可复用逻辑抽出来按需混入,避免\"为了复用而强行继承\"的长继承链。Flutter 源码大量使用,如 class _X with RouteAware。" + }, + { + "id": 115, + "lv": 4, + "q": "类内部要声明一个\"仅供本文件使用\"的私有字段,正确写法是?", + "opts": [ + "private String _token;", + "String _token;", + "-String token;", + "#token" + ], + "ans": 1, + "exp": "Dart 没有 private 关键字,以下划线开头表示库内私有:_token。注意这是\"库\"级别私有(同文件可见),不是类级别。" + }, + { + "id": 116, + "lv": 4, + "q": "用户类的 name 字段希望赋值时自动去掉首尾空格,最佳做法是?", + "opts": [ + "声明 getter/setter,在 setter 里 trim", + "直接写公共字段,调用方自己 trim", + "用 final 禁止修改", + "用 static 修饰" + ], + "ans": 0, + "exp": "setter 在赋值入口统一处理,所有调用方(包括构造函数)都自动享受规范化,不用每个地方都记得 trim。写法:String get nameset name(String v)。" + }, + { + "id": 117, + "lv": 4, + "q": "class Dog implements Animalextends 继承的关键区别是?", + "opts": [ + "implements 要求实现 Animal 的全部成员(含字段),extends 可复用已有实现", + "两者完全一样", + "implements 只能用于抽象类", + "extends 要求实现全部成员" + ], + "ans": 0, + "exp": "implements 把 Animal 当接口:所有成员都要自己重写,即使父类已有实现;extends 继承实现,只覆写需要改的部分。implements 用于\"规定能力\",extends 用于\"复用实现\"。" + }, + { + "id": 118, + "lv": 5, + "q": "用户对象可能为 null:User? u = await fetchUser();,以下哪种使用方式更安全?", + "opts": [ + "if (u != null) { print(u.name); } 判空后直接用 u", + "if (u != null) { print(u!.name); } 必须加 !", + "print(u?.name ?? \"\");", + "A 和 C 都可以" + ], + "ans": 3, + "exp": "A:局部变量判空后编译器会自动提升为非空类型,直接使用即可;C?. 安全调用 + ?? 兜底,一行搞定。只有被闭包捕获的变量才需要 !。" + }, + { + "id": 119, + "lv": 5, + "q": "详情页要同时请求\"用户信息\"和\"订单列表\"两个接口,都拿到再渲染,用哪个方法?", + "opts": [ + "Future.wait([fetchUser(), fetchOrders()])", + "串行写两个 await", + "Future.any", + "Future.delayed 等待" + ], + "ans": 0, + "exp": "Future.wait 并发启动多个 Future,全部完成后统一返回结果列表,总耗时约等于最慢的那个;串行 await 则是两个请求耗时相加。Future.any 只等第一个完成的。" + }, + { + "id": 120, + "lv": 5, + "q": "某个字段要等登录成功后才能赋值,但又想声明成非空类型,正确声明方式是?", + "opts": [ + "late String token;", + "String? token = \"\";", + "dynamic token;", + "final token = null;" + ], + "ans": 0, + "exp": "late 表示\"推迟初始化,首次访问时才赋值\",适合依赖异步结果的字段或计算代价高的属性。注意:late 变量在赋值前被访问会抛 LateInitializationError。" + }, + { + "id": 121, + "lv": 5, + "q": "把解析结果转成目标类型:var age = data[\"age\"] as int;,当 age 实际是 String 时会发生?", + "opts": [ + "返回 null", + "抛出 TypeError(类型转换失败)", + "静默忽略", + "自动转成 int" + ], + "ans": 1, + "exp": "as强制类型转换,类型不符立即抛异常。安全做法:先用 is 判断类型,或先统一规范数据(如 num 一律 .toInt())。" + }, + { + "id": 122, + "lv": 5, + "q": "对可能为 null 的 String? s 使用 s!,若 s 确实为 null 会?", + "opts": [ + "返回 null", + "抛出 TypeError(空检查失败)", + "静默继续", + "编译错误" + ], + "ans": 1, + "exp": "!强制解包运算符,向编译器承诺\"这里不可能为 null\",承诺落空时运行时抛 TypeError。能用判空提升或 ?? 的地方尽量别用 !,减少运行时崩溃点。" + }, + { + "id": 123, + "lv": 6, + "q": "Flutter 中常见 ButtonStyle()..backgroundColor = ... ..shape = ... 写法,.. 级联操作符的核心行为是?", + "opts": [ + "对同一对象连续操作且返回对象本身", + "链式调用并返回最后一次结果", + "只能用于 List", + "与 ... 展开等价" + ], + "ans": 0, + "exp": "级联操作符 .. 对同一对象连续执行多个操作,不改变返回值(始终是原对象),适合集中配置对象。. 返回的是方法调用的返回值,两者行为不同。" + }, + { + "id": 124, + "lv": 6, + "q": "Dart 3 中,函数想直接返回\"用户名 + 年龄\"这组小数据,又不想专门建一个类,推荐用?", + "opts": [ + "Map<String, dynamic>", + "Record 记录类型:(String name, int age)", + "List<dynamic>", + "用全局变量" + ], + "ans": 1, + "exp": "Record 是 Dart 3 的轻量聚合类型:var r = (\"张三\", 18); 或命名字段 (name: \"张三\", age: 18),用 r.$1 / r.name 取值,还能配合模式匹配解构,比 Map 更类型安全。" + }, + { + "id": 125, + "lv": 6, + "q": "Dart 3 的 switch 可以直接当表达式赋值:var s = switch (status) { \"ok\" => \"成功\", _ => \"未知\" }; 这种写法?", + "opts": [ + "合法:switch 表达式返回一个值", + "非法:switch 不能作表达式", + "必须每个分支加 break", + "只能匹配字符串" + ], + "ans": 0, + "exp": "switch 表达式=> 代替冒号,整体作为值赋给变量,配合模式匹配可以解构、判断类型,比旧式 switch 简洁,也天然避免了漏 break 的坑。" + }, + { + "id": 126, + "lv": 6, + "q": "想给系统类 String 加一个 isEmail() 方法,但 String 不能改源码,用?", + "opts": [ + "extension 扩展:extension EmailCheck on String { bool isEmail() {...} }", + "继承 String", + "用 mixin 混入", + "写静态工具类" + ], + "ans": 0, + "exp": "extension 可以不修改原类给已有类型添加方法,之后 \"a@b.com\".isEmail() 直接调用。注意扩展方法是静态分发(编译期决定),不参与运行时多态。" + }, + { + "id": 127, + "lv": 6, + "q": "泛型容器 class Box<T extends num> {}extends num 的含义是?", + "opts": [ + "T 只能是 int、double 等数值类型", + "T 可以是任意类型", + "T 必须可空", + "T 必须是集合类型" + ], + "ans": 0, + "exp": "泛型约束限定 T 必须是 num 及其子类(int/double),这样 Box 内部就能安全调用数值运算。不加约束时 T 等价于 Object?。" + } + ], + "lvNames": [ + "基础语法", + "控制流与函数", + "集合", + "面向对象", + "空安全与异步", + "Dart 高级特性" + ] + }, + { + "id": 10, + "name": "Flutter 开发实战", + "questions": [ + { + "id": 128, + "lv": 1, + "q": "新建 Flutter 项目后,把应用真正跑起来的入口代码是?", + "opts": [ + "void main() => runApp(MyApp());", + "void main() { MyApp(); }", + "runApp() { MyApp }", + "main() { MaterialApp(); }" + ], + "ans": 0, + "exp": "Dart 程序入口是 main(),Flutter 通过 runApp(Widget) 把根组件挂载到引擎上。注意 MyApp() 是创建 Widget 实例,不能少了括号。" + }, + { + "id": 129, + "lv": 1, + "q": "实现一个\"3 秒倒计时\"按钮,每秒刷新显示剩余秒数,应使用?", + "opts": [ + "StatelessWidget", + "StatefulWidget", + "Container", + "Scaffold" + ], + "ans": 1, + "exp": "有动态变化的数据(倒计时数字)就需要 StatefulWidget:数据保存在 State 里,变化时调 setState() 刷新。没有可变状态时应优先用 StatelessWidget。" + }, + { + "id": 130, + "lv": 1, + "q": "界面上要显示一行固定文案\"登录成功\",最合适的组件是?", + "opts": [ + "Text", + "Label", + "Paragraph", + "RichText" + ], + "ans": 0, + "exp": "Text 是最常用的文本组件,可通过 style 配置字号、颜色、字重等。RichText 用于一段文字中混排多种样式的富文本场景。" + }, + { + "id": 131, + "lv": 1, + "q": "点击\"确认\"按钮后分数要 +1 并立即显示到界面,正确流程是?", + "opts": [ + "修改 State 字段后调用 setState()", + "直接修改字段就行", + "重启整个 App", + "修改 Widget 的构造参数" + ], + "ans": 0, + "exp": "State 里的字段只是普通数据,必须调用 setState() 通知框架\"数据变了\",框架才会重新执行 build 刷新 UI。注意 setState 只重建当前 State 的子树,不是整棵树。" + }, + { + "id": 132, + "lv": 1, + "q": "要展示 10000 条商品记录,性能最优的做法是?", + "opts": [ + "ListView.builder(按需构建可见项)", + "Column 里循环放 10000 个组件", + "SingleChildScrollView 包 Column", + "GridView.count 全部构建" + ], + "ans": 0, + "exp": "ListView.builder 只构建当前可见的列表项(懒加载),滚动时回收复用;Column + for 会一次性构建全部组件,万条数据直接卡死。数据量大优先 builder 系列。" + }, + { + "id": 133, + "lv": 2, + "q": "登录页要垂直排列\"输入框、输入框、登录按钮\",用哪个布局组件?", + "opts": [ + "Column", + "Row", + "Stack", + "Wrap" + ], + "ans": 0, + "exp": "Column 垂直方向排列子组件,Row 水平方向,Stack 叠加并支持定位,Wrap 在空间不足时自动换行。" + }, + { + "id": 134, + "lv": 2, + "q": "Row 里想让一个按钮占满剩余宽度(剩余空间全给它),按钮外层套?", + "opts": [ + "Expanded", + "Padding", + "Center", + "Spacer" + ], + "ans": 0, + "exp": "Expanded 让子组件占据主轴的剩余空间(Row 的主轴是水平方向)。多个 Expanded 可通过 flex 参数按比例分配空间。" + }, + { + "id": 135, + "lv": 2, + "q": "Column 里有几个按钮,想让它们在水平方向居中,应该设置?", + "opts": [ + "crossAxisAlignment: CrossAxisAlignment.center", + "mainAxisAlignment: MainAxisAlignment.center", + "alignment: Alignment.center", + "textAlign: TextAlign.center" + ], + "ans": 0, + "exp": "Column 的主轴是垂直方向、交叉轴是水平方向。水平居中=沿交叉轴居中=crossAxisAlignment.center;mainAxisAlignment 控制的是垂直方向。主/交叉轴搞反是新手最常踩的坑。" + }, + { + "id": 136, + "lv": 2, + "q": "封面图要铺满整个区域且不拉伸变形(多出的部分裁掉),fit 参数应该用?", + "opts": [ + "BoxFit.cover", + "BoxFit.fill", + "BoxFit.none", + "BoxFit.scaleDown" + ], + "ans": 0, + "exp": "cover:等比缩放填满、超出裁剪,常用于头像、封面;fill 会拉伸变形;contain 完整显示但可能留白;none 不缩放。" + }, + { + "id": 137, + "lv": 2, + "q": "商品名过长时希望单行显示,超出部分显示省略号,Text 的配置是?", + "opts": [ + "maxLines: 1, overflow: TextOverflow.ellipsis", + "只设 overflow: TextOverflow.clip", + "只设 maxLines: 3", + "softWrap: false 就够了" + ], + "ans": 0, + "exp": "maxLines 限制最大行数 + overflow: TextOverflow.ellipsis 指定超出的表现(省略号/裁剪/fade),两者配合才能出现\"…\"效果。" + }, + { + "id": 138, + "lv": 3, + "q": "输入框需要\"预填默认值 + 按钮点击后读取内容 + 随时清空\",推荐用?", + "opts": [ + "TextEditingController", + "每次读 onChanged 参数", + "GlobalKey 临时查找", + "无法实现" + ], + "ans": 0, + "exp": "TextEditingController 是输入框的\"数据源\":TextEditingController(text: \"预填\") 初始化,controller.text 读取,controller.clear() 清空。用完后在 dispose 里释放。" + }, + { + "id": 139, + "lv": 3, + "q": "页面销毁时,取消监听、释放 Controller 的代码应该写在哪里?", + "opts": [ + "dispose()", + "initState()", + "build()", + "didChangeDependencies()" + ], + "ans": 0, + "exp": "dispose() 是 State 被移除时的清理钩子:取消 Timer、Stream 订阅、释放 Controller。不清理会造成内存泄漏和 \"setState called after dispose\" 报错。" + }, + { + "id": 140, + "lv": 3, + "q": "登录状态、用户信息要被多个页面共享,又不想层层传构造参数,项目常用方案是?", + "opts": [ + "Provider / InheritedWidget", + "定义全局变量直接读写", + "每个页面重新请求接口", + "把状态塞进路由参数" + ], + "ans": 0, + "exp": "Flutter 官方的底层方案是 InheritedWidgetProvider 是对它的优雅封装:共享数据放顶层,子组件 context.watch() 取用并自动刷新。全局变量没有通知机制,页面不会自动更新。" + }, + { + "id": 141, + "lv": 3, + "q": "商品列表页要支持下拉刷新,应该把 ListView 包进哪个组件?", + "opts": [ + "RefreshIndicator", + "GestureDetector", + "AnimatedList", + "Scrollbar" + ], + "ans": 0, + "exp": "RefreshIndicator 包住可滚动组件,配合 onRefresh 返回的 Future 实现下拉刷新动画。这是 Material 规范自带的下拉刷新方案。" + }, + { + "id": 142, + "lv": 3, + "q": "App 切到后台、再回到前台时,需要暂停/恢复计时器,通过什么监听?", + "opts": [ + "WidgetsBindingObserver", + "RouteObserver", + "NavigatorObserver", + "MediaQuery" + ], + "ans": 0, + "exp": "给 State 混入 with WidgetsBindingObserver,在 didChangeAppLifecycleState() 中监听 AppLifecycleState(paused / resumed / inactive),即可感知前后台切换。" + }, + { + "id": 143, + "lv": 4, + "q": "列表页点击商品跳转详情页,标准写法是?", + "opts": [ + "Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(id: id)))", + "Navigator.go(DetailPage(id: id))", + "Route.open(DetailPage())", + "DetailPage(id: id).show()" + ], + "ans": 0, + "exp": "Navigator.push 把新路由压入导航栈,MaterialPageRoute 提供转场动画,返回上一页用 Navigator.pop(context)。" + }, + { + "id": 144, + "lv": 4, + "q": "详情页编辑完要把结果回传给列表页,详情页返回时应该写?", + "opts": [ + "Navigator.pop(context, result)", + "Navigator.back(result)", + "Navigator.pushResult(result)", + "context.popAndPush(result)" + ], + "ans": 0, + "exp": "pop(context, 返回值) 的第二个参数就是回传的数据,列表页 await push(...) 的返回值即可收到。注意 pop 的参数是\"返回给上一页的数据\",不是新路由。" + }, + { + "id": 145, + "lv": 4, + "q": "跳转时需要传递商品 id、名称等参数,团队协作中最推荐的传参方式是?", + "opts": [ + "DetailPage(id: 1, name: \"商品\") 构造参数传递", + "全局变量临时存", + "static 静态字段存", + "URL 字符串拼接" + ], + "ans": 0, + "exp": "构造参数显式、类型安全、可读性好——跳转处一眼看到传了什么。全局变量/静态字段是隐藏依赖,页面多起来后非常难维护。" + }, + { + "id": 146, + "lv": 4, + "q": "在 App 里注册命名路由(如 /login/home),配置位置是?", + "opts": [ + "MaterialApp(routes: {...})", + "main() 函数里", + "runApp() 参数", + "每个页面里各自声明" + ], + "ans": 0, + "exp": "命名路由集中配置在 MaterialApproutes 表,之后用 Navigator.pushNamed(context, \"/login\") 跳转;需要传参时配合 arguments 参数。" + }, + { + "id": 147, + "lv": 4, + "q": "跳到选择页,用户选完返回后要根据选择结果继续处理,列表页怎么写?", + "opts": [ + "final r = await Navigator.push(...); 拿到返回值再处理", + "无法等待结果", + "用 setState 轮询", + "跳转时把回调函数传过去" + ], + "ans": 0, + "exp": "push 返回 Future<T>,await 后拿到 pop 回传的值,再执行后续逻辑(如刷新选中项)。这是\"页面间取结果\"的标准姿势。" + }, + { + "id": 148, + "lv": 5, + "q": "接口返回 JSON 字符串,执行 var data = jsonDecode(text); 后 data 的类型是?", + "opts": [ + "dynamic(实际可能是 Map 或 List)", + "String", + "Map<String, dynamic>", + "Object" + ], + "ans": 0, + "exp": "jsonDecode 的返回类型是 dynamic:JSON 对象变 Map<String, dynamic>、数组变 List<dynamic>、数字/布尔原样返回。通常需要手动转换:jsonDecode(text) as Map<String, dynamic>。" + }, + { + "id": 149, + "lv": 5, + "q": "要把用户对象 User(id: 1, name: \"张三\") 转成 JSON 字符串发给服务器,正确流程是?", + "opts": [ + "给 User 写 toJson(),再 jsonEncode(user.toJson())", + "jsonEncode(user) 直接序列化", + "user.toString()", + "JSON.stringify(user)" + ], + "ans": 0, + "exp": "Dart 默认不会自动序列化任意对象,惯例是模型类自己实现 Map<String, dynamic> toJson(),再用 jsonEncode 转字符串;反序列化则对应 User.fromJson(Map) 工厂构造。" + }, + { + "id": 150, + "lv": 5, + "q": "网络请求可能超时/断网,用户点\"登录\"时抛出了异常,正确的处理是?", + "opts": [ + "用 try-catch 包裹 await 请求,捕获异常并提示用户", + "不做处理", + "打印日志后忽略", + "延迟 3 秒自动重试" + ], + "ans": 0, + "exp": "try { await api.login(...) } catch (e) { 提示\"网络异常,请重试\" } 是最基础的错误处理;生产环境还会区分 SocketException(断网)、TimeoutException(超时)等做分类提示。" + }, + { + "id": 151, + "lv": 5, + "q": "请求进行中需要展示\"加载中\"状态,最合适用?", + "opts": [ + "CircularProgressIndicator", + "Text(\"加载中...\")", + "Opacity 半透明遮罩", + "AnimatedContainer 闪烁" + ], + "ans": 0, + "exp": "Material 标准的加载指示器是 CircularProgressIndicator(转圈)和 LinearProgressIndicator(条),配合 loading 状态变量做条件渲染。" + }, + { + "id": 152, + "lv": 5, + "q": "http 请求要设置 10 秒超时,正确写法是?", + "opts": [ + "await http.get(url).timeout(Duration(seconds: 10))", + "http.get(url, timeout: 10)", + "Timer(Duration(seconds: 10), ...)", + "Future.any 自动超时" + ], + "ans": 0, + "exp": "Future 自带 .timeout():超时抛 TimeoutException,可再接 onTimeout 参数兜底。http 包本身没有 timeout 参数,这是常见的误解。" + }, + { + "id": 153, + "lv": 6, + "q": "首页同时请求\"轮播图、公告、商品列表\"三个接口,全部完成再渲染页面,用?", + "opts": [ + "Future.wait([...]) 并发等待", + "三个 await 串行", + "依次 catch 分别显示", + "用 Future.any" + ], + "ans": 0, + "exp": "并发场景用 Future.wait,总耗时约等于最慢的请求而不是三个相加。生产上可配合记录单个请求失败做局部降级(某块失败不影响整页)。" + }, + { + "id": 154, + "lv": 6, + "q": "商品列表滑到底部要自动加载下一页,监听滚动的方案是?", + "opts": [ + "ScrollController 监听 pixels 接近 maxScrollExtent 时触发", + "用 Timer 每 3 秒加载", + "下拉刷新代替", + "GestureDetector 监听手势" + ], + "ans": 0, + "exp": "给 ListView 挂 ScrollController,在 addListener 里判断 position.pixels >= position.maxScrollExtent - 200(预留阈值)就加载下一页,配合 loading 状态防止重复触发。" + }, + { + "id": 155, + "lv": 6, + "q": "商品标题中要红色高亮\"热卖\"二字、其余部分灰色,用?", + "opts": [ + "RichText + TextSpan 组合不同样式", + "拆成多个 Text 排一排", + "用换行符拼接字符串", + "TextStyle 做不到,只能整段同色" + ], + "ans": 0, + "exp": "RichTextTextSpan 支持树状嵌套:每个 span 可以有自己的 style,是\"一段文字多种样式\"的标准方案。" + }, + { + "id": 156, + "lv": 6, + "q": "界面出现黄黑条纹溢出警告(RenderFlex overflowed),说明?", + "opts": [ + "子组件超出了父组件给定的约束空间", + "主题颜色配置错误", + "内存泄漏", + "字体加载失败" + ], + "ans": 0, + "exp": "Row/Column 空间不够时子组件溢出,debug 模式显示黄黑条纹并打印 \"RenderFlex overflowed by XX pixels\"。解决思路:Expanded/Flexible 让出空间、SingleChildScrollView 滚动、FittedBox 缩放。" + }, + { + "id": 157, + "lv": 6, + "q": "瀑布流商品图列表很卡、图片解码占内存高,优先优化手段是?", + "opts": [ + "cacheWidth / ResizeImage 限制解码尺寸", + "给图片加圆角", + "提高图片的清晰度", + "减少列表项数量" + ], + "ans": 0, + "exp": "Image.network 默认按原图尺寸解码,一张 4000px 大图在手机上就是内存杀手。用 cacheWidth: 300ResizeImage 让引擎按需解码成小尺寸位图,内存占用和滚动性能立竿见影。" + } + ], + "lvNames": [ + "Widget 基础", + "布局与组件", + "状态管理", + "路由与导航", + "数据与网络", + "实战综合" + ] + }, + { + "id": 11, + "name": "Dart 基础巩固", + "lvNames": [ + "基础语法", + "控制流与函数", + "集合", + "面向对象", + "空安全与异步", + "Dart 高级特性" + ], + "questions": [ + { + "id": 158, + "lv": 1, + "q": "Dart 中布尔类型 bool 的字面量写法是?", + "opts": [ + "TRUE", + "true", + "1", + "yes" + ], + "ans": 1, + "exp": "bool 只有两个值:truefalse(全小写)。TRUE、1、yes 都不是 Dart 的合法写法。" + }, + { + "id": 159, + "lv": 1, + "q": "要存储 3.14 这样的浮点数,应声明为什么类型?", + "opts": [ + "float", + "decimal", + "big", + "double" + ], + "ans": 3, + "exp": "浮点数用 double,整数用 int,两者的共同父类是 num。Dart 没有 float、decimal 类型。" + }, + { + "id": 160, + "lv": 1, + "q": "定义一个内容跨多行的字符串,应该用哪组引号包裹?", + "opts": [ + "''' 三个单引号(或 \"\"\")", + "\" 单个双引号", + "' 单个单引号", + "` 反引号" + ], + "ans": 0, + "exp": "三个单引号 ''' 或三个双引号 \"\"\" 包裹的字符串可以直接跨行书写,换行会被保留;单引号/双引号字符串内不能直接换行。Dart 没有反引号模板字符串。" + }, + { + "id": 161, + "lv": 1, + "q": "关于 Dart 字符串的特性,以下说法正确的是?", + "opts": [ + "字符串创建后可随意修改内部字符", + "字符串只能用双引号包裹", + "字符串方法返回新字符串,原字符串不变", + "字符串本质是数字类型" + ], + "ans": 2, + "exp": "Dart 字符串不可变toUpperCase() 等方法返回新字符串,原值不变。频繁拼接字符串时建议用 StringBuffer 提升性能。" + }, + { + "id": 162, + "lv": 1, + "q": "String s = \"Hello\"; 要得到全大写的 \"HELLO\",应调用?", + "opts": [ + "s.upper()", + "s.toUpperCase()", + "s.up()", + "String.upper(s)" + ], + "ans": 1, + "exp": "转大写用 s.toUpperCase(),转小写用 s.toLowerCase()。注意返回的是新字符串,原字符串不变。" + }, + { + "id": 163, + "lv": 1, + "q": "把数字 42 转成字符串 \"42\",正确写法是?", + "opts": [ + "(String) 42", + "int.stringify(42)", + "42.toString()", + "String.valueOf(42)" + ], + "ans": 2, + "exp": "调用 toString()42.toString()。注意 double 的 toString 会带小数:3.0.toString() 得到 \"3.0\"。(String) 强转、String.valueOf 是其它语言的习惯,Dart 没有。" + }, + { + "id": 164, + "lv": 2, + "q": "取 a、b 中较大的值,用一行表达式应写?", + "opts": [ + "a ?? b", + "a > b ? a : b", + "a || b", + "a && b" + ], + "ans": 1, + "exp": "三元运算符 条件 ? 表达式1 : 表达式2:条件成立取第一个,否则取第二个。a ?? b 是空值合并(a 为 null 才取 b),|| 和 && 是逻辑运算,都不能比较大小。" + }, + { + "id": 165, + "lv": 2, + "q": "bool ok = false; print(ok && check()); 中 check() 会被执行吗?", + "opts": [ + "会执行", + "报错", + "不会执行,&& 左侧为 false 时短路", + "执行两次" + ], + "ans": 2, + "exp": "短路求值&& 左侧为 false 时结果已确定为 false,右侧不会执行|| 左侧为 true 时同理。常用于安全判断:user != null && user.isVip。" + }, + { + "id": 166, + "lv": 2, + "q": "循环里要跳过当前这一次迭代、继续下一次,用哪个关键字?", + "opts": [ + "break", + "return", + "continue", + "skip" + ], + "ans": 2, + "exp": "continue 结束本次迭代、立即进入下一轮;break 直接退出整个循环;return 退出的是整个函数。" + }, + { + "id": 167, + "lv": 2, + "q": "var f = (x) => x * 2; 这种没有名字的函数写法是?", + "opts": [ + "命名函数", + "构造函数", + "静态函数", + "匿名函数(Lambda 表达式)" + ], + "ans": 3, + "exp": "箭头 => 配合无名参数列表就是匿名函数 / Lambda,常作为回调、比较器传递:list.sort((a, b) => a - b)。" + }, + { + "id": 168, + "lv": 2, + "q": "void greet(String name, [String word = \"你好\"]),调用 greet(\"小明\") 时 word 的值是?", + "opts": [ + "null", + "报错", + "\"你好\"", + "空字符串" + ], + "ans": 2, + "exp": "[ ] 包裹的是可选位置参数,不传时使用默认值 \"你好\"。它与命名参数({} 包裹、必须按名传)不同,是按顺序传参的:greet(\"小明\", \"早上好\")。" + }, + { + "id": 169, + "lv": 2, + "q": "循环里要立即终止整个循环,用哪个关键字?", + "opts": [ + "break", + "continue", + "exit", + "stop" + ], + "ans": 0, + "exp": "break 立即结束当前循环;continue 只跳过本次。break 也常用于 switch 的 case 结尾——Dart 要求非空 case 必须以 break、return、throw 等结束。" + }, + { + "id": 170, + "lv": 3, + "q": "var data = {'name': '张三', 'age': 18}; data 的类型是?", + "opts": [ + "Set", + "List", + "Map", + "Tuple" + ], + "ans": 2, + "exp": "{} 且含冒号键值对时创建 Map;无冒号的 {1, 2, 3} 是 Set;[] 是 List。Dart 没有 Tuple 类型。" + }, + { + "id": 171, + "lv": 3, + "q": "var map = {'a': 1}; map['b'] = 2; 之后 map 的内容是?", + "opts": [ + "{'a': 1}", + "{'b': 2}", + "报错", + "{'a': 1, 'b': 2}" + ], + "ans": 3, + "exp": "map[key] = value 写入时,key 已存在则更新值,不存在则新增条目,所以得到 {'a': 1, 'b': 2}。" + }, + { + "id": 172, + "lv": 3, + "q": "['2024', '2025', '2026'].join('-') 的结果是?", + "opts": [ + "'2024,2025,2026'", + "报错", + "'2024-2025-2026'", + "'202420252026'" + ], + "ans": 2, + "exp": "join(分隔符) 把列表元素按分隔符拼成一个字符串,得到 2024-2025-2026。注意不传参数时是无分隔符直接拼接。" + }, + { + "id": 173, + "lv": 3, + "q": "从 [1, 2, 3, 4] 中挑出偶数(2、4),应调用哪个方法?", + "opts": [ + "map", + "reduce", + "addAll", + "where" + ], + "ans": 3, + "exp": "where() 按条件过滤[1,2,3,4].where((n) => n % 2 == 0) 得到 2、4。map() 是变换每个元素,reduce() 是聚合,addAll() 是批量添加。" + }, + { + "id": 174, + "lv": 3, + "q": "var nums = [3, 1, 2]; nums.sort(); 排序后 nums 是?", + "opts": [ + "[3, 1, 2]", + "[1, 2, 3]", + "报错", + "[2, 3, 1]" + ], + "ans": 1, + "exp": "sort() 原地升序排序,结果 [1, 2, 3]。注意它直接修改原列表并返回 void;要降序需传比较器:sort((a, b) => b - a)。" + }, + { + "id": 175, + "lv": 3, + "q": "[1, 2, 3].contains(5) 返回?", + "opts": [ + "true", + "5", + "null", + "false" + ], + "ans": 3, + "exp": "contains() 判断是否包含指定元素,包含返回 true、否则 false。判断 Map 的键用 containsKey,判断值用 containsValue。" + }, + { + "id": 176, + "lv": 4, + "q": "class Point { Point(this.x, this.y); int x; int y; } 中 this.x 的作用是?", + "opts": [ + "创建私有变量", + "引用父类构造函数", + "把构造参数自动赋给同名成员变量", + "声明静态字段" + ], + "ans": 2, + "exp": "构造参数写成 this.x 时,Dart 会把参数值自动赋给同名成员变量,省去手写 this.x = x; 的样板代码。" + }, + { + "id": 177, + "lv": 4, + "q": "想给 Point 提供便捷构造 Point.origin()(x、y 初始化为 0),应该用?", + "opts": [ + "静态方法", + "构造重载", + "默认构造函数", + "命名构造函数" + ], + "ans": 3, + "exp": "命名构造函数Point.origin() : x = 0, y = 0;,调用 Point.origin()。Dart 不支持构造重载,用命名构造区分多种初始化方式。" + }, + { + "id": 178, + "lv": 4, + "q": "统计 Point 类一共创建了多少个实例,计数变量应声明为?", + "opts": [ + "final int count", + "static int count", + "int count", + "late int count" + ], + "ans": 1, + "exp": "static 成员属于类本身、所有实例共享一份,在构造函数里 count++ 即可统计,读取用 Point.count,不需要实例。" + }, + { + "id": 179, + "lv": 4, + "q": "私有字段 _name 想对外只读(能读不能写),应该声明?", + "opts": [ + "void name() {}", + "static String name;", + "String get name => _name;", + "set String name;" + ], + "ans": 2, + "exp": "getter 提供只读访问:String get name => _name;,外部用 p.name 读取;需要可写时再补 set name(String v)。" + }, + { + "id": 180, + "lv": 4, + "q": "@override 注解放在重写方法上的作用是?", + "opts": [ + "调用父类方法", + "声明重写父类方法,签名不符会编译报错", + "创建新方法", + "删除父类方法" + ], + "ans": 1, + "exp": "@override 声明\"此方法重写了父类/接口方法\",签名不一致时编译报错,防止手滑写错。调用父类实现用 super.xxx()。" + }, + { + "id": 181, + "lv": 4, + "q": "子类构造函数里显式调用父类构造函数,用哪个关键字?", + "opts": [ + "this", + "base", + "super", + "parent" + ], + "ans": 2, + "exp": "用 superDog(String n) : super(n);。父类有默认构造时可省略(自动调用);需要传参或指定父类命名构造时才显式写 super。" + }, + { + "id": 182, + "lv": 5, + "q": "String? s;(可能为 null),直接写 print(s.length) 会?", + "opts": [ + "输出 0", + "输出 null", + "编译报错:可空类型不能直接调用方法", + "正常运行" + ], + "ans": 2, + "exp": "空安全下 String? 可能为 null,不能直接调用方法,编译期直接报错——这是空安全的核心价值:把空指针问题拦在编译期。需先判空,或用 ? / !。" + }, + { + "id": 183, + "lv": 5, + "q": "希望\"s 为 null 时整句返回 null,否则调用 length\",应该用?", + "opts": [ + "s.length", + "s?.length", + "s!.length", + "s ?? length" + ], + "ans": 1, + "exp": "?. 安全调用:s 为 null 时结果为 null、不执行调用。常与 ?? 配合兜底:s?.length ?? 0s!.length 是强制解包,s 为 null 时运行时报错。" + }, + { + "id": 184, + "lv": 5, + "q": "int? x = null; x ??= 10; 的作用是?", + "opts": [ + "把 x 强制转为非空", + "x 为 null 时才赋值为 10,否则保持原值", + "报错", + "x 永远等于 10" + ], + "ans": 1, + "exp": "??= 是\"若空则赋值\":x 为 null 时赋 10,若已有值则保持不变。等价于 x = x ?? 10,常用于给可空变量设置默认值。" + }, + { + "id": 185, + "lv": 5, + "q": "执行 throw FormatException('格式错误'); 会?", + "opts": [ + "打印日志后继续执行", + "抛出异常,未被捕获则程序中断", + "返回 null", + "编译错误" + ], + "ans": 1, + "exp": "throw 主动抛出异常并中断当前执行,外层 try-catch 能接住就继续运行,否则异常冒泡导致程序崩溃。适合校验不通过时立即终止。" + }, + { + "id": 186, + "lv": 5, + "q": "处理可能出错的代码,正确的结构是?", + "opts": [ + "catch { } try { }", + "if (error) { }", + "try { } catch (e) { } finally { }", + "except: { }" + ], + "ans": 2, + "exp": "try-catch-finally:try 放可能异常的代码,catch 捕获处理(可写 on FormatException 精确分类),finally 无论成败都会执行,适合释放资源。" + }, + { + "id": 187, + "lv": 6, + "q": "定义星期、状态码这类固定的一组取值,应使用?", + "opts": [ + "typedef", + "enum", + "const 列表", + "abstract 类" + ], + "ans": 1, + "exp": "enum 枚举:enum Status { pending, done, failed },用 Status.done 取值,配合 switch 模式匹配处理,比散落的字符串常量更类型安全。" + }, + { + "id": 188, + "lv": 6, + "q": "写一个\"返回列表首个元素\"的通用函数,任意类型都能用,正确的签名是?", + "opts": [ + "String first(List<String> list)", + "void first(List list)", + "<T> T first(List<T> list)", + "函数不能带类型参数" + ], + "ans": 2, + "exp": "泛型函数<T> T first(List<T> list),T 是类型占位,调用时自动推断:first([1, 2]) 返回 int,first(['a']) 返回 String,既通用又保留类型信息。" + }, + { + "id": 189, + "lv": 6, + "q": "typedef Compare = int Function(int a, int b); 的作用是?", + "opts": [ + "创建了一个新函数", + "声明一个类", + "定义枚举", + "给函数类型起别名" + ], + "ans": 3, + "exp": "typedef 给函数类型起别名:之后 Compare cmp; 或作为参数类型 void sort(Compare cmp),签名一目了然。Dart 2.13+ 还能给普通类型起别名。" + }, + { + "id": 190, + "lv": 6, + "q": "var (name, age) = ('张三', 18); 之后 name 和 age 的值是?", + "opts": [ + "name=18,age='张三'", + "name='张三',age=18", + "报错:不能解构", + "两者都是 null" + ], + "ans": 1, + "exp": "这是 Dart 3 的解构:把 Record ('张三', 18) 按位置拆开,name='张三'、age=18。函数返回 Record 时配合解构取值非常简洁。" + } + ] + } + ], + "nextSetId": 12, + "nextQId": 191 +} \ No newline at end of file diff --git a/scripts/data/set-dart-dev.json b/scripts/data/set-dart-dev.json new file mode 100644 index 0000000..5c4b216 --- /dev/null +++ b/scripts/data/set-dart-dev.json @@ -0,0 +1,216 @@ +{ + "name": "Dart 开发实战", + "lvNames": ["基础语法", "控制流与函数", "集合", "面向对象", "空安全与异步", "Dart 高级特性"], + "questions": [ + { + "lv": 1, + "q": "开发注册页时,用户输入的年龄是字符串,需要转成 int 保存。已知 String ageText = \"18\";,正确的转换写法是?", + "opts": ["int(ageText)", "toInt(ageText)", "int.parse(ageText)", "ageText.parseInt()"], + "ans": 2, + "exp": "int.parse() 把字符串解析成整数,解析失败会抛 FormatException,更稳妥的是 int.tryParse()(失败返回 null)。注意 Dart 没有 \"18\".parseInt() 这种写法,int(...) 也不是转换函数。" + }, + { + "lv": 1, + "q": "项目里要定义一个\"永远不允许修改\"的 API 地址常量,以下哪种声明最合适?", + "opts": ["final String api = \"https://api.example.com\";", "const String api = \"https://api.example.com\";", "String api = \"https://api.example.com\";", "var api = \"https://api.example.com\";"], + "ans": 1, + "exp": "const 是编译期常量:值在编译时就确定,且不可变,编译器可直接内联替换,还能配合 const 构造函数做实例复用。final 是运行期赋值一次后不可变。给字面量常量用 const 是更优选择。" + }, + { + "lv": 1, + "q": "你在重构代码时写下:var x = 10; x = \"hello\"; 会发生什么?", + "opts": ["正常运行,x 变成字符串", "编译报错:x 已被推断为 int 类型", "运行时报错", "x 自动变为 null"], + "ans": 1, + "exp": "var 声明时由初始值自动推断类型,推断后类型就固定了:x 是 int,再赋 String 在编译期直接报错。想存任意类型,应显式声明 dynamicObject?。" + }, + { + "lv": 1, + "q": "给变量起名时,以下哪个不是合法的 Dart 标识符?", + "opts": ["_score", "2x", "中文名", "name_2"], + "ans": 1, + "exp": "Dart 标识符不能以数字开头_score 合法(下划线开头表示库内私有,是 Dart 的约定俗成);Dart 支持 Unicode 标识符,中文变量名同样合法。" + }, + { + "lv": 1, + "q": "结算功能计算人均消费:int total = 10; int people = 4; double avg = total / people; 打印 avg 得到?", + "opts": ["2", "2.5", "2.0", "报错"], + "ans": 1, + "exp": "Dart 中 int 除以 int 的结果一定是 double:10 / 4 = 2.5。想得到整除结果用 ~/10 ~/ 4 == 2,取余用 %。" + }, + { + "lv": 2, + "q": "写优惠逻辑:订单数 > 10 打 9 折,> 50 打 8 折。对 orders = 60,以下代码会得到什么折扣?
double d; if (orders > 10) { d = 0.9; } else if (orders > 50) { d = 0.8; }", + "opts": ["0.8", "0.9", "1.0", "0.7"], + "ans": 1, + "exp": "if-else 链从上到下匹配,命中即止:60 > 10 先成立就直接取 0.9,后面的 else if 不再执行。正确写法应把范围大的条件放前面(先判断 > 50)。判断顺序是 if-else 链最经典的坑。" + }, + { + "lv": 2, + "q": "接口返回 Map<String, dynamic> data,想遍历所有 key 打印出来,以下哪种写法正确?", + "opts": ["for (var k in data) print(k);", "for (var k in data.keys) print(k);", "data.keys.forEach(print);", "B 和 C 都可以"], + "ans": 3, + "exp": "Map 本身不能直接 for-in 遍历,要先取 keys / values / entries。B 是标准写法;C 中 forEach(print) 利用了函数引用,同样正确。两种都行,按团队规范选择。" + }, + { + "lv": 2, + "q": "写一个返回用户名的函数:String getUserName() { },忘了写 return,会怎样?", + "opts": ["返回 null", "编译报错", "运行时崩溃", "自动返回空字符串"], + "ans": 1, + "exp": "空安全下,声明了非空返回类型 String 的函数必须有 return,漏写直接编译报错,把隐患消灭在编译期。如果确实允许无返回值,应把返回类型声明为 String?。" + }, + { + "lv": 2, + "q": "问候函数:void greet(String name, {String greeting = \"你好\"}),调用 greet(\"小明\", \"早上好\") 会发生什么?", + "opts": ["正常执行,greeting 为 \"早上好\"", "编译报错:命名参数不能按位置传参", "greeting 变为 null", "编译警告但能运行"], + "ans": 1, + "exp": "{} 包裹的是命名参数,必须写成 greet(\"小明\", greeting: \"早上好\")[] 才是可选位置参数。命名参数让调用意图清晰,是团队协作中常用的做法。" + }, + { + "lv": 2, + "q": "写\"重试发送验证码\"逻辑:要求至少发送一次,失败则继续重试。哪种循环语义最贴切?", + "opts": ["while", "do-while", "for", "repeat"], + "ans": 1, + "exp": "do-while 先执行循环体再判断条件,保证至少执行一次,正是\"先发一次、不行再重试\"的语义。while 先判断后执行,可能一次都不执行。" + }, + { + "lv": 3, + "q": "实现\"删除待办\"功能,list.remove(3)list.removeAt(3) 的区别是?", + "opts": ["remove 按值删除,removeAt 按下标删除", "两者完全相同", "remove 删下标,removeAt 按值删除", "remove 会删除所有等于 3 的元素"], + "ans": 0, + "exp": "remove(value) 删除第一个值相等的元素并返回是否成功;removeAt(index) 按下标删除并返回被删的元素,越界抛 RangeError。写代码前想清楚你要删的是\"值\"还是\"位置\"。" + }, + { + "lv": 3, + "q": "解析接口数据:用 data[\"name\"] 取一个不存在的 key 时,返回什么?", + "opts": ["抛异常", "null", "空字符串", "抛出 NoSuchMethodError"], + "ans": 1, + "exp": "Map 用 [] 取值时,key 不存在返回 null 而不是抛异常。所以 data[\"name\"] 可能为 null,取值后判空再使用是常规操作(配合 ?? 兜底)。" + }, + { + "lv": 3, + "q": "配置列表按条件插入元素:var l = [1, 2, if (isVip) 3];isVip 为 false 时 l 是?", + "opts": ["[1, 2]", "[1, 2, 3]", "[1, 2, false]", "编译错误"], + "ans": 0, + "exp": "这是 collection-if 语法:条件不成立时该元素直接不生成。Flutter 里常用于条件渲染组件列表,配合 ... 展开和 collection-for 一起使用。" + }, + { + "lv": 3, + "q": "两个用户各自关注了一批标签,要找出共同关注的标签,用哪种写法?", + "opts": ["a & b", "a.intersect(b)", "a.toSet().intersection(b.toSet())", "a.union(b)"], + "ans": 2, + "exp": "List 没有交集方法,要先转 Set。intersection() 返回两个集合的共同元素;union() 是并集。注意方法名是 intersection(),不是其它语言的 intersect()。" + }, + { + "lv": 3, + "q": "计算购物车总价:List<double> prices = []; 空列表调用 prices.reduce((a, b) => a + b) 会?", + "opts": ["返回 0", "抛出 StateError 异常", "返回 null", "返回空列表"], + "ans": 1, + "exp": "reduce 要求列表至少有一个元素,空列表直接抛 \"Bad state: No element\"。更稳的写法是 fold(0, (a, b) => a + b):带初始值,空列表返回 0。" + }, + { + "lv": 4, + "q": "写日期工具类,希望 DateUtil.parse(\"2024-01-01\") 在字符串不合法时返回 null 而不是抛异常,用哪种构造函数?", + "opts": ["factory 构造函数", "普通构造函数", "命名构造函数", "const 构造函数"], + "ans": 0, + "exp": "factory 构造函数可以不返回新实例——可以返回缓存实例、子类实例,甚至返回 null。它把\"按需创建/校验\"的逻辑封装在构造入口,调用方写法统一且安全。" + }, + { + "lv": 4, + "q": "日志打印、权限检查这类能力要被多个互不相关的类复用,又不想强制继承关系,Dart 的推荐做法是?", + "opts": ["extends 继承", "implements 实现", "with 混入 Mixin", "abstract 抽象类"], + "ans": 2, + "exp": "Mixinwith)把可复用逻辑抽出来按需混入,避免\"为了复用而强行继承\"的长继承链。Flutter 源码大量使用,如 class _X with RouteAware。" + }, + { + "lv": 4, + "q": "类内部要声明一个\"仅供本文件使用\"的私有字段,正确写法是?", + "opts": ["private String _token;", "String _token;", "-String token;", "#token"], + "ans": 1, + "exp": "Dart 没有 private 关键字,以下划线开头表示库内私有:_token。注意这是\"库\"级别私有(同文件可见),不是类级别。" + }, + { + "lv": 4, + "q": "用户类的 name 字段希望赋值时自动去掉首尾空格,最佳做法是?", + "opts": ["声明 getter/setter,在 setter 里 trim", "直接写公共字段,调用方自己 trim", "用 final 禁止修改", "用 static 修饰"], + "ans": 0, + "exp": "setter 在赋值入口统一处理,所有调用方(包括构造函数)都自动享受规范化,不用每个地方都记得 trim。写法:String get nameset name(String v)。" + }, + { + "lv": 4, + "q": "class Dog implements Animalextends 继承的关键区别是?", + "opts": ["implements 要求实现 Animal 的全部成员(含字段),extends 可复用已有实现", "两者完全一样", "implements 只能用于抽象类", "extends 要求实现全部成员"], + "ans": 0, + "exp": "implements 把 Animal 当接口:所有成员都要自己重写,即使父类已有实现;extends 继承实现,只覆写需要改的部分。implements 用于\"规定能力\",extends 用于\"复用实现\"。" + }, + { + "lv": 5, + "q": "用户对象可能为 null:User? u = await fetchUser();,以下哪种使用方式更安全?", + "opts": ["if (u != null) { print(u.name); } 判空后直接用 u", "if (u != null) { print(u!.name); } 必须加 !", "print(u?.name ?? \"\");", "A 和 C 都可以"], + "ans": 3, + "exp": "A:局部变量判空后编译器会自动提升为非空类型,直接使用即可;C?. 安全调用 + ?? 兜底,一行搞定。只有被闭包捕获的变量才需要 !。" + }, + { + "lv": 5, + "q": "详情页要同时请求\"用户信息\"和\"订单列表\"两个接口,都拿到再渲染,用哪个方法?", + "opts": ["Future.wait([fetchUser(), fetchOrders()])", "串行写两个 await", "Future.any", "Future.delayed 等待"], + "ans": 0, + "exp": "Future.wait 并发启动多个 Future,全部完成后统一返回结果列表,总耗时约等于最慢的那个;串行 await 则是两个请求耗时相加。Future.any 只等第一个完成的。" + }, + { + "lv": 5, + "q": "某个字段要等登录成功后才能赋值,但又想声明成非空类型,正确声明方式是?", + "opts": ["late String token;", "String? token = \"\";", "dynamic token;", "final token = null;"], + "ans": 0, + "exp": "late 表示\"推迟初始化,首次访问时才赋值\",适合依赖异步结果的字段或计算代价高的属性。注意:late 变量在赋值前被访问会抛 LateInitializationError。" + }, + { + "lv": 5, + "q": "把解析结果转成目标类型:var age = data[\"age\"] as int;,当 age 实际是 String 时会发生?", + "opts": ["返回 null", "抛出 TypeError(类型转换失败)", "静默忽略", "自动转成 int"], + "ans": 1, + "exp": "as强制类型转换,类型不符立即抛异常。安全做法:先用 is 判断类型,或先统一规范数据(如 num 一律 .toInt())。" + }, + { + "lv": 5, + "q": "对可能为 null 的 String? s 使用 s!,若 s 确实为 null 会?", + "opts": ["返回 null", "抛出 TypeError(空检查失败)", "静默继续", "编译错误"], + "ans": 1, + "exp": "!强制解包运算符,向编译器承诺\"这里不可能为 null\",承诺落空时运行时抛 TypeError。能用判空提升或 ?? 的地方尽量别用 !,减少运行时崩溃点。" + }, + { + "lv": 6, + "q": "Flutter 中常见 ButtonStyle()..backgroundColor = ... ..shape = ... 写法,.. 级联操作符的核心行为是?", + "opts": ["对同一对象连续操作且返回对象本身", "链式调用并返回最后一次结果", "只能用于 List", "与 ... 展开等价"], + "ans": 0, + "exp": "级联操作符 .. 对同一对象连续执行多个操作,不改变返回值(始终是原对象),适合集中配置对象。. 返回的是方法调用的返回值,两者行为不同。" + }, + { + "lv": 6, + "q": "Dart 3 中,函数想直接返回\"用户名 + 年龄\"这组小数据,又不想专门建一个类,推荐用?", + "opts": ["Map<String, dynamic>", "Record 记录类型:(String name, int age)", "List<dynamic>", "用全局变量"], + "ans": 1, + "exp": "Record 是 Dart 3 的轻量聚合类型:var r = (\"张三\", 18); 或命名字段 (name: \"张三\", age: 18),用 r.$1 / r.name 取值,还能配合模式匹配解构,比 Map 更类型安全。" + }, + { + "lv": 6, + "q": "Dart 3 的 switch 可以直接当表达式赋值:var s = switch (status) { \"ok\" => \"成功\", _ => \"未知\" }; 这种写法?", + "opts": ["合法:switch 表达式返回一个值", "非法:switch 不能作表达式", "必须每个分支加 break", "只能匹配字符串"], + "ans": 0, + "exp": "switch 表达式=> 代替冒号,整体作为值赋给变量,配合模式匹配可以解构、判断类型,比旧式 switch 简洁,也天然避免了漏 break 的坑。" + }, + { + "lv": 6, + "q": "想给系统类 String 加一个 isEmail() 方法,但 String 不能改源码,用?", + "opts": ["extension 扩展:extension EmailCheck on String { bool isEmail() {...} }", "继承 String", "用 mixin 混入", "写静态工具类"], + "ans": 0, + "exp": "extension 可以不修改原类给已有类型添加方法,之后 \"a@b.com\".isEmail() 直接调用。注意扩展方法是静态分发(编译期决定),不参与运行时多态。" + }, + { + "lv": 6, + "q": "泛型容器 class Box<T extends num> {}extends num 的含义是?", + "opts": ["T 只能是 int、double 等数值类型", "T 可以是任意类型", "T 必须可空", "T 必须是集合类型"], + "ans": 0, + "exp": "泛型约束限定 T 必须是 num 及其子类(int/double),这样 Box 内部就能安全调用数值运算。不加约束时 T 等价于 Object?。" + } + ] +} diff --git a/scripts/data/set-flutter-dev.json b/scripts/data/set-flutter-dev.json new file mode 100644 index 0000000..97cfe15 --- /dev/null +++ b/scripts/data/set-flutter-dev.json @@ -0,0 +1,216 @@ +{ + "name": "Flutter 开发实战", + "lvNames": ["Widget 基础", "布局与组件", "状态管理", "路由与导航", "数据与网络", "实战综合"], + "questions": [ + { + "lv": 1, + "q": "新建 Flutter 项目后,把应用真正跑起来的入口代码是?", + "opts": ["void main() => runApp(MyApp());", "void main() { MyApp(); }", "runApp() { MyApp }", "main() { MaterialApp(); }"], + "ans": 0, + "exp": "Dart 程序入口是 main(),Flutter 通过 runApp(Widget) 把根组件挂载到引擎上。注意 MyApp() 是创建 Widget 实例,不能少了括号。" + }, + { + "lv": 1, + "q": "实现一个\"3 秒倒计时\"按钮,每秒刷新显示剩余秒数,应使用?", + "opts": ["StatelessWidget", "StatefulWidget", "Container", "Scaffold"], + "ans": 1, + "exp": "有动态变化的数据(倒计时数字)就需要 StatefulWidget:数据保存在 State 里,变化时调 setState() 刷新。没有可变状态时应优先用 StatelessWidget。" + }, + { + "lv": 1, + "q": "界面上要显示一行固定文案\"登录成功\",最合适的组件是?", + "opts": ["Text", "Label", "Paragraph", "RichText"], + "ans": 0, + "exp": "Text 是最常用的文本组件,可通过 style 配置字号、颜色、字重等。RichText 用于一段文字中混排多种样式的富文本场景。" + }, + { + "lv": 1, + "q": "点击\"确认\"按钮后分数要 +1 并立即显示到界面,正确流程是?", + "opts": ["修改 State 字段后调用 setState()", "直接修改字段就行", "重启整个 App", "修改 Widget 的构造参数"], + "ans": 0, + "exp": "State 里的字段只是普通数据,必须调用 setState() 通知框架\"数据变了\",框架才会重新执行 build 刷新 UI。注意 setState 只重建当前 State 的子树,不是整棵树。" + }, + { + "lv": 1, + "q": "要展示 10000 条商品记录,性能最优的做法是?", + "opts": ["ListView.builder(按需构建可见项)", "Column 里循环放 10000 个组件", "SingleChildScrollView 包 Column", "GridView.count 全部构建"], + "ans": 0, + "exp": "ListView.builder 只构建当前可见的列表项(懒加载),滚动时回收复用;Column + for 会一次性构建全部组件,万条数据直接卡死。数据量大优先 builder 系列。" + }, + { + "lv": 2, + "q": "登录页要垂直排列\"输入框、输入框、登录按钮\",用哪个布局组件?", + "opts": ["Column", "Row", "Stack", "Wrap"], + "ans": 0, + "exp": "Column 垂直方向排列子组件,Row 水平方向,Stack 叠加并支持定位,Wrap 在空间不足时自动换行。" + }, + { + "lv": 2, + "q": "Row 里想让一个按钮占满剩余宽度(剩余空间全给它),按钮外层套?", + "opts": ["Expanded", "Padding", "Center", "Spacer"], + "ans": 0, + "exp": "Expanded 让子组件占据主轴的剩余空间(Row 的主轴是水平方向)。多个 Expanded 可通过 flex 参数按比例分配空间。" + }, + { + "lv": 2, + "q": "Column 里有几个按钮,想让它们在水平方向居中,应该设置?", + "opts": ["crossAxisAlignment: CrossAxisAlignment.center", "mainAxisAlignment: MainAxisAlignment.center", "alignment: Alignment.center", "textAlign: TextAlign.center"], + "ans": 0, + "exp": "Column 的主轴是垂直方向、交叉轴是水平方向。水平居中=沿交叉轴居中=crossAxisAlignment.center;mainAxisAlignment 控制的是垂直方向。主/交叉轴搞反是新手最常踩的坑。" + }, + { + "lv": 2, + "q": "封面图要铺满整个区域且不拉伸变形(多出的部分裁掉),fit 参数应该用?", + "opts": ["BoxFit.cover", "BoxFit.fill", "BoxFit.none", "BoxFit.scaleDown"], + "ans": 0, + "exp": "cover:等比缩放填满、超出裁剪,常用于头像、封面;fill 会拉伸变形;contain 完整显示但可能留白;none 不缩放。" + }, + { + "lv": 2, + "q": "商品名过长时希望单行显示,超出部分显示省略号,Text 的配置是?", + "opts": ["maxLines: 1, overflow: TextOverflow.ellipsis", "只设 overflow: TextOverflow.clip", "只设 maxLines: 3", "softWrap: false 就够了"], + "ans": 0, + "exp": "maxLines 限制最大行数 + overflow: TextOverflow.ellipsis 指定超出的表现(省略号/裁剪/fade),两者配合才能出现\"…\"效果。" + }, + { + "lv": 3, + "q": "输入框需要\"预填默认值 + 按钮点击后读取内容 + 随时清空\",推荐用?", + "opts": ["TextEditingController", "每次读 onChanged 参数", "GlobalKey 临时查找", "无法实现"], + "ans": 0, + "exp": "TextEditingController 是输入框的\"数据源\":TextEditingController(text: \"预填\") 初始化,controller.text 读取,controller.clear() 清空。用完后在 dispose 里释放。" + }, + { + "lv": 3, + "q": "页面销毁时,取消监听、释放 Controller 的代码应该写在哪里?", + "opts": ["dispose()", "initState()", "build()", "didChangeDependencies()"], + "ans": 0, + "exp": "dispose() 是 State 被移除时的清理钩子:取消 Timer、Stream 订阅、释放 Controller。不清理会造成内存泄漏和 \"setState called after dispose\" 报错。" + }, + { + "lv": 3, + "q": "登录状态、用户信息要被多个页面共享,又不想层层传构造参数,项目常用方案是?", + "opts": ["Provider / InheritedWidget", "定义全局变量直接读写", "每个页面重新请求接口", "把状态塞进路由参数"], + "ans": 0, + "exp": "Flutter 官方的底层方案是 InheritedWidgetProvider 是对它的优雅封装:共享数据放顶层,子组件 context.watch() 取用并自动刷新。全局变量没有通知机制,页面不会自动更新。" + }, + { + "lv": 3, + "q": "商品列表页要支持下拉刷新,应该把 ListView 包进哪个组件?", + "opts": ["RefreshIndicator", "GestureDetector", "AnimatedList", "Scrollbar"], + "ans": 0, + "exp": "RefreshIndicator 包住可滚动组件,配合 onRefresh 返回的 Future 实现下拉刷新动画。这是 Material 规范自带的下拉刷新方案。" + }, + { + "lv": 3, + "q": "App 切到后台、再回到前台时,需要暂停/恢复计时器,通过什么监听?", + "opts": ["WidgetsBindingObserver", "RouteObserver", "NavigatorObserver", "MediaQuery"], + "ans": 0, + "exp": "给 State 混入 with WidgetsBindingObserver,在 didChangeAppLifecycleState() 中监听 AppLifecycleState(paused / resumed / inactive),即可感知前后台切换。" + }, + { + "lv": 4, + "q": "列表页点击商品跳转详情页,标准写法是?", + "opts": ["Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(id: id)))", "Navigator.go(DetailPage(id: id))", "Route.open(DetailPage())", "DetailPage(id: id).show()"], + "ans": 0, + "exp": "Navigator.push 把新路由压入导航栈,MaterialPageRoute 提供转场动画,返回上一页用 Navigator.pop(context)。" + }, + { + "lv": 4, + "q": "详情页编辑完要把结果回传给列表页,详情页返回时应该写?", + "opts": ["Navigator.pop(context, result)", "Navigator.back(result)", "Navigator.pushResult(result)", "context.popAndPush(result)"], + "ans": 0, + "exp": "pop(context, 返回值) 的第二个参数就是回传的数据,列表页 await push(...) 的返回值即可收到。注意 pop 的参数是\"返回给上一页的数据\",不是新路由。" + }, + { + "lv": 4, + "q": "跳转时需要传递商品 id、名称等参数,团队协作中最推荐的传参方式是?", + "opts": ["DetailPage(id: 1, name: \"商品\") 构造参数传递", "全局变量临时存", "static 静态字段存", "URL 字符串拼接"], + "ans": 0, + "exp": "构造参数显式、类型安全、可读性好——跳转处一眼看到传了什么。全局变量/静态字段是隐藏依赖,页面多起来后非常难维护。" + }, + { + "lv": 4, + "q": "在 App 里注册命名路由(如 /login/home),配置位置是?", + "opts": ["MaterialApp(routes: {...})", "main() 函数里", "runApp() 参数", "每个页面里各自声明"], + "ans": 0, + "exp": "命名路由集中配置在 MaterialApproutes 表,之后用 Navigator.pushNamed(context, \"/login\") 跳转;需要传参时配合 arguments 参数。" + }, + { + "lv": 4, + "q": "跳到选择页,用户选完返回后要根据选择结果继续处理,列表页怎么写?", + "opts": ["final r = await Navigator.push(...); 拿到返回值再处理", "无法等待结果", "用 setState 轮询", "跳转时把回调函数传过去"], + "ans": 0, + "exp": "push 返回 Future<T>,await 后拿到 pop 回传的值,再执行后续逻辑(如刷新选中项)。这是\"页面间取结果\"的标准姿势。" + }, + { + "lv": 5, + "q": "接口返回 JSON 字符串,执行 var data = jsonDecode(text); 后 data 的类型是?", + "opts": ["dynamic(实际可能是 Map 或 List)", "String", "Map<String, dynamic>", "Object"], + "ans": 0, + "exp": "jsonDecode 的返回类型是 dynamic:JSON 对象变 Map<String, dynamic>、数组变 List<dynamic>、数字/布尔原样返回。通常需要手动转换:jsonDecode(text) as Map<String, dynamic>。" + }, + { + "lv": 5, + "q": "要把用户对象 User(id: 1, name: \"张三\") 转成 JSON 字符串发给服务器,正确流程是?", + "opts": ["给 User 写 toJson(),再 jsonEncode(user.toJson())", "jsonEncode(user) 直接序列化", "user.toString()", "JSON.stringify(user)"], + "ans": 0, + "exp": "Dart 默认不会自动序列化任意对象,惯例是模型类自己实现 Map<String, dynamic> toJson(),再用 jsonEncode 转字符串;反序列化则对应 User.fromJson(Map) 工厂构造。" + }, + { + "lv": 5, + "q": "网络请求可能超时/断网,用户点\"登录\"时抛出了异常,正确的处理是?", + "opts": ["用 try-catch 包裹 await 请求,捕获异常并提示用户", "不做处理", "打印日志后忽略", "延迟 3 秒自动重试"], + "ans": 0, + "exp": "try { await api.login(...) } catch (e) { 提示\"网络异常,请重试\" } 是最基础的错误处理;生产环境还会区分 SocketException(断网)、TimeoutException(超时)等做分类提示。" + }, + { + "lv": 5, + "q": "请求进行中需要展示\"加载中\"状态,最合适用?", + "opts": ["CircularProgressIndicator", "Text(\"加载中...\")", "Opacity 半透明遮罩", "AnimatedContainer 闪烁"], + "ans": 0, + "exp": "Material 标准的加载指示器是 CircularProgressIndicator(转圈)和 LinearProgressIndicator(条),配合 loading 状态变量做条件渲染。" + }, + { + "lv": 5, + "q": "http 请求要设置 10 秒超时,正确写法是?", + "opts": ["await http.get(url).timeout(Duration(seconds: 10))", "http.get(url, timeout: 10)", "Timer(Duration(seconds: 10), ...)", "Future.any 自动超时"], + "ans": 0, + "exp": "Future 自带 .timeout():超时抛 TimeoutException,可再接 onTimeout 参数兜底。http 包本身没有 timeout 参数,这是常见的误解。" + }, + { + "lv": 6, + "q": "首页同时请求\"轮播图、公告、商品列表\"三个接口,全部完成再渲染页面,用?", + "opts": ["Future.wait([...]) 并发等待", "三个 await 串行", "依次 catch 分别显示", "用 Future.any"], + "ans": 0, + "exp": "并发场景用 Future.wait,总耗时约等于最慢的请求而不是三个相加。生产上可配合记录单个请求失败做局部降级(某块失败不影响整页)。" + }, + { + "lv": 6, + "q": "商品列表滑到底部要自动加载下一页,监听滚动的方案是?", + "opts": ["ScrollController 监听 pixels 接近 maxScrollExtent 时触发", "用 Timer 每 3 秒加载", "下拉刷新代替", "GestureDetector 监听手势"], + "ans": 0, + "exp": "给 ListView 挂 ScrollController,在 addListener 里判断 position.pixels >= position.maxScrollExtent - 200(预留阈值)就加载下一页,配合 loading 状态防止重复触发。" + }, + { + "lv": 6, + "q": "商品标题中要红色高亮\"热卖\"二字、其余部分灰色,用?", + "opts": ["RichText + TextSpan 组合不同样式", "拆成多个 Text 排一排", "用换行符拼接字符串", "TextStyle 做不到,只能整段同色"], + "ans": 0, + "exp": "RichTextTextSpan 支持树状嵌套:每个 span 可以有自己的 style,是\"一段文字多种样式\"的标准方案。" + }, + { + "lv": 6, + "q": "界面出现黄黑条纹溢出警告(RenderFlex overflowed),说明?", + "opts": ["子组件超出了父组件给定的约束空间", "主题颜色配置错误", "内存泄漏", "字体加载失败"], + "ans": 0, + "exp": "Row/Column 空间不够时子组件溢出,debug 模式显示黄黑条纹并打印 \"RenderFlex overflowed by XX pixels\"。解决思路:Expanded/Flexible 让出空间、SingleChildScrollView 滚动、FittedBox 缩放。" + }, + { + "lv": 6, + "q": "瀑布流商品图列表很卡、图片解码占内存高,优先优化手段是?", + "opts": ["cacheWidth / ResizeImage 限制解码尺寸", "给图片加圆角", "提高图片的清晰度", "减少列表项数量"], + "ans": 0, + "exp": "Image.network 默认按原图尺寸解码,一张 4000px 大图在手机上就是内存杀手。用 cacheWidth: 300ResizeImage 让引擎按需解码成小尺寸位图,内存占用和滚动性能立竿见影。" + } + ] +} diff --git a/scripts/extract-questions.js b/scripts/extract-questions.js new file mode 100644 index 0000000..7ccd904 --- /dev/null +++ b/scripts/extract-questions.js @@ -0,0 +1,31 @@ +const fs = require('fs'); +const vm = require('vm'); + +const html = fs.readFileSync('index.html', 'utf8'); +const start = html.indexOf('const questions'); +const arrStart = html.indexOf('[', start); +let depth = 0, inStr = false, esc = false, end = -1; +for (let i = arrStart; i < html.length; i++) { + const c = html[i]; + if (inStr) { + if (esc) { esc = false; continue; } + if (c === '\\') { esc = true; continue; } + if (c === "'") inStr = false; + continue; + } + if (c === "'") { inStr = true; continue; } + if (c === '[') depth++; + else if (c === ']') { depth--; if (depth === 0) { end = i; break; } } +} +let src = html.slice(arrStart, end + 1); +src = src.replace(/\/\/\s*=====[^']*?=====\s*/g, ' '); + +const questions = vm.runInNewContext(src); +const withIds = questions.map((q, i) => ({ id: i + 1, ...q })); +const data = { + sets: [{ id: 1, name: 'Dart & Flutter 学习测验', questions: withIds }], + nextSetId: 2, + nextQId: withIds.length + 1 +}; +fs.writeFileSync('questions.json', JSON.stringify(data, null, 2), 'utf8'); +console.log('已提取', withIds.length, '道题 -> questions.json'); diff --git a/scripts/fix-escaping.js b/scripts/fix-escaping.js new file mode 100644 index 0000000..f83d213 --- /dev/null +++ b/scripts/fix-escaping.js @@ -0,0 +1,34 @@ +const fs = require('fs'); +const path = require('path'); + +(async () => { + const { escapeSmartHtml, decodeEntities } = await import('../web/src/htmlUtil.mjs'); + + const file = path.join(__dirname, '..', 'questions.json'); + const data = JSON.parse(fs.readFileSync(file, 'utf8')); + let fixed = 0; + + for (const set of data.sets) { + for (const q of set.questions) { + const nq = escapeSmartHtml(decodeEntities(q.q)); + const nx = escapeSmartHtml(decodeEntities(q.exp || '')); + if (nq !== q.q) { q.q = nq; fixed++; } + if (nx !== q.exp) { q.exp = nx; fixed++; } + const no = q.opts.map(o => escapeSmartHtml(decodeEntities(o))); + if (JSON.stringify(no) !== JSON.stringify(q.opts)) { q.opts = no; fixed++; } + } + } + + fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8'); + console.log('清洗完成,修正字段数:', fixed); + console.log('幂等检查(再跑一遍应无变化)...'); + let again = 0; + for (const set of data.sets) { + for (const q of set.questions) { + if (escapeSmartHtml(decodeEntities(q.q)) !== q.q) again++; + if (escapeSmartHtml(decodeEntities(q.exp || '')) !== q.exp) again++; + if (JSON.stringify(q.opts.map(o => escapeSmartHtml(decodeEntities(o)))) !== JSON.stringify(q.opts)) again++; + } + } + console.log(again === 0 ? '幂等 ✓' : `仍会变化: ${again} 处 ✗`); +})(); diff --git a/scripts/import-sets.js b/scripts/import-sets.js new file mode 100644 index 0000000..2b5db1d --- /dev/null +++ b/scripts/import-sets.js @@ -0,0 +1,38 @@ +const fs = require('fs'); +const path = require('path'); + +const BASE = 'http://localhost:3030'; +const FILES = ['set-dart-dev.json', 'set-flutter-dev.json']; + +async function main() { + const loginRes = await fetch(BASE + '/api/admin/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: 'admin', password: 'admin123' }) + }); + const login = await loginRes.json(); + if (!loginRes.ok) throw new Error('登录失败: ' + login.error); + + for (const file of FILES) { + const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', file), 'utf8')); + const res = await fetch(BASE + '/api/admin/import', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + login.token + }, + body: JSON.stringify(data) + }); + const result = await res.json(); + if (!res.ok) { + console.log(`✗ ${data.name} 导入失败: ${result.error}`); + } else { + console.log(`✓ ${data.name}: 导入 ${result.imported} 题, 套题 id=${result.set.id}`); + } + } +} + +main().catch(e => { + console.error(e.message); + process.exit(1); +}); diff --git a/scripts/verify-questions.js b/scripts/verify-questions.js new file mode 100644 index 0000000..2217346 --- /dev/null +++ b/scripts/verify-questions.js @@ -0,0 +1,48 @@ +const d = require('../questions.json'); +let total = 0, bad = []; +for (const set of d.sets) { + for (const q of set.questions) { + total++; + if (!q.q || !Array.isArray(q.opts) || q.opts.length < 2) { bad.push(`[${set.name} #${q.id}] 选项不足`); continue; } + if (!Number.isInteger(q.ans) || q.ans < 0 || q.ans >= q.opts.length) { bad.push(`[${set.name} #${q.id}] ans 越界`); continue; } + if (!q.exp) { bad.push(`[${set.name} #${q.id}] 缺解析`); continue; } + if (!Number.isInteger(q.lv) || q.lv < 1 || q.lv > 6) { bad.push(`[${set.name} #${q.id}] lv 非法`); continue; } + const dup = q.opts.findIndex((o, i) => q.opts.indexOf(o) !== i); + if (dup !== -1) { bad.push(`[${set.name} #${q.id}] 选项重复: "${q.opts[dup]}"`); } + } +} +console.log('总题数:', total); +if (bad.length) { console.log('问题:'); bad.forEach(b => console.log(' -', b)); } +else console.log('结构校验全部通过'); + +const checks = [ + ['Dart 开发实战', 'var x = 10', 1], + ['Dart 开发实战', 'orders = 60', 1], + ['Dart 开发实战', 'remove(3)', 0], + ['Dart 开发实战', '不存在的 key', 1], + ['Dart 开发实战', 'if (isVip) 3', 0], + ['Dart 开发实战', 'reduce((a, b)', 1], + ['Dart 开发实战', 'factory 构造函数', 0], + ['Dart 开发实战', 'mixin', 2], + ['Dart 开发实战', 's!', 1], + ['Dart 开发实战', 'late String token', 0], + ['Flutter 开发实战', 'runApp(MyApp())', 0], + ['Flutter 开发实战', 'crossAxisAlignment', 0], + ['Flutter 开发实战', 'TextOverflow.ellipsis', 0], + ['Flutter 开发实战', 'RefreshIndicator', 0], + ['Flutter 开发实战', 'Navigator.pop(context, result)', 0], + ['Flutter 开发实战', 'jsonDecode(text)', 0], + ['Flutter 开发实战', '.timeout(Duration', 0], + ['Flutter 开发实战', 'cacheWidth', 0], + ['Flutter 开发实战', 'RichText', 0], + ['Flutter 开发实战', 'dispose()', 0] +]; +let fail = 0; +for (const [setName, keyword, expect] of checks) { + const set = d.sets.find(s => s.name === setName); + const q = set.questions.find(x => (x.q + x.opts.join('') + x.exp).includes(keyword)); + if (!q) { console.log('✗ 未找到题:', setName, keyword); fail++; continue; } + const ok = q.ans === expect; + if (!ok) { console.log(`✗ [${setName}] ${keyword} → ans=${q.ans} 期望 ${expect},正确选项内容: "${q.opts[q.ans]}"`); fail++; } +} +console.log(fail ? `抽查 ${checks.length} 题,${fail} 题不符` : `抽查 ${checks.length} 道关键题答案全部正确`); diff --git a/server.js b/server.js new file mode 100644 index 0000000..f8b3c67 --- /dev/null +++ b/server.js @@ -0,0 +1,238 @@ +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const PORT = process.env.PORT || 3030; +const ADMIN_USER = process.env.ADMIN_USER || 'admin'; +const ADMIN_PASS = process.env.ADMIN_PASS || 'admin123'; +const DATA_FILE = path.join(__dirname, 'questions.json'); + +const app = express(); +app.use(express.json({ limit: '2mb' })); + +const DIST = path.join(__dirname, 'dist'); +if (fs.existsSync(DIST)) { + app.use(express.static(DIST)); + app.get(/^\/(?!api\/).*/, (req, res) => { + res.sendFile(path.join(DIST, 'index.html')); + }); +} else { + console.warn('未找到 dist/ 目录,请先执行 npm run build 再访问页面'); +} + +function loadData() { + if (!fs.existsSync(DATA_FILE)) return { sets: [], nextSetId: 1, nextQId: 1 }; + try { + const raw = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); + if (Array.isArray(raw)) { + const qs = raw; + const migrated = { + sets: [{ id: 1, name: 'Dart & Flutter 学习测验', questions: qs }], + nextSetId: 2, + nextQId: qs.length ? Math.max(...qs.map(q => q.id)) + 1 : 1 + }; + fs.writeFileSync(DATA_FILE, JSON.stringify(migrated, null, 2), 'utf8'); + return migrated; + } + if (raw && Array.isArray(raw.sets)) return raw; + return { sets: [], nextSetId: 1, nextQId: 1 }; + } catch { + return { sets: [], nextSetId: 1, nextQId: 1 }; + } +} + +const data = loadData(); + +function saveData() { + fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8'); +} + +const tokens = new Set(); + +function findQuestion(qid) { + for (const set of data.sets) { + const q = set.questions.find(x => x.id === qid); + if (q) return { set, q }; + } + return null; +} + +function toPublic(q) { + return { id: q.id, lv: q.lv, q: q.q, opts: q.opts }; +} + +function validateBody(body) { + const { lv, q, opts, ans, exp } = body || {}; + if (!q || typeof q !== 'string' || q.trim() === '') return '题目内容不能为空'; + if (!Array.isArray(opts) || opts.length < 2) return '至少需要 2 个选项'; + if (opts.some(o => typeof o !== 'string' || o.trim() === '')) return '选项不能为空'; + if (typeof ans !== 'number' || !Number.isInteger(ans) || ans < 0 || ans >= opts.length) return '答案序号不正确'; + if (lv !== undefined && (!Number.isInteger(lv) || lv < 1 || lv > 6)) return '难度需为 1-6 的整数'; + return null; +} + +function buildQuestion(body) { + const { lv, q, opts, ans, exp } = body; + return { id: data.nextQId++, lv: lv === undefined ? 1 : lv, q: q.trim(), opts, ans, exp: (exp || '').trim() }; +} + +function validateLvNames(v) { + if (v === undefined || v === null) return null; + if (!Array.isArray(v) || v.length !== 6 || v.some(x => typeof x !== 'string' || !x.trim())) { + return '难度名称必须为 6 个非空字符串'; + } + return null; +} + +app.get('/api/sets', (req, res) => { + res.json({ sets: data.sets.map(s => ({ id: s.id, name: s.name, count: s.questions.length, lvNames: s.lvNames || null })) }); +}); + +app.get('/api/questions', (req, res) => { + const setId = req.query.set ? Number(req.query.set) : (data.sets[0] ? data.sets[0].id : null); + const set = data.sets.find(s => s.id === setId); + if (!set) return res.status(404).json({ error: '套题不存在' }); + res.json({ + set: { id: set.id, name: set.name, lvNames: set.lvNames || null }, + count: set.questions.length, + questions: set.questions.map(toPublic) + }); +}); + +app.post('/api/check', (req, res) => { + const { id, answer } = req.body || {}; + const hit = findQuestion(id); + if (!hit) return res.status(404).json({ error: '题目不存在' }); + if (!Number.isInteger(answer)) return res.status(400).json({ error: 'answer 必须为数字' }); + res.json({ correct: hit.q.ans === answer, answer: hit.q.ans, exp: hit.q.exp }); +}); + +app.post('/api/admin/login', (req, res) => { + const { username, password } = req.body || {}; + if (username === ADMIN_USER && password === ADMIN_PASS) { + const token = crypto.randomBytes(24).toString('hex'); + tokens.add(token); + res.json({ token }); + } else { + res.status(401).json({ error: '用户名或密码错误' }); + } +}); + +function requireAuth(req, res, next) { + const token = (req.headers.authorization || '').replace(/^Bearer\s+/i, ''); + if (token && tokens.has(token)) return next(); + res.status(401).json({ error: '未登录或登录已过期' }); +} + +app.get('/api/admin/sets', requireAuth, (req, res) => { + res.json({ sets: data.sets }); +}); + +app.post('/api/admin/sets', requireAuth, (req, res) => { + const { name } = req.body || {}; + if (!name || typeof name !== 'string' || name.trim() === '') return res.status(400).json({ error: '套题名称不能为空' }); + const lvErr = validateLvNames(req.body.lvNames); + if (lvErr) return res.status(400).json({ error: lvErr }); + const set = { id: data.nextSetId++, name: name.trim(), questions: [] }; + if (req.body.lvNames !== undefined) set.lvNames = req.body.lvNames; + data.sets.push(set); + saveData(); + res.json(set); +}); + +app.put('/api/admin/sets/:id', requireAuth, (req, res) => { + const id = Number(req.params.id); + const set = data.sets.find(s => s.id === id); + if (!set) return res.status(404).json({ error: '套题不存在' }); + const { name } = req.body || {}; + if (!name || typeof name !== 'string' || name.trim() === '') return res.status(400).json({ error: '套题名称不能为空' }); + const lvErr = validateLvNames(req.body.lvNames); + if (lvErr) return res.status(400).json({ error: lvErr }); + set.name = name.trim(); + if (req.body.lvNames !== undefined) set.lvNames = req.body.lvNames; + saveData(); + res.json(set); +}); + +app.delete('/api/admin/sets/:id', requireAuth, (req, res) => { + const id = Number(req.params.id); + const idx = data.sets.findIndex(s => s.id === id); + if (idx === -1) return res.status(404).json({ error: '套题不存在' }); + data.sets.splice(idx, 1); + saveData(); + res.json({ ok: true }); +}); + +app.post('/api/admin/import', requireAuth, (req, res) => { + const { name, questions } = req.body || {}; + if (!name || typeof name !== 'string' || name.trim() === '') return res.status(400).json({ error: '套题名称不能为空' }); + if (!Array.isArray(questions)) return res.status(400).json({ error: 'questions 必须为数组' }); + const lvErr = validateLvNames(req.body.lvNames); + if (lvErr) return res.status(400).json({ error: lvErr }); + for (let i = 0; i < questions.length; i++) { + const err = validateBody(questions[i]); + if (err) return res.status(400).json({ error: '第 ' + (i + 1) + ' 题格式错误:' + err }); + } + const set = { id: data.nextSetId++, name: name.trim(), questions: questions.map(buildQuestion) }; + if (req.body.lvNames !== undefined) set.lvNames = req.body.lvNames; + data.sets.push(set); + saveData(); + res.json({ set: { id: set.id, name: set.name, count: set.questions.length }, imported: set.questions.length }); +}); + +app.get('/api/admin/sets/:id/export', requireAuth, (req, res) => { + const id = Number(req.params.id); + const set = data.sets.find(s => s.id === id); + if (!set) return res.status(404).json({ error: '套题不存在' }); + res.json({ + name: set.name, + lvNames: set.lvNames || null, + exportedAt: new Date().toISOString(), + questions: set.questions.map(q => ({ lv: q.lv, q: q.q, opts: q.opts, ans: q.ans, exp: q.exp })) + }); +}); + +app.post('/api/admin/sets/:id/questions', requireAuth, (req, res) => { + const id = Number(req.params.id); + const set = data.sets.find(s => s.id === id); + if (!set) return res.status(404).json({ error: '套题不存在' }); + const err = validateBody(req.body); + if (err) return res.status(400).json({ error: err }); + const item = buildQuestion(req.body); + set.questions.push(item); + saveData(); + res.json(item); +}); + +app.put('/api/admin/questions/:qid', requireAuth, (req, res) => { + const hit = findQuestion(Number(req.params.qid)); + if (!hit) return res.status(404).json({ error: '题目不存在' }); + const err = validateBody(req.body); + if (err) return res.status(400).json({ error: err }); + const { lv, q, opts, ans, exp } = req.body; + const item = hit.q; + item.lv = lv === undefined ? item.lv : lv; + item.q = q.trim(); + item.opts = opts; + item.ans = ans; + item.exp = (exp || '').trim(); + saveData(); + res.json(item); +}); + +app.delete('/api/admin/questions/:qid', requireAuth, (req, res) => { + const hit = findQuestion(Number(req.params.qid)); + if (!hit) return res.status(404).json({ error: '题目不存在' }); + const idx = hit.set.questions.indexOf(hit.q); + hit.set.questions.splice(idx, 1); + saveData(); + res.json({ ok: true }); +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log('测验系统已启动: http://localhost:' + PORT); + console.log('答题页: http://localhost:' + PORT + '/'); + console.log('管理后台: http://localhost:' + PORT + '/admin.html'); + console.log('管理员账号: ' + ADMIN_USER + ' / ' + ADMIN_PASS); +}); diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..7dc24f5 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + root: 'web', + plugins: [vue()], + build: { + outDir: '../dist', + emptyOutDir: true + }, + server: { + port: 5173, + proxy: { + '/api': 'http://localhost:3030' + } + } +}) diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..b40a325 --- /dev/null +++ b/web/index.html @@ -0,0 +1,15 @@ + + + + + + 砚 · Dart 测验 + + + + + +
+ + + diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..f946767 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,26 @@ + + + diff --git a/web/src/api.js b/web/src/api.js new file mode 100644 index 0000000..5ff7490 --- /dev/null +++ b/web/src/api.js @@ -0,0 +1,34 @@ +const TOKEN_KEY = 'quiz_token' + +export function getToken() { + return localStorage.getItem(TOKEN_KEY) || '' +} + +export function setToken(token) { + localStorage.setItem(TOKEN_KEY, token) +} + +export function clearToken() { + localStorage.removeItem(TOKEN_KEY) +} + +export async function api(path, options = {}) { + const headers = Object.assign({}, options.headers || {}) + if (getToken()) headers.Authorization = 'Bearer ' + getToken() + if (options.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json' + const res = await fetch('/api' + path, Object.assign({}, options, { headers })) + const data = await res.json().catch(() => ({})) + if (!res.ok) { + const err = new Error(data.error || '请求失败') + err.status = res.status + throw err + } + return data +} + +export async function apiPublic(path, options = {}) { + const res = await fetch('/api' + path, options) + const data = await res.json().catch(() => ({})) + if (!res.ok) throw new Error(data.error || '请求失败') + return data +} diff --git a/web/src/htmlUtil.mjs b/web/src/htmlUtil.mjs new file mode 100644 index 0000000..20ed712 --- /dev/null +++ b/web/src/htmlUtil.mjs @@ -0,0 +1,36 @@ +const ALLOWED_TAGS = [ + 'b', 'strong', 'i', 'em', 'code', 'br', 'span', 'div', 'p', + 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'blockquote', 'pre', 'u', 's', 'mark', 'small', 'sub', 'sup' +] + +const TAG_RE = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^<>]*>/g + +export function escapeSmartHtml(html) { + const placeholders = [] + const text = String(html).replace(TAG_RE, (m, tag) => { + if (ALLOWED_TAGS.includes(tag.toLowerCase())) { + placeholders.push(m) + return '\u0000' + (placeholders.length - 1) + '\u0000' + } + return m + }) + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\u0000(\d+)\u0000/g, (_, i) => placeholders[Number(i)]) +} + +export function decodeEntities(text) { + return String(text) + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&/g, '&') +} + +export function stripHtmlAndDecode(html) { + return decodeEntities(String(html).replace(/<[^>]*>/g, '')) +} diff --git a/web/src/main.js b/web/src/main.js new file mode 100644 index 0000000..2d2f526 --- /dev/null +++ b/web/src/main.js @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import './styles/theme.css' + +createApp(App).use(router).mount('#app') diff --git a/web/src/router.js b/web/src/router.js new file mode 100644 index 0000000..e0dbd1c --- /dev/null +++ b/web/src/router.js @@ -0,0 +1,14 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import QuizView from './views/QuizView.vue' +import AdminView from './views/AdminView.vue' + +const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { path: '/', name: 'quiz', component: QuizView }, + { path: '/quiz', name: 'quiz-alt', component: QuizView }, + { path: '/admin', name: 'admin', component: AdminView } + ] +}) + +export default router diff --git a/web/src/styles/theme.css b/web/src/styles/theme.css new file mode 100644 index 0000000..bbaefd4 --- /dev/null +++ b/web/src/styles/theme.css @@ -0,0 +1,295 @@ +:root { + --surface: #faf7f0; + --surface-alt: #f3efe5; + --surface-high: #ece7d9; + --primary: #3d6b5e; + --on-primary: #ffffff; + --primary-container: #d9e9e2; + --on-primary-container: #123c31; + --secondary: #5c6b74; + --secondary-container: #e2e9ee; + --on-secondary-container: #1d2b33; + --ink: #28251f; + --ink-secondary: #6f6a5f; + --ink-faint: #9a9387; + --error: #b23a2f; + --error-container: #fbe4e0; + --on-error-container: #7c1f16; + --success: #4c7a4a; + --success-container: #e2eedc; + --on-success-container: #23411f; + --hairline: #e0dbcf; + --hairline-strong: #cfc8b8; + --shadow: 0 1px 2px rgba(40, 37, 31, .05), 0 8px 24px rgba(40, 37, 31, .07); + --shadow-lift: 0 2px 4px rgba(40, 37, 31, .06), 0 16px 40px rgba(40, 37, 31, .12); + --radius-s: 10px; + --radius-m: 16px; + --radius-l: 24px; + --serif: 'Noto Serif SC', 'Songti SC', 'SimSun', serif; + --sans: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +html { -webkit-font-smoothing: antialiased; } + +body { + font-family: var(--sans); + color: var(--ink); + min-height: 100vh; + background: + radial-gradient(900px 500px at 85% -10%, rgba(61, 107, 94, .10), transparent 60%), + radial-gradient(800px 480px at -10% 110%, rgba(92, 107, 116, .10), transparent 60%), + var(--surface); +} + +#app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* ---------- 品牌头 ---------- */ +.app-header { + display: flex; + align-items: center; + gap: 14px; + padding: 26px 32px 18px; + max-width: 860px; + width: 100%; + margin: 0 auto; +} + +.brand-seal { + width: 46px; + height: 46px; + border-radius: 12px; + background: linear-gradient(160deg, #b23a2f, #8f2b22); + color: #fdf6ec; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--serif); + font-weight: 900; + font-size: 24px; + box-shadow: 0 4px 12px rgba(178, 58, 47, .35); + flex-shrink: 0; +} + +.brand-name { + font-family: var(--serif); + font-weight: 900; + font-size: 22px; + letter-spacing: 2px; + color: var(--ink); +} + +.brand-sub { + font-size: 11px; + color: var(--ink-faint); + letter-spacing: 3px; + margin-top: 2px; +} + +.app-nav { + margin-left: auto; + display: flex; + gap: 6px; +} + +.nav-link { + text-decoration: none; + color: var(--ink-secondary); + font-size: 13px; + font-weight: 500; + padding: 7px 14px; + border-radius: 999px; + transition: all .2s; +} + +.nav-link:hover { background: var(--surface-high); } + +.nav-link.router-link-active { + background: var(--ink); + color: var(--surface); +} + +.app-main { + flex: 1; + width: 100%; + max-width: 860px; + margin: 0 auto; + padding: 10px 20px 40px; +} + +.app-footer { + text-align: center; + padding: 18px; + color: var(--ink-faint); + font-size: 12px; + letter-spacing: 1px; +} + +/* ---------- 卡片 ---------- */ +.card { + background: var(--surface); + border: 1px solid var(--hairline); + border-radius: var(--radius-l); + box-shadow: var(--shadow); + padding: 28px 30px; +} + +.card + .card { margin-top: 20px; } + +/* ---------- 按钮 ---------- */ +.btn { + border: none; + border-radius: 12px; + padding: 11px 22px; + font-size: 14px; + font-weight: 700; + font-family: inherit; + cursor: pointer; + transition: transform .1s, box-shadow .2s, background .2s, color .2s, opacity .2s; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.btn:active { transform: scale(.97); } +.btn:disabled { opacity: .45; cursor: not-allowed; } + +.btn-filled { background: var(--ink); color: var(--surface); } +.btn-filled:hover:not(:disabled) { box-shadow: var(--shadow-lift); } + +.btn-primary { background: var(--primary); color: var(--on-primary); } +.btn-primary:hover:not(:disabled) { box-shadow: var(--shadow-lift); } + +.btn-tonal { background: var(--primary-container); color: var(--on-primary-container); } +.btn-tonal:hover:not(:disabled) { filter: brightness(.97); } + +.btn-outline { + background: transparent; + border: 1px solid var(--hairline-strong); + color: var(--ink-secondary); +} +.btn-outline:hover:not(:disabled) { border-color: var(--ink); color: var(--ink); } + +.btn-danger { background: var(--error-container); color: var(--on-error-container); } +.btn-danger:hover:not(:disabled) { filter: brightness(.97); } + +.btn-sm { padding: 7px 14px; font-size: 12.5px; border-radius: 10px; } + +/* ---------- 表单 ---------- */ +.field { margin-bottom: 16px; } +.field label { + display: block; + font-size: 12.5px; + font-weight: 700; + color: var(--ink-secondary); + margin-bottom: 7px; + letter-spacing: .5px; +} + +.input, .textarea, .select { + width: 100%; + border: 1px solid var(--hairline); + background: var(--surface-alt); + border-radius: 12px; + padding: 11px 14px; + font-size: 14px; + font-family: inherit; + color: var(--ink); + transition: border-color .2s, background .2s, box-shadow .2s; +} + +.input:focus, .textarea:focus, .select:focus { + outline: none; + border-color: var(--primary); + background: var(--surface); + box-shadow: 0 0 0 3px rgba(61, 107, 94, .15); +} + +.textarea { min-height: 80px; resize: vertical; line-height: 1.6; } + +/* ---------- 徽章/章 ---------- */ +.seal { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 700; + letter-spacing: .5px; + padding: 3px 10px; + border-radius: 999px; + white-space: nowrap; +} + +.seal-lv1 { background: #e2e9ee; color: #2c4b5e; } +.seal-lv2 { background: #e2eedc; color: #2c5226; } +.seal-lv3 { background: #f6e9d7; color: #7a4a13; } +.seal-lv4 { background: #efe2ee; color: #5c2f5c; } +.seal-lv5 { background: #fbe4e0; color: #8c2c21; } +.seal-lv6 { background: #d9e9e2; color: #1e5241; } + +.msg { font-size: 13px; margin-top: 8px; min-height: 18px; } +.msg.err { color: var(--error); } +.msg.ok { color: var(--success); } + +/* ---------- 表格 ---------- */ +.table { width: 100%; border-collapse: collapse; font-size: 14px; } +.table th { + text-align: left; + color: var(--ink-faint); + font-size: 12px; + font-weight: 700; + padding: 10px 10px; + border-bottom: 1px solid var(--hairline); + letter-spacing: .5px; +} +.table td { + padding: 12px 10px; + border-bottom: 1px solid var(--hairline); + vertical-align: middle; +} +.table tbody tr { transition: background .15s; } +.table tbody tr:hover { background: var(--surface-alt); } + +/* ---------- 过渡动画 ---------- */ +.fade-enter-active, .fade-leave-active { transition: opacity .25s ease; } +.fade-enter-from, .fade-leave-to { opacity: 0; } + +.slide-enter-active { transition: all .3s ease; } +.slide-enter-from { opacity: 0; transform: translateY(14px); } + +.pop-enter-active { transition: all .35s cubic-bezier(.2, .9, .3, 1.3); } +.pop-enter-from { opacity: 0; transform: scale(.92); } + +@keyframes fadeUp { + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes popIn { + from { opacity: 0; transform: scale(.9); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes shakeX { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-4px); } + 75% { transform: translateX(4px); } +} + +.empty-state { + text-align: center; + color: var(--ink-faint); + padding: 48px 0; + font-size: 14px; +} + +@media (max-width: 640px) { + .app-header { padding: 20px 16px 12px; } + .card { padding: 20px; border-radius: 20px; } + .brand-name { font-size: 18px; } +} diff --git a/web/src/views/AdminView.vue b/web/src/views/AdminView.vue new file mode 100644 index 0000000..bd1ddfc --- /dev/null +++ b/web/src/views/AdminView.vue @@ -0,0 +1,617 @@ + + + + + diff --git a/web/src/views/QuizView.vue b/web/src/views/QuizView.vue new file mode 100644 index 0000000..a5f65ff --- /dev/null +++ b/web/src/views/QuizView.vue @@ -0,0 +1,398 @@ + + + + +