1st_version
This commit is contained in:
346
vue/package-lock.json
generated
346
vue/package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"": {
|
||||
"name": "test1",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"vue": "^3.5.25"
|
||||
@@ -17,6 +18,7 @@
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.18",
|
||||
"vue-tsc": "^3.1.5"
|
||||
}
|
||||
},
|
||||
@@ -880,6 +882,31 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -914,6 +941,127 @@
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
||||
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.0.18",
|
||||
"@vitest/utils": "4.0.18",
|
||||
"chai": "^6.2.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
|
||||
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.18",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
||||
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
|
||||
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.18",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
||||
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
|
||||
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
|
||||
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "2.4.27",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz",
|
||||
@@ -1085,6 +1233,26 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -1103,6 +1271,13 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
@@ -1151,6 +1326,16 @@
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -1218,6 +1403,17 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
@@ -1225,6 +1421,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1317,6 +1520,13 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1326,6 +1536,37 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -1343,6 +1584,16 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
||||
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -1439,6 +1690,84 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"@vitest/runner": "4.0.18",
|
||||
"@vitest/snapshot": "4.0.18",
|
||||
"@vitest/spy": "4.0.18",
|
||||
"@vitest/utils": "4.0.18",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"expect-type": "^1.2.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^3.10.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"vite": "^6.0.0 || ^7.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.18",
|
||||
"@vitest/browser-preview": "4.0.18",
|
||||
"@vitest/browser-webdriverio": "4.0.18",
|
||||
"@vitest/ui": "4.0.18",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
@@ -1483,6 +1812,23 @@
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
@@ -25,6 +26,7 @@
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.18",
|
||||
"vue-tsc": "^3.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
612
vue/src/App.vue
612
vue/src/App.vue
@@ -13,6 +13,8 @@ import {
|
||||
Trophy,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch, type Component } from 'vue'
|
||||
import { isRectVisible } from './lighting'
|
||||
import { calculateRevealRadius, resolveInitialTheme, toggleTheme, type Theme } from './theme'
|
||||
|
||||
type SectionId = 'overview' | 'explorer' | 'games' | 'school'
|
||||
type ExplorerItemKind = 'folder' | 'file'
|
||||
@@ -47,6 +49,19 @@ interface ExplorerFolderTreeItem {
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
interface SectionMeta {
|
||||
eyebrow: string
|
||||
title: string
|
||||
description: string
|
||||
badge: string
|
||||
}
|
||||
|
||||
interface LightingTarget {
|
||||
element: HTMLElement
|
||||
left: number
|
||||
top: number
|
||||
}
|
||||
|
||||
const text = {
|
||||
skip: '跳到主要内容',
|
||||
loginTitle: 'Workspace Login',
|
||||
@@ -59,6 +74,14 @@ const text = {
|
||||
welcome: '欢迎回来',
|
||||
}
|
||||
|
||||
const THEME_STORAGE_KEY = 'workspace-theme'
|
||||
const THEME_REVEAL_DURATION_MS = 520
|
||||
const THEME_APPLY_OFFSET_MS = 320
|
||||
const THEME_CANVAS_COLORS: Record<Theme, string> = {
|
||||
light: '#f5f8fc',
|
||||
dark: '#070d16',
|
||||
}
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const loginError = ref('')
|
||||
@@ -81,12 +104,30 @@ const sidebarIndicatorStyle = ref({
|
||||
opacity: '0',
|
||||
})
|
||||
const sidebarIndicatorJelly = ref(false)
|
||||
const theme = ref<Theme>('light')
|
||||
const followsSystemTheme = ref(true)
|
||||
const themeRevealActive = ref(false)
|
||||
const themeRevealStyle = ref<Record<string, string>>({
|
||||
'--reveal-x': '50vw',
|
||||
'--reveal-y': '50vh',
|
||||
'--reveal-radius': '0px',
|
||||
'--reveal-color': THEME_CANVAS_COLORS.light,
|
||||
})
|
||||
let sidebarJellyTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let glowFrameId: number | null = null
|
||||
let lightingTargetRefreshFrameId: number | null = null
|
||||
let themeApplyTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let themeRevealCleanupTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let systemThemeMediaQuery: MediaQueryList | null = null
|
||||
let systemThemeChangeHandler: ((event: MediaQueryListEvent) => void) | null = null
|
||||
let latestPointer: { x: number; y: number } | null = null
|
||||
let latestPointerTarget: 'workspace' | 'login' | null = null
|
||||
let workspaceLightTargets: LightingTarget[] = []
|
||||
let loginLightTargets: LightingTarget[] = []
|
||||
let workspaceLightObserver: MutationObserver | null = null
|
||||
let loginLightObserver: MutationObserver | null = null
|
||||
const lightTargetSelector =
|
||||
'.nav-item, .ghost-btn, .primary-btn, .icon-btn, .game-card, .file-main, .folder-item, .topbar, .sidebar, .panel, .hero-card, .metric-card, .explorer-toolbar, .folder-list, .file-list, .file-card, .game-player, .study-card, .path-segment, .status'
|
||||
'.nav-item, .ghost-btn, .primary-btn, .icon-btn, .game-card, .file-main, .folder-item, .path-segment'
|
||||
const loginLightTargetSelector = '.login-card, .login-form button, .login-input-shell, .login-card h1'
|
||||
|
||||
const navItems: SectionNavItem[] = [
|
||||
@@ -113,6 +154,74 @@ const gameOptions: GameOption[] = [
|
||||
]
|
||||
|
||||
const currentUser = computed(() => username.value.trim() || 'Guest')
|
||||
const isDarkTheme = computed(() => theme.value === 'dark')
|
||||
const themeToggleAriaLabel = computed(() => (isDarkTheme.value ? '切换为浅色主题' : '切换为深色主题'))
|
||||
const sectionMeta = computed<SectionMeta>(() => {
|
||||
switch (activeSection.value) {
|
||||
case 'explorer':
|
||||
return {
|
||||
eyebrow: 'Explorer',
|
||||
title: '文件编排',
|
||||
description: '用更清晰的目录树和文件卡片处理工作区结构。',
|
||||
badge: `${explorerCurrentFolder.value?.name ?? 'Workspace'} · ${explorerChildItems.value.length} 项`,
|
||||
}
|
||||
case 'games':
|
||||
return {
|
||||
eyebrow: 'Arcade',
|
||||
title: '游戏中心',
|
||||
description: '切换到内置小游戏,支持沉浸式全屏与快速返回。',
|
||||
badge: activeGame.value ? `Now Playing · ${activeGame.value.label}` : `${gameOptions.length} 个可玩项目`,
|
||||
}
|
||||
case 'school':
|
||||
return {
|
||||
eyebrow: 'Learning',
|
||||
title: '学习路径',
|
||||
description: '把阶段任务、知识结构与部署能力集中到同一视图。',
|
||||
badge: '3 条核心进阶路线',
|
||||
}
|
||||
default:
|
||||
return {
|
||||
eyebrow: 'Overview',
|
||||
title: '总览驾驶舱',
|
||||
description: '把常用入口、当前状态与近期重点放在同一块主画布里。',
|
||||
badge: `${navItems.length} 个模块 · ${explorerFolders.value.length} 个目录`,
|
||||
}
|
||||
}
|
||||
})
|
||||
const overviewSignals = computed(() => [
|
||||
{
|
||||
label: '当前模块',
|
||||
value: sectionMeta.value.title,
|
||||
note: '主工作流状态',
|
||||
},
|
||||
{
|
||||
label: '路径深度',
|
||||
value: `${explorerPath.value.length} 层`,
|
||||
note: '当前文件位置',
|
||||
},
|
||||
{
|
||||
label: '主题模式',
|
||||
value: isDarkTheme.value ? 'Dark' : 'Light',
|
||||
note: followsSystemTheme.value ? '跟随系统' : '手动切换',
|
||||
},
|
||||
])
|
||||
const studyTracks = [
|
||||
{
|
||||
title: 'Frontend Systems',
|
||||
description: 'Vue + TypeScript 组件拆分、状态设计、可访问性。',
|
||||
meta: '组件化 · 可访问性 · 工程化',
|
||||
},
|
||||
{
|
||||
title: 'Graphics',
|
||||
description: '游戏渲染循环、碰撞检测、输入系统与性能优化。',
|
||||
meta: 'Canvas · 循环调度 · 性能',
|
||||
},
|
||||
{
|
||||
title: 'Deployment',
|
||||
description: '构建产物、缓存策略、静态资源托管与监控。',
|
||||
meta: '构建 · 托管 · 监控',
|
||||
},
|
||||
]
|
||||
|
||||
const explorerCurrentFolder = computed(
|
||||
() => explorerItems.value.find((item) => item.id === explorerCurrentFolderId.value) ?? explorerItems.value[0],
|
||||
@@ -196,38 +305,181 @@ function setSection(nextSection: SectionId) {
|
||||
statusMessage.value = `已切换到${navItems.find((item) => item.id === nextSection)?.title ?? ''}视图。`
|
||||
}
|
||||
|
||||
function applyMouseLighting(clientX: number, clientY: number) {
|
||||
const workspace = workspaceRef.value
|
||||
if (!workspace) return
|
||||
|
||||
const targets = workspace.querySelectorAll<HTMLElement>(lightTargetSelector)
|
||||
for (const target of targets) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
target.style.setProperty('--lx', `${clientX - rect.left}px`)
|
||||
target.style.setProperty('--ly', `${clientY - rect.top}px`)
|
||||
function readStoredTheme(): Theme | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(THEME_STORAGE_KEY)
|
||||
return raw === 'light' || raw === 'dark' ? raw : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredTheme(nextTheme: Theme) {
|
||||
try {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, nextTheme)
|
||||
} catch {
|
||||
// Ignore storage write errors (private mode, quota, etc.)
|
||||
}
|
||||
}
|
||||
|
||||
function updateThemeMeta(nextTheme: Theme) {
|
||||
let meta = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta')
|
||||
meta.name = 'theme-color'
|
||||
document.head.appendChild(meta)
|
||||
}
|
||||
meta.content = THEME_CANVAS_COLORS[nextTheme]
|
||||
}
|
||||
|
||||
function applyTheme(nextTheme: Theme) {
|
||||
theme.value = nextTheme
|
||||
document.documentElement.setAttribute('data-theme', nextTheme)
|
||||
updateThemeMeta(nextTheme)
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
|
||||
function clearThemeTimers() {
|
||||
if (themeApplyTimer) {
|
||||
clearTimeout(themeApplyTimer)
|
||||
themeApplyTimer = null
|
||||
}
|
||||
if (themeRevealCleanupTimer) {
|
||||
clearTimeout(themeRevealCleanupTimer)
|
||||
themeRevealCleanupTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleThemeToggle(event: MouseEvent) {
|
||||
const nextTheme = toggleTheme(theme.value)
|
||||
followsSystemTheme.value = false
|
||||
writeStoredTheme(nextTheme)
|
||||
|
||||
const toggleButton = event.currentTarget instanceof HTMLElement ? event.currentTarget : null
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
if (reduceMotion || !toggleButton) {
|
||||
themeRevealActive.value = false
|
||||
clearThemeTimers()
|
||||
applyTheme(nextTheme)
|
||||
return
|
||||
}
|
||||
|
||||
clearThemeTimers()
|
||||
|
||||
const rect = toggleButton.getBoundingClientRect()
|
||||
const originX = rect.left + rect.width / 2
|
||||
const originY = rect.top + rect.height / 2
|
||||
const radius = calculateRevealRadius(originX, originY, window.innerWidth, window.innerHeight)
|
||||
|
||||
themeRevealStyle.value = {
|
||||
'--reveal-x': `${originX}px`,
|
||||
'--reveal-y': `${originY}px`,
|
||||
'--reveal-radius': `${radius}px`,
|
||||
'--reveal-color': THEME_CANVAS_COLORS[nextTheme],
|
||||
}
|
||||
|
||||
themeRevealActive.value = false
|
||||
requestAnimationFrame(() => {
|
||||
themeRevealActive.value = true
|
||||
})
|
||||
|
||||
themeApplyTimer = setTimeout(() => {
|
||||
applyTheme(nextTheme)
|
||||
themeApplyTimer = null
|
||||
}, THEME_APPLY_OFFSET_MS)
|
||||
|
||||
themeRevealCleanupTimer = setTimeout(() => {
|
||||
themeRevealActive.value = false
|
||||
themeRevealCleanupTimer = null
|
||||
}, THEME_REVEAL_DURATION_MS)
|
||||
}
|
||||
|
||||
function initializeTheme() {
|
||||
const storedTheme = readStoredTheme()
|
||||
followsSystemTheme.value = storedTheme === null
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
applyTheme(resolveInitialTheme(storedTheme, prefersDark))
|
||||
}
|
||||
|
||||
function buildLightingTargets(root: HTMLElement, selector: string) {
|
||||
const viewportWidth = window.innerWidth
|
||||
const viewportHeight = window.innerHeight
|
||||
const targets: LightingTarget[] = []
|
||||
for (const element of root.querySelectorAll<HTMLElement>(selector)) {
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (!isRectVisible(rect, viewportWidth, viewportHeight)) continue
|
||||
const target: LightingTarget = {
|
||||
element,
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
}
|
||||
element.style.setProperty('--target-left', `${target.left}px`)
|
||||
element.style.setProperty('--target-top', `${target.top}px`)
|
||||
targets.push(target)
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
function refreshLightingTargets() {
|
||||
lightingTargetRefreshFrameId = null
|
||||
workspaceLightTargets = workspaceRef.value ? buildLightingTargets(workspaceRef.value, lightTargetSelector) : []
|
||||
loginLightTargets = loginRef.value ? buildLightingTargets(loginRef.value, loginLightTargetSelector) : []
|
||||
}
|
||||
|
||||
function scheduleLightingTargetsRefresh() {
|
||||
if (lightingTargetRefreshFrameId !== null) return
|
||||
lightingTargetRefreshFrameId = requestAnimationFrame(refreshLightingTargets)
|
||||
}
|
||||
|
||||
function setContainerPointer(container: HTMLElement | null, clientX: number, clientY: number) {
|
||||
if (!container) return
|
||||
container.style.setProperty('--pointer-x', `${clientX}px`)
|
||||
container.style.setProperty('--pointer-y', `${clientY}px`)
|
||||
}
|
||||
|
||||
function clearContainerPointer(container: HTMLElement | null) {
|
||||
if (!container) return
|
||||
container.style.setProperty('--pointer-x', '-9999px')
|
||||
container.style.setProperty('--pointer-y', '-9999px')
|
||||
}
|
||||
|
||||
function reconnectLightingObservers() {
|
||||
if (workspaceLightObserver) {
|
||||
workspaceLightObserver.disconnect()
|
||||
workspaceLightObserver = null
|
||||
}
|
||||
if (loginLightObserver) {
|
||||
loginLightObserver.disconnect()
|
||||
loginLightObserver = null
|
||||
}
|
||||
|
||||
if (workspaceRef.value) {
|
||||
workspaceLightObserver = new MutationObserver(scheduleLightingTargetsRefresh)
|
||||
workspaceLightObserver.observe(workspaceRef.value, { childList: true, subtree: true })
|
||||
}
|
||||
if (loginRef.value) {
|
||||
loginLightObserver = new MutationObserver(scheduleLightingTargetsRefresh)
|
||||
loginLightObserver.observe(loginRef.value, { childList: true, subtree: true })
|
||||
}
|
||||
}
|
||||
|
||||
function applyMouseLighting(clientX: number, clientY: number) {
|
||||
setContainerPointer(workspaceRef.value, clientX, clientY)
|
||||
}
|
||||
|
||||
function applyLoginLighting(clientX: number, clientY: number) {
|
||||
const login = loginRef.value
|
||||
if (!login) return
|
||||
login.classList.add('lighting-active')
|
||||
|
||||
const targets = login.querySelectorAll<HTMLElement>(loginLightTargetSelector)
|
||||
for (const target of targets) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
const x = `${clientX - rect.left}px`
|
||||
const y = `${clientY - rect.top}px`
|
||||
target.style.setProperty('--lx', x)
|
||||
target.style.setProperty('--ly', y)
|
||||
target.style.setProperty('--mx', x)
|
||||
target.style.setProperty('--my', y)
|
||||
if (login) {
|
||||
login.classList.add('lighting-active')
|
||||
}
|
||||
setContainerPointer(login, clientX, clientY)
|
||||
}
|
||||
|
||||
function flushMouseLighting() {
|
||||
glowFrameId = null
|
||||
if (document.hidden) return
|
||||
if (!latestPointer) return
|
||||
|
||||
if (latestPointerTarget === 'workspace') {
|
||||
applyMouseLighting(latestPointer.x, latestPointer.y)
|
||||
} else if (latestPointerTarget === 'login') {
|
||||
@@ -236,6 +488,9 @@ function flushMouseLighting() {
|
||||
}
|
||||
|
||||
function onWorkspacePointerMove(event: PointerEvent) {
|
||||
if (!workspaceLightTargets.length) {
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
latestPointerTarget = 'workspace'
|
||||
latestPointer = { x: event.clientX, y: event.clientY }
|
||||
if (glowFrameId !== null) return
|
||||
@@ -243,17 +498,14 @@ function onWorkspacePointerMove(event: PointerEvent) {
|
||||
}
|
||||
|
||||
function onWorkspacePointerLeave() {
|
||||
const workspace = workspaceRef.value
|
||||
if (!workspace) return
|
||||
const targets = workspace.querySelectorAll<HTMLElement>(lightTargetSelector)
|
||||
for (const target of targets) {
|
||||
target.style.setProperty('--lx', '-9999px')
|
||||
target.style.setProperty('--ly', '-9999px')
|
||||
}
|
||||
clearContainerPointer(workspaceRef.value)
|
||||
latestPointerTarget = null
|
||||
}
|
||||
|
||||
function onLoginPointerMove(event: PointerEvent) {
|
||||
if (!loginLightTargets.length) {
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
latestPointerTarget = 'login'
|
||||
latestPointer = { x: event.clientX, y: event.clientY }
|
||||
if (glowFrameId !== null) return
|
||||
@@ -264,13 +516,7 @@ function onLoginPointerLeave() {
|
||||
const login = loginRef.value
|
||||
if (!login) return
|
||||
login.classList.remove('lighting-active')
|
||||
const targets = login.querySelectorAll<HTMLElement>(loginLightTargetSelector)
|
||||
for (const target of targets) {
|
||||
target.style.setProperty('--lx', '-9999px')
|
||||
target.style.setProperty('--ly', '-9999px')
|
||||
target.style.setProperty('--mx', '-9999px')
|
||||
target.style.setProperty('--my', '-9999px')
|
||||
}
|
||||
clearContainerPointer(login)
|
||||
latestPointerTarget = null
|
||||
}
|
||||
|
||||
@@ -506,6 +752,15 @@ function onFullscreenChange() {
|
||||
|
||||
function onWindowResize() {
|
||||
updateSidebarIndicator(false)
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
|
||||
function onAnyScroll() {
|
||||
scheduleLightingTargetsRefresh()
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
initializeTheme()
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -513,98 +768,165 @@ watch(
|
||||
async () => {
|
||||
await nextTick()
|
||||
updateSidebarIndicator(true)
|
||||
scheduleLightingTargetsRefresh()
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
watch(isLoggedIn, async (loggedIn) => {
|
||||
if (!loggedIn) return
|
||||
await nextTick()
|
||||
updateSidebarIndicator(false)
|
||||
if (loggedIn) {
|
||||
updateSidebarIndicator(false)
|
||||
}
|
||||
refreshLightingTargets()
|
||||
reconnectLightingObservers()
|
||||
clearContainerPointer(workspaceRef.value)
|
||||
clearContainerPointer(loginRef.value)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('fullscreenchange', onFullscreenChange)
|
||||
window.addEventListener('resize', onWindowResize)
|
||||
document.addEventListener('scroll', onAnyScroll, true)
|
||||
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
systemThemeChangeHandler = (event) => {
|
||||
if (!followsSystemTheme.value) return
|
||||
applyTheme(event.matches ? 'dark' : 'light')
|
||||
}
|
||||
systemThemeMediaQuery.addEventListener('change', systemThemeChangeHandler)
|
||||
nextTick(() => {
|
||||
updateSidebarIndicator(false)
|
||||
refreshLightingTargets()
|
||||
reconnectLightingObservers()
|
||||
clearContainerPointer(workspaceRef.value)
|
||||
clearContainerPointer(loginRef.value)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('fullscreenchange', onFullscreenChange)
|
||||
window.removeEventListener('resize', onWindowResize)
|
||||
document.removeEventListener('scroll', onAnyScroll, true)
|
||||
if (systemThemeMediaQuery && systemThemeChangeHandler) {
|
||||
systemThemeMediaQuery.removeEventListener('change', systemThemeChangeHandler)
|
||||
}
|
||||
clearThemeTimers()
|
||||
if (sidebarJellyTimer) {
|
||||
clearTimeout(sidebarJellyTimer)
|
||||
}
|
||||
if (glowFrameId !== null) {
|
||||
cancelAnimationFrame(glowFrameId)
|
||||
}
|
||||
if (lightingTargetRefreshFrameId !== null) {
|
||||
cancelAnimationFrame(lightingTargetRefreshFrameId)
|
||||
}
|
||||
if (workspaceLightObserver) {
|
||||
workspaceLightObserver.disconnect()
|
||||
}
|
||||
if (loginLightObserver) {
|
||||
loginLightObserver.disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a class="skip-link" href="#main-content">{{ text.skip }}</a>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="theme-reveal-layer"
|
||||
:class="{ active: themeRevealActive }"
|
||||
:style="themeRevealStyle"
|
||||
></div>
|
||||
|
||||
<main
|
||||
v-if="!isLoggedIn"
|
||||
id="main-content"
|
||||
ref="loginRef"
|
||||
class="login-view"
|
||||
@pointermove="onLoginPointerMove"
|
||||
@pointerleave="onLoginPointerLeave"
|
||||
>
|
||||
<section class="login-card" aria-labelledby="login-title">
|
||||
<p class="eyebrow">Workspace Console</p>
|
||||
<h1 id="login-title">{{ text.loginTitle }}</h1>
|
||||
<p class="subtitle">{{ text.loginSubtitle }}</p>
|
||||
<Transition name="view-swap" mode="out-in">
|
||||
<main
|
||||
v-if="!isLoggedIn"
|
||||
id="main-content"
|
||||
key="login"
|
||||
ref="loginRef"
|
||||
class="login-view"
|
||||
@pointermove="onLoginPointerMove"
|
||||
@pointerleave="onLoginPointerLeave"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle theme-toggle-floating"
|
||||
:class="{ 'is-dark': isDarkTheme }"
|
||||
:aria-label="themeToggleAriaLabel"
|
||||
@click="handleThemeToggle"
|
||||
>
|
||||
<span class="theme-toggle-track" aria-hidden="true">
|
||||
<span class="theme-toggle-sun">浅</span>
|
||||
<span class="theme-toggle-moon">深</span>
|
||||
<span class="theme-toggle-thumb"></span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<form class="login-form" @submit.prevent="submitLogin">
|
||||
<label for="username">{{ text.username }}</label>
|
||||
<div class="login-input-shell">
|
||||
<input
|
||||
id="username"
|
||||
v-model="username"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
:placeholder="text.usernamePlaceholder"
|
||||
/>
|
||||
</div>
|
||||
<section class="login-card" aria-labelledby="login-title">
|
||||
<p class="eyebrow">Workspace Console</p>
|
||||
<h1 id="login-title">{{ text.loginTitle }}</h1>
|
||||
<p class="subtitle">{{ text.loginSubtitle }}</p>
|
||||
|
||||
<label for="password">{{ text.password }}</label>
|
||||
<div class="login-input-shell">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
type="password"
|
||||
:placeholder="text.passwordPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
<form class="login-form" @submit.prevent="submitLogin">
|
||||
<label for="username">{{ text.username }}</label>
|
||||
<div class="login-input-shell">
|
||||
<input
|
||||
id="username"
|
||||
v-model="username"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
:placeholder="text.usernamePlaceholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="loginError" class="form-error" aria-live="polite">{{ loginError }}</p>
|
||||
<button type="submit">{{ text.loginButton }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<label for="password">{{ text.password }}</label>
|
||||
<div class="login-input-shell">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
type="password"
|
||||
:placeholder="text.passwordPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<main
|
||||
v-else
|
||||
id="main-content"
|
||||
ref="workspaceRef"
|
||||
class="workspace-view"
|
||||
@pointermove="onWorkspacePointerMove"
|
||||
@pointerleave="onWorkspacePointerLeave"
|
||||
>
|
||||
<p v-if="loginError" class="form-error" aria-live="polite">{{ loginError }}</p>
|
||||
<button type="submit">{{ text.loginButton }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main
|
||||
v-else
|
||||
id="main-content"
|
||||
key="workspace"
|
||||
ref="workspaceRef"
|
||||
class="workspace-view"
|
||||
@pointermove="onWorkspacePointerMove"
|
||||
@pointerleave="onWorkspacePointerLeave"
|
||||
>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow">Workspace</p>
|
||||
<h1>Personal Command Center</h1>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
:class="{ 'is-dark': isDarkTheme }"
|
||||
:aria-label="themeToggleAriaLabel"
|
||||
@click="handleThemeToggle"
|
||||
>
|
||||
<span class="theme-toggle-track" aria-hidden="true">
|
||||
<span class="theme-toggle-sun">浅</span>
|
||||
<span class="theme-toggle-moon">深</span>
|
||||
<span class="theme-toggle-thumb"></span>
|
||||
</span>
|
||||
</button>
|
||||
<p class="user-chip">{{ currentUser }}</p>
|
||||
<button type="button" class="ghost-btn" @click="logout">退出</button>
|
||||
</div>
|
||||
@@ -638,33 +960,73 @@ onUnmounted(() => {
|
||||
</aside>
|
||||
|
||||
<section class="panel" aria-live="polite">
|
||||
<div v-if="activeSection === 'overview'" class="panel-body overview-panel">
|
||||
<article class="hero-card">
|
||||
<h2>一眼进入高频任务</h2>
|
||||
<p>从这里切换到文件、游戏或学习模块。相比原先桌面拖拽窗口模式,这里改为稳定导航 + 单主工作区,减少操作成本。</p>
|
||||
<div class="hero-actions">
|
||||
<button type="button" class="primary-btn" @click="setSection('explorer')">打开文件管理</button>
|
||||
<button type="button" class="ghost-btn" @click="setSection('games')">打开游戏中心</button>
|
||||
<div class="panel-body">
|
||||
<header class="panel-intro">
|
||||
<div>
|
||||
<p class="eyebrow">{{ sectionMeta.eyebrow }}</p>
|
||||
<h2>{{ sectionMeta.title }}</h2>
|
||||
<p>{{ sectionMeta.description }}</p>
|
||||
</div>
|
||||
</article>
|
||||
<span class="panel-intro-badge">{{ sectionMeta.badge }}</span>
|
||||
</header>
|
||||
|
||||
<div class="metric-grid">
|
||||
<article class="metric-card">
|
||||
<p>Folders</p>
|
||||
<strong>{{ explorerFolders.length }}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<p>Items In Current Folder</p>
|
||||
<strong>{{ explorerChildItems.length }}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<p>Games</p>
|
||||
<strong>{{ gameOptions.length }}</strong>
|
||||
</article>
|
||||
<div v-if="activeSection === 'overview'" class="panel-section overview-panel">
|
||||
<div class="overview-hero-grid">
|
||||
<article class="hero-card hero-card-featured">
|
||||
<div class="hero-copy">
|
||||
<span class="hero-kicker">Workspace Flow</span>
|
||||
<h3>一眼进入高频任务</h3>
|
||||
<p>
|
||||
从这里切换到文件、游戏或学习模块。界面改为稳定导航 + 单主工作区,并用更强的层次、光感与状态反馈降低切换成本。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<button type="button" class="primary-btn" @click="setSection('explorer')">打开文件管理</button>
|
||||
<button type="button" class="ghost-btn" @click="setSection('games')">打开游戏中心</button>
|
||||
</div>
|
||||
<div class="hero-stat-strip" aria-label="总览状态">
|
||||
<div v-for="signal in overviewSignals" :key="signal.label" class="hero-stat">
|
||||
<small>{{ signal.label }}</small>
|
||||
<strong>{{ signal.value }}</strong>
|
||||
<span>{{ signal.note }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="insight-rail" aria-label="工作区重点">
|
||||
<article class="insight-card ambient">
|
||||
<span class="insight-pill">Live Pulse</span>
|
||||
<strong>主工作区在线</strong>
|
||||
<p>导航、主题切换与局部高光已统一到同一套交互语言。</p>
|
||||
</article>
|
||||
<article class="insight-card">
|
||||
<span class="insight-pill neutral">Focus</span>
|
||||
<strong>{{ explorerCurrentFolder?.name ?? 'Workspace' }}</strong>
|
||||
<p>当前目录可直接新建、重命名与回退,减少跳转路径。</p>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="metric-grid">
|
||||
<article class="metric-card">
|
||||
<p>Folders</p>
|
||||
<strong>{{ explorerFolders.length }}</strong>
|
||||
<span>工作区目录总数</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<p>Items In Current Folder</p>
|
||||
<strong>{{ explorerChildItems.length }}</strong>
|
||||
<span>当前层级可操作项</span>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<p>Games</p>
|
||||
<strong>{{ gameOptions.length }}</strong>
|
||||
<span>支持快速进入沉浸模式</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeSection === 'explorer'" class="panel-body explorer-panel">
|
||||
<div v-else-if="activeSection === 'explorer'" class="panel-section explorer-panel">
|
||||
<header class="explorer-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
@@ -773,7 +1135,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeSection === 'games'" class="panel-body games-panel">
|
||||
<div v-else-if="activeSection === 'games'" class="panel-section games-panel">
|
||||
<div v-if="!activeGame" class="game-grid">
|
||||
<button
|
||||
v-for="game in gameOptions"
|
||||
@@ -787,6 +1149,7 @@ onUnmounted(() => {
|
||||
</span>
|
||||
<strong>{{ game.label }}</strong>
|
||||
<small>{{ game.subtitle }}</small>
|
||||
<span class="game-meta">Launch Experience</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -816,31 +1179,26 @@ onUnmounted(() => {
|
||||
allow="fullscreen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="panel-body school-panel">
|
||||
<div v-else class="panel-section school-panel">
|
||||
<article class="hero-card compact">
|
||||
<h2>学习路径</h2>
|
||||
<p>你可以把课程链接、阶段任务、周计划集中放在这里。</p>
|
||||
<h3>学习路径</h3>
|
||||
<p>把课程链接、阶段任务和周计划集中到一个高可读性的学习面板里。</p>
|
||||
</article>
|
||||
<div class="study-grid">
|
||||
<article class="study-card">
|
||||
<h3>Frontend</h3>
|
||||
<p>Vue + TypeScript 组件拆分、状态设计、可访问性。</p>
|
||||
</article>
|
||||
<article class="study-card">
|
||||
<h3>Graphics</h3>
|
||||
<p>游戏渲染循环、碰撞检测、输入系统与性能优化。</p>
|
||||
</article>
|
||||
<article class="study-card">
|
||||
<h3>Deployment</h3>
|
||||
<p>构建产物、缓存策略、静态资源托管与监控。</p>
|
||||
<article v-for="track in studyTracks" :key="track.title" class="study-card">
|
||||
<span class="study-meta">{{ track.meta }}</span>
|
||||
<h3>{{ track.title }}</h3>
|
||||
<p>{{ track.description }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<p class="status" aria-live="polite">{{ statusMessage }}</p>
|
||||
</main>
|
||||
</main>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
47
vue/src/lighting.spec.ts
Normal file
47
vue/src/lighting.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isRectNearPointer, isRectVisible, pickNearestRectIndices, shouldRunLightingFrame } from './lighting'
|
||||
|
||||
describe('isRectVisible', () => {
|
||||
it('returns true when rect intersects viewport', () => {
|
||||
expect(isRectVisible({ left: 10, top: 10, right: 110, bottom: 110 }, 400, 300)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when rect is far outside viewport', () => {
|
||||
expect(isRectVisible({ left: 900, top: 900, right: 980, bottom: 980 }, 400, 300)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isRectNearPointer', () => {
|
||||
it('returns true when pointer is inside rect', () => {
|
||||
expect(isRectNearPointer({ left: 100, top: 100, right: 200, bottom: 200 }, 150, 150, 160)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true when pointer is close to rect edge', () => {
|
||||
expect(isRectNearPointer({ left: 100, top: 100, right: 200, bottom: 200 }, 250, 150, 60)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when pointer is far from rect', () => {
|
||||
expect(isRectNearPointer({ left: 100, top: 100, right: 200, bottom: 200 }, 500, 500, 100)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldRunLightingFrame', () => {
|
||||
it('throttles frames above max fps', () => {
|
||||
expect(shouldRunLightingFrame(20, 0, 30)).toBe(false)
|
||||
expect(shouldRunLightingFrame(40, 0, 30)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pickNearestRectIndices', () => {
|
||||
it('returns nearest indices and honors max count', () => {
|
||||
const rects = [
|
||||
{ left: 0, top: 0, right: 60, bottom: 60 },
|
||||
{ left: 100, top: 0, right: 160, bottom: 60 },
|
||||
{ left: 200, top: 0, right: 260, bottom: 60 },
|
||||
{ left: 300, top: 0, right: 360, bottom: 60 },
|
||||
]
|
||||
const indices = pickNearestRectIndices(rects, 120, 20, 240, 2)
|
||||
expect(indices).toEqual([1, 0])
|
||||
})
|
||||
})
|
||||
67
vue/src/lighting.ts
Normal file
67
vue/src/lighting.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export interface RectLike {
|
||||
left: number
|
||||
top: number
|
||||
right: number
|
||||
bottom: number
|
||||
}
|
||||
|
||||
export function isRectVisible(
|
||||
rect: RectLike,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
overscan = 96,
|
||||
) {
|
||||
return !(
|
||||
rect.right < -overscan ||
|
||||
rect.bottom < -overscan ||
|
||||
rect.left > viewportWidth + overscan ||
|
||||
rect.top > viewportHeight + overscan
|
||||
)
|
||||
}
|
||||
|
||||
export function isRectNearPointer(rect: RectLike, x: number, y: number, influence = 180) {
|
||||
return !(
|
||||
x < rect.left - influence ||
|
||||
x > rect.right + influence ||
|
||||
y < rect.top - influence ||
|
||||
y > rect.bottom + influence
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldRunLightingFrame(now: number, last: number, maxFps: number) {
|
||||
if (maxFps <= 0) return true
|
||||
const minDelta = 1000 / maxFps
|
||||
return now - last >= minDelta
|
||||
}
|
||||
|
||||
function rectDistanceSquared(rect: RectLike, x: number, y: number) {
|
||||
const clampedX = Math.max(rect.left, Math.min(x, rect.right))
|
||||
const clampedY = Math.max(rect.top, Math.min(y, rect.bottom))
|
||||
const dx = x - clampedX
|
||||
const dy = y - clampedY
|
||||
return dx * dx + dy * dy
|
||||
}
|
||||
|
||||
export function pickNearestRectIndices(
|
||||
rects: RectLike[],
|
||||
x: number,
|
||||
y: number,
|
||||
influence = 180,
|
||||
maxCount = 10,
|
||||
) {
|
||||
if (!rects.length || maxCount <= 0) return []
|
||||
const influenceSquared = influence * influence
|
||||
const matches: Array<{ i: number; d: number }> = []
|
||||
|
||||
for (let i = 0; i < rects.length; i += 1) {
|
||||
const rect = rects[i]
|
||||
if (!rect) continue
|
||||
const d = rectDistanceSquared(rect, x, y)
|
||||
if (d <= influenceSquared) {
|
||||
matches.push({ i, d })
|
||||
}
|
||||
}
|
||||
|
||||
matches.sort((a, b) => a.d - b.d)
|
||||
return matches.slice(0, maxCount).map((item) => item.i)
|
||||
}
|
||||
1087
vue/src/style.css
1087
vue/src/style.css
File diff suppressed because it is too large
Load Diff
29
vue/src/theme.spec.ts
Normal file
29
vue/src/theme.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { calculateRevealRadius, resolveInitialTheme, toggleTheme } from './theme'
|
||||
|
||||
describe('resolveInitialTheme', () => {
|
||||
it('uses stored theme when present', () => {
|
||||
expect(resolveInitialTheme('dark', false)).toBe('dark')
|
||||
expect(resolveInitialTheme('light', true)).toBe('light')
|
||||
})
|
||||
|
||||
it('falls back to system preference when no stored theme', () => {
|
||||
expect(resolveInitialTheme(null, true)).toBe('dark')
|
||||
expect(resolveInitialTheme(null, false)).toBe('light')
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggleTheme', () => {
|
||||
it('toggles between light and dark', () => {
|
||||
expect(toggleTheme('light')).toBe('dark')
|
||||
expect(toggleTheme('dark')).toBe('light')
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateRevealRadius', () => {
|
||||
it('returns max distance from origin to viewport corners', () => {
|
||||
const radius = calculateRevealRadius(100, 100, 1000, 600)
|
||||
expect(radius).toBeCloseTo(Math.hypot(900, 500), 4)
|
||||
})
|
||||
})
|
||||
27
vue/src/theme.ts
Normal file
27
vue/src/theme.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export type Theme = 'light' | 'dark'
|
||||
|
||||
export function resolveInitialTheme(storedTheme: Theme | null, prefersDark: boolean): Theme {
|
||||
if (storedTheme === 'light' || storedTheme === 'dark') {
|
||||
return storedTheme
|
||||
}
|
||||
return prefersDark ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
export function toggleTheme(currentTheme: Theme): Theme {
|
||||
return currentTheme === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
|
||||
export function calculateRevealRadius(
|
||||
originX: number,
|
||||
originY: number,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
) {
|
||||
const distances = [
|
||||
Math.hypot(originX, originY),
|
||||
Math.hypot(viewportWidth - originX, originY),
|
||||
Math.hypot(originX, viewportHeight - originY),
|
||||
Math.hypot(viewportWidth - originX, viewportHeight - originY),
|
||||
]
|
||||
return Math.max(...distances)
|
||||
}
|
||||
Reference in New Issue
Block a user