chore: 完成项目多模块迭代更新

本次更新包含:
1.  初始化git提交规范配置文件
2.  添加claude调试配置
3.  后端:修复WebSocket依赖、优化秒杀/客服模块代码、修复文件存储bug、统一包结构
4.  前端:重构登录页面、新增组件类型声明、添加自动导入配置、新增WebSocket依赖包
This commit is contained in:
sparksfly 2026-06-05 14:48:52 +08:00
parent 3288d12fbd
commit ff0d020015
31 changed files with 1013 additions and 105 deletions

11
.claude/launch.json Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "web-snack",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 5174
}
]
}

View File

@ -0,0 +1,8 @@
---
alwaysApply: true
scene: git_message
language: zh-CN
---
在此处编写规则,自定义 AI 生成提交信息的风格。
中文提交信息

View File

@ -11,6 +11,9 @@ importers:
'@element-plus/icons-vue': '@element-plus/icons-vue':
specifier: ^2.3.1 specifier: ^2.3.1
version: 2.3.2(vue@3.5.35(typescript@5.9.3)) version: 2.3.2(vue@3.5.35(typescript@5.9.3))
'@stomp/stompjs':
specifier: ^7.3.0
version: 7.3.0
'@vueuse/core': '@vueuse/core':
specifier: ^12.0.0 specifier: ^12.0.0
version: 12.8.2(typescript@5.9.3) version: 12.8.2(typescript@5.9.3)
@ -50,6 +53,9 @@ importers:
pinia-plugin-persistedstate: pinia-plugin-persistedstate:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.7.1(pinia@2.3.1(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3))) version: 4.7.1(pinia@2.3.1(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3)))
sockjs-client:
specifier: ^1.6.1
version: 1.6.1
vue: vue:
specifier: ^3.5.13 specifier: ^3.5.13
version: 3.5.35(typescript@5.9.3) version: 3.5.35(typescript@5.9.3)
@ -930,6 +936,9 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@stomp/stompjs@7.3.0':
resolution: {integrity: sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==}
'@sxzz/popperjs-es@2.11.8': '@sxzz/popperjs-es@2.11.8':
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
@ -1509,6 +1518,14 @@ packages:
de-indent@1.0.2: de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.3: debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -1690,6 +1707,10 @@ packages:
event-emitter@0.3.5: event-emitter@0.3.5:
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
eventsource@2.0.2:
resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==}
engines: {node: '>=12.0.0'}
exsolve@1.0.8: exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
@ -1712,6 +1733,10 @@ packages:
fastq@1.20.1: fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
faye-websocket@0.11.4:
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
engines: {node: '>=0.8.0'}
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -1824,6 +1849,9 @@ packages:
html-void-elements@2.0.1: html-void-elements@2.0.1:
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
http-parser-js@0.5.10:
resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -1856,6 +1884,9 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'} engines: {node: '>=0.8.19'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
is-binary-path@2.1.0: is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2205,6 +2236,9 @@ packages:
quansync@0.2.11: quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -2216,6 +2250,9 @@ packages:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -2232,6 +2269,9 @@ packages:
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
sass@1.100.0: sass@1.100.0:
resolution: {integrity: sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==} resolution: {integrity: sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==}
engines: {node: '>=20.19.0'} engines: {node: '>=20.19.0'}
@ -2276,6 +2316,10 @@ packages:
resolution: {integrity: sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==} resolution: {integrity: sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==}
engines: {node: '>=12.17.0'} engines: {node: '>=12.17.0'}
sockjs-client@1.6.1:
resolution: {integrity: sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==}
engines: {node: '>=12'}
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2420,6 +2464,9 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -2543,6 +2590,14 @@ packages:
webpack-virtual-modules@0.6.2: webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
websocket-driver@0.7.4:
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
engines: {node: '>=0.8.0'}
websocket-extensions@0.1.4:
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
engines: {node: '>=0.8.0'}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -3185,6 +3240,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.61.0': '@rollup/rollup-win32-x64-msvc@4.61.0':
optional: true optional: true
'@stomp/stompjs@7.3.0': {}
'@sxzz/popperjs-es@2.11.8': {} '@sxzz/popperjs-es@2.11.8': {}
'@transloadit/prettier-bytes@0.0.7': {} '@transloadit/prettier-bytes@0.0.7': {}
@ -3945,6 +4002,10 @@ snapshots:
de-indent@1.0.2: {} de-indent@1.0.2: {}
debug@3.2.7:
dependencies:
ms: 2.1.3
debug@4.4.3: debug@4.4.3:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@ -4223,6 +4284,8 @@ snapshots:
d: 1.0.2 d: 1.0.2
es5-ext: 0.10.64 es5-ext: 0.10.64
eventsource@2.0.2: {}
exsolve@1.0.8: {} exsolve@1.0.8: {}
ext@1.7.0: ext@1.7.0:
@ -4247,6 +4310,10 @@ snapshots:
dependencies: dependencies:
reusify: 1.1.0 reusify: 1.1.0
faye-websocket@0.11.4:
dependencies:
websocket-driver: 0.7.4
fdir@6.5.0(picomatch@4.0.4): fdir@6.5.0(picomatch@4.0.4):
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
@ -4344,6 +4411,8 @@ snapshots:
html-void-elements@2.0.1: {} html-void-elements@2.0.1: {}
http-parser-js@0.5.10: {}
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2
@ -4381,6 +4450,8 @@ snapshots:
imurmurhash@0.1.4: {} imurmurhash@0.1.4: {}
inherits@2.0.4: {}
is-binary-path@2.1.0: is-binary-path@2.1.0:
dependencies: dependencies:
binary-extensions: 2.3.0 binary-extensions: 2.3.0
@ -4661,6 +4732,8 @@ snapshots:
quansync@0.2.11: {} quansync@0.2.11: {}
querystringify@2.2.0: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
readdirp@3.6.0: readdirp@3.6.0:
@ -4669,6 +4742,8 @@ snapshots:
readdirp@5.0.0: {} readdirp@5.0.0: {}
requires-port@1.0.0: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
reusify@1.1.0: {} reusify@1.1.0: {}
@ -4708,6 +4783,8 @@ snapshots:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
safe-buffer@5.2.1: {}
sass@1.100.0: sass@1.100.0:
dependencies: dependencies:
chokidar: 5.0.0 chokidar: 5.0.0
@ -4751,6 +4828,16 @@ snapshots:
snabbdom@3.6.3: {} snabbdom@3.6.3: {}
sockjs-client@1.6.1:
dependencies:
debug: 3.2.7
eventsource: 2.0.2
faye-websocket: 0.11.4
inherits: 2.0.4
url-parse: 1.5.10
transitivePeerDependencies:
- supports-color
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
ssr-window@3.0.0: {} ssr-window@3.0.0: {}
@ -4920,6 +5007,11 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
vite@6.4.3(@types/node@22.19.19)(jiti@2.7.0)(sass@1.100.0)(tsx@4.22.4): vite@6.4.3(@types/node@22.19.19)(jiti@2.7.0)(sass@1.100.0)(tsx@4.22.4):
@ -5017,6 +5109,14 @@ snapshots:
webpack-virtual-modules@0.6.2: {} webpack-virtual-modules@0.6.2: {}
websocket-driver@0.7.4:
dependencies:
http-parser-js: 0.5.10
safe-buffer: 5.2.1
websocket-extensions: 0.1.4
websocket-extensions@0.1.4: {}
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0

View File

@ -23,6 +23,7 @@ declare module 'vue' {
ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@ -51,6 +52,8 @@ declare module 'vue' {
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
ImageUploader: typeof import('./../components/business/ImageUploader.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SearchBar: typeof import('./../components/common/SearchBar.vue')['default'] SearchBar: typeof import('./../components/common/SearchBar.vue')['default']

View File

@ -35,9 +35,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-websocket</artifactId> <artifactId>spring-boot-starter-websocket</artifactId>
<version>6.2.15</version>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -10,6 +10,8 @@ import java.io.Serializable;
@Data @Data
public class Result<T> implements Serializable { public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code; private Integer code;
private String message; private String message;
private T data; private T data;

View File

@ -68,7 +68,7 @@ public class LocalFileStorage implements FileStorage {
String storedName = IdUtil.fastSimpleUUID() + "." + ext; String storedName = IdUtil.fastSimpleUUID() + "." + ext;
File target = new File(dir.toFile(), storedName); File target = new File(dir.toFile(), storedName);
try { try {
FileUtil.writeBytes(target, bytes); FileUtil.writeBytes(bytes, target);
} catch (Exception e) { } catch (Exception e) {
log.error("写入文件失败 {}", target.getAbsolutePath(), e); log.error("写入文件失败 {}", target.getAbsolutePath(), e);
throw new RuntimeException("写入文件失败", e); throw new RuntimeException("写入文件失败", e);

View File

@ -49,7 +49,7 @@ public class S3FileStorage implements FileStorage, DisposableBean {
"S3 存储配置不完整:请在 application.yml 配置 snack.s3.{endpoint,access-key,secret-key,bucket}"); "S3 存储配置不完整:请在 application.yml 配置 snack.s3.{endpoint,access-key,secret-key,bucket}");
} }
this.s3Client = S3Client.builder() this.s3Client = S3Client.builder()
.endpoint(URI.create(props.getEndpoint())) .endpointOverride(URI.create(props.getEndpoint()))
.region(Region.of(props.getRegion())) .region(Region.of(props.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create( .credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(props.getAccessKey(), props.getSecretKey()))) AwsBasicCredentials.create(props.getAccessKey(), props.getSecretKey())))

View File

@ -1,4 +1,4 @@
package com.snack.server.config; package com.snack.server.config;
import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.router.SaRouter;

View File

@ -1,4 +1,4 @@
package com.snack.server.config; package com.snack.server.config;
import com.snack.server.module.chat.websocket.ChatHandshakeInterceptor; import com.snack.server.module.chat.websocket.ChatHandshakeInterceptor;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@ -5,21 +5,6 @@ package com.snack.server.constant;
*/ */
public interface RedisKey { public interface RedisKey {
/** 抢购商品库存seckill:stock:{activityId}:{productId} */
String SECKILL_STOCK = "seckill:stock:%d:%d";
/** 用户抢购记录seckill:user:{userId}:{activityId}:{productId} */
String SECKILL_USER_RECORD = "seckill:user:%d:%d:%d";
/** 客服在线用户集合 */
String CHAT_ONLINE_USERS = "chat:online:users";
/** 会话未读消息数chat:unread:{sessionId} */
String CHAT_UNREAD = "chat:unread:%d";
/** 验证码captcha:{uuid} */
String CAPTCHA = "captcha:%s";
// ==================== 客服模块 ==================== // ==================== 客服模块 ====================
/** 客服在线用户集合Set 存放 "user:{id}" / "admin:{id}" */ /** 客服在线用户集合Set 存放 "user:{id}" / "admin:{id}" */
@ -31,6 +16,9 @@ public interface RedisKey {
/** 客服 WebSocket Session 索引 */ /** 客服 WebSocket Session 索引 */
String CHAT_WS_ADMIN = "chat:ws:admin:%d"; String CHAT_WS_ADMIN = "chat:ws:admin:%d";
/** 会话未读消息数chat:unread:{sessionId} */
String CHAT_UNREAD = "chat:unread:%d";
// ==================== 抢购模块 ==================== // ==================== 抢购模块 ====================
/** 抢购商品库存seckill:stock:{activityId}:{skuId} */ /** 抢购商品库存seckill:stock:{activityId}:{skuId} */
@ -44,4 +32,9 @@ public interface RedisKey {
/** 活动预热标记seckill:warmup:{activityId}1 表示 Redis 库存已同步) */ /** 活动预热标记seckill:warmup:{activityId}1 表示 Redis 库存已同步) */
String SECKILL_WARMUP = "seckill:warmup:%d"; String SECKILL_WARMUP = "seckill:warmup:%d";
// ==================== 公共 ====================
/** 验证码captcha:{uuid} */
String CAPTCHA = "captcha:%s";
} }

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.controller; package com.snack.server.module.chat.controller;
import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.controller; package com.snack.server.module.chat.controller;
import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.controller; package com.snack.server.module.chat.controller;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.snack.server.common.Result; import com.snack.server.common.Result;

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.service; package com.snack.server.module.chat.service;
import com.snack.server.module.chat.dto.req.ChatAckReq; import com.snack.server.module.chat.dto.req.ChatAckReq;
import com.snack.server.module.chat.dto.req.ChatSendMessageReq; import com.snack.server.module.chat.dto.req.ChatSendMessageReq;

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.service; package com.snack.server.module.chat.service;
import com.snack.server.constant.RedisKey; import com.snack.server.constant.RedisKey;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.service; package com.snack.server.module.chat.service;
import com.snack.server.module.chat.dto.req.QuickReplySaveReq; import com.snack.server.module.chat.dto.req.QuickReplySaveReq;
import com.snack.server.module.chat.vo.QuickReplyVO; import com.snack.server.module.chat.vo.QuickReplyVO;

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.service.impl; package com.snack.server.module.chat.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;

View File

@ -36,7 +36,7 @@ import java.util.stream.Collectors;
* 客服会话服务实现 * 客服会话服务实现
*/ */
@Slf4j @Slf4j
Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class ChatSessionServiceImpl implements ChatSessionService { public class ChatSessionServiceImpl implements ChatSessionService {
@ -179,10 +179,12 @@ public class ChatSessionServiceImpl implements ChatSessionService {
List<Long> adminIds = records.stream().map(ChatSession::getAdminId) List<Long> adminIds = records.stream().map(ChatSession::getAdminId)
.filter(java.util.Objects::nonNull).distinct().toList(); .filter(java.util.Objects::nonNull).distinct().toList();
Map<Long, User> userMap = userIds.isEmpty() ? Collections.emptyMap() Map<Long, User> userMap = userIds.isEmpty() ? Collections.emptyMap()
: userMapper.selectBatchIds(userIds).stream() : userMapper.selectList(
new LambdaQueryWrapper<User>().in(User::getId, userIds)).stream()
.collect(Collectors.toMap(User::getId, Function.identity())); .collect(Collectors.toMap(User::getId, Function.identity()));
Map<Long, Admin> adminMap = adminIds.isEmpty() ? Collections.emptyMap() Map<Long, Admin> adminMap = adminIds.isEmpty() ? Collections.emptyMap()
: adminMapper.selectBatchIds(adminIds).stream() : adminMapper.selectList(
new LambdaQueryWrapper<Admin>().in(Admin::getId, adminIds)).stream()
.collect(Collectors.toMap(Admin::getId, Function.identity())); .collect(Collectors.toMap(Admin::getId, Function.identity()));
// 关键字过滤用户名 / 会话编号发生在拿到 VO 之后 // 关键字过滤用户名 / 会话编号发生在拿到 VO 之后

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.service.impl; package com.snack.server.module.chat.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.websocket; package com.snack.server.module.chat.websocket;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpRequest;

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.websocket; package com.snack.server.module.chat.websocket;
import com.snack.server.module.chat.dto.req.ChatAckReq; import com.snack.server.module.chat.dto.req.ChatAckReq;
import com.snack.server.module.chat.dto.req.ChatSendMessageReq; import com.snack.server.module.chat.dto.req.ChatSendMessageReq;
@ -15,7 +15,6 @@ import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import java.security.Principal; import java.security.Principal;
/** /**
* WebSocket STOMP 控制器 * WebSocket STOMP 控制器
* *

View File

@ -1,4 +1,4 @@
package com.snack.server.module.chat.websocket; package com.snack.server.module.chat.websocket;
import com.snack.server.module.chat.service.ChatOnlineService; import com.snack.server.module.chat.service.ChatOnlineService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;

View File

@ -2,7 +2,7 @@ package com.snack.server.module.file.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.snack.server.common.exception.BusinessException; import com.snack.server.exception.BusinessException;
import com.snack.server.common.storage.FileStorage; import com.snack.server.common.storage.FileStorage;
import com.snack.server.common.storage.FileStoreResult; import com.snack.server.common.storage.FileStoreResult;
import com.snack.server.module.file.entity.UploadFile; import com.snack.server.module.file.entity.UploadFile;

View File

@ -90,7 +90,7 @@ public class SeckillServiceImpl implements SeckillService {
.gt(SeckillActivity::getEndTime, LocalDateTime.now().minusHours(1)) // 兜底展示刚结束 1 小时的 .gt(SeckillActivity::getEndTime, LocalDateTime.now().minusHours(1)) // 兜底展示刚结束 1 小时的
.orderByAsc(SeckillActivity::getStartTime) .orderByAsc(SeckillActivity::getStartTime)
); );
if (CollUtil.isEmpty(activities)) return new ArrayList<>(); if (CollUtil.isEmpty(activities)) return new ArrayList<SeckillActivityVO>();
return activities.stream().map(a -> enrichActivityVO(a, false)).toList(); return activities.stream().map(a -> enrichActivityVO(a, false)).toList();
} }
@ -133,9 +133,15 @@ public class SeckillServiceImpl implements SeckillService {
// 批量取 SPU / SKU // 批量取 SPU / SKU
Set<Long> spuIds = products.stream().map(SeckillProduct::getProductId).collect(Collectors.toSet()); Set<Long> spuIds = products.stream().map(SeckillProduct::getProductId).collect(Collectors.toSet());
Set<Long> skuIds = products.stream().map(SeckillProduct::getSkuId).collect(Collectors.toSet()); Set<Long> skuIds = products.stream().map(SeckillProduct::getSkuId).collect(Collectors.toSet());
Map<Long, Product> productMap = productEntityMapper.selectBatchIds(spuIds).stream() Map<Long, Product> productMap = spuIds.isEmpty() ? java.util.Collections.emptyMap()
: productEntityMapper.selectList(
new LambdaQueryWrapper<Product>().in(Product::getId, spuIds))
.stream()
.collect(Collectors.toMap(Product::getId, p -> p)); .collect(Collectors.toMap(Product::getId, p -> p));
Map<Long, ProductSku> skuMap = productSkuMapper.selectBatchIds(skuIds).stream() Map<Long, ProductSku> skuMap = skuIds.isEmpty() ? java.util.Collections.emptyMap()
: productSkuMapper.selectList(
new LambdaQueryWrapper<ProductSku>().in(ProductSku::getId, skuIds))
.stream()
.collect(Collectors.toMap(ProductSku::getId, s -> s)); .collect(Collectors.toMap(ProductSku::getId, s -> s));
vo.setProducts(products.stream().map(sp -> { vo.setProducts(products.stream().map(sp -> {
@ -167,7 +173,7 @@ public class SeckillServiceImpl implements SeckillService {
return pvo; return pvo;
}).toList()); }).toList());
} else { } else {
vo.setProducts(new ArrayList<>()); vo.setProducts(new ArrayList<SeckillProductVO>());
} }
} }
return vo; return vo;
@ -184,7 +190,7 @@ public class SeckillServiceImpl implements SeckillService {
} }
Integer nowStatus = SeckillActivityStatusEnum.calcStatus(activity.getStartTime(), activity.getEndTime()); Integer nowStatus = SeckillActivityStatusEnum.calcStatus(activity.getStartTime(), activity.getEndTime());
if (nowStatus != SeckillActivityStatusEnum.IN_PROGRESS.getCode()) { if (nowStatus != SeckillActivityStatusEnum.IN_PROGRESS.getCode()) {
throw new BusinessException(nowStatus == SeckillActivityStatusEnum.NOT_STARTED.getCode() throw new BusinessException(Objects.equals(nowStatus, SeckillActivityStatusEnum.NOT_STARTED.getCode())
? ResultCode.SECKILL_NOT_STARTED : ResultCode.SECKILL_ENDED); ? ResultCode.SECKILL_NOT_STARTED : ResultCode.SECKILL_ENDED);
} }

View File

@ -0,0 +1,96 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"ShallowRef": true,
"Slot": true,
"Slots": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
"computed": true,
"createApp": true,
"createPinia": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"effectScope": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"getCurrentWatcher": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"isShallow": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useId": true,
"useLink": true,
"useModel": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"useTemplateRef": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

View File

@ -11,9 +11,15 @@ importers:
'@element-plus/icons-vue': '@element-plus/icons-vue':
specifier: ^2.3.2 specifier: ^2.3.2
version: 2.3.2(vue@3.5.35(typescript@6.0.3)) version: 2.3.2(vue@3.5.35(typescript@6.0.3))
'@stomp/stompjs':
specifier: ^7.3.0
version: 7.3.0
'@types/mockjs': '@types/mockjs':
specifier: ^1.0.10 specifier: ^1.0.10
version: 1.0.10 version: 1.0.10
'@types/nprogress':
specifier: ^0.2.3
version: 0.2.3
axios: axios:
specifier: ^1.17.0 specifier: ^1.17.0
version: 1.17.0 version: 1.17.0
@ -23,12 +29,18 @@ importers:
mockjs: mockjs:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0 version: 1.1.0
nprogress:
specifier: ^0.2.0
version: 0.2.0
pinia: pinia:
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(typescript@6.0.3)(vue@3.5.35(typescript@6.0.3)) version: 3.0.4(typescript@6.0.3)(vue@3.5.35(typescript@6.0.3))
sass: sass:
specifier: ^1.100.0 specifier: ^1.100.0
version: 1.100.0 version: 1.100.0
sockjs-client:
specifier: ^1.6.1
version: 1.6.1
unplugin-auto-import: unplugin-auto-import:
specifier: ^21.0.0 specifier: ^21.0.0
version: 21.0.0(@vueuse/core@14.3.0(vue@3.5.35(typescript@6.0.3))) version: 21.0.0(@vueuse/core@14.3.0(vue@3.5.35(typescript@6.0.3)))
@ -453,6 +465,9 @@ packages:
'@rolldown/pluginutils@1.0.1': '@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@stomp/stompjs@7.3.0':
resolution: {integrity: sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==}
'@sxzz/popperjs-es@2.11.8': '@sxzz/popperjs-es@2.11.8':
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
@ -477,6 +492,9 @@ packages:
'@types/node@24.12.4': '@types/node@24.12.4':
resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==}
'@types/nprogress@0.2.3':
resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
'@types/web-bluetooth@0.0.21': '@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
@ -682,6 +700,14 @@ packages:
dayjs@1.11.21: dayjs@1.11.21:
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.3: debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -760,9 +786,17 @@ packages:
estree-walker@3.0.3: estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
eventsource@2.0.2:
resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==}
engines: {node: '>=12.0.0'}
exsolve@1.0.8: exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
faye-websocket@0.11.4:
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
engines: {node: '>=0.8.0'}
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -824,6 +858,9 @@ packages:
hookable@5.5.3: hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
http-parser-js@0.5.10:
resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -831,6 +868,9 @@ packages:
immutable@5.1.6: immutable@5.1.6:
resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==} resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
is-docker@3.0.0: is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -1051,6 +1091,9 @@ packages:
engines: {node: ^20.5.0 || >=22.0.0, npm: '>= 10'} engines: {node: ^20.5.0 || >=22.0.0, npm: '>= 10'}
hasBin: true hasBin: true
nprogress@0.2.0:
resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
obug@2.1.1: obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
@ -1119,6 +1162,9 @@ packages:
quansync@0.2.11: quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
read-package-json-fast@4.0.0: read-package-json-fast@4.0.0:
resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==} resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==}
engines: {node: ^18.17.0 || >=20.5.0} engines: {node: ^18.17.0 || >=20.5.0}
@ -1127,6 +1173,9 @@ packages:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
rfdc@1.4.1: rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
@ -1139,6 +1188,9 @@ packages:
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
sass@1.100.0: sass@1.100.0:
resolution: {integrity: sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==} resolution: {integrity: sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==}
engines: {node: '>=20.19.0'} engines: {node: '>=20.19.0'}
@ -1167,6 +1219,10 @@ packages:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'} engines: {node: '>=18'}
sockjs-client@1.6.1:
resolution: {integrity: sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==}
engines: {node: '>=12'}
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1248,6 +1304,9 @@ packages:
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
vite-dev-rpc@2.0.0: vite-dev-rpc@2.0.0:
resolution: {integrity: sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==} resolution: {integrity: sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==}
peerDependencies: peerDependencies:
@ -1350,6 +1409,14 @@ packages:
webpack-virtual-modules@0.6.2: webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
websocket-driver@0.7.4:
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
engines: {node: '>=0.8.0'}
websocket-extensions@0.1.4:
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
engines: {node: '>=0.8.0'}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -1735,6 +1802,8 @@ snapshots:
'@rolldown/pluginutils@1.0.1': {} '@rolldown/pluginutils@1.0.1': {}
'@stomp/stompjs@7.3.0': {}
'@sxzz/popperjs-es@2.11.8': {} '@sxzz/popperjs-es@2.11.8': {}
'@tsconfig/node24@24.0.4': {} '@tsconfig/node24@24.0.4': {}
@ -1758,6 +1827,8 @@ snapshots:
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
'@types/nprogress@0.2.3': {}
'@types/web-bluetooth@0.0.21': {} '@types/web-bluetooth@0.0.21': {}
'@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@24.12.4)(sass@1.100.0))(vue@3.5.35(typescript@6.0.3))': '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@24.12.4)(sass@1.100.0))(vue@3.5.35(typescript@6.0.3))':
@ -2007,6 +2078,10 @@ snapshots:
dayjs@1.11.21: {} dayjs@1.11.21: {}
debug@3.2.7:
dependencies:
ms: 2.1.3
debug@4.4.3: debug@4.4.3:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@ -2080,8 +2155,14 @@ snapshots:
dependencies: dependencies:
'@types/estree': 1.0.9 '@types/estree': 1.0.9
eventsource@2.0.2: {}
exsolve@1.0.8: {} exsolve@1.0.8: {}
faye-websocket@0.11.4:
dependencies:
websocket-driver: 0.7.4
fdir@6.5.0(picomatch@4.0.4): fdir@6.5.0(picomatch@4.0.4):
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
@ -2135,6 +2216,8 @@ snapshots:
hookable@5.5.3: {} hookable@5.5.3: {}
http-parser-js@0.5.10: {}
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2
@ -2144,6 +2227,8 @@ snapshots:
immutable@5.1.6: {} immutable@5.1.6: {}
inherits@2.0.4: {}
is-docker@3.0.0: {} is-docker@3.0.0: {}
is-extglob@2.1.1: is-extglob@2.1.1:
@ -2308,6 +2393,8 @@ snapshots:
shell-quote: 1.8.4 shell-quote: 1.8.4
which: 5.0.0 which: 5.0.0
nprogress@0.2.0: {}
obug@2.1.1: {} obug@2.1.1: {}
ohash@2.0.11: {} ohash@2.0.11: {}
@ -2368,6 +2455,8 @@ snapshots:
quansync@0.2.11: {} quansync@0.2.11: {}
querystringify@2.2.0: {}
read-package-json-fast@4.0.0: read-package-json-fast@4.0.0:
dependencies: dependencies:
json-parse-even-better-errors: 4.0.0 json-parse-even-better-errors: 4.0.0
@ -2375,6 +2464,8 @@ snapshots:
readdirp@5.0.0: {} readdirp@5.0.0: {}
requires-port@1.0.0: {}
rfdc@1.4.1: {} rfdc@1.4.1: {}
rolldown@1.0.3: rolldown@1.0.3:
@ -2400,6 +2491,8 @@ snapshots:
run-applescript@7.1.0: {} run-applescript@7.1.0: {}
safe-buffer@5.2.1: {}
sass@1.100.0: sass@1.100.0:
dependencies: dependencies:
chokidar: 5.0.0 chokidar: 5.0.0
@ -2426,6 +2519,16 @@ snapshots:
mrmime: 2.0.1 mrmime: 2.0.1
totalist: 3.0.1 totalist: 3.0.1
sockjs-client@1.6.1:
dependencies:
debug: 3.2.7
eventsource: 2.0.2
faye-websocket: 0.11.4
inherits: 2.0.4
url-parse: 1.5.10
transitivePeerDependencies:
- supports-color
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
speakingurl@14.0.1: {} speakingurl@14.0.1: {}
@ -2519,6 +2622,11 @@ snapshots:
escalade: 3.2.0 escalade: 3.2.0
picocolors: 1.1.1 picocolors: 1.1.1
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
vite-dev-rpc@2.0.0(vite@8.0.16(@types/node@24.12.4)(sass@1.100.0)): vite-dev-rpc@2.0.0(vite@8.0.16(@types/node@24.12.4)(sass@1.100.0)):
dependencies: dependencies:
birpc: 4.0.0 birpc: 4.0.0
@ -2610,6 +2718,14 @@ snapshots:
webpack-virtual-modules@0.6.2: {} webpack-virtual-modules@0.6.2: {}
websocket-driver@0.7.4:
dependencies:
http-parser-js: 0.5.10
safe-buffer: 5.2.1
websocket-extensions: 0.1.4
websocket-extensions@0.1.4: {}
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0

View File

@ -58,8 +58,12 @@ function logout() {
<a @click="goUser" class="link">个人中心</a> <a @click="goUser" class="link">个人中心</a>
<a @click="logout" class="link">退出</a> <a @click="logout" class="link">退出</a>
</template> </template>
<a @click="router.push('/notice')" class="link"><el-icon><Bell /></el-icon> </a> <a @click="router.push('/notice')" class="link notice-link">
<span class="hotline"><el-icon><Phone /></el-icon> 400-888-8888</span> <span class="notice-icon">
<el-icon :size="14"><Bell /></el-icon>
</span>
公告
</a> <span class="hotline"><el-icon><Phone /></el-icon> 400-888-8888</span>
</div> </div>
</div> </div>
</div> </div>
@ -87,9 +91,15 @@ function logout() {
<div class="header-actions"> <div class="header-actions">
<el-badge :value="cartStore.totalCount" :hidden="cartStore.totalCount === 0" class="action-item"> <el-badge :value="cartStore.totalCount" :hidden="cartStore.totalCount === 0" class="action-item">
<el-button :icon="ShoppingCart" @click="router.push('/cart')">购物车</el-button> <div class="action-card" @click="router.push('/cart')">
<el-icon :size="22" class="action-card-icon"><ShoppingCart /></el-icon>
<span class="action-card-text">购物车</span>
</div>
</el-badge> </el-badge>
<el-button :icon="User" @click="goUser" class="action-item">我的</el-button> <div class="action-card" @click="goUser">
<el-icon :size="22" class="action-card-icon"><User /></el-icon>
<span class="action-card-text">我的</span>
</div>
</div> </div>
</div> </div>
</header> </header>
@ -200,6 +210,26 @@ function logout() {
.top-right .link:hover { .top-right .link:hover {
color: #fff; color: #fff;
} }
.notice-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 4px;
}
.notice-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.15);
color: #fff;
transition: background 150ms;
}
.notice-link:hover .notice-icon {
background: rgba(255, 255, 255, 0.3);
}
.hotline { .hotline {
display: flex; display: flex;
align-items: center; align-items: center;
@ -238,9 +268,36 @@ function logout() {
.header-actions { .header-actions {
display: flex; display: flex;
gap: 12px; gap: 12px;
align-items: center;
} }
.action-item { .action-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
min-width: 56px;
height: 56px;
padding: 0 12px;
border: 1px solid #f3f4f6;
border-radius: 8px;
background: #fff;
color: #4b5563;
cursor: pointer; cursor: pointer;
transition: border-color 150ms, color 150ms, transform 150ms;
user-select: none;
}
.action-card:hover {
border-color: var(--color-primary);
color: var(--color-primary);
transform: translateY(-1px);
}
.action-card-icon {
color: var(--color-primary);
}
.action-card-text {
font-size: 12px;
line-height: 1;
} }
/* Nav */ /* Nav */

90
web-snack/src/types/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,90 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const createPinia: typeof import('pinia').createPinia
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineStore: typeof import('pinia').defineStore
const effectScope: typeof import('vue').effectScope
const getActivePinia: typeof import('pinia').getActivePinia
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const mapActions: typeof import('pinia').mapActions
const mapGetters: typeof import('pinia').mapGetters
const mapState: typeof import('pinia').mapState
const mapStores: typeof import('pinia').mapStores
const mapWritableState: typeof import('pinia').mapWritableState
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const storeToRefs: typeof import('pinia').storeToRefs
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useLink: typeof import('vue-router').useLink
const useModel: typeof import('vue').useModel
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

42
web-snack/src/types/components.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ElBadge: typeof import('element-plus/es')['ElBadge']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@ -4,13 +4,14 @@ import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { loginApi, buildUserInfo } from '@/api/auth' import { loginApi, buildUserInfo } from '@/api/auth'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { User, Lock } from '@element-plus/icons-vue' import { User, Lock, ShoppingBag, Promotion, ChatDotRound, View, Hide } from '@element-plus/icons-vue'
const router = useRouter() const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const form = ref({ username: '', password: '' }) const form = ref({ username: '', password: '' })
const loading = ref(false) const loading = ref(false)
const showPwd = ref(false)
async function onSubmit() { async function onSubmit() {
if (!form.value.username || !form.value.password) { if (!form.value.username || !form.value.password) {
@ -33,84 +34,467 @@ async function onSubmit() {
<template> <template>
<div class="auth-page"> <div class="auth-page">
<div class="auth-box"> <!-- 左侧品牌展示区 -->
<div class="auth-header"> <section class="auth-hero" aria-hidden="true">
<el-icon :size="48" color="#DC2626"><Shop /></el-icon> <div class="hero-deco hero-deco-1" />
<h2>零食商城</h2> <div class="hero-deco hero-deco-2" />
<p>登录您的账号</p> <div class="hero-deco hero-deco-3" />
</div>
<el-form :model="form" @keyup.enter="onSubmit"> <div class="hero-inner">
<el-form-item> <div class="hero-brand">
<el-input <el-icon :size="32" color="#FCA5A5"><ShoppingBag /></el-icon>
v-model="form.username" <span class="hero-brand-name">零食商城</span>
placeholder="用户名" </div>
<h1 class="hero-title">
吃货的<br />
<span class="hero-title-accent">快乐星球</span>
</h1>
<p class="hero-subtitle">严选全球好零食 · 99 包邮 · 7 天无忧退换</p>
<ul class="hero-features">
<li>
<span class="hero-feature-icon"><el-icon :size="22"><Promotion /></el-icon></span>
<div>
<strong>每日限时秒杀</strong>
<span>低至 1 折起 · 抢到就是赚到</span>
</div>
</li>
<li>
<span class="hero-feature-icon"><el-icon :size="22"><ChatDotRound /></el-icon></span>
<div>
<strong>7×24 客服在线</strong>
<span>订单 / 售后 / 咨询秒回应</span>
</div>
</li>
</ul>
</div>
</section>
<!-- 右侧登录卡片 -->
<section class="auth-panel">
<div class="auth-card">
<header class="auth-card-header">
<h2>欢迎回来 👋</h2>
<p>登录账号开启零食之旅</p>
</header>
<el-form :model="form" class="auth-form" @keyup.enter="onSubmit">
<el-form-item>
<label class="auth-label" for="login-username">用户名</label>
<div class="auth-field">
<span class="auth-field-icon"><el-icon :size="18"><User /></el-icon></span>
<input
id="login-username"
v-model="form.username"
class="auth-field-input"
type="text"
placeholder="请输入用户名"
autocomplete="username"
/>
</div>
</el-form-item>
<el-form-item>
<label class="auth-label" for="login-password">密码</label>
<div class="auth-field">
<span class="auth-field-icon"><el-icon :size="18"><Lock /></el-icon></span>
<input
id="login-password"
v-model="form.password"
class="auth-field-input"
:type="showPwd ? 'text' : 'password'"
placeholder="请输入密码"
autocomplete="current-password"
/>
<button
type="button"
class="auth-field-eye"
:aria-label="showPwd ? '隐藏密码' : '显示密码'"
@click="showPwd = !showPwd"
>
<el-icon :size="18">
<component :is="showPwd ? View : Hide" />
</el-icon>
</button>
</div>
</el-form-item>
<div class="auth-row">
<el-checkbox>记住我</el-checkbox>
<a class="auth-link" @click="ElMessage.info('请联系客服重置密码')">忘记密码</a>
</div>
<el-button
type="primary"
size="large" size="large"
:prefix-icon="User" class="auth-submit"
/> :loading="loading"
</el-form-item> @click="onSubmit"
<el-form-item> >
<el-input
v-model="form.password"
type="password"
placeholder="密码"
size="large"
show-password
:prefix-icon="Lock"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="large" style="width: 100%" :loading="loading" @click="onSubmit">
登录
</el-button> </el-button>
</el-form-item> </el-form>
</el-form>
<div class="auth-footer"> <div class="auth-divider"><span>其他方式登录</span></div>
<span>还没有账号</span> <div class="auth-social" aria-label="其他登录方式占位">
<a @click="router.push('/register')">立即注册</a> <button class="auth-social-btn" type="button" aria-label="微信登录">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true">
<path d="M8.5 4C4.36 4 1 6.69 1 10c0 1.86 1.06 3.53 2.74 4.65L3 17l2.6-1.3c.7.2 1.45.3 2.2.3h.55c-.2-.6-.3-1.2-.3-1.83 0-3.25 3.13-5.9 7-5.9.27 0 .54.02.8.05C15.4 5.66 12.27 4 8.5 4M6 7.5a1 1 0 1 1 0 2 1 1 0 0 1 0-2m5 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2m4 2.27c-3.22 0-5.83 2.18-5.83 4.88 0 2.7 2.61 4.88 5.83 4.88.66 0 1.3-.09 1.9-.26L19 20l-.6-1.9c1.4-.95 2.27-2.4 2.27-4 0-2.7-2.6-4.88-5.83-4.88m-2 3.23a.75.75 0 1 1 0 1.5.75.75 0 0 1 0-1.5m4 0a.75.75 0 1 1 0 1.5.75.75 0 0 1 0-1.5"/>
</svg>
</button>
<button class="auth-social-btn" type="button" aria-label="QQ 登录">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true">
<path d="M12 2C7.6 2 4 5.4 4 9.7c0 1.7.5 3.3 1.4 4.6-.3.4-.7.8-1.1 1.1-.6.4-.6 1 0 1.4.7.4 1.7.5 2.6.2.4.5.8.9 1.3 1.2-.3.5-.7 1.1-1 1.4-.3.4 0 .9.5.9 1.1 0 2.4-.7 3.1-1.4.4.1.8.1 1.2.1s.8 0 1.2-.1c.7.7 2 1.4 3.1 1.4.5 0 .8-.5.5-.9-.3-.3-.7-.9-1-1.4.5-.3.9-.7 1.3-1.2.9.3 1.9.2 2.6-.2.6-.4.6-1 0-1.4-.4-.3-.8-.7-1.1-1.1.9-1.3 1.4-2.9 1.4-4.6C20 5.4 16.4 2 12 2"/>
</svg>
</button>
<button class="auth-social-btn" type="button" aria-label="手机号登录">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true">
<path d="M17 1H7c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-2-2-2m-5 21c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m5-4H7V4h10z"/>
</svg>
</button>
</div>
<footer class="auth-card-footer">
<span>还没有账号</span>
<a class="auth-link" @click="router.push('/register')">立即注册</a>
</footer>
</div> </div>
</div> </section>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.auth-page { .auth-page {
min-height: 100vh; min-height: 100vh;
display: grid;
grid-template-columns: 1.05fr 1fr;
background: #fff;
}
/* ==================== 左侧品牌区 ==================== */
.auth-hero {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, #FEF2F2 0%, #fff 100%); background:
radial-gradient(circle at 20% 20%, #FCA5A5 0%, transparent 45%),
radial-gradient(circle at 80% 80%, #F59E0B 0%, transparent 35%),
linear-gradient(135deg, #DC2626 0%, #B91C1C 100%);
color: #fff;
overflow: hidden;
padding: 64px;
} }
.auth-box {
width: 400px; .hero-deco {
background: #fff; position: absolute;
border-radius: 16px; border-radius: 50%;
padding: 40px; background: rgba(255, 255, 255, 0.08);
box-shadow: var(--shadow-lg); pointer-events: none;
} }
.auth-header { .hero-deco-1 { top: 12%; left: 8%; width: 120px; height: 120px; }
text-align: center; .hero-deco-2 { bottom: 16%; right: 10%; width: 180px; height: 180px; background: rgba(252, 165, 165, 0.18); }
.hero-deco-3 { top: 50%; left: 55%; width: 64px; height: 64px; background: rgba(255, 255, 255, 0.12); }
.hero-inner {
position: relative;
z-index: 1;
max-width: 480px;
}
.hero-brand {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 6px 14px;
background: rgba(255, 255, 255, 0.15);
border-radius: 999px;
font-weight: 600;
font-size: 14px;
margin-bottom: 32px; margin-bottom: 32px;
backdrop-filter: blur(4px);
} }
.auth-header h2 {
margin-top: 12px; .hero-title {
margin-bottom: 6px; font-size: 52px;
font-size: 24px; line-height: 1.15;
font-weight: 700;
margin: 0 0 16px;
letter-spacing: -0.5px;
} }
.auth-header p { .hero-title-accent {
color: #999; display: inline-block;
background: linear-gradient(90deg, #FDE68A 0%, #FBBF24 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.hero-subtitle {
font-size: 17px;
opacity: 0.92;
margin: 0 0 48px;
}
.hero-features {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 18px;
}
.hero-features li {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 20px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 14px;
backdrop-filter: blur(6px);
transition: background 200ms, transform 200ms;
}
.hero-features li:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateX(4px);
}
.hero-feature-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 10px;
background: rgba(252, 211, 77, 0.95);
color: #7C2D12;
flex-shrink: 0;
}
.hero-features li div {
display: flex;
flex-direction: column;
gap: 2px;
}
.hero-features li strong {
font-size: 15px;
font-weight: 600;
}
.hero-features li span {
font-size: 13px;
opacity: 0.85;
}
/* ==================== 右侧登录卡片 ==================== */
.auth-panel {
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
background: #FEF2F2;
}
.auth-card {
width: 100%;
max-width: 440px;
background: #fff;
border-radius: 18px;
padding: 48px 44px;
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.04),
0 18px 40px -12px rgba(220, 38, 38, 0.15);
}
.auth-card-header h2 {
font-size: 28px;
font-weight: 700;
color: #1F2937;
margin: 0 0 8px;
letter-spacing: -0.3px;
}
.auth-card-header p {
font-size: 14px; font-size: 14px;
color: #6B7280;
margin: 0 0 32px;
} }
.auth-footer {
text-align: center; .auth-form :deep(.el-form-item) { margin-bottom: 20px; }
margin-top: 20px;
font-size: 14px; /* 自绘 input 字段 —— 完全摆脱 el-input 内部双层框问题 */
color: #666; .auth-field {
position: relative;
display: flex;
align-items: center;
height: 44px;
padding: 0 12px;
background: #fff;
border: 1px solid #E5E7EB;
border-radius: 10px;
transition: border-color 180ms, box-shadow 180ms;
} }
.auth-footer a { .auth-field:hover { border-color: #FCA5A5; }
color: var(--color-primary); .auth-field:focus-within {
cursor: pointer; border-color: #DC2626;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.12);
}
.auth-field-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
margin-right: 8px;
color: #9CA3AF;
flex-shrink: 0;
}
.auth-field-input {
flex: 1;
min-width: 0;
height: 100%;
border: 0;
outline: 0;
background: transparent;
font-size: 15px;
color: #1F2937;
}
.auth-field-input::placeholder { color: #9CA3AF; }
.auth-field-input:focus { outline: 0; }
.auth-field-eye {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
margin-left: 4px; margin-left: 4px;
border: 0;
background: transparent;
color: #9CA3AF;
cursor: pointer;
border-radius: 6px;
transition: color 150ms, background 150ms;
}
.auth-field-eye:hover { color: #DC2626; background: #FEF2F2; }
.auth-field-eye:focus-visible { outline: 2px solid #DC2626; outline-offset: 1px; }
.auth-form :deep(.el-input__prefix-inner) {
color: #9CA3AF;
display: inline-flex;
align-items: center;
height: 100%;
}
.auth-form :deep(.el-input__prefix .el-icon),
.auth-form :deep(.el-input__prefix svg) {
width: 18px;
height: 18px;
}
.auth-label {
display: block;
font-size: 13px;
font-weight: 500;
color: #374151;
margin-bottom: 6px;
}
.auth-row {
display: flex;
justify-content: space-between;
align-items: center;
margin: 4px 0 24px;
font-size: 13px;
}
.auth-link {
color: #DC2626;
cursor: pointer;
font-weight: 500;
text-decoration: none;
transition: color 150ms;
}
.auth-link:hover { color: #B91C1C; }
.auth-submit {
width: 100%;
height: 46px;
font-size: 15px;
font-weight: 600;
letter-spacing: 4px;
border-radius: 10px;
background: linear-gradient(135deg, #DC2626 0%, #B91C1C 100%);
border: none;
box-shadow: 0 8px 18px -6px rgba(220, 38, 38, 0.5);
transition: transform 150ms, box-shadow 200ms;
}
.auth-submit:hover:not(.is-loading) {
transform: translateY(-1px);
box-shadow: 0 12px 22px -6px rgba(220, 38, 38, 0.55);
}
.auth-submit:active:not(.is-loading) { transform: translateY(0); }
.auth-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 28px 0 20px;
color: #9CA3AF;
font-size: 12px;
}
.auth-divider::before,
.auth-divider::after {
content: '';
flex: 1;
height: 1px;
background: #E5E7EB;
}
.auth-social {
display: flex;
justify-content: center;
gap: 16px;
}
.auth-social-btn {
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid #E5E7EB;
background: #fff;
color: #6B7280;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: border-color 150ms, color 150ms, transform 150ms, background 150ms;
}
.auth-social-btn:hover {
border-color: #DC2626;
color: #DC2626;
background: #FEF2F2;
transform: translateY(-2px);
}
.auth-social-btn:focus-visible {
outline: 2px solid #DC2626;
outline-offset: 2px;
}
.auth-card-footer {
margin-top: 28px;
text-align: center;
font-size: 14px;
color: #6B7280;
}
.auth-card-footer .auth-link { margin-left: 6px; }
/* ==================== 响应式 ==================== */
@media (max-width: 960px) {
.auth-page { grid-template-columns: 1fr; }
.auth-hero { display: none; }
.auth-panel { padding: 24px 16px; background: linear-gradient(180deg, #DC2626 0%, #B91C1C 320px); }
.auth-card { padding: 36px 24px; }
.auth-card-header h2 { font-size: 24px; }
}
@media (prefers-reduced-motion: reduce) {
.auth-submit,
.hero-features li,
.auth-social-btn,
.auth-field { transition: none; }
} }
</style> </style>