Compare commits

..

3 Commits

Author SHA1 Message Date
马丁
67d9c204dd 优化:一级菜单移到app 2026-05-19 14:12:58 +08:00
马丁
59edb13f53 优化:目录状态做成全局状态 2026-05-19 13:05:31 +08:00
马丁
85a2590676 优化:自适应v0.1 2026-05-19 13:02:13 +08:00
12 changed files with 951 additions and 596 deletions

View File

@ -54,6 +54,8 @@
"oxlint": "~1.42.0", "oxlint": "~1.42.0",
"postcss": "^8.5.8", "postcss": "^8.5.8",
"prettier": "3.8.1", "prettier": "3.8.1",
"sass": "^1.99.0",
"sass-loader": "^16.0.8",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-node-polyfills": "^0.25.0", "vite-plugin-node-polyfills": "^0.25.0",

309
pnpm-lock.yaml generated
View File

@ -56,13 +56,13 @@ importers:
version: 24.10.13 version: 24.10.13
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.0.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)) version: 6.0.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))
'@vitejs/plugin-vue-jsx': '@vitejs/plugin-vue-jsx':
specifier: ^5.1.3 specifier: ^5.1.3
version: 5.1.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)) version: 5.1.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))
'@vitest/eslint-plugin': '@vitest/eslint-plugin':
specifier: ^1.6.6 specifier: ^1.6.6
version: 1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(yaml@2.8.2)) version: 1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(sass@1.99.0)(yaml@2.8.2))
'@vue/eslint-config-typescript': '@vue/eslint-config-typescript':
specifier: ^14.6.0 specifier: ^14.6.0
version: 14.7.0(eslint-plugin-vue@10.7.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.2(jiti@2.6.1))))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 14.7.0(eslint-plugin-vue@10.7.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.2(jiti@2.6.1))))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
@ -111,21 +111,27 @@ importers:
prettier: prettier:
specifier: 3.8.1 specifier: 3.8.1
version: 3.8.1 version: 3.8.1
sass:
specifier: ^1.99.0
version: 1.99.0
sass-loader:
specifier: ^16.0.8
version: 16.0.8(sass@1.99.0)
typescript: typescript:
specifier: ~5.9.3 specifier: ~5.9.3
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^7.3.1 specifier: ^7.3.1
version: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) version: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
vite-plugin-node-polyfills: vite-plugin-node-polyfills:
specifier: ^0.25.0 specifier: ^0.25.0
version: 0.25.0(rollup@4.57.1)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)) version: 0.25.0(rollup@4.57.1)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))
vite-plugin-vue-devtools: vite-plugin-vue-devtools:
specifier: ^8.0.5 specifier: ^8.0.5
version: 8.0.6(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)) version: 8.0.6(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))
vitest: vitest:
specifier: ^4.0.18 specifier: ^4.0.18
version: 4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(yaml@2.8.2) version: 4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(sass@1.99.0)(yaml@2.8.2)
vue-tsc: vue-tsc:
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4(typescript@5.9.3) version: 3.2.4(typescript@5.9.3)
@ -646,6 +652,94 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@parcel/watcher-android-arm64@2.5.6':
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [android]
'@parcel/watcher-darwin-arm64@2.5.6':
resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [darwin]
'@parcel/watcher-darwin-x64@2.5.6':
resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [darwin]
'@parcel/watcher-freebsd-x64@2.5.6':
resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [freebsd]
'@parcel/watcher-linux-arm-glibc@2.5.6':
resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.6':
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [win32]
'@parcel/watcher-win32-ia32@2.5.6':
resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
engines: {node: '>= 10.0.0'}
cpu: [ia32]
os: [win32]
'@parcel/watcher-win32-x64@2.5.6':
resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [win32]
'@parcel/watcher@2.5.6':
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
engines: {node: '>= 10.0.0'}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -1310,6 +1404,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chokidar@5.0.0: chokidar@5.0.0:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
@ -1435,6 +1533,10 @@ packages:
des.js@1.1.0: des.js@1.1.0:
resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
diffie-hellman@5.0.3: diffie-hellman@5.0.3:
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
@ -1786,6 +1888,9 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
immutable@5.1.5:
resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==}
import-fresh@3.3.1: import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -2057,6 +2162,12 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-releases@2.0.27: node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@ -2303,6 +2414,10 @@ packages:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
readdirp@5.0.0: readdirp@5.0.0:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
@ -2353,6 +2468,32 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
sass-loader@16.0.8:
resolution: {integrity: sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==}
engines: {node: '>= 18.12.0'}
peerDependencies:
'@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0
node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
sass: ^1.3.0
sass-embedded: '*'
webpack: ^5.0.0
peerDependenciesMeta:
'@rspack/core':
optional: true
node-sass:
optional: true
sass:
optional: true
sass-embedded:
optional: true
webpack:
optional: true
sass@1.99.0:
resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==}
engines: {node: '>=14.0.0'}
hasBin: true
saxes@6.0.0: saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'} engines: {node: '>=v12.22.7'}
@ -3354,6 +3495,67 @@ snapshots:
'@oxlint/win32-x64@1.42.0': '@oxlint/win32-x64@1.42.0':
optional: true optional: true
'@parcel/watcher-android-arm64@2.5.6':
optional: true
'@parcel/watcher-darwin-arm64@2.5.6':
optional: true
'@parcel/watcher-darwin-x64@2.5.6':
optional: true
'@parcel/watcher-freebsd-x64@2.5.6':
optional: true
'@parcel/watcher-linux-arm-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-arm-musl@2.5.6':
optional: true
'@parcel/watcher-linux-arm64-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-arm64-musl@2.5.6':
optional: true
'@parcel/watcher-linux-x64-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-x64-musl@2.5.6':
optional: true
'@parcel/watcher-win32-arm64@2.5.6':
optional: true
'@parcel/watcher-win32-ia32@2.5.6':
optional: true
'@parcel/watcher-win32-x64@2.5.6':
optional: true
'@parcel/watcher@2.5.6':
dependencies:
detect-libc: 2.1.2
is-glob: 4.0.3
node-addon-api: 7.1.1
picomatch: 4.0.3
optionalDependencies:
'@parcel/watcher-android-arm64': 2.5.6
'@parcel/watcher-darwin-arm64': 2.5.6
'@parcel/watcher-darwin-x64': 2.5.6
'@parcel/watcher-freebsd-x64': 2.5.6
'@parcel/watcher-linux-arm-glibc': 2.5.6
'@parcel/watcher-linux-arm-musl': 2.5.6
'@parcel/watcher-linux-arm64-glibc': 2.5.6
'@parcel/watcher-linux-arm64-musl': 2.5.6
'@parcel/watcher-linux-x64-glibc': 2.5.6
'@parcel/watcher-linux-x64-musl': 2.5.6
'@parcel/watcher-win32-arm64': 2.5.6
'@parcel/watcher-win32-ia32': 2.5.6
'@parcel/watcher-win32-x64': 2.5.6
optional: true
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@ -3600,32 +3802,32 @@ snapshots:
'@typescript-eslint/types': 8.56.0 '@typescript-eslint/types': 8.56.0
eslint-visitor-keys: 5.0.0 eslint-visitor-keys: 5.0.0
'@vitejs/plugin-vue-jsx@5.1.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))': '@vitejs/plugin-vue-jsx@5.1.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
'@rolldown/pluginutils': 1.0.0-rc.4 '@rolldown/pluginutils': 1.0.0-rc.4
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0) '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
vue: 3.5.28(typescript@5.9.3) vue: 3.5.28(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))': '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-rc.2 '@rolldown/pluginutils': 1.0.0-rc.2
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
vue: 3.5.28(typescript@5.9.3) vue: 3.5.28(typescript@5.9.3)
'@vitest/eslint-plugin@1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(yaml@2.8.2))': '@vitest/eslint-plugin@1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(sass@1.99.0)(yaml@2.8.2))':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.56.0 '@typescript-eslint/scope-manager': 8.56.0
'@typescript-eslint/utils': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/utils': 8.56.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1) eslint: 9.39.2(jiti@2.6.1)
optionalDependencies: optionalDependencies:
typescript: 5.9.3 typescript: 5.9.3
vitest: 4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(yaml@2.8.2) vitest: 4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(sass@1.99.0)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -3638,13 +3840,13 @@ snapshots:
chai: 6.2.2 chai: 6.2.2
tinyrainbow: 3.0.3 tinyrainbow: 3.0.3
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2))': '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))':
dependencies: dependencies:
'@vitest/spy': 4.0.18 '@vitest/spy': 4.0.18
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
optionalDependencies: optionalDependencies:
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
'@vitest/pretty-format@4.0.18': '@vitest/pretty-format@4.0.18':
dependencies: dependencies:
@ -3788,14 +3990,14 @@ snapshots:
dependencies: dependencies:
'@vue/devtools-kit': 8.0.6 '@vue/devtools-kit': 8.0.6
'@vue/devtools-core@8.0.6(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))': '@vue/devtools-core@8.0.6(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))':
dependencies: dependencies:
'@vue/devtools-kit': 8.0.6 '@vue/devtools-kit': 8.0.6
'@vue/devtools-shared': 8.0.6 '@vue/devtools-shared': 8.0.6
mitt: 3.0.1 mitt: 3.0.1
nanoid: 5.1.6 nanoid: 5.1.6
pathe: 2.0.3 pathe: 2.0.3
vite-hot-client: 2.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)) vite-hot-client: 2.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))
vue: 3.5.28(typescript@5.9.3) vue: 3.5.28(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- vite - vite
@ -4098,6 +4300,10 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
chokidar@5.0.0: chokidar@5.0.0:
dependencies: dependencies:
readdirp: 5.0.0 readdirp: 5.0.0
@ -4239,6 +4445,9 @@ snapshots:
inherits: 2.0.4 inherits: 2.0.4
minimalistic-assert: 1.0.1 minimalistic-assert: 1.0.1
detect-libc@2.1.2:
optional: true
diffie-hellman@5.0.3: diffie-hellman@5.0.3:
dependencies: dependencies:
bn.js: 4.12.2 bn.js: 4.12.2
@ -4643,6 +4852,8 @@ snapshots:
ignore@7.0.5: {} ignore@7.0.5: {}
immutable@5.1.5: {}
import-fresh@3.3.1: import-fresh@3.3.1:
dependencies: dependencies:
parent-module: 1.0.1 parent-module: 1.0.1
@ -4895,6 +5106,11 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
neo-async@2.6.2: {}
node-addon-api@7.1.1:
optional: true
node-releases@2.0.27: {} node-releases@2.0.27: {}
node-stdlib-browser@1.3.1: node-stdlib-browser@1.3.1:
@ -5176,6 +5392,8 @@ snapshots:
string_decoder: 1.3.0 string_decoder: 1.3.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
readdirp@4.1.2: {}
readdirp@5.0.0: {} readdirp@5.0.0: {}
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
@ -5244,6 +5462,20 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-regex: 1.2.1 is-regex: 1.2.1
sass-loader@16.0.8(sass@1.99.0):
dependencies:
neo-async: 2.6.2
optionalDependencies:
sass: 1.99.0
sass@1.99.0:
dependencies:
chokidar: 4.0.3
immutable: 5.1.5
source-map-js: 1.2.1
optionalDependencies:
'@parcel/watcher': 2.5.6
saxes@6.0.0: saxes@6.0.0:
dependencies: dependencies:
xmlchars: 2.2.0 xmlchars: 2.2.0
@ -5499,17 +5731,17 @@ snapshots:
is-typed-array: 1.1.15 is-typed-array: 1.1.15
which-typed-array: 1.1.20 which-typed-array: 1.1.20
vite-dev-rpc@1.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)): vite-dev-rpc@1.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)):
dependencies: dependencies:
birpc: 2.9.0 birpc: 2.9.0
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
vite-hot-client: 2.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)) vite-hot-client: 2.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))
vite-hot-client@2.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)): vite-hot-client@2.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)):
dependencies: dependencies:
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
vite-plugin-inspect@11.3.3(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)): vite-plugin-inspect@11.3.3(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)):
dependencies: dependencies:
ansis: 4.2.0 ansis: 4.2.0
debug: 4.4.3 debug: 4.4.3
@ -5519,34 +5751,34 @@ snapshots:
perfect-debounce: 2.1.0 perfect-debounce: 2.1.0
sirv: 3.0.2 sirv: 3.0.2
unplugin-utils: 0.3.1 unplugin-utils: 0.3.1
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
vite-dev-rpc: 1.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)) vite-dev-rpc: 1.1.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite-plugin-node-polyfills@0.25.0(rollup@4.57.1)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)): vite-plugin-node-polyfills@0.25.0(rollup@4.57.1)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)):
dependencies: dependencies:
'@rollup/plugin-inject': 5.0.5(rollup@4.57.1) '@rollup/plugin-inject': 5.0.5(rollup@4.57.1)
node-stdlib-browser: 1.3.1 node-stdlib-browser: 1.3.1
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
vite-plugin-vue-devtools@8.0.6(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)): vite-plugin-vue-devtools@8.0.6(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)):
dependencies: dependencies:
'@vue/devtools-core': 8.0.6(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)) '@vue/devtools-core': 8.0.6(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))
'@vue/devtools-kit': 8.0.6 '@vue/devtools-kit': 8.0.6
'@vue/devtools-shared': 8.0.6 '@vue/devtools-shared': 8.0.6
sirv: 3.0.2 sirv: 3.0.2
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
vite-plugin-inspect: 11.3.3(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)) vite-plugin-inspect: 11.3.3(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))
vite-plugin-vue-inspector: 5.3.2(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)) vite-plugin-vue-inspector: 5.3.2(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- supports-color - supports-color
- vue - vue
vite-plugin-vue-inspector@5.3.2(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)): vite-plugin-vue-inspector@5.3.2(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)):
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
@ -5557,11 +5789,11 @@ snapshots:
'@vue/compiler-dom': 3.5.28 '@vue/compiler-dom': 3.5.28
kolorist: 1.8.0 kolorist: 1.8.0
magic-string: 0.30.21 magic-string: 0.30.21
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2): vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2):
dependencies: dependencies:
esbuild: 0.27.3 esbuild: 0.27.3
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@ -5573,12 +5805,13 @@ snapshots:
'@types/node': 24.10.13 '@types/node': 24.10.13
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1 jiti: 2.6.1
sass: 1.99.0
yaml: 2.8.2 yaml: 2.8.2
vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(yaml@2.8.2): vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(sass@1.99.0)(yaml@2.8.2):
dependencies: dependencies:
'@vitest/expect': 4.0.18 '@vitest/expect': 4.0.18
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2)) '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2))
'@vitest/pretty-format': 4.0.18 '@vitest/pretty-format': 4.0.18
'@vitest/runner': 4.0.18 '@vitest/runner': 4.0.18
'@vitest/snapshot': 4.0.18 '@vitest/snapshot': 4.0.18
@ -5595,7 +5828,7 @@ snapshots:
tinyexec: 1.0.2 tinyexec: 1.0.2
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinyrainbow: 3.0.3 tinyrainbow: 3.0.3
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(yaml@2.8.2) vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(sass@1.99.0)(yaml@2.8.2)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 24.10.13 '@types/node': 24.10.13

View File

@ -5,6 +5,9 @@ import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { useUserStore } from './stores/user' import { useUserStore } from './stores/user'
import { useLocaleStore } from './stores/locale' import { useLocaleStore } from './stores/locale'
import { useMenuStore } from './stores/menu'
import { storeToRefs } from 'pinia'
import { useSearchHistory } from './composables/useSearchHistory'
import type { LocaleCode } from './plugins/i18n' import type { LocaleCode } from './plugins/i18n'
import Toast from './components/Toast.vue' import Toast from './components/Toast.vue'
@ -13,8 +16,16 @@ const { t } = useI18n()
const router = useRouter() const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const localeStore = useLocaleStore() const localeStore = useLocaleStore()
const menuStore = useMenuStore()
const localeMenuOpen = ref(false) const localeMenuOpen = ref(false)
const { categoryLayers, layerActiveValues } = storeToRefs(menuStore)
const searchHistory = useSearchHistory()
const searchHistoryList = computed(() => searchHistory.list.value)
const searchExpanded = ref(false)
const searchKeyword = ref('')
const searchInputRef = ref<{ focus: () => void } | null>(null)
function chooseLocale(loc: LocaleCode) { function chooseLocale(loc: LocaleCode) {
localeStore.setLocale(loc) localeStore.setLocale(loc)
localeMenuOpen.value = false localeMenuOpen.value = false
@ -61,6 +72,39 @@ async function refreshUserData() {
await userStore.fetchPositionsValue() await userStore.fetchPositionsValue()
} }
function expandSearch() {
searchExpanded.value = true
nextTick(() => {
searchInputRef.value?.focus()
})
}
function collapseSearch() {
searchExpanded.value = false
searchKeyword.value = ''
}
function onSearchBlur() {
setTimeout(() => {
collapseSearch()
}, 200)
}
function onSearchSubmit() {
const kw = searchKeyword.value.trim()
if (kw) {
searchHistory.add(kw)
// store router
router.push({ path: '/', query: { q: kw } })
}
collapseSearch()
}
function selectHistoryItem(item: string) {
searchKeyword.value = item
onSearchSubmit()
}
onMounted(() => { onMounted(() => {
refreshUserData() refreshUserData()
}) })
@ -75,90 +119,109 @@ watch(
<template> <template>
<v-app> <v-app>
<v-app-bar color="surface" elevation="0"> <v-app-bar color="surface" elevation="0" height="112">
<div class="app-bar-inner"> <div class="flex-column header-content">
<v-btn <div class="app-bar-inner">
v-if="currentRoute !== '/'" <v-btn v-if="currentRoute !== '/'" icon variant="text" class="back-btn" :aria-label="t('common.back')"
icon @click="onBackClick">
variant="text" <v-icon>mdi-arrow-left</v-icon>
class="back-btn" </v-btn>
:aria-label="t('common.back')" <v-app-bar-title v-if="currentRoute === '/'" class="brand-title">
@click="onBackClick" <div class="brand-lockup">
> <div class="brand-mark-wrap">
<v-icon>mdi-arrow-left</v-icon> <img src="/brand-logo.svg?v=2" alt="Alpha Market logo" class="brand-logo" />
</v-btn> </div>
<v-app-bar-title v-if="currentRoute === '/'" class="brand-title"> <div class="brand-copy">
<div class="brand-lockup"> <span class="brand-kicker">Prediction Markets</span>
<div class="brand-mark-wrap"> <span class="brand-name">Alpha Market</span>
<img src="/brand-logo.svg?v=2" alt="Alpha Market logo" class="brand-logo" /> </div>
</div>
<div class="brand-copy">
<span class="brand-kicker">Prediction Markets</span>
<span class="brand-name">Alpha Market</span>
</div> </div>
</v-app-bar-title>
<v-spacer></v-spacer>
<template v-if="!userStore.isLoggedIn">
<v-menu v-model="localeMenuOpen" :close-on-content-click="true" location="bottom"
transition="scale-transition">
<template #activator="{ props }">
<v-btn icon variant="text" class="locale-btn" :aria-label="t('profile.selectLanguage')" v-bind="props">
<v-icon>mdi-earth</v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item v-for="opt in localeStore.localeOptions" :key="opt.value"
:active="localeStore.currentLocale === opt.value" @click="chooseLocale(opt.value)">
<v-list-item-title>{{ opt.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn text to="/login" :class="{ active: currentRoute === '/login' }">
{{ t('common.login') }}
</v-btn>
</template>
<template v-else>
<v-btn class="balance-btn" variant="text" min-width="auto" padding="4 12" @click="$router.push('/wallet')">
<span class="balance-text">${{ userStore.totalAssetValue }}</span>
</v-btn>
<v-btn icon variant="text" class="avatar-btn" :aria-label="t('common.user')"
@click="$router.push('/profile')">
<v-avatar size="36" color="primary">
<v-img v-if="userStore.avatarUrl" :src="userStore.avatarUrl" cover alt="avatar" />
<v-icon v-else>mdi-account</v-icon>
</v-avatar>
</v-btn>
</template>
</div>
<!-- 提取的顶部菜单栏与搜索功能 -->
<div v-if="currentRoute === '/' && categoryLayers.length > 0" class="home-category-layer1-wrap">
<div class="home-category-layer1-row">
<v-tabs :model-value="layerActiveValues[0]" class="home-tab-bar home-tab-bar--inline" height="48"
@update:model-value="menuStore.onCategorySelect(0, $event)">
<v-tab v-for="item in categoryLayers[0]" :key="item.id" :value="item.id" :ripple="false" >
{{ item.label }}
</v-tab>
</v-tabs>
<v-btn icon variant="text" class="home-search-btn" :aria-label="t('common.search')" @click="expandSearch">
<v-icon size="24">mdi-magnify</v-icon>
</v-btn>
</div> </div>
</v-app-bar-title>
<v-spacer></v-spacer> <!-- 搜索展开时浮层输入框 + 历史记录 -->
<template v-if="!userStore.isLoggedIn"> <transition name="home-search-overlay">
<v-menu <div v-if="searchExpanded" class="home-search-overlay">
v-model="localeMenuOpen" <div class="home-search-overlay-inner">
:close-on-content-click="true" <v-text-field ref="searchInputRef" v-model="searchKeyword" density="compact" hide-details
location="bottom" :placeholder="t('home.searchPlaceholder')" prepend-inner-icon="mdi-magnify" variant="outlined"
transition="scale-transition" class="home-search-overlay-field" @blur="onSearchBlur" @keydown.enter="onSearchSubmit" />
> <v-btn icon variant="text" size="small" class="home-search-close-btn" :aria-label="t('common.collapse')"
<template #activator="{ props }"> @click="collapseSearch">
<v-btn <v-icon size="22">mdi-close</v-icon>
icon </v-btn>
variant="text" </div>
class="locale-btn" <div v-if="searchHistoryList.length > 0" class="home-search-history">
:aria-label="t('profile.selectLanguage')" <div class="home-search-history-header">
v-bind="props" <span class="home-search-history-title">{{ t('home.searchHistory') }}</span>
> <v-btn variant="text" size="x-small" color="primary" class="home-search-history-clear"
<v-icon>mdi-earth</v-icon> @click="searchHistory.clearAll">
</v-btn> {{ t('common.clear') }}
</template> </v-btn>
<v-list density="compact"> </div>
<v-list-item <ul class="home-search-history-list">
v-for="opt in localeStore.localeOptions" <li v-for="(item, idx) in searchHistoryList" :key="`${item}-${idx}`" class="home-search-history-item">
:key="opt.value" <button type="button" class="home-search-history-text" @click="selectHistoryItem(item)">
:active="localeStore.currentLocale === opt.value" {{ item }}
@click="chooseLocale(opt.value)" </button>
> <v-btn icon variant="text" size="x-small" class="home-search-history-delete"
<v-list-item-title>{{ opt.label }}</v-list-item-title> :aria-label="t('common.delete')" @click.stop="searchHistory.remove(idx)">
</v-list-item> <v-icon size="20">mdi-close</v-icon>
</v-list> </v-btn>
</v-menu> </li>
<v-btn </ul>
text </div>
to="/login" </div>
:class="{ active: currentRoute === '/login' }" </transition>
> </div>
{{ t('common.login') }}
</v-btn>
</template>
<template v-else>
<v-btn
class="balance-btn"
variant="text"
min-width="auto"
padding="4 12"
@click="$router.push('/wallet')"
>
<span class="balance-text">${{ userStore.totalAssetValue }}</span>
</v-btn>
<v-btn
icon
variant="text"
class="avatar-btn"
:aria-label="t('common.user')"
@click="$router.push('/profile')"
>
<v-avatar size="36" color="primary">
<v-img v-if="userStore.avatarUrl" :src="userStore.avatarUrl" cover alt="avatar" />
<v-icon v-else>mdi-account</v-icon>
</v-avatar>
</v-btn>
</template>
</div> </div>
</v-app-bar> </v-app-bar>
<v-main class="app-main"> <v-main class="app-main">
@ -173,13 +236,7 @@ watch(
</div> </div>
</v-main> </v-main>
<v-bottom-navigation <v-bottom-navigation v-if="showBottomNav" v-model="bottomNavValue" app height="56" elevation="0">
v-if="showBottomNav"
v-model="bottomNavValue"
app
height="56"
elevation="0"
>
<v-btn value="/" :ripple="false"> <v-btn value="/" :ripple="false">
<v-icon size="24">mdi-home-outline</v-icon> <v-icon size="24">mdi-home-outline</v-icon>
<span>{{ t('nav.home') }}</span> <span>{{ t('nav.home') }}</span>
@ -202,9 +259,15 @@ watch(
</v-app> </v-app>
</template> </template>
<style scoped> <style scoped lang="scss">
.header-content {
width: 100%;
max-width: 1440px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
height: 112px;
}
.app-bar-inner { .app-bar-inner {
max-width: 1280px;
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
display: flex; display: flex;
@ -281,15 +344,15 @@ watch(
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.app-main-scroll { .app-main-scroll {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
.main-content { .main-content {
max-width: 1280px;
margin: 0 auto;
width: 100%; width: 100%;
} }
@ -352,10 +415,12 @@ watch(
background: #fff !important; background: #fff !important;
transform: translateZ(0); transform: translateZ(0);
} }
/* 底部导航:三个入口等分屏幕宽度 */ /* 底部导航:三个入口等分屏幕宽度 */
:deep(.v-bottom-navigation__content) { :deep(.v-bottom-navigation__content) {
width: 100%; width: 100%;
} }
:deep(.v-bottom-navigation__content > .v-btn) { :deep(.v-bottom-navigation__content > .v-btn) {
flex: 1 1 0; flex: 1 1 0;
min-width: 0; min-width: 0;
@ -365,6 +430,7 @@ watch(
:deep(.v-bottom-navigation__content > .v-btn:not(.v-btn--selected):not(.v-btn--active)) { :deep(.v-bottom-navigation__content > .v-btn:not(.v-btn--selected):not(.v-btn--active)) {
color: rgba(0, 0, 0, 0.45); color: rgba(0, 0, 0, 0.45);
} }
:deep(.v-bottom-navigation__content > .v-btn:not(.v-btn--selected):not(.v-btn--active) .v-icon) { :deep(.v-bottom-navigation__content > .v-btn:not(.v-btn--selected):not(.v-btn--active) .v-icon) {
color: rgba(0, 0, 0, 0.45); color: rgba(0, 0, 0, 0.45);
} }
@ -375,6 +441,7 @@ watch(
font-weight: 700; font-weight: 700;
background: transparent !important; background: transparent !important;
} }
:deep(.v-bottom-navigation__content > .v-btn .v-btn__overlay), :deep(.v-bottom-navigation__content > .v-btn .v-btn__overlay),
:deep(.v-bottom-navigation__content > .v-btn .v-btn__underlay) { :deep(.v-bottom-navigation__content > .v-btn .v-btn__underlay) {
display: none !important; display: none !important;
@ -384,18 +451,138 @@ watch(
display: none !important; display: none !important;
} }
/* 全局:禁止 body 滚动,由 app-main-scroll 内部滚动,滚动条不覆盖底部导航 */ /* 第一层置顶、全宽sticky 参照 app-main-scrolltop:0 贴容器顶(即 app-bar 下方) */
:global(html), .home-category-layer1-wrap {
:global(body) { width: 100vw;
background: rgb(252, 252, 252); background-color: white;
height: 100%;
overflow: hidden;
} }
:global(.v-application) {
background: rgb(252, 252, 252); .home-category-layer1-row {
height: 100vh; max-width: 1440px;
overflow: hidden; margin: 0 auto;
display: flex;
align-items: center;
}
.home-tab-bar--inline {
flex: 1;
min-width: 0;
}
.home-tab-bar--inline :deep(.v-tabs) {
height: 48px;
}
.home-tab-bar--inline :deep(.v-tab) {
height: 48px;
padding: 0 8px 0 8px;
}
.home-category-layer1-actions {
display: flex;
flex-shrink: 0;
gap: 0;
}
.home-search-btn {
flex-shrink: 0;
}
.home-search-overlay-enter-active,
.home-search-overlay-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.home-search-overlay-enter-from,
.home-search-overlay-leave-to {
opacity: 0;
transform: translateY(-8px);
}
.v-tabs-height {
height: 48px;
}
.home-search-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
background: #fff;
z-index: 2000;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: 8px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px;
}
.home-search-overlay-inner {
display: flex;
align-items: center;
gap: 8px;
}
.home-search-overlay-field {
flex: 1;
}
.home-search-history {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 4px;
}
.home-search-history-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.home-search-history-title {
font-size: 0.85rem;
font-weight: 600;
color: rgba(0, 0, 0, 0.6);
}
.home-search-history-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.home-search-history-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
border-radius: 6px;
transition: background-color 0.2s;
}
.home-search-history-item:hover {
background-color: rgba(0, 0, 0, 0.04);
}
.home-search-history-text {
flex: 1;
text-align: left;
font-size: 0.9rem;
color: rgba(0, 0, 0, 0.87);
background: transparent;
border: none;
cursor: pointer;
padding: 0;
}
@media (max-width: 600px) {
.home-category-layer1-wrap {
margin-left: 12px;
}
} }
</style> </style>

View File

@ -0,0 +1,37 @@
body {
font-size: 14px;
color: $text-primary;
background-color: #ffffff;
}
/* Chrome / Edge / Safari 隐藏滚动条 */
::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
background: transparent !important;
-webkit-appearance: none !important;
}
/* Firefox 隐藏滚动条 */
html, body {
scrollbar-width: none !important; /* 火狐专用 */
-ms-overflow-style: none !important; /* IE 专用 */
}
/* 全局所有容器统一隐藏滚动条 */
* {
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.flex-column {
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,17 @@
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.w-full { width: 100%; }
.h-full { height: 100%; }
// 外边距
.mt-sm { margin-top: $space-sm; }
.mt-md { margin-top: $space-md; }
.mb-sm { margin-bottom: $space-sm; }
// 内边距
.p-sm { padding: $space-sm; }
.p-md { padding: $space-md; }

View File

@ -0,0 +1,93 @@
// // 水平垂直居中
// @mixin flex-center {
// display: flex;
// align-items: center;
// justify-content: center;
// }
// // 单行省略
// @mixin text-ellipsis {
// overflow: hidden;
// white-space: nowrap;
// text-overflow: ellipsis;
// }
// // 多行省略
// @mixin line-ellipsis($line: 2) {
// display: -webkit-box;
// -webkit-line-clamp: $line;
// -webkit-box-orient: vertical;
// overflow: hidden;
// }
// // 适配移动端1px边框
// @mixin border-1px($color:#eee) {
// position: relative;
// &::after {
// content: '';
// position: absolute;
// left: 0;
// bottom: 0;
// width: 100%;
// height: 1px;
// background: $color;
// transform: scaleY(0.5);
// }
// }
// 全平台响应式 Mixin手机+平板+PC
@mixin m1 {
@media screen and (max-width: 767px) {
@content;
}
}
@mixin gt768 {
@media screen and (min-width: 768px) {
@content;
}
}
@mixin m768-1023 {
@media screen and (min-width: 768px) and (max-width: 1023px) {
@content;
}
}
@mixin m1024-1439 {
@media screen and (min-width: 1024px) and (max-width: 1439px) {
@content;
}
}
@mixin gt1024 {
@media screen and (min-width: 1024px) {
@content;
}
}
@mixin gt1440 {
@media screen and (min-width: 1440px) {
@content;
}
}
@mixin auto-space($prop) {
// 默认手机
#{$prop}: $space-xs;
// 平板 768-1023
@include m768-1023 {
#{$prop}: $space-sm;
}
// PC 1024-1439
@include m1024-1439 {
#{$prop}: $space-md;
}
// 大屏 1440+
@include gt1440 {
#{$prop}: $space-lg;
}
}

View File

@ -0,0 +1,38 @@
/* 全局:禁止 body 滚动,由 app-main-scroll 内部滚动,滚动条不覆盖底部导航 */
:global(html),
:global(body) {
background: rgb(252, 252, 252);
height: 100%;
overflow: hidden;
}
:global(.v-application) {
background: rgb(252, 252, 252);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,body {
height: 100%;
}
ul,ol {
list-style: none;
}
a {
text-decoration: none;
color: inherit;
}
img {
border: none;
}
button {
border: none;
background: none;
cursor: pointer;
}

View File

@ -0,0 +1,24 @@
// 主题色
$primary: #1677ff;
$success: #00b42a;
$warning: #ff7d00;
$danger: #f53f3f;
// 文字色
$text-primary: #1d2129;
$text-secondary: #666;
$text-placeholder: #c9cdd4;
// 间距
$space-xs: 4px;
$space-sm: 8px;
$space-md: 16px;
$space-lg: 24px;
// 圆角
$radius-sm: 4px;
$radius-md: 8px;
$radius-lg: 12px;
// 阴影
$shadow: 0 2px 12px 0 rgba(0,0,0,0.08);

View File

@ -8,6 +8,10 @@ import { i18n } from './plugins/i18n'
import { setHttpUnauthorizedHandler } from './api/request' import { setHttpUnauthorizedHandler } from './api/request'
import { useUserStore } from './stores/user' import { useUserStore } from './stores/user'
import '@/assets/styles/reset.scss'
import '@/assets/styles/base.scss'
import '@/assets/styles/global.scss'
const app = createApp(App) const app = createApp(App)
const pinia = createPinia() const pinia = createPinia()

93
src/stores/menu.ts Normal file
View File

@ -0,0 +1,93 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { CategoryTreeNode } from '@/api/category'
export const useMenuStore = defineStore('menu', () => {
/** 分类树(顶层) */
const categoryTree = ref<CategoryTreeNode[]>([])
/** 每层选中的 id[layer0, layer1?, layer2?] */
const layerActiveValues = ref<string[]>([])
/** 过滤:仅当 forceShow 明确为 false 时不显示其他null/undefined/true 等)均显示;排除 forceHide 的节点 */
function filterVisible(nodes: CategoryTreeNode[] | undefined): CategoryTreeNode[] {
if (!nodes?.length) return []
return nodes.filter((n) => n.forceShow !== false && !n.forceHide)
}
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
const categoryLayers = computed(() => {
const root = filterVisible(categoryTree.value)
if (root.length === 0) return []
const layers: CategoryTreeNode[][] = [root]
const active = layerActiveValues.value
let currentNodes = root
for (let i = 0; i < 2; i++) {
const selectedId = active[i]
const node = selectedId ? currentNodes.find((n) => n.id === selectedId) : currentNodes[0]
const children = filterVisible(node?.children)
if (children.length === 0) break
layers.push(children)
currentNodes = children
}
return layers
})
/** 分类切换时的防抖状态,防止重复请求 */
let categorySelectTimer: ReturnType<typeof setTimeout> | null = null
/** 触发事件加载的回调函数(由 Home 页面注册) */
const loadEventsCallback = ref<(() => void) | null>(null)
/** 触发清除事件列表缓存的回调函数(由 Home 页面注册) */
const clearCacheCallback = ref<(() => void) | null>(null)
/** 分类选中时:若有 children 则展开下一层并默认选中第一个,并重新加载列表 */
function onCategorySelect(layerIndex: number, selectedId: string) {
if (!selectedId || layerActiveValues.value[layerIndex] === selectedId) {
return
}
if (categorySelectTimer) {
clearTimeout(categorySelectTimer)
}
const nextValues = [...layerActiveValues.value]
nextValues[layerIndex] = selectedId
nextValues.length = layerIndex + 1
const layers = categoryLayers.value
const layer = layers[layerIndex]
const node = layer?.find((n) => n.id === selectedId)
const children = filterVisible(node?.children)
const firstChild = children[0]
if (firstChild && layerIndex < 2) {
nextValues.push(firstChild.id)
}
layerActiveValues.value = nextValues
if (clearCacheCallback.value) {
clearCacheCallback.value()
}
categorySelectTimer = setTimeout(() => {
categorySelectTimer = null
if (loadEventsCallback.value) {
loadEventsCallback.value()
}
}, 100)
}
return {
categoryTree,
layerActiveValues,
categoryLayers,
filterVisible,
onCategorySelect,
loadEventsCallback,
clearCacheCallback
}
})

View File

@ -1,120 +1,22 @@
<template> <template>
<div class="home-page"> <div class="home-page">
<!-- 第一层单行紧凑布局tabs + 搜索/筛选图标 -->
<div v-if="categoryLayers.length > 0" class="home-category-layer1-wrap">
<div class="home-category-layer1-row">
<v-tabs
:model-value="layerActiveValues[0]"
class="home-tab-bar home-tab-bar--inline"
@update:model-value="onCategorySelect(0, $event)"
>
<v-tab v-for="item in categoryLayers[0]" :key="item.id" :value="item.id" :ripple="false">
{{ item.label }}
</v-tab>
</v-tabs>
</div>
<!-- 搜索展开时浮层输入框 + 历史记录 -->
<transition name="home-search-overlay">
<div v-if="searchExpanded" class="home-search-overlay">
<div class="home-search-overlay-inner">
<v-text-field
ref="searchInputRef"
v-model="searchKeyword"
density="compact"
hide-details
:placeholder="t('home.searchPlaceholder')"
prepend-inner-icon="mdi-magnify"
variant="outlined"
class="home-search-overlay-field"
@blur="onSearchBlur"
@keydown.enter="onSearchSubmit"
/>
<v-btn
icon
variant="text"
size="small"
class="home-search-close-btn"
:aria-label="t('common.collapse')"
@click="collapseSearch"
>
<v-icon size="22">mdi-close</v-icon>
</v-btn>
</div>
<div v-if="searchHistoryList.length > 0" class="home-search-history">
<div class="home-search-history-header">
<span class="home-search-history-title">{{ t('home.searchHistory') }}</span>
<v-btn
variant="text"
size="x-small"
color="primary"
class="home-search-history-clear"
@click="searchHistory.clearAll"
>
{{ t('common.clear') }}
</v-btn>
</div>
<ul class="home-search-history-list">
<li
v-for="(item, idx) in searchHistoryList"
:key="`${item}-${idx}`"
class="home-search-history-item"
>
<button
type="button"
class="home-search-history-text"
@click="selectHistoryItem(item)"
>
{{ item }}
</button>
<v-btn
icon
variant="text"
size="x-small"
class="home-search-history-delete"
:aria-label="t('common.delete')"
@click.stop="searchHistory.remove(idx)"
>
<v-icon size="20">mdi-close</v-icon>
</v-btn>
</li>
</ul>
</div>
</div>
</transition>
</div>
<v-container fluid class="home-container"> <v-container fluid class="home-container">
<!-- 第二三层随内容滚动回顶部时与列表第一行一起出现 --> <!-- 第二三层随内容滚动回顶部时与列表第一行一起出现 -->
<div v-if="categoryLayers.length >= 2" class="home-category-layers-23-scroll"> <div v-if="categoryLayers.length > 1" class="home-category-layers-23-scroll">
<div class="home-category-layer home-category-layer--icon"> <div class="home-category-layer home-category-layer--icon">
<div class="home-category-icon-row"> <div class="home-category-icon-row">
<v-chip <v-chip v-for="item in categoryLayers[1]" :key="item.id" class="home-category-icon-item"
v-for="item in categoryLayers[1]"
:key="item.id"
class="home-category-icon-item"
:color="layerActiveValues[1] === item.id ? 'primary' : undefined" :color="layerActiveValues[1] === item.id ? 'primary' : undefined"
:variant="layerActiveValues[1] === item.id ? 'tonal' : 'outlined'" :variant="layerActiveValues[1] === item.id ? 'tonal' : 'outlined'" size="small"
size="small" @click="menuStore.onCategorySelect(1, item.id)">
@click="onCategorySelect(1, item.id)"
>
<span class="home-category-icon-label">{{ item.label }}</span> <span class="home-category-icon-label">{{ item.label }}</span>
</v-chip> </v-chip>
</div> </div>
</div> </div>
<div <div v-if="categoryLayers.length >= 3" class="home-category-layer home-category-layer--third">
v-if="categoryLayers.length >= 3" <v-tabs :model-value="layerActiveValues[2]" class="home-tab-bar home-tab-bar--compact"
class="home-category-layer home-category-layer--third" @update:model-value="menuStore.onCategorySelect(2, $event)">
> <v-tab v-for="item in categoryLayers[2]" :key="item.id" :value="item.id" :ripple="false">
<v-tabs
:model-value="layerActiveValues[2]"
class="home-tab-bar home-tab-bar--compact"
@update:model-value="onCategorySelect(2, $event)"
>
<v-tab
v-for="item in categoryLayers[2]"
:key="item.id"
:value="item.id"
:ripple="false"
>
{{ item.label }} {{ item.label }}
</v-tab> </v-tab>
</v-tabs> </v-tabs>
@ -125,28 +27,13 @@
<v-pull-to-refresh class="pull-to-refresh" @load="onRefresh"> <v-pull-to-refresh class="pull-to-refresh" @load="onRefresh">
<div class="pull-to-refresh-inner"> <div class="pull-to-refresh-inner">
<div ref="listRef" class="home-list" :style="gridListStyle"> <div ref="listRef" class="home-list" :style="gridListStyle">
<MarketCard <MarketCard v-for="card in eventList" :key="card.id" :id="card.id" :slug="card.slug"
v-for="card in eventList" :market-title="card.marketTitle" :chance-value="card.chanceValue" :market-info="card.marketInfo"
:key="card.id" :image-url="card.imageUrl" :category="card.category" :expires-at="card.expiresAt"
:id="card.id" :display-type="card.displayType" :outcomes="card.outcomes" :yes-label="card.yesLabel"
:slug="card.slug" :no-label="card.noLabel" :is-new="card.isNew" :market-id="card.marketId"
:market-title="card.marketTitle" :clob-token-ids="card.clobTokenIds" :yes-price="card.yesPrice" :no-price="card.noPrice"
:chance-value="card.chanceValue" @open-trade="onCardOpenTrade" />
:market-info="card.marketInfo"
:image-url="card.imageUrl"
:category="card.category"
:expires-at="card.expiresAt"
:display-type="card.displayType"
:outcomes="card.outcomes"
:yes-label="card.yesLabel"
:no-label="card.noLabel"
:is-new="card.isNew"
:market-id="card.marketId"
:clob-token-ids="card.clobTokenIds"
:yes-price="card.yesPrice"
:no-price="card.noPrice"
@open-trade="onCardOpenTrade"
/>
<div v-if="eventListLoading" class="home-list-empty home-list-loading"> <div v-if="eventListLoading" class="home-list-empty home-list-loading">
<v-progress-circular indeterminate size="40" width="2" /> <v-progress-circular indeterminate size="40" width="2" />
<span>{{ t('common.loading') }}</span> <span>{{ t('common.loading') }}</span>
@ -162,14 +49,8 @@
<span>{{ t('common.loading') }}</span> <span>{{ t('common.loading') }}</span>
</div> </div>
<div v-else-if="noMoreEvents" class="no-more-tip">{{ t('home.noMore') }}</div> <div v-else-if="noMoreEvents" class="no-more-tip">{{ t('home.noMore') }}</div>
<v-btn <v-btn v-else class="load-more-btn" variant="outlined" color="primary" :disabled="loadingMore"
v-else @click="loadMore">
class="load-more-btn"
variant="outlined"
color="primary"
:disabled="loadingMore"
@click="loadMore"
>
{{ t('home.loadMore') }} {{ t('home.loadMore') }}
</v-btn> </v-btn>
</div> </div>
@ -178,43 +59,28 @@
</div> </div>
<!-- PC对话框手机底部 sheet直接显示交易表单 --> <!-- PC对话框手机底部 sheet直接显示交易表单 -->
<v-dialog <v-dialog v-if="!isMobile" v-model="tradeDialogOpen" max-width="420" scrollable
v-if="!isMobile" content-class="trade-dialog trade-dialog--bare" transition="dialog-transition"
v-model="tradeDialogOpen" @click:outside="tradeDialogOpen = false">
max-width="420" <TradeComponent :key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`" :market="homeTradeMarketPayload"
scrollable :initial-option="tradeDialogSide" @order-success="onOrderSuccess" />
content-class="trade-dialog trade-dialog--bare"
transition="dialog-transition"
@click:outside="tradeDialogOpen = false"
>
<TradeComponent
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
:market="homeTradeMarketPayload"
:initial-option="tradeDialogSide"
@order-success="onOrderSuccess"
/>
</v-dialog> </v-dialog>
<v-bottom-sheet v-else v-model="tradeDialogOpen" content-class="trade-bottom-sheet"> <v-bottom-sheet v-else v-model="tradeDialogOpen" content-class="trade-bottom-sheet">
<TradeComponent <TradeComponent :key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`" :market="homeTradeMarketPayload"
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`" :initial-option="tradeDialogSide" embedded-in-sheet @order-success="onOrderSuccess" />
:market="homeTradeMarketPayload"
:initial-option="tradeDialogSide"
embedded-in-sheet
@order-success="onOrderSuccess"
/>
</v-bottom-sheet> </v-bottom-sheet>
</v-container> </v-container>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = withDefaults( // const props = withDefaults(
defineProps<{ // defineProps<{
/** 进入页面时是否自动展开搜索(供 /search 路由使用) */ // /** /search 使 */
initialSearchExpanded?: boolean // initialSearchExpanded?: boolean
}>(), // }>(),
{ initialSearchExpanded: false }, // { initialSearchExpanded: false },
) // )
defineOptions({ name: 'HomePage' }) defineOptions({ name: 'HomePage' })
import { import {
@ -244,68 +110,23 @@ import {
enrichWithIcons, enrichWithIcons,
getPmTagMain, getPmTagMain,
MOCK_CATEGORY_TREE, MOCK_CATEGORY_TREE,
type CategoryTreeNode,
} from '../api/category' } from '../api/category'
import { USE_MOCK_CATEGORY } from '../config/mock' import { USE_MOCK_CATEGORY } from '../config/mock'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useSearchHistory } from '../composables/useSearchHistory'
import { useToastStore } from '../stores/toast' import { useToastStore } from '../stores/toast'
import { useLocaleStore } from '../stores/locale' import { useLocaleStore } from '../stores/locale'
import { useMenuStore } from '../stores/menu'
import { storeToRefs } from 'pinia'
const { mobile } = useDisplay() const { mobile } = useDisplay()
const { t } = useI18n() const { t } = useI18n()
const searchHistory = useSearchHistory()
const searchHistoryList = computed(() => searchHistory.list.value)
const isMobile = computed(() => mobile.value) const isMobile = computed(() => mobile.value)
/** 分类树(顶层) */ const menuStore = useMenuStore()
const categoryTree = ref<CategoryTreeNode[]>([]) const { categoryTree, layerActiveValues, categoryLayers } = storeToRefs(menuStore)
/** 每层选中的 id[layer0, layer1?, layer2?] */ const { filterVisible } = menuStore
const layerActiveValues = ref<string[]>([])
/** 第三层搜索框是否展开 */
const searchExpanded = ref(false)
/** 搜索关键词 */
const searchKeyword = ref('')
const searchInputRef = ref<{ focus: () => void } | null>(null)
function expandSearch() {
searchExpanded.value = true
nextTick(() => {
const el = searchInputRef.value as { focus?: () => void } | null
el?.focus?.()
})
}
function collapseSearch() {
searchExpanded.value = false
searchKeyword.value = ''
if (activeSearchKeyword.value) {
activeSearchKeyword.value = ''
clearEventListCache()
eventPage.value = 1
loadEvents(1, false, '')
}
}
function onSearchBlur() {
setTimeout(() => {
if (!searchKeyword.value.trim()) searchExpanded.value = false
}, 100)
}
function onSearchSubmit() {
const k = searchKeyword.value.trim()
if (k) {
searchHistory.add(k)
doSearch(k)
}
}
function selectHistoryItem(item: string) {
searchKeyword.value = item
searchHistory.add(item)
doSearch(item)
}
function doSearch(keyword: string) { function doSearch(keyword: string) {
activeSearchKeyword.value = keyword activeSearchKeyword.value = keyword
@ -314,12 +135,6 @@ function doSearch(keyword: string) {
loadEvents(1, false, keyword) loadEvents(1, false, keyword)
} }
/** 过滤:仅当 forceShow 明确为 false 时不显示其他null/undefined/true 等)均显示;排除 forceHide 的节点 */
function filterVisible(nodes: CategoryTreeNode[] | undefined): CategoryTreeNode[] {
if (!nodes?.length) return []
return nodes.filter((n) => n.forceShow !== false && !n.forceHide)
}
/** 当前选中分类的 tagIds收集所有选中层级节点的 tagId 数组(含父级),用于事件筛选 */ /** 当前选中分类的 tagIds收集所有选中层级节点的 tagId 数组(含父级),用于事件筛选 */
const activeTagIds = computed(() => { const activeTagIds = computed(() => {
const activeIds = layerActiveValues.value const activeIds = layerActiveValues.value
@ -344,59 +159,6 @@ const activeTagIds = computed(() => {
return Array.from(tagIdSet) return Array.from(tagIdSet)
}) })
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
const categoryLayers = computed(() => {
const root = filterVisible(categoryTree.value)
if (root.length === 0) return []
const layers: CategoryTreeNode[][] = [root]
const active = layerActiveValues.value
let currentNodes = root
for (let i = 0; i < 2; i++) {
const selectedId = active[i]
const node = selectedId ? currentNodes.find((n) => n.id === selectedId) : currentNodes[0]
const children = filterVisible(node?.children)
if (children.length === 0) break
layers.push(children)
currentNodes = children
}
return layers
})
/** 分类选中时:若有 children 则展开下一层并默认选中第一个,并重新加载列表 */
function onCategorySelect(layerIndex: number, selectedId: string) {
if (!selectedId || layerActiveValues.value[layerIndex] === selectedId) {
return
}
if (categorySelectTimer) {
clearTimeout(categorySelectTimer)
}
const nextValues = [...layerActiveValues.value]
nextValues[layerIndex] = selectedId
nextValues.length = layerIndex + 1
const layers = categoryLayers.value
const layer = layers[layerIndex]
const node = layer?.find((n) => n.id === selectedId)
const children = filterVisible(node?.children)
const firstChild = children[0]
if (firstChild && layerIndex < 2) {
nextValues.push(firstChild.id)
}
layerActiveValues.value = nextValues
clearEventListCache()
eventPage.value = 1
categorySelectTimer = setTimeout(() => {
categorySelectTimer = null
loadEvents(1, false)
}, 100)
}
/** 初始化分类选中:默认选中第一个,若有 children 则递归展开 */ /** 初始化分类选中:默认选中第一个,若有 children 则递归展开 */
function initCategorySelection() { function initCategorySelection() {
@ -419,7 +181,7 @@ function initCategorySelection() {
const PAGE_SIZE = 10 const PAGE_SIZE = 10
/** 分类切换时的防抖状态,防止重复请求 */ /** 分类切换时的防抖状态,防止重复请求 */
let categorySelectTimer: ReturnType<typeof setTimeout> | null = null
/** 接口返回的列表(已映射为卡片所需结构) */ /** 接口返回的列表(已映射为卡片所需结构) */
const eventList = ref<EventCardItem[]>([]) const eventList = ref<EventCardItem[]>([])
@ -571,8 +333,21 @@ function updateGridColumns() {
gridColumns.value = Math.max(1, n) gridColumns.value = Math.max(1, n)
} }
import { useRoute } from 'vue-router'
const route = useRoute()
/** 当前生效的搜索关键词(用于分页加载) */ /** 当前生效的搜索关键词(用于分页加载) */
const activeSearchKeyword = ref('') const activeSearchKeyword = ref((route.query.q as string) || '')
watch(
() => route.query.q,
(newQ) => {
if (newQ !== undefined) {
doSearch(newQ as string)
}
}
)
/** 请求事件列表并追加或覆盖到 eventList公开接口无需鉴权成功后会更新内存缓存 */ /** 请求事件列表并追加或覆盖到 eventList公开接口无需鉴权成功后会更新内存缓存 */
async function loadEvents(page: number, append: boolean, keyword?: string) { async function loadEvents(page: number, append: boolean, keyword?: string) {
@ -652,7 +427,8 @@ onBeforeRouteLeave(() => {
}) })
onMounted(() => { onMounted(() => {
if (props.initialSearchExpanded) expandSearch() // emit
// if (props.initialSearchExpanded) expandSearch()
loadCategory() loadCategory()
nextTick(() => { nextTick(() => {
const scrollEl = getMainScrollEl() const scrollEl = getMainScrollEl()
@ -718,18 +494,14 @@ onActivated(() => {
}) })
</script> </script>
<style scoped> <style scoped lang="scss">
/* fluid 后无断点 max-width用自定义 max-width 让列表在 2137px 等宽屏下能算到 6 列 */
.home-container { .home-container {
min-height: 100vh; flex: 1 1 0;
max-width: 2560px; display: flex;
margin-left: auto; flex-direction: column;
margin-right: auto; min-height: 0;
padding: 0 8px !important; padding: 0;
} margin: 112px 0 0 0;
.home-header {
margin-bottom: 20px;
} }
.home-title { .home-title {
@ -779,7 +551,7 @@ onActivated(() => {
gap: 12px; gap: 12px;
} }
.home-list > * { .home-list>* {
min-width: 0; min-width: 0;
width: 100%; width: 100%;
} }
@ -823,6 +595,7 @@ onActivated(() => {
padding: 0; padding: 0;
overflow: visible; overflow: visible;
} }
.trade-dialog--bare :deep(.v-card) { .trade-dialog--bare :deep(.v-card) {
box-shadow: none; box-shadow: none;
} }
@ -845,164 +618,6 @@ onActivated(() => {
margin-top: 40px; margin-top: 40px;
} }
/* 第一层置顶、全宽sticky 参照 app-main-scrolltop:0 贴容器顶(即 app-bar 下方) */
.home-category-layer1-wrap {
width: 100vw;
max-width: 100vw;
margin-left: calc(50% - 50vw);
position: sticky;
top: 0;
z-index: 10;
background-color: white;
/* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
}
.home-category-layer1-row {
display: flex;
align-items: center;
min-height: 48px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.home-tab-bar--inline {
flex: 1;
min-width: 0;
}
.home-tab-bar--inline :deep(.v-tabs) {
min-height: 48px;
}
.home-tab-bar--inline :deep(.v-tab) {
min-height: 48px;
}
.home-category-layer1-actions {
display: flex;
flex-shrink: 0;
gap: 0;
}
/* 搜索浮层:绝对定位,遮挡第二三层,不顶开布局 */
.home-search-overlay {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 11;
padding: 8px 12px 12px;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
.home-search-overlay-inner {
display: flex;
align-items: center;
gap: 8px;
}
.home-search-overlay-field {
flex: 1;
min-width: 0;
font-size: 14px;
}
.home-search-close-btn {
flex-shrink: 0;
}
.home-search-overlay-field :deep(.v-field) {
background-color: #f8fafc;
}
.home-search-overlay-field :deep(.v-field__prepend-inner .v-icon) {
font-size: 22px;
}
.home-search-overlay-enter-active,
.home-search-overlay-leave-active {
transition:
opacity 0.15s ease,
transform 0.15s ease;
}
.home-search-overlay-enter-from,
.home-search-overlay-leave-to {
opacity: 0;
}
.home-search-history {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
.home-search-history-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.home-search-history-title {
font-size: 12px;
color: #64748b;
}
.home-search-history-clear {
min-width: auto;
padding: 0 4px;
font-size: 12px;
}
.home-search-history-list {
list-style: none;
margin: 0;
padding: 0;
max-height: 160px;
overflow-y: auto;
}
.home-search-history-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
.home-search-history-item:last-child {
border-bottom: none;
}
.home-search-history-text {
flex: 1;
min-width: 0;
padding: 0;
border: none;
background: none;
font-size: 14px;
color: #1e293b;
text-align: left;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.home-search-history-text:hover {
color: rgb(var(--v-theme-primary));
}
.home-search-history-delete {
flex-shrink: 0;
min-width: 28px;
color: #94a3b8;
}
.home-search-history-delete:hover {
color: #ef4444;
}
/* 第二三层:随内容滚动,全宽 */ /* 第二三层:随内容滚动,全宽 */
.home-category-layers-23-scroll { .home-category-layers-23-scroll {
@ -1011,6 +626,10 @@ onActivated(() => {
margin-left: calc(50% - 50vw); margin-left: calc(50% - 50vw);
margin-bottom: 0; margin-bottom: 0;
background-color: white; background-color: white;
@include gt1024 {
display: none;
}
} }
.home-category-layer { .home-category-layer {
@ -1043,6 +662,7 @@ onActivated(() => {
} }
.home-category-icon-item { .home-category-icon-item {
padding: 0 8px 0 8px;
flex-shrink: 0; flex-shrink: 0;
} }
@ -1077,13 +697,7 @@ onActivated(() => {
.home-tab-bar { .home-tab-bar {
position: relative; position: relative;
top: auto;
left: auto;
transform: none; transform: none;
width: 100%;
background-color: transparent;
margin-bottom: 0;
box-shadow: none;
} }
.home-tab-bar :deep(.v-tab__slider), .home-tab-bar :deep(.v-tab__slider),
@ -1109,6 +723,7 @@ onActivated(() => {
.home-tab-bar :deep(.v-btn__underlay) { .home-tab-bar :deep(.v-btn__underlay) {
display: none !important; display: none !important;
} }
.home-tab-bar :deep(.v-tab:hover) { .home-tab-bar :deep(.v-tab:hover) {
background-color: transparent !important; background-color: transparent !important;
} }
@ -1131,10 +746,12 @@ onActivated(() => {
min-height: 28px !important; min-height: 28px !important;
height: 28px !important; height: 28px !important;
} }
.home-tab-bar--compact :deep(.v-slide-group__container) { .home-tab-bar--compact :deep(.v-slide-group__container) {
min-height: 28px !important; min-height: 28px !important;
height: 28px !important; height: 28px !important;
} }
.home-tab-bar--compact :deep(.v-slide-group__content) { .home-tab-bar--compact :deep(.v-slide-group__content) {
align-items: center; align-items: center;
} }
@ -1170,10 +787,9 @@ onActivated(() => {
} }
} }
/* 页面布局flex 列 */
.home-page { .home-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; height: 100%;
} }
</style> </style>

View File

@ -29,6 +29,17 @@ export default defineConfig({
define: { define: {
'process.env': {}, 'process.env': {},
}, },
css: {
preprocessorOptions: {
scss: {
// 全局注入变量 + 混入
additionalData: `
@import "@/assets/styles/variables.scss";
@import "@/assets/styles/mixin.scss";
`
}
}
},
build: { build: {
target: 'es2020', target: 'es2020',
cssTarget: 'chrome64', cssTarget: 'chrome64',