Compare commits

...

9 Commits

Author SHA1 Message Date
f971782480 feat: add three js 2026-02-27 00:58:59 +08:00
af270b3d6a feat: optimize header and footer 2026-02-26 22:23:54 +08:00
ca5c3c8d21 feat: add lenis 2026-02-26 20:53:51 +08:00
e1defbd2dc feat: footer better, font 2026-02-26 03:05:05 +08:00
6153c7e87e feat: dynamic language change 2026-02-18 01:56:07 +08:00
1a8afd9573 feat: add translation 2026-02-18 01:19:02 +08:00
df4fad2c5a feat: add login system backend 2026-02-18 01:08:26 +08:00
dc1b192f47 feat: set nav by reset scroll 2026-02-17 22:04:35 +08:00
235c130a6d feat: login component 2026-02-16 02:12:51 +08:00
50 changed files with 1822 additions and 125 deletions

View File

@@ -1 +1 @@
VITE_SERVER=http://localhost:8080
VITE_SERVER=http://localhost:48080

View File

@@ -12,6 +12,8 @@
"daisyui": "^5.0.50",
"echarts": "^6.0.0",
"element-plus": "^2.13.2",
"gsap": "^3.14.2",
"lenis": "^1.3.17",
"lucide-vue-next": "^0.563.0",
"motion-v": "^1.6.1",
"pinia": "^3.0.3",
@@ -19,12 +21,15 @@
"radash": "^12.1.1",
"reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0",
"three": "^0.183.1",
"vue": "^3.5.17",
"vue-echarts": "^8.0.1",
"vue-i18n": "11",
"vue-router": "^4.5.1",
},
"devDependencies": {
"@iconify-json/material-symbols": "^1.2.55",
"@iconify/tailwind4": "^1.2.1",
"@tailwindcss/vite": "^4.1.11",
"@types/node": "^24.1.0",
"@vitejs/plugin-vue": "^6.0.0",
@@ -106,6 +111,8 @@
"@ctrl/tinycolor": ["@ctrl/tinycolor@3.6.1", "", {}, "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA=="],
"@cyberalien/svg-utils": ["@cyberalien/svg-utils@1.1.4", "", { "dependencies": { "@iconify/types": "^2.0.0" } }, "sha512-LlBw+9nsQPMO1QNJMuJSgXGWK1/r4D0PohTK0Uq1ehyGE420Qxq4+LVYkA9W0xkrNOCVYvRegbSy5By4f30W7w=="],
"@docsearch/css": ["@docsearch/css@4.5.3", "", {}, "sha512-kUpHaxn0AgI3LQfyzTYkNUuaFY4uEz/Ym9/N/FvyDE+PzSgZsCyDH9jE49B6N6f1eLCm9Yp64J9wENd6vypdxA=="],
"@docsearch/js": ["@docsearch/js@4.5.3", "", {}, "sha512-rcBiUMCXbZLqrLIT6F6FDcrG/tyvM2WM0zum6NPbIiQNDQxbSgmNc+/bToS0rxBsXaxiU64esiWoS02WqrWLsg=="],
@@ -200,10 +207,16 @@
"@iconify-json/logos": ["@iconify-json/logos@1.2.10", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-qxaXKJ6fu8jzTMPQdHtNxlfx6tBQ0jXRbHZIYy5Ilh8Lx9US9FsAdzZWUR8MXV8PnWTKGDFO4ZZee9VwerCyMA=="],
"@iconify-json/material-symbols": ["@iconify-json/material-symbols@1.2.55", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-9EHfqSpzXxQb5neA5/n5GcF15rccKG+oBJDwh/s8mmxfYswY1A4dKYPOWXCqrWMe/J3CmHb0tXFloscVarrqkQ=="],
"@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.68", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-bQPl1zuZlX6AnofreA1v7J+hoPncrFMppqGboe/SH54jZO37meiBUGBqNOxEpc0HKfZGxJaVVJwZd4gdMYu3hw=="],
"@iconify-json/vscode-icons": ["@iconify-json/vscode-icons@1.2.40", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-Q7JIWAxENwmcRg4EGRY+u16gBwrAj6mWeuSmuyuPvNvoTJHh8Ss8qoeDhrFYNgtWqNkzH5hSf4b2T9XLK5MsrA=="],
"@iconify/tailwind4": ["@iconify/tailwind4@1.2.1", "", { "dependencies": { "@iconify/tools": "^5.0.2", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0" }, "peerDependencies": { "tailwindcss": ">= 4.0.0" } }, "sha512-Hd7k8y7uzT3hk8ltw0jGku0r0wA8sc3d2iMvVTYv/9tMxBb+frZtWZGD9hDMU3EYuE+lMn58wi2lS8R2ZbwFcQ=="],
"@iconify/tools": ["@iconify/tools@5.0.3", "", { "dependencies": { "@cyberalien/svg-utils": "^1.1.1", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0", "fflate": "^0.8.2", "modern-tar": "^0.7.3", "pathe": "^2.0.3", "svgo": "^4.0.0" } }, "sha512-W5nbH5fNv20TvU49Al19Foos/ViAnmppbCNV9ieGl6/dRMDRzxeFol6peXX/NAgaOytQwZZxTTJRq/Kxd4eWsA=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
@@ -518,6 +531,8 @@
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
@@ -528,8 +543,16 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"daisyui": ["daisyui@5.1.10", "", {}, "sha512-p1J/HME2WmaSiy6u2alIbeP3gd5PNVft3+6Bdll0BRSm/UdI4084+pD01LxFug/5wGexNewWqbcEL6nB2n2o+Q=="],
@@ -568,6 +591,14 @@
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"echarts": ["echarts@6.0.0", "", { "dependencies": { "tslib": "2.3.0", "zrender": "6.0.0" } }, "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ=="],
@@ -640,6 +671,8 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -688,6 +721,8 @@
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"gsap": ["gsap@3.14.2", "", {}, "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA=="],
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
@@ -800,6 +835,8 @@
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"lenis": ["lenis@1.3.17", "", { "peerDependencies": { "@nuxt/kit": ">=3.0.0", "react": ">=17.0.0", "vue": ">=3.0.0" }, "optionalPeers": ["@nuxt/kit", "react", "vue"] }, "sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
@@ -850,6 +887,8 @@
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
"memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
@@ -884,6 +923,8 @@
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
"modern-tar": ["modern-tar@0.7.3", "", {}, "sha512-4W79zekKGyYU4JXVmB78DOscMFaJth2gGhgfTl2alWE4rNe3nf4N2pqenQ0rEtIewrnD79M687Ouba3YGTLOvg=="],
"motion-dom": ["motion-dom@12.23.12", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw=="],
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
@@ -1018,6 +1059,8 @@
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -1072,6 +1115,8 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
@@ -1082,6 +1127,8 @@
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"three": ["three@0.183.1", "", {}, "sha512-Psv6bbd3d/M/01MT2zZ+VmD0Vj2dbWTNhfe4CuSg7w5TuW96M3NOyCVuh9SZQ05CpGmD7NEcJhZw4GVjhCYxfQ=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@@ -1224,6 +1271,8 @@
"aria-hidden/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
"element-plus/@vueuse/core": ["@vueuse/core@10.11.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "10.11.1", "@vueuse/shared": "10.11.1", "vue-demi": ">=0.14.8" } }, "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww=="],
"eslint-plugin-vue/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@@ -1284,6 +1333,8 @@
"@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
"element-plus/@vueuse/core/@types/web-bluetooth": ["@types/web-bluetooth@0.0.20", "", {}, "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="],
"element-plus/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@10.11.1", "", {}, "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw=="],

28
components.d.ts vendored
View File

@@ -13,6 +13,10 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AnimatePresence: typeof import('motion-v')['AnimatePresence']
AnimText: typeof import('./src/components/special/AnimText.vue')['default']
AuthDialog: typeof import('./src/components/dialog/AuthDialog.vue')['default']
BasicIntroCard: typeof import('./src/components/card/BasicIntroCard.vue')['default']
BiliBiliIcon: typeof import('./src/components/icon/BiliBiliIcon.vue')['default']
Button: typeof import('./src/components/ui/button/Button.vue')['default']
Calendar: typeof import('./src/components/ui/calendar/Calendar.vue')['default']
CalendarCell: typeof import('./src/components/ui/calendar/CalendarCell.vue')['default']
@@ -28,12 +32,20 @@ declare module 'vue' {
CalendarPrevButton: typeof import('./src/components/ui/calendar/CalendarPrevButton.vue')['default']
ChangeLanguageDropdownButton: typeof import('./src/components/button/ChangeLanguageDropdownButton.vue')['default']
ChangeThemeDropdownButton: typeof import('./src/components/button/ChangeThemeDropdownButton.vue')['default']
CurtainReveal: typeof import('./src/components/layout/CurtainReveal.vue')['default']
DatePicker: typeof import('./src/components/date-picker/DatePicker.vue')['default']
DatePickerDisplayCard: typeof import('./src/components/card/DatePickerDisplayCard.vue')['default']
DevelopProgressCard: typeof import('./src/components/card/DevelopProgressCard.vue')['default']
DevelopProgressDiagram: typeof import('./src/components/diagram/DevelopProgressDiagram.vue')['default']
DouYinIcon: typeof import('./src/components/icon/DouYinIcon.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
FooterBar: typeof import('./src/components/layout/FooterBar.vue')['default']
FooterBarV2: typeof import('./src/components/layout/FooterBarV2.vue')['default']
FooterBarV2Placeholder: typeof import('./src/components/layout/FooterBarV2Placeholder.vue')['default']
FooterBarV2Space: typeof import('./src/components/layout/FooterBarV2Space.vue')['default']
LogoIcon: typeof import('./src/components/icon/LogoIcon.vue')['default']
LogoModel: typeof import('./src/components/three/LogoModel.vue')['default']
ModelViewer: typeof import('./src/components/three/ModelViewer.vue')['default']
NativeSelect: typeof import('./src/components/ui/native-select/NativeSelect.vue')['default']
NativeSelectOptGroup: typeof import('./src/components/ui/native-select/NativeSelectOptGroup.vue')['default']
NativeSelectOption: typeof import('./src/components/ui/native-select/NativeSelectOption.vue')['default']
@@ -42,14 +54,20 @@ declare module 'vue' {
PopoverAnchor: typeof import('./src/components/ui/popover/PopoverAnchor.vue')['default']
PopoverContent: typeof import('./src/components/ui/popover/PopoverContent.vue')['default']
PopoverTrigger: typeof import('./src/components/ui/popover/PopoverTrigger.vue')['default']
RedBookIcon: typeof import('./src/components/icon/RedBookIcon.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
UserAuthNavButton: typeof import('./src/components/button/UserAuthNavButton.vue')['default']
}
}
// For TSX support
declare global {
const AnimatePresence: typeof import('motion-v')['AnimatePresence']
const AnimText: typeof import('./src/components/special/AnimText.vue')['default']
const AuthDialog: typeof import('./src/components/dialog/AuthDialog.vue')['default']
const BasicIntroCard: typeof import('./src/components/card/BasicIntroCard.vue')['default']
const BiliBiliIcon: typeof import('./src/components/icon/BiliBiliIcon.vue')['default']
const Button: typeof import('./src/components/ui/button/Button.vue')['default']
const Calendar: typeof import('./src/components/ui/calendar/Calendar.vue')['default']
const CalendarCell: typeof import('./src/components/ui/calendar/CalendarCell.vue')['default']
@@ -65,12 +83,20 @@ declare global {
const CalendarPrevButton: typeof import('./src/components/ui/calendar/CalendarPrevButton.vue')['default']
const ChangeLanguageDropdownButton: typeof import('./src/components/button/ChangeLanguageDropdownButton.vue')['default']
const ChangeThemeDropdownButton: typeof import('./src/components/button/ChangeThemeDropdownButton.vue')['default']
const CurtainReveal: typeof import('./src/components/layout/CurtainReveal.vue')['default']
const DatePicker: typeof import('./src/components/date-picker/DatePicker.vue')['default']
const DatePickerDisplayCard: typeof import('./src/components/card/DatePickerDisplayCard.vue')['default']
const DevelopProgressCard: typeof import('./src/components/card/DevelopProgressCard.vue')['default']
const DevelopProgressDiagram: typeof import('./src/components/diagram/DevelopProgressDiagram.vue')['default']
const DouYinIcon: typeof import('./src/components/icon/DouYinIcon.vue')['default']
const ElButton: typeof import('element-plus/es')['ElButton']
const FooterBar: typeof import('./src/components/layout/FooterBar.vue')['default']
const FooterBarV2: typeof import('./src/components/layout/FooterBarV2.vue')['default']
const FooterBarV2Placeholder: typeof import('./src/components/layout/FooterBarV2Placeholder.vue')['default']
const FooterBarV2Space: typeof import('./src/components/layout/FooterBarV2Space.vue')['default']
const LogoIcon: typeof import('./src/components/icon/LogoIcon.vue')['default']
const LogoModel: typeof import('./src/components/three/LogoModel.vue')['default']
const ModelViewer: typeof import('./src/components/three/ModelViewer.vue')['default']
const NativeSelect: typeof import('./src/components/ui/native-select/NativeSelect.vue')['default']
const NativeSelectOptGroup: typeof import('./src/components/ui/native-select/NativeSelectOptGroup.vue')['default']
const NativeSelectOption: typeof import('./src/components/ui/native-select/NativeSelectOption.vue')['default']
@@ -79,6 +105,8 @@ declare global {
const PopoverAnchor: typeof import('./src/components/ui/popover/PopoverAnchor.vue')['default']
const PopoverContent: typeof import('./src/components/ui/popover/PopoverContent.vue')['default']
const PopoverTrigger: typeof import('./src/components/ui/popover/PopoverTrigger.vue')['default']
const RedBookIcon: typeof import('./src/components/icon/RedBookIcon.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
const UserAuthNavButton: typeof import('./src/components/button/UserAuthNavButton.vue')['default']
}

View File

@@ -29,6 +29,7 @@ export default defineConfig({
{ text: "图表", link: "/cli-feature/chart" },
{ text: "辅助工具", link: "/cli-feature/radash" },
{ text: "动画", link: "/cli-feature/motion" },
{ text: "字体", link: "/cli-feature/font" },
],
},
],

53
docs/cli-feature/font.md Normal file
View File

@@ -0,0 +1,53 @@
# 字体
## 自定义字体
脚手架提供了两种自定义字体
- [HarmonyOS_Sans_Regular.woff2](https://developer.huawei.com/consumer/cn/design/resource-V1/)
- [LXGWBright-Regular.woff2](https://github.com/lxgw/LxgwWenKai)
## 使用字体
您可以通过 tailwindcss 的类来调整局部字体,例如:
```html
<div class="font-Harmony">
这是 Harmony 字体
</div>
<div class="font-LXGW">
这是落霞孤鹜字体
</div>
```
您可以在 `style.css` 中定义全局字体,例如:
```css
@plugin "daisyui/theme" {
name: "light";
--color-primary: #5092d4;
--color-secondary: #bfd7f1;
--color-accent: #76b9ef;
/* 自定义字体在 App.vue 中加载 */
--font-sans: '"Harmony", "Microsoft YaHei", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
}
```
如果您需要其他自定义字体,请在 `App.vue` 中定义 font-face例如
```vue
<style>
@font-face {
font-family: "LXGW";
src: url("/fonts/LXGWBright-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
</style>
```
## 后续目标
后续我们将推出能够实时修改全局字体的组件

41
docs/cli-feature/icon.md Normal file
View File

@@ -0,0 +1,41 @@
# 图标
## 使用图标
可以通过 tailwindcss 中的图标 class 来直接使用图标,例如
```html
<span class="icon-[material-symbols--person-outline]"></span>
```
而你需要的图标可以在 [Iconify Material 图标库](https://icon-sets.iconify.design/material-symbols/) 中搜索。
通过点击图标库中的某个图标,你需要在详情栏中选择 CSS - Tailwind Css 栏,其中会告知你该图标对应的 class 名称。
## 调整图标
如果您需要调整颜色,您可以在 class 中添加 `text-<color>` 来调整图标颜色,例如
```html
<span class="icon-[material-symbols--person-outline] text-red-500"></span>
```
如果您需要调整图标大小,您可以在 class 中添加 `text-<size>` 来调整图标大小,例如
```html
<span class="icon-[material-symbols--person-outline] text-2xl"></span>
```
更多的调整方法可以参考 [Iconify 图标库调整](https://iconify.design/docs/usage/css/tailwind/tailwind4/size-color.html)
## 需要更多图标
您可以在 [Iconify 图标库](https://icon-sets.iconify.design/) 中搜索更多图标。
我们为您预装的是 Material Symbols 图标库。如果您需要使用别的图标库,可以通过 `@iconify-json/{prefix}` 来 install注意 {prefix} 的名称要改成中划线模式,例如您需要安装名为 Material Symbols Light 的图标库
```bash
bun add -D @iconify-json/material-symbols-light
```
更多的安装方法可以参考 [Iconify 图标库安装](https://iconify.design/docs/usage/css/tailwind/tailwind4/)

View File

@@ -31,3 +31,7 @@ Hucky 安装了 [motion-v](https://motion.net.cn/docs/vue) 库,您可以在组
:::tip 提示
exit 会自动为元素的进入也添加动画效果。
:::
## 3D 动画
Hucky 选用 [GSAP](https://gsap.com/docs/v3/) 作为 3D 动画库,您可以学习相关知识

View File

@@ -0,0 +1,13 @@
# 滚动
## 平滑滚动
Hucky 选择了 [Lenis](https://lenis.darkroom.engineering/) 为项目提供平滑滚动,您可以在 `App.vue` 中控制平滑滚动的开关。
## 滚动失效
如果您遇到了滚动失效的情况,这是因为 Lenis 和普通滚动冲突,您可以通过添加 `data-lenis-prevent` 属性来防止 lenis 平滑滚动。例如:
```html
<div data-lenis-prevent></div>
```

19
docs/cli-feature/three.md Normal file
View File

@@ -0,0 +1,19 @@
# 3D
## Three.js
Hucky 选用 [three.js](https://threejs.org/manual/#zh) 作为 3D 框架,您可以学习相关知识
## 首页
Hucky 的首页层叠关系非常复杂,在这里做解释
- z-10 导航栏 fixed 位于所有元素的最上方
- z-8 悬浮文字 仅次于导航栏,要求用户最先看到
- z-7 Three.js Canvas 位于悬浮文字下方,要求用户在看到悬浮文字后立即看到 3D 模型,同时将 canvas 背景透明,让用户可以看到 canvas 下方的主内容
- z-5 主内容区域 位于 Three.js Canvas 下方,主要提供了背景颜色
- z-1 页脚 位于所有元素的最下方且 fixed完成了类似幕布拉开的效果
## 3D 动画
Hucky 选用 [GSAP](https://gsap.com/docs/v3/) 作为 3D 动画库,您可以学习相关知识

View File

@@ -23,6 +23,8 @@
"daisyui": "^5.0.50",
"echarts": "^6.0.0",
"element-plus": "^2.13.2",
"gsap": "^3.14.2",
"lenis": "^1.3.17",
"lucide-vue-next": "^0.563.0",
"motion-v": "^1.6.1",
"pinia": "^3.0.3",
@@ -30,12 +32,15 @@
"radash": "^12.1.1",
"reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0",
"three": "^0.183.1",
"vue": "^3.5.17",
"vue-echarts": "^8.0.1",
"vue-i18n": "11",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@iconify-json/material-symbols": "^1.2.55",
"@iconify/tailwind4": "^1.2.1",
"@tailwindcss/vite": "^4.1.11",
"@types/node": "^24.1.0",
"@vitejs/plugin-vue": "^6.0.0",

BIN
public/contact/wechat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,21 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useLanguageStore } from "./stores/LanguageStore";
import Lenis from "lenis";
// 开启 lenis 平滑滚动
const lenis = new Lenis({
// 通过添加 data-lenis-prevent 属性来防止 lenis 平滑滚动
prevent: (node) =>
node.tagName === "SELECT" || node.hasAttribute("data-lenis-prevent"),
});
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
onMounted(() => {
// 加载 i18n 初始语言
@@ -14,4 +29,22 @@ onMounted(() => {
</div>
</template>
<style scoped></style>
<style>
/* 接触 scoped 开启全局模式加载字体 */
@font-face {
font-family: "LXGW";
src: url("/fonts/LXGWBright-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Harmony";
src: url("/fonts/HarmonyOS_Sans_Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
</style>

28
src/apis/system/oauth.ts Normal file
View File

@@ -0,0 +1,28 @@
import { httpWithCustomReturn } from "@/utils/http";
export const loginByEmailPasswordWithOauthApi = ({ email, password }) => {
return httpWithCustomReturn<{
code: number;
msg: string;
data: {
scope: string;
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
};
}>({
url: "/admin-api/system/oauth2/token",
method: "POST",
params: {
grant_type: "password",
username: email,
password,
client_id: "rc",
client_secret: "rc",
},
headers: {
"tenant-id": 1,
},
});
};

26
src/apis/system/user.ts Normal file
View File

@@ -0,0 +1,26 @@
import { httpWithCustomReturn } from "@/utils/http";
export const getUserInfoApi = () => {
return httpWithCustomReturn<{
code: number;
msg: string;
data: {
id: number;
username: string;
nickname: string;
email: string;
mobile: string;
sex: number;
avatar: string;
loginIp: string;
loginDate: null;
createTime: number;
roles: object[];
dept: null;
posts: null;
};
}>({
url: "/admin-api/system/user/profile/get",
method: "GET",
});
};

View File

@@ -0,0 +1,17 @@
import { httpWithCustomReturn } from "@/utils/http";
/**
*
* @returns 直接返回图片二进制数据
*/
export const getAuthQrcodeApi = () => {
return httpWithCustomReturn<Blob>({
url: "https://api.qrtool.cn",
method: "get",
responseType: "blob",
params: {
text: "测试",
size: 600,
},
});
};

View File

@@ -1,19 +0,0 @@
import http from "../utils/http";
/**
* This is an example, please remove if not needed
*/
export const getUserList = () => {
return http({
url: "/user/list",
method: "get",
});
};
export const insertUser = (data: { account: string; passowrd: string }) => {
return http({
url: "/user/insert",
method: "post",
data,
});
};

22
src/assets/logo.svg Normal file
View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="100%" height="100%">
<g fill="none" stroke="#000000" stroke-width="8" stroke-linejoin="miter" stroke-linecap="square">
<!-- Bottom horizontal line has been removed -->
<!-- Left Angle Bracket < ─ farther left + shorter -->
<polyline points="60,80 30,100 60,120" />
<!-- Right Angle Bracket > ─ farther right + shorter -->
<polyline points="140,80 170,100 140,120" />
<!-- Ladder Vertical Rails ─ unchanged width -->
<line x1="80" y1="50" x2="80" y2="150" />
<line x1="120" y1="50" x2="120" y2="150" />
<!-- Ladder Rungs ─ unchanged -->
<line x1="80" y1="75" x2="120" y2="75" />
<line x1="80" y1="100" x2="120" y2="100" />
<line x1="80" y1="125" x2="120" y2="125" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 827 B

View File

@@ -18,8 +18,6 @@ const onClickChangeLocal = (newLanguage: string) => {
tabindex="0"
role="button"
class="btn btn-sm btn-ghost gap-1 px-1.5 font-bold"
aria-label="Language"
title="Change Language"
>
<svg
class="text-base-content/70 size-4"
@@ -35,13 +33,10 @@ const onClickChangeLocal = (newLanguage: string) => {
d="M12 21a9 9 0 1 0 0-18m0 18a9 9 0 1 1 0-18m0 18c2.761 0 3.941-5.163 3.941-9S14.761 3 12 3m0 18c-2.761 0-3.941-5.163-3.941-9S9.239 3 12 3M3.5 9h17m-17 6h17"
/>
</svg>
<svg
class="mt-px hidden size-2 fill-current opacity-60 sm:inline-block"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2048 2048"
>
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z" />
</svg>
<span class="text-base-content/70">{{ t("nav.locale") }}</span>
<span
class="icon-[material-symbols--keyboard-arrow-down-rounded] text-lg text-base-content/70"
/>
</div>
<div
tabindex="0"

View File

@@ -1,6 +1,17 @@
<script setup lang="ts">
import { useGlobalThemeHook } from "@/hooks/globalThemeHook";
import { useI18n } from "vue-i18n";
import Lenis from "lenis";
// 开启 lenis 平滑滚动
const lenis = new Lenis();
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
const { t } = useI18n();
@@ -20,17 +31,13 @@ const { changeGlobalTheme, curGlobalTheme, optionalThemes } =
<div class="bg-secondary size-1 rounded-full" />
<div class="bg-accent size-1 rounded-full" />
</div>
<svg
class="mt-px hidden size-2 fill-current opacity-60 sm:inline-block"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2048 2048"
>
<path
d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"
/>
</svg>
<span class="text-base-content/70">{{ t("nav.theme") }}</span>
<span
class="icon-[material-symbols--keyboard-arrow-down-rounded] text-lg text-base-content/70"
/>
</div>
<ul
data-lenis-prevent
tabindex="-1"
class="dropdown-content bg-base-300 rounded-box z-1 w-52 p-2 shadow-2xl max-h-84 overflow-auto mt-6"
>

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import { renderAuthDialog } from "@/hooks/dialog/renderAuthDialog";
import { useUserStore } from "@/stores/UserStore";
// 用户 store
const userStore = useUserStore();
const dropdownVisible = ref(true);
const closeDropdown = () => {
dropdownVisible.value = false;
setTimeout(() => {
dropdownVisible.value = true;
}, 300);
};
// 退出登录
const handleLogout = () => {
userStore.clearUserInfo();
};
</script>
<template>
<div class="dropdown dropdown-hover dropdown-end">
<div
v-if="!userStore.checkUserLogin()"
role="button"
class="btn btn-sm btn-primary px-3"
@click="
renderAuthDialog('login');
closeDropdown();
"
>
<span>
{{ $t("nav.login") }}
</span>
<span class="icon-[material-symbols--login-rounded] text-sm" />
</div>
<div
v-else
role="button"
class="text-sm text-base-content/70 px-3 cursor-pointer"
>
{{ userStore.userInfo?.nickname || userStore.userInfo?.username }}
</div>
<!-- 未登录用户下拉框 -->
<ul
v-if="dropdownVisible && !userStore.checkUserLogin()"
tabindex="-1"
class="dropdown-content z-1 w-100 p-2"
>
<div class="card bg-base-200 w-96 mt-5 shadow-sm">
<div class="card-body">
<h2 class="card-title">{{ $t("nav.login_more_title") }}</h2>
<p>{{ $t("nav.login_more") }}</p>
<div class="card-actions justify-end">
<button
class="btn btn-sm"
@click="
renderAuthDialog('register');
closeDropdown();
"
>
{{ $t("nav.register") }}
</button>
<button
class="btn btn-primary btn-sm"
@click="
renderAuthDialog('login');
closeDropdown();
"
>
{{ $t("nav.login") }}
</button>
</div>
</div>
</div>
</ul>
<!-- 已登录用户下拉框 -->
<ul
v-if="dropdownVisible && userStore.checkUserLogin()"
tabindex="-1"
class="dropdown-content z-1 w-100 p-2"
>
<div class="card bg-base-200 w-96 mt-5 shadow-sm">
<div class="card-body">
<h2 class="card-title">{{ $t("nav.logout") }}</h2>
<p>{{ $t("nav.logout_more") }}</p>
<div class="card-actions justify-end">
<button class="btn btn-primary btn-sm" @click="handleLogout">
{{ $t("nav.logout") }}
</button>
</div>
</div>
</div>
</ul>
<!-- 登录弹窗 -->
<dialog id="my_modal" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold">Hello!</h3>
<p class="py-4">Press ESC key or click outside to close</p>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import AnimText from "@/components/special/AnimText.vue";
import { navigateTo } from "@/utils/navigator";
</script>
<template>
<div class="hero flex-1">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">
<AnimText text="home.welcome" />
</h1>
<div class="py-6">
<AnimText text="home.intro_line1" /><br />
<AnimText text="home.intro_line2" /><br />
<AnimText text="home.intro_line3" />
</div>
<button
class="btn btn-primary"
@click="navigateTo('http://localhost:5174')"
>
<AnimText text="home.read_doc" />
</button>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,429 @@
<script setup lang="ts">
import { loginByEmailPasswordWithOauthApi } from "@/apis/system/oauth";
import { getUserInfoApi } from "@/apis/system/user";
import { getAuthQrcodeApi } from "@/apis/temp/auth-qrcode";
import { closeAuthDialog } from "@/hooks/dialog/renderAuthDialog";
import { useUserStore } from "@/stores/UserStore";
import { AnimatePresence, motion } from "motion-v";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps({
defaultAuthFormType: {
type: String as PropType<"login" | "register">,
default: "login",
},
});
const authFormType = ref<"login" | "register" | "transition">(
props.defaultAuthFormType,
);
const changeAuthFormType = (type: "login" | "register") => {
authFormType.value = "transition";
setTimeout(() => {
authFormType.value = type;
}, 300);
};
const isNotClose = ref(true);
const closeAuthDialogWaitAnim = () => {
isNotClose.value = false;
setTimeout(() => {
closeAuthDialog();
}, 500);
};
// 登录方式
const loginType = ref<"password" | "phone">("password");
// 登录二维码
const authQrcode = ref("");
onMounted(async () => {
const qrImg = await getAuthQrcodeApi();
authQrcode.value = URL.createObjectURL(qrImg);
});
// 控制登录表单宽度
const loginFormWidth = computed(() => {
return authFormType.value === "login" ? "900px" : "550px";
});
// 登录表单数据绑定
const loginForm = reactive({
email: "",
password: "",
});
// 登录按钮点击事件
const clickLoginButtonEvent = async () => {
const { email, password } = loginForm;
if (!email || !password) {
return;
}
const res = await loginByEmailPasswordWithOauthApi({
email,
password,
});
if (res.code === 0) {
const store = useUserStore();
store.setAccessToken(res.data.access_token);
store.setRefreshToken(res.data.refresh_token);
// 进一步获取用户信息
const userInfoRes = await getUserInfoApi();
store.setUserInfo(userInfoRes.data);
closeAuthDialogWaitAnim();
}
};
</script>
<template>
<div>
<div class="fixed inset-0 flex items-center justify-center z-50">
<AnimatePresence>
<motion.div
v-show="isNotClose"
class="wrapper"
:initial="{ opacity: 0, scale: 0 }"
:animate="{ opacity: 1, scale: 1, width: loginFormWidth }"
:transition="{
duration: 0.4,
scale: { type: 'spring', visualDuration: 0.4, bounce: 0.5 },
}"
:exit="{
opacity: 0,
scale: 0.2,
}"
>
<span
class="icon-close bg-primary text-primary-content"
@click="closeAuthDialogWaitAnim"
>
<span class="icon-[material-symbols--close-small]" />
</span>
<!-- 登录表单 -->
<AnimatePresence>
<motion.div
v-if="authFormType === 'login'"
class="form-box login"
:initial="{
y: -100,
opacity: 0,
}"
:animate="{
y: 0,
opacity: 1,
}"
:transition="{
duration: 0.3,
scale: { type: 'spring', visualDuration: 0.3, bounce: 0.5 },
}"
:exit="{
y: 100,
opacity: 0,
}"
>
<div class="flex items-center gap-16">
<!-- 二维码登录区域 -->
<div class="flex flex-col items-center gap-4 w-[200px]">
<div class="text-2xl select-none">
{{ t("auth.scan_qr") }}
</div>
<img
v-if="authQrcode"
:src="authQrcode"
alt="登录二维码"
class="h-48 w-48"
/>
<div v-if="!authQrcode" class="skeleton h-48 w-48" />
<div class="text-sm text-gray-500 select-none">
{{ t("auth.scan_qr_more") }}
</div>
</div>
<div class="h-48 w-[1px] bg-gray-300/70" />
<!-- 账号登录区域 -->
<div>
<div class="flex justify-center items-center gap-3">
<div
class="text-2xl cursor-pointer transition duration-300"
:class="{ 'text-gray-400': loginType === 'phone' }"
:transition="{
duration: 0.3,
}"
@click="loginType = 'password'"
>
{{ t("auth.login_password") }}
</div>
<div class="h-8 w-[1px] bg-gray-300/70" />
<!-- 注册暂未开放 -->
<div class="tooltip">
<div class="tooltip-content">
<div class="font-black">{{ t("auth.not_open") }}</div>
</div>
<div
class="text-2xl cursor-pointer transition duration-300"
:class="{ 'text-gray-400': loginType === 'password' }"
:transition="{
duration: 0.3,
}"
>
{{ t("auth.login_phone") }}
</div>
</div>
</div>
<!-- 表单区域 -->
<form action="#">
<div class="input-box">
<span class="icon">
<span class="icon-[material-symbols--mail-outline]" />
</span>
<input v-model="loginForm.email" type="text" required />
<label>{{ t("auth.email") }}</label>
</div>
<div class="input-box">
<span class="icon">
<span class="icon-[material-symbols--lock-outline]" />
</span>
<input
v-model="loginForm.password"
type="password"
required
/>
<label>{{ t("auth.password") }}</label>
</div>
<div class="remember-forgot">
<label class="label">
<input type="checkbox" class="checkbox checkbox-xs" />
{{ t("auth.remember_me") }}
</label>
<a class="label" href="#">{{
t("auth.forget_password")
}}</a>
</div>
<button
class="btn bg-primary text-primary-content"
@click="clickLoginButtonEvent"
>
{{ t("auth.login") }}
</button>
<div
class="login-register"
@click="changeAuthFormType('register')"
>
<p>
{{ t("auth.no_account") }}
<a class="register-link cursor-pointer">
{{ t("auth.register") }}
</a>
</p>
</div>
</form>
</div>
</div>
</motion.div>
</AnimatePresence>
<!-- 注册表单 -->
<AnimatePresence>
<motion.div
v-if="authFormType === 'register'"
class="form-box register"
:initial="{
y: -100,
opacity: 0,
}"
:animate="{
y: 0,
opacity: 1,
}"
:transition="{
duration: 0.3,
scale: { type: 'spring', visualDuration: 0.3, bounce: 0.5 },
}"
:exit="{
y: 100,
opacity: 0,
}"
>
<div class="flex justify-center items-center gap-3">
<div class="text-2xl cursor-pointer">
{{ t("auth.register") }}
</div>
</div>
<form action="#">
<div class="input-box">
<span class="icon">
<span class="icon-[material-symbols--mail-outline]" />
</span>
<input type="text" required />
<label>{{ t("auth.email") }}</label>
</div>
<div class="input-box">
<span class="icon">
<span class="icon-[material-symbols--lock-outline]" />
</span>
<input type="password" required />
<label>{{ t("auth.password") }}</label>
</div>
<div class="remember-forgot">
<label class="label">
<input type="checkbox" class="checkbox checkbox-xs" />
{{ t("auth.agree_terms") }}
</label>
</div>
<button
type="submit"
class="btn bg-primary text-primary-content"
>
{{ t("auth.register") }}
</button>
<div
class="login-register"
@click="changeAuthFormType('login')"
>
<p>
{{ t("auth.have_account") }}
<a class="login-link cursor-pointer">
{{ t("auth.login") }}
</a>
</p>
</div>
</form>
</motion.div>
</AnimatePresence>
</motion.div>
</AnimatePresence>
</div>
</div>
</template>
<style scoped>
.wrapper {
height: 430px;
position: relative;
background: transparent;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 20px;
backdrop-filter: blur(20px);
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.wrapper .form-box {
padding: 40px 80px;
}
.wrapper .icon-close {
position: absolute;
top: 0;
right: 0;
width: 45px;
height: 45px;
font-size: 2em;
display: flex;
justify-content: center;
align-items: center;
border-bottom-left-radius: 20px;
cursor: pointer;
z-index: 1;
}
.input-box {
position: relative;
width: 350px;
height: 50px;
border-bottom: 2px solid;
margin: 30px 0;
}
.input-box label {
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
font-size: 1em;
font-weight: 500;
pointer-events: none;
transition: 0.5s;
}
.input-box input:focus ~ label,
.input-box input:valid ~ label {
top: -5px;
}
.input-box input {
width: 100%;
height: 100%;
background: transparent;
border: none;
outline: none;
font-size: 1em;
font-weight: 600;
padding: 0 35px 0 5px;
}
.input-box .icon {
position: absolute;
right: 8px;
font-size: 1.2rem;
line-height: 57px;
}
.remember-forgot {
font-size: 0.9em;
font-weight: 500;
margin: -15px 0 15px;
display: flex;
justify-content: space-between;
}
.remember-forgot label input {
margin-right: 3px;
}
.remember-forgot a {
text-decoration: none;
}
.remember-forgot a:hover {
text-decoration: underline;
}
.btn {
width: 100%;
height: 45px;
border: none;
outline: none;
border-radius: 6px;
cursor: pointer;
}
.login-register {
font-size: 0.9em;
text-align: center;
font-weight: 500;
margin: 25px 0 10px;
}
.login-register p a {
text-decoration: none;
font-weight: 600;
}
.login-register p a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts"></script>
<template>
<svg
height="2500"
viewBox="51.2 51.2 921.6 921.6"
width="2500"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="#f16c8d">
<path
d="m729.329 373.95c-9.795-5.945-19.062-6.785-19.144-6.785l-1.065-.05c-57.2-3.866-121.165-5.832-190.126-5.832l-13.988.005c-68.956 0-132.925 1.96-190.12 5.831l-1.066.052c-.082 0-9.349.84-19.144 6.784-15.047 9.129-24.273 25.948-27.417 49.97-10.071 76.913-4.383 173.65.19 251.393 2.938 49.966 33.407 62.459 85.048 67.149 10.782.988 69.089 5.867 159.508 5.893v-.005c90.42-.02 148.726-4.905 159.514-5.888 51.64-4.69 82.11-17.183 85.043-67.15 4.577-77.741 10.26-174.479.19-251.391-3.15-24.028-12.376-40.848-27.423-49.977zm-390.99 172.718a23.65 23.65 0 0 1 -31.687-10.845 23.68 23.68 0 0 1 10.844-31.687c2.038-1.004 50.693-24.725 110.541-43.065a23.68 23.68 0 1 1 13.88 45.292c-56.294 17.25-103.111 40.074-103.577 40.305zm268.898 35.886c-.44 2.232-11.269 54.64-50.939 54.64-21.442 0-36.1-14.049-44.984-26.772-8.694 12.708-22.805 26.772-42.655 26.772-35.533 0-50.135-48.266-51.681-53.77a11.366 11.366 0 0 1 21.878-6.17c2.75 9.652 14.13 37.202 29.798 37.202 16.374 0 28.892-23.644 31.985-31.928a11.372 11.372 0 0 1 10.65-7.388h.06a11.376 11.376 0 0 1 10.63 7.506c.107.286 11.965 31.815 34.314 31.815 20.864 0 28.565-35.952 28.641-36.32a11.346 11.346 0 0 1 13.358-8.94 11.361 11.361 0 0 1 8.945 13.353zm110.116-46.736a23.68 23.68 0 0 1 -31.683 10.844c-.47-.23-47.472-23.116-103.572-40.31a23.69 23.69 0 0 1 -15.708-29.583 23.67 23.67 0 0 1 29.578-15.703c59.848 18.34 108.498 42.061 110.551 43.065a23.68 23.68 0 0 1 10.834 31.687z"
/>
<path
d="m849.92 51.2h-675.84c-67.866 0-122.88 55.014-122.88 122.88v675.84c0 67.87 55.014 122.88 122.88 122.88h675.84c67.87 0 122.88-55.01 122.88-122.88v-675.84c0-67.86-55.01-122.88-122.88-122.88zm-36.603 627.45c-2.626 44.58-21.821 78.634-55.516 98.49-25.682 15.134-54.175 19.486-81.137 21.938-32.455 2.95-92.718 6.098-164.664 6.119-71.941-.02-132.209-3.164-164.664-6.119-26.962-2.452-55.455-6.804-81.132-21.939-33.695-19.855-52.89-53.903-55.51-98.483-4.706-80.133-10.574-179.855.194-262.108 10.654-81.383 70.102-104.976 100.612-106.168a2482.642 2482.642 0 0 1 81.423-4.086c-7.536-8.535-19.88-23.322-28.815-38.114-13.737-22.737 8.53-41.687 8.53-41.687s23.68-20.367 44.528 5.213c15.698 19.266 38.38 55.997 48.62 72.954l53.207-.215c13.26 0 26.332.072 39.22.215 10.24-16.957 32.92-53.683 48.619-72.954 20.843-25.58 44.528-5.213 44.528-5.213s22.262 18.95 8.525 41.687c-8.934 14.792-21.279 29.579-28.815 38.114 28.36.978 55.562 2.34 81.423 4.08 30.515 1.198 89.958 24.791 100.613 106.174 10.778 82.248 4.915 181.97.21 262.103z"
/>
</g>
</svg>
</template>
<style scoped></style>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts"></script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
t="1772042599876"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
p-id="1645"
width="200"
height="200"
>
<path
d="M855.52032 0H165.44032A165.696 165.696 0 0 0 0.00032 165.92v689.376a165.696 165.696 0 0 0 165.344 166.016h690.272a165.696 165.696 0 0 0 165.408-165.984V166.016A165.76 165.76 0 0 0 855.52032 0z"
fill="#170B1A"
p-id="1646"
/>
<path
d="M511.42432 302.08c0.576-64.32 0-128.576 0.576-192.832h131.392c-0.576 11.328 1.152 22.72 2.88 33.6h-96.704v522.24a124.8 124.8 0 0 1-16 63.68 107.84 107.84 0 0 1-83.008 52.864 111.424 111.424 0 0 1-63.168-13.12 108.416 108.416 0 0 1-36.928-32.96c33.536 18.816 77.312 17.088 109.76-3.968A111.68 111.68 0 0 0 512.00032 638.272c-0.576-112.064-0.576-224.128-0.576-336.192z m216.768-36.992c18.176 11.392 38.656 20.48 59.712 25.024 12.544 2.88 25.024 4.032 38.08 4.032v29.568a187.36 187.36 0 0 1-97.792-58.56v-0.064z"
fill="#25F4EE"
p-id="1647"
/>
<path
d="M274.75232 428.928a238.016 238.016 0 0 1 159.36-33.6v31.36c-14.72 0.576-29.376 2.272-43.84 5.12a249.44 249.44 0 0 0-97.92 43.84c-31.232 23.296-55.104 55.104-71.68 90.368a243.424 243.424 0 0 0-23.296 108.16c0 40.96 11.392 80.768 30.72 116.608 9.152 16.448 19.392 32.384 33.024 45.44a233.44 233.44 0 0 1-68.288-75.072 246.24 246.24 0 0 1-33.6-131.392 250.144 250.144 0 0 1 35.904-120 240.352 240.352 0 0 1 79.616-80.832z"
fill="#25F4EE"
p-id="1648"
/>
<path
d="M549.56832 142.784h97.28c3.392 18.752 10.24 36.416 18.752 53.504 13.632 26.176 33.024 49.472 58.048 64.832 1.664 1.152 2.88 2.304 3.968 3.968a186.688 186.688 0 0 0 98.432 58.56c0.576 34.176 0 68.864 0 103.04a308.16 308.16 0 0 1-180.928-57.472c0 81.92 0 163.84 0.576 245.76 0 10.816 0.576 21.632 0 32.96a268.8 268.8 0 0 1-35.264 113.792 247.392 247.392 0 0 1-68.288 77.44 219.296 219.296 0 0 1-124.608 42.56c-22.72 0.64-45.44-0.512-67.648-5.632a243.52 243.52 0 0 1-87.04-38.08l-1.728-1.728a202.464 202.464 0 0 1-33.024-45.504 244.32 244.32 0 0 1-30.72-116.672 245.6 245.6 0 0 1 23.36-108.096c16.512-35.2 40.96-67.072 71.68-90.432a249.44 249.44 0 0 1 141.632-48.896c0.576 13.12 0 26.176 0.576 38.656v66.56a94.176 94.176 0 0 0-51.776-1.728 128.32 128.32 0 0 0-55.68 27.328c-9.824 8.448-17.952 18.688-23.936 30.144a112.16 112.16 0 0 0-11.392 63.744c2.432 21.216 11.168 41.216 25.024 57.472 9.088 11.328 21.056 19.904 33.024 27.84 9.664 13.632 22.144 25.024 36.928 33.024 19.392 10.24 41.536 14.72 63.168 13.12 34.112-2.304 65.984-23.36 83.072-52.992a124.8 124.8 0 0 0 15.936-63.68c1.152-175.232 0.576-349.312 0.576-523.392z"
fill="#FFFFFF"
p-id="1649"
/>
<path
d="M646.84832 142.784c11.328 0.576 22.72 0 34.688 0 0 38.08 11.968 76.224 34.112 107.52 2.88 3.968 5.76 7.36 8.576 10.816-25.088-15.36-44.992-38.656-58.048-64.832a214.208 214.208 0 0 1-19.328-53.504z m179.2 180.928c12.48 2.88 24.96 3.968 38.08 3.968v132.544c-64.832 0.576-129.664-21.056-182.592-59.136v262.784a229.76 229.76 0 0 1-5.696 59.2 244.32 244.32 0 0 1-96.704 147.328c-25.92 18.56-55.36 31.744-86.464 38.656a240.448 240.448 0 0 1-113.792-1.664 239.776 239.776 0 0 1-115.52-69.44 234.016 234.016 0 0 0 87.104 38.08c22.144 5.184 44.928 6.336 67.648 5.76a219.296 219.296 0 0 0 124.608-42.688 254.784 254.784 0 0 0 68.288-77.376 269.024 269.024 0 0 0 35.2-113.792 319.072 319.072 0 0 0 0-32.96c-0.512-81.92-0.512-163.84-0.512-245.76a308.16 308.16 0 0 0 180.928 57.472c-0.576-34.176 0-68.8-0.576-102.976z"
fill="#FE2C55"
p-id="1650"
/>
<path
d="M434.62432 426.112c12.544 0 25.6 0.576 38.08 2.24v136a103.872 103.872 0 0 0-57.408-2.304c-35.84 8-65.984 35.264-78.528 70.016-12.48 34.112-7.36 73.92 14.208 102.912a119.552 119.552 0 0 1-32.96-27.84 107.328 107.328 0 0 1-25.024-57.472 112.16 112.16 0 0 1 11.392-63.68c5.632-11.392 14.208-21.632 23.872-30.208 15.936-13.632 35.84-22.144 55.744-27.264a94.176 94.176 0 0 1 51.776 1.664V463.68c-1.152-11.36-0.576-24.48-1.152-37.536v-0.032z"
fill="#FE2C55"
p-id="1651"
/>
</svg>
</template>
<style scoped></style>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts"></script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 200"
stroke="currentColor"
width="100%"
height="100%"
>
<g
fill="none"
stroke-width="8"
stroke-linejoin="miter"
stroke-linecap="square"
>
<!-- Bottom horizontal line has been removed -->
<!-- Left Angle Bracket < farther left + shorter -->
<polyline points="60,80 30,100 60,120" />
<!-- Right Angle Bracket > farther right + shorter -->
<polyline points="140,80 170,100 140,120" />
<!-- Ladder Vertical Rails unchanged width -->
<line x1="80" y1="50" x2="80" y2="150" />
<line x1="120" y1="50" x2="120" y2="150" />
<!-- Ladder Rungs unchanged -->
<line x1="80" y1="75" x2="120" y2="75" />
<line x1="80" y1="100" x2="120" y2="100" />
<line x1="80" y1="125" x2="120" y2="125" />
</g>
</svg>
</template>
<style scoped></style>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts"></script>
<template>
<svg
version="1.1"
id="svg1"
width="256"
height="256"
viewBox="0 0 256 256"
sodipodi:docname="XiaohongshuLOGO.png"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
>
<defs id="defs1" />
<g inkscape:groupmode="layer" inkscape:label="Image" id="g1">
<path
style="fill: #ff2842; stroke: none"
d="M 29,0.33332825 C 13.959937,3.4666748 1.5356731,15.204498 0,31 -1.586103,47.314209 0,64.597672 0,81 v 102 c 0,18.76035 -4.7369685,44.19888 7.3333335,60 C 20.372129,260.06897 44.156731,256 63,256 h 111 35 c 5.78276,0 12.33244,0.84741 18,-0.33333 15.0401,-3.13336 27.46432,-14.87115 29,-30.66667 1.58612,-16.31419 0,-33.59769 0,-50 V 73 C 256,54.239685 260.73697,28.801102 248.66667,13 235.62787,-4.0689697 211.84329,0 193,0 H 82 47 C 41.217228,0 34.667561,-0.84741211 29,0.33332825 M 120,91 l -7,19 h 12 l -10,24 9,1 c -0.98794,2.68155 -2.31718,7.73317 -4.33334,9.83334 C 118.18945,146.3721 115.92654,146 114,146 c -4.35942,0 -13.16798,1.80539 -15.5,-3 -1.069664,-2.20416 0.465553,-4.98451 1.333336,-7 1.813624,-4.21228 4.222554,-8.51549 5.166664,-13 -2.17548,0 -4.92464,0.42967 -7,-0.33333 -7.778526,-2.85974 0.874031,-15.36435 2.66666,-19.66667 1.25875,-3.020981 2.75652,-9.584732 5.5,-11.5 C 110.01874,88.810822 115.88325,90.674988 120,91 m -79,63 c 2.750713,0 6.837379,0.81721 8.5,-2 1.769028,-2.99753 0.5,-9.58963 0.5,-13 V 106 C 50,102.90659 48.438198,93.464493 51.166668,91.5 53.41069,89.884308 62.832935,90.226166 63.833332,93 65.47065,97.539825 64,105.16241 64,110 v 32 c 0,5.48389 0.949112,11.8645 -1.333332,17 -2.177158,4.89861 -12.303417,9.27243 -17.333336,5.5 C 43.120155,162.84012 41.545292,156.59013 41,154 M 193,91 v 5 c 3.72887,0 8.4108,-0.763367 12,0.333328 11.97635,3.659424 11,15.422502 11,25.666672 1.99706,0 4.04419,-0.15562 6,0.33333 11.49335,2.87334 10,14.36401 10,23.66667 0,4.95615 0.93086,10.82184 -2.33333,15 -3.59567,4.60246 -9.48195,4 -14.66667,4 -1.6116,0 -4.26318,0.51051 -5.66667,-0.5 -2.62326,-1.88875 -3.78159,-7.50485 -4.33333,-10.5 3.28711,0 9.2179,1.12517 11.83333,-1.33334 C 219.9164,149.76859 218.65411,138.43454 215,136.5 c -1.93661,-1.02527 -4.88672,-0.5 -7,-0.5 h -15 v 29 h -14 v -29 h -14 v -14 h 14 v -12 h -9 V 96 h 9 v -5 h 14 m -32,5 v 14 h -8 v 42 h 13 v 13 H 120 L 125.33334,152.5 138,152 v -42 h -8 V 96 h 31 m 57,14 c 0,-2.84204 -0.51608,-6.25871 0.33333,-9 3.34434,-10.793121 19.61577,-2.093994 11.5,6.83333 -0.92279,1.01507 -2.54419,1.51106 -3.83333,1.83334 C 223.43948,110.30679 220.61993,110 218,110 M 41,110 36.833332,147 30,159 24,143 27,110 h 14 m 46,0 3,33 -6,15 h -2 c -5.366936,-8.49765 -6.053299,-17.26251 -7,-27 -0.672195,-6.91406 -2,-14.04004 -2,-21 h 14 m 106,0 v 12 h 9 v -12 h -9 m -75,42 -5,13 H 91 L 96.333336,151.5 104,151.66666 Z"
id="path1"
/>
</g>
</svg>
</template>
<style scoped></style>

View File

@@ -4,7 +4,7 @@
<div>
<footer class="footer sm:footer-horizontal bg-base-300 items-center p-4">
<aside class="grid-flow-col items-center">
<img src="@/assets/hucky.png" alt="hucky" class="h-18" />
<img src="@/assets/logo.svg" alt="hucky" class="h-18" />
<p>Copyright © {{ new Date().getFullYear() }} - All right reserved</p>
</aside>
<nav class="grid-flow-col gap-4 md:place-self-center md:justify-self-end">

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
import BiliBiliIcon from "../icon/BiliBiliIcon.vue";
import DouYinIcon from "../icon/DouYinIcon.vue";
import LogoIcon from "../icon/LogoIcon.vue";
import RedBookIcon from "../icon/RedBookIcon.vue";
</script>
<template>
<footer
class="w-full pt-16 pb-10 px-6 md:px-12 lg:px-20 text-sm font-[LXGW] lg:h-102 md:h-122 h-154"
>
<!-- Background overlay -->
<div
class="absolute inset-0 bg-gradient-to-t from-(--color-primary)/50 via-(--color-primary)/20 to-(--color-base-200) pointer-events-none"
/>
<div
class="relative max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10 md:gap-6"
>
<!-- Left column - Logo + slogan -->
<div class="space-y-4">
<div class="flex items-center gap-3">
<div class="text-4xl font-bold tracking-tight">
<LogoIcon class="h-16" />
</div>
<div>
<div class="text-lg font-medium tracking-wide">Hucky</div>
<div class="text-xs text-gray-500">专注于开发体验提升</div>
</div>
</div>
<div class="text-lg">重塑下一代前端脚手架</div>
</div>
<!-- Middle scattered contact blocks -->
<div class="space-y-8 md:space-y-10 lg:col-start-2">
<!-- Email & resume -->
<div>
<div class="mb-1 text-gray-500">简历投递</div>
<a href="mailto:w56038009@outlook.com" class="font-bold underline">
w56038009@outlook.com
</a>
</div>
<!-- WeChat / phone -->
<div>
<div class="mb-1 text-gray-500">业务联系(微信)</div>
<div class="font-bold">gushen622140</div>
<!-- QR Code area -->
<div class="mt-3">
<div
class="inline-block bg-white/10 backdrop-blur-sm rounded-lg border border-white/10"
>
<img src="/contact/wechat.png" alt="wechat" class="h-22" />
</div>
</div>
</div>
<!-- Phone & address (right side feel) -->
<div class="lg:hidden">
<div class="font-bold">15381666371</div>
<div class="text-gray-500 mt-1 text-xs leading-relaxed">
浙江省金华市婺城区迎宾大道 667 号浙江师范大学<br />
667, Zhejiang Normal University<br />
Jinhua, Zhejiang, China
</div>
</div>
</div>
<!-- Right column - Phone + Address (desktop layout) -->
<div class="hidden lg:block space-y-2">
<div>
<div class="font-bold text-lg">15381666371</div>
</div>
<div class="text-gray-500 leading-relaxed text-xs">
浙江省金华市婺城区迎宾大道 667 号浙江师范大学<br />
667, Zhejiang Normal University<br />
Jinhua, Zhejiang, China
</div>
<div class="flex mt-4 gap-2">
<a
title="小红书"
href="https://www.xiaohongshu.com/user/profile/61cf083b000000001000c7b2"
target="_blank"
rel="nofollow"
>
<RedBookIcon class="h-6 w-6" />
</a>
<a
title="B站"
href="https://space.bilibili.com/34875940"
target="_blank"
rel="nofollow"
>
<BiliBiliIcon class="h-6 w-6" />
</a>
<a
title="抖音"
href="https://www.douyin.com/user/MS4wLjABAAAAf6Q5VoHL_UfFQE8fmH3mgYCgL0tGxlpoSMLM9ZcKbpY"
target="_blank"
rel="nofollow"
>
<DouYinIcon class="h-6 w-6" />
</a>
</div>
</div>
</div>
<!-- Bottom legal line -->
<div class="mt-8 text-xs text-gray-500 text-left">
<div class="max-w-7xl mx-auto">
Copyright © 2026 sunway.icu | 备案号浙ICP备2023010223号 |
增值电信业务经营许可证浙B2-20110004
</div>
</div>
</footer>
</template>
<style scoped></style>

View File

@@ -5,6 +5,7 @@ import { useI18n } from "vue-i18n";
import { throttle } from "radash";
import { AnimatePresence, motion } from "motion-v";
import { navigateTo } from "@/utils/navigator";
import UserAuthNavButton from "../button/UserAuthNavButton.vue";
const { t } = useI18n();
@@ -22,6 +23,30 @@ const handleThrottleScroll = throttle(100, () => {
});
window.addEventListener("scroll", handleThrottleScroll);
const navPageByResetScroll = (path: string) => {
window.scrollTo({ top: 0, behavior: "smooth" });
navigateTo(path);
};
const menuItems = ref([
{
path: "/",
label: "nav.home",
},
{
path: "/about",
label: "nav.about",
},
{
path: "/demo",
label: "nav.demo",
},
{
path: "/join",
label: "nav.join",
},
]);
</script>
<template>
@@ -40,48 +65,32 @@ window.addEventListener("scroll", handleThrottleScroll);
<div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg>
<span class="icon-[material-symbols--menu-rounded] text-2xl" />
</div>
<ul
tabindex="-1"
class="menu dropdown-content bg-base-100 rounded-box z-1 mt-5 w-52 p-2 shadow"
class="menu dropdown-content bg-base-100 rounded-box z-1 mt-5 w-48 p-2 shadow"
>
<li>
<a @click="navigateTo('/')">{{ t("nav.home") }}</a>
</li>
<li>
<a @click="navigateTo('/about')">{{ t("nav.about") }}</a>
<li v-for="item in menuItems" :key="item.path">
<a @click="navPageByResetScroll(item.path)">{{
t(item.label)
}}</a>
</li>
</ul>
</div>
<a class="btn btn-ghost text-xl">Hucky</a>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li>
<a @click="navigateTo('/')">{{ t("nav.home") }}</a>
</li>
<li>
<a @click="navigateTo('/about')">{{ t("nav.about") }}</a>
<ul class="menu menu-horizontal pl-6 hidden lg:flex gap-3">
<li v-for="item in menuItems" :key="item.path">
<a @click="navPageByResetScroll(item.path)">{{
t(item.label)
}}</a>
</li>
</ul>
</div>
<div class="navbar-end">
<div class="navbar-end gap-2">
<ChangeThemeDropdownButton />
<ChangeLanguageDropdownButton />
<UserAuthNavButton />
</div>
</motion.div>
</AnimatePresence>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import { motion, useAnimate } from "motion-v";
import { useI18n } from "vue-i18n";
import { languageChangeBus } from "@/hooks/bus/languageChangeBus";
import type { Key, ResourceKeys } from "vue-i18n";
const { t } = useI18n();
const props = defineProps<{
text: Key | ResourceKeys;
}>();
const [scope, animate] = useAnimate();
languageChangeBus.on(async () => {
await animate(scope.current, { opacity: 0, y: "-20px" });
await animate(scope.current, { opacity: 1, y: "0" });
});
</script>
<template>
<motion.span ref="scope">
{{ t(props.text) }}
</motion.span>
</template>
<style scoped></style>

View File

@@ -0,0 +1,130 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as THREE from "three";
import { gsap } from "gsap";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { M } from "motion-v";
const canvasRef = ref<HTMLCanvasElement | null>(null);
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let cube: THREE.Mesh;
// 动画循环的 raf id用于销毁时清理
let animationId: number;
function init() {
if (!canvasRef.value) return; // 防御性编程
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000,
);
camera.position.z = 5;
renderer = new THREE.WebGLRenderer({
canvas: canvasRef.value,
alpha: true,
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// 透明
renderer.setClearColor(0x000000, 0);
// ------------------- 几何体 -------------------
const loader = new GLTFLoader();
loader.load(
"/model/christmas_ball.glb",
(gltf) => {
cube = gltf.scene;
// 可选:调整模型大小、位置
cube.scale.set(0.01, 0.01, 0.01);
cube.position.set(4, 0, 0);
scene.add(cube);
console.log("模型加载成功!", cube);
},
(xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + "% 已加载");
},
(error) => {
console.error("模型加载失败:", error);
},
);
// 加一点环境光 + 点光(让边缘更亮)
scene.add(new THREE.AmbientLight(0xffffff, 1.2));
const dirLight = new THREE.DirectionalLight(0xffffff, 2);
dirLight.position.set(5, 10, 7.5);
scene.add(dirLight);
animate();
}
function animate() {
animationId = requestAnimationFrame(animate);
renderer.render(scene, camera);
}
onMounted(() => {
init();
// 监听滚动事件
window.addEventListener("scroll", () => {
// 计算滚动进度
const scrollY = window.scrollY;
const maxScroll =
document.documentElement.scrollHeight - window.innerHeight;
const progress = scrollY / maxScroll;
cube.position.set(4 + progress, 0, 0);
cube.rotation.y = -progress * Math.PI * 1;
cube.scale.set(
0.01 + progress * 0.001,
0.01 + progress * 0.001,
0.01 + progress * 0.001,
);
// 接近页的脚时候淡出
// if (progress > 0.8) {
// gsap.to(cube.material, {
// opacity: 0,
// duration: 0.5,
// });
// }
// if (progress < 0.8) {
// gsap.to(cube.material, {
// opacity: 1,
// duration: 0.5,
// });
// }
});
// 监听窗口大小变化
window.addEventListener("resize", () => {
if (!camera || !renderer) return;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
});
onUnmounted(() => {
cancelAnimationFrame(animationId);
window.removeEventListener("resize", onResize); // 如果加了
renderer?.dispose(); // 释放资源(推荐)
});
</script>
<template>
<canvas ref="canvasRef" class="pointer-events-none" />
</template>

View File

@@ -0,0 +1,120 @@
<template>
<div ref="container" class="three-container" />
</template>
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const container = ref(null);
let renderer = null;
let scene = null;
let camera = null;
let controls = null;
let animationFrameId = null;
onMounted(() => {
if (!container.value) return;
// ================== 初始化 ==================
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1f);
camera = new THREE.PerspectiveCamera(
60,
container.value.clientWidth / container.value.clientHeight,
0.1,
1000,
);
camera.position.set(0, 1.5, 5);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.value.appendChild(renderer.domElement);
// 控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.enablePan = true;
// 灯光(很重要,否则模型可能很暗)
scene.add(new THREE.AmbientLight(0xffffff, 1.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 3);
dirLight.position.set(5, 10, 7.5);
scene.add(dirLight);
// ================== 加载模型 ==================
const loader = new GLTFLoader();
const modelUrl = "/model/christmas_ball.glb"; // ← 修改成你自己的模型路径
loader.load(
modelUrl,
(gltf) => {
const model = gltf.scene;
// 可选调整
model.scale.set(0.01, 0.01, 0.01); // 放大倍数,根据模型大小调整
model.position.y = -0.5; // 稍微下移居中
// model.rotation.y = Math.PI / 4 // 初始旋转(可选)
scene.add(model);
console.log("模型加载成功", model);
},
(xhr) => {
console.log(`加载进度: ${((xhr.loaded / xhr.total) * 100).toFixed(1)}%`);
},
(error) => {
console.error("模型加载失败:", error);
},
);
// ================== 动画循环 ==================
function animate() {
animationFrameId = requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
});
onUnmounted(() => {
if (animationFrameId) cancelAnimationFrame(animationFrameId);
if (controls) controls.dispose();
if (renderer) {
renderer.dispose();
renderer.forceContextLoss();
}
scene = null;
camera = null;
renderer = null;
controls = null;
});
// 窗口 resize 处理
const onResize = () => {
if (!container.value || !camera || !renderer) return;
const width = container.value.clientWidth;
const height = container.value.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
};
window.addEventListener("resize", onResize);
onUnmounted(() => window.removeEventListener("resize", onResize));
</script>
<style scoped>
.three-container {
width: 100%;
height: 100vh; /* 或你想要的高度,例如 600px */
overflow: hidden;
background: #000;
}
</style>

4
src/hooks/bus/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export const keys = {
user: "user",
language: "language",
};

View File

@@ -0,0 +1,4 @@
import { useEventBus } from "@vueuse/core";
import { keys } from "./index";
export const languageChangeBus = useEventBus<string>(keys.language);

View File

@@ -0,0 +1,22 @@
import AuthDialog from "@/components/dialog/AuthDialog.vue";
import i18n from "@/i18n";
export const renderAuthDialog = (defaultAuthFormType: "login" | "register") => {
const authDialogContainer = document.createElement("div");
authDialogContainer.id = "auth-dialog-container";
document.body.appendChild(authDialogContainer);
createApp(AuthDialog, {
defaultAuthFormType,
})
.use(i18n)
.mount(authDialogContainer);
};
export const closeAuthDialog = () => {
const authDialogContainer = document.getElementById("auth-dialog-container");
if (authDialogContainer) {
authDialogContainer.remove();
}
};

View File

@@ -3,6 +3,7 @@ import {
GlobalLanguage,
languageMap,
} from "@/stores/LanguageStore";
import { languageChangeBus } from "./bus/languageChangeBus";
const languageStore = useLanguageStore();
@@ -20,8 +21,13 @@ function changeGlobalLanguage(
language: GlobalLanguage,
locale: WritableComputedRef<string>,
) {
languageStore.setLanguage(language);
locale.value = language;
// 延迟切换先通知其他组件播放动画
languageChangeBus.emit("language changed");
setTimeout(() => {
languageStore.setLanguage(language);
locale.value = language;
}, 300);
}
export const useGlobalLanguageHook = () => ({

View File

@@ -4,7 +4,40 @@ export const en_USMessages: messagesInterface = {
nav: {
home: "Home",
about: "About",
demo: "Demo",
join: "Join",
theme: "Theme",
locale: "Locale",
login: "Login",
register: "Register",
logout: "Logout",
logout_more_title: "Logout Confirmation",
logout_more: "Ready to change account?",
login_more_title: "Join us Now!",
login_more: "Start your new develop experience",
},
auth: {
login: "Login",
login_password: "Password Login",
login_phone: "Phone Login",
register: "Register",
email: "Email",
password: "Password",
remember_me: "Remember Me",
forget_password: "Forget Password",
no_account: "Don't have an account?",
agree_terms: "Agree to the Terms & Conditions",
have_account: "Already have an account?",
logout: "Logout",
scan_qr: "Scan QR Code",
scan_qr_more: "Please open the phone's WeChat APP to scan the QR code",
not_open: "Not Open Yet",
},
home: {
welcome: "Welcome to Hucky",
intro_line1: "A Vue 3 + TypeScript + Vite template",
intro_line2: "with Vue I18n, Vue Router, and Vuex",
intro_line3: "and more...",
read_doc: "Read Documentation",
},
};

View File

@@ -6,8 +6,41 @@ export interface messagesInterface {
nav: {
home: string;
about: string;
demo: string;
join: string;
theme: string;
locale: string;
login: string;
register: string;
logout: string;
logout_more_title: string;
logout_more: string;
login_more_title: string;
login_more: string;
};
auth: {
login: string;
login_password: string;
login_phone: string;
register: string;
email: string;
password: string;
remember_me: string;
forget_password: string;
no_account: string;
agree_terms: string;
have_account: string;
logout: string;
scan_qr: string;
scan_qr_more: string;
not_open: string;
};
home: {
welcome: string;
intro_line1: string;
intro_line2: string;
intro_line3: string;
read_doc: string;
};
}

View File

@@ -4,7 +4,40 @@ export const zh_CNMessages: messagesInterface = {
nav: {
home: "首页",
about: "关于",
demo: "演示",
join: "加入",
theme: "主题",
locale: "语言",
login: "登录",
register: "注册",
logout: "退出登录",
logout_more_title: "退出登录",
logout_more: "准备切换新的账号吗或者是退出以保证您的安全吗?",
login_more_title: "加入我们",
login_more: "开始您的新开发体验",
},
auth: {
login: "登录",
login_password: "密码登录",
login_phone: "短信登录",
register: "注册",
email: "邮箱",
password: "密码",
remember_me: "记住我",
forget_password: "忘记密码",
no_account: "没有账号?",
agree_terms: "我同意《用户协议》相关条款和条件",
have_account: "已经有账号?",
logout: "退出登录",
scan_qr: "扫描二维码登录",
scan_qr_more: "请打开手机微信 APP 扫码登录",
not_open: "暂未开放",
},
home: {
welcome: "欢迎来到 Hucky",
intro_line1: "超现代化的 Vue3 Based 脚手架",
intro_line2: "赋予您极致高效且规范的开发体验",
intro_line3: "使用 bun docs 命令启动 Hucky 的文档服务器",
read_doc: "阅读文档",
},
};

View File

@@ -5,6 +5,7 @@ import router from "./router";
import i18n from "./i18n";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import "lenis/dist/lenis.css";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

View File

@@ -62,7 +62,7 @@ import { motion } from "motion-v";
:while-hover="{ scale: 1.1 }"
@click="
navigateTo(
'https://www.douyin.com/user/MS4wLjABAAAAf6Q5VoHL_UfFQE8fmH3mgYCgL0tGxlpoSMLM9ZcKbpY?from_tab_name=main',
'https://www.douyin.com/user/MS4wLjABAAAAf6Q5VoHL_UfFQE8fmH3mgYCgL0tGxlpoSMLM9ZcKbpY',
)
"
>

22
src/pages/demo.vue Normal file
View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import NavBar from "@/components/menu/NavBar.vue";
import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
import ModelViewer from "@/components/three/ModelViewer.vue";
</script>
<template>
<div class="lg:pb-102 md:pb-122 pb-154">
<!-- 这里开 relative 形成 stack context -->
<div class="bg-base-200 relative z-2">
<div class="h-screen flex flex-col">
<NavBar class="fixed top-0 left-0 z-10" />
<!-- 同高度占位颜色叠加 -->
<div class="h-16 bg-base-300" />
<ModelViewer />
</div>
</div>
<FooterBarV2 class="fixed bottom-0 left-0 z-1" />
</div>
</template>
<style scoped></style>

View File

@@ -1,9 +1,10 @@
<script lang="ts" setup>
import BasicIntroCard from "@/components/card/BasicIntroCard.vue";
import DatePickerDisplayCard from "@/components/card/DatePickerDisplayCard.vue";
import DevelopProgressCard from "@/components/card/DevelopProgressCard.vue";
import FooterBar from "@/components/layout/FooterBar.vue";
import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
import NavBar from "@/components/menu/NavBar.vue";
import { navigateTo } from "@/utils/navigator";
import LogoModel from "@/components/three/LogoModel.vue";
const progress = ref([
{
@@ -38,50 +39,46 @@ const progress = ref([
</script>
<template>
<div class="bg-base-200">
<div class="h-screen flex flex-col">
<NavBar class="fixed top-0 left-0 z-10" />
<!-- 同高度占位颜色叠加 -->
<div class="h-16 bg-base-300" />
<div class="hero flex-1">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">欢迎加入</h1>
<p class="py-6">
超现代化的 Vue3 Based 脚手架<br />
赋予您高效的开发体验<br />
使用 bun docs 命令启动 Hucky 的文档服务器
</p>
<button
class="btn btn-primary"
@click="navigateTo('http://localhost:5174')"
>
查阅文档
</button>
</div>
<div class="lg:pb-102 md:pb-122 pb-154">
<!-- 主内容区域 footer 上方 -->
<div class="bg-base-200 relative z-5">
<!-- 顶部与首页内容 -->
<div class="h-screen flex flex-col">
<NavBar class="fixed top-0 left-0 z-10" />
<!-- 同高度占位颜色叠加 -->
<div class="h-16 bg-base-300" />
<BasicIntroCard class="isolate z-8" />
</div>
<!-- fixed three js 背景 -->
<LogoModel class="fixed left-0 top-0 h-screen w-full z-7" />
<!-- 脚手架开发进度 -->
<div class="p-4">
<h1 class="text-4xl font-bold mb-12 ml-10">脚手架开发进度</h1>
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
<DevelopProgressCard
v-for="item in progress"
:key="item.title"
:completed="item.completed"
:pending="item.pending"
:title="item.title"
:intro="item.intro"
/>
</div>
</div>
<!-- 组件演示 -->
<div class="p-4">
<h1 class="text-4xl font-bold mb-12 ml-10">组件演示</h1>
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
<DatePickerDisplayCard />
</div>
</div>
</div>
<div class="p-4">
<h1 class="text-4xl font-bold mb-12 ml-10">脚手架开发进度</h1>
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
<DevelopProgressCard
v-for="item in progress"
:key="item.title"
:completed="item.completed"
:pending="item.pending"
:title="item.title"
:intro="item.intro"
/>
</div>
</div>
<div class="p-4">
<h1 class="text-4xl font-bold mb-12 ml-10">组件演示</h1>
<div class="w-full grid grid-cols-3 gap-4 justify-items-center">
<DatePickerDisplayCard />
</div>
</div>
<FooterBar />
<!-- fixed footer -->
<FooterBarV2 class="fixed bottom-0 left-0 z-1" />
</div>
</template>

20
src/pages/join.vue Normal file
View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import NavBar from "@/components/menu/NavBar.vue";
import FooterBarV2 from "@/components/layout/FooterBarV2.vue";
</script>
<template>
<div class="lg:pb-102 md:pb-122 pb-154">
<!-- 这里开 relative 形成 stack context -->
<div class="bg-base-200 relative z-2">
<div class="h-screen flex flex-col">
<NavBar class="fixed top-0 left-0 z-10" />
<!-- 同高度占位颜色叠加 -->
<div class="h-16 bg-base-300" />
</div>
</div>
<FooterBarV2 />
</div>
</template>
<style scoped></style>

View File

@@ -1,18 +1,89 @@
import { ref } from "vue";
import { defineStore } from "pinia";
export interface UserInfo {
id: number;
username: string;
nickname: string;
email: string;
mobile: string;
sex: number;
avatar: string;
loginIp: string;
loginDate: null;
createTime: number;
roles: object[];
dept: null;
posts: null;
}
export const useUserStore = defineStore(
"user",
() => {
const token = ref("");
const accessToken = ref("");
const refreshToken = ref("");
const setToken = (newToken: string) => {
token.value = newToken;
const setAccessToken = (newToken: string) => {
accessToken.value = newToken;
};
const setRefreshToken = (newToken: string) => {
refreshToken.value = newToken;
};
const userInfo = ref<UserInfo>({
id: 0,
username: "",
nickname: "",
email: "",
mobile: "",
sex: 0,
avatar: "",
loginIp: "",
loginDate: null,
createTime: 0,
roles: [],
dept: null,
posts: null,
});
const setUserInfo = (newInfo: UserInfo) => {
userInfo.value = newInfo;
};
const checkUserLogin = () => {
return accessToken.value !== "";
};
const clearUserInfo = () => {
accessToken.value = "";
refreshToken.value = "";
userInfo.value = {
id: 0,
username: "",
nickname: "",
email: "",
mobile: "",
sex: 0,
avatar: "",
loginIp: "",
loginDate: null,
createTime: 0,
roles: [],
dept: null,
posts: null,
};
};
return {
token,
setToken,
accessToken,
setAccessToken,
refreshToken,
setRefreshToken,
userInfo,
setUserInfo,
checkUserLogin,
clearUserInfo,
};
},
{

View File

@@ -1,11 +1,22 @@
@import "tailwindcss";
@import "tw-animate-css";
@plugin "@iconify/tailwind4";
@custom-variant dark (&:is(.dark *));
@plugin "daisyui" {
themes: all;
};
@plugin "daisyui/theme" {
name: "light";
--color-primary: #5092d4;
--color-secondary: #bfd7f1;
--color-accent: #76b9ef;
/* 自定义字体在 App.vue 中加载 */
--font-sans: '"Harmony", "Microsoft YaHei", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
}
/* Custom Scrollbar Styles */
::-webkit-scrollbar {
width: 8px;

View File

@@ -5,22 +5,30 @@ const instance = axios.create({
baseURL: import.meta.env.VITE_SERVER,
});
// 所有请求自动加入 token
instance.interceptors.request.use((config) => {
const store = useUserStore();
if (!store.token) {
if (!store.accessToken) {
return config;
}
const token = store.token;
const token = store.accessToken;
if (!token) {
return config;
}
config.headers["token"] = token;
config.headers["Authorization"] = `Bearer ${token}`;
return config;
});
const http = async <T>(config: AxiosRequestConfig): Promise<Result<T>> => {
export const http = async <T>(
config: AxiosRequestConfig,
): Promise<Result<T>> => {
const { data } = await instance.request<Result<T>>(config);
return data;
};
export default http;
export const httpWithCustomReturn = async <T>(
config: AxiosRequestConfig,
): Promise<T> => {
const { data } = await instance.request<T>(config);
return data;
};