fix(webui): setup loop, reactive selects, escape handler, axios/i18n and dep bumps

- Clear needSetup after completion so the wizard stops bouncing back to itself
- ab-select watches modelValue (config selects showed stale defaults); notification-type dropdown renders options
- Search modal Escape handler no longer opens the modal from anywhere; ab-button defaults type=button (no double login)
- axios surfaces 400/404/422; background polls suppress the network-error toast storm; calendar re-reads locally on activate
- Search subscribe uses the active provider + i18n; provider settings go through axios; dead search-store code removed
- Bump axios ^1.7 (CVE-2023-45857) and vue-i18n ^9.14.2 (XSS); add missing i18n keys

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
This commit is contained in:
Estrella Pan
2026-07-02 17:05:50 +02:00
parent 04c913fdf7
commit 7733bd18f7
18 changed files with 318 additions and 273 deletions

View File

@@ -23,11 +23,11 @@
"@headlessui/vue": "^1.7.23",
"@vueuse/components": "^10.11.1",
"@vueuse/core": "^10.11.1",
"axios": "^0.27.2",
"axios": "^1.7.9",
"naive-ui": "^2.39.0",
"pinia": "^2.2.2",
"vue": "^3.5.8",
"vue-i18n": "^9.14.0",
"vue-i18n": "^9.14.2",
"vue-inline-svg": "^3.1.4",
"vue-router": "^4.4.5",
"vuedraggable": "^4.1.0"

217
webui/pnpm-lock.yaml generated
View File

@@ -18,8 +18,8 @@ importers:
specifier: ^10.11.1
version: 10.11.1(vue@3.5.8(typescript@4.9.5))
axios:
specifier: ^0.27.2
version: 0.27.2
specifier: ^1.7.9
version: 1.18.1
naive-ui:
specifier: ^2.39.0
version: 2.39.0(vue@3.5.8(typescript@4.9.5))
@@ -30,8 +30,8 @@ importers:
specifier: ^3.5.8
version: 3.5.8(typescript@4.9.5)
vue-i18n:
specifier: ^9.14.0
version: 9.14.0(vue@3.5.8(typescript@4.9.5))
specifier: ^9.14.2
version: 9.14.5(vue@3.5.8(typescript@4.9.5))
vue-inline-svg:
specifier: ^3.1.4
version: 3.1.4(vue@3.5.8(typescript@4.9.5))
@@ -50,7 +50,7 @@ importers:
version: 1.4.2(vue@3.5.8(typescript@4.9.5))
'@intlify/unplugin-vue-i18n':
specifier: ^0.11.0
version: 0.11.0(rollup@2.79.1)(vue-i18n@9.14.0(vue@3.5.8(typescript@4.9.5)))(webpack-sources@3.2.3)
version: 0.11.0(rollup@2.79.1)(vue-i18n@9.14.5(vue@3.5.8(typescript@4.9.5)))(webpack-sources@3.2.3)
'@storybook/addon-essentials':
specifier: ^7.6.20
version: 7.6.20(@types/react@18.3.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(webpack-sources@3.2.3)
@@ -1057,20 +1057,20 @@ packages:
vue-i18n:
optional: true
'@intlify/core-base@9.14.0':
resolution: {integrity: sha512-zJn0imh9HIsZZUtt9v8T16PeVstPv6bP2YzlrYJwoF8F30gs4brZBwW2KK6EI5WYKFi3NeqX6+UU4gniz5TkGg==}
'@intlify/core-base@9.14.5':
resolution: {integrity: sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==}
engines: {node: '>= 16'}
'@intlify/message-compiler@9.14.0':
resolution: {integrity: sha512-sXNsoMI0YsipSXW8SR75drmVK56tnJHoYbPXUv2Cf9lz6FzvwsosFm6JtC1oQZI/kU+n7qx0qRrEWkeYFTgETA==}
'@intlify/message-compiler@9.14.5':
resolution: {integrity: sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==}
engines: {node: '>= 16'}
'@intlify/message-compiler@9.3.0-beta.17':
resolution: {integrity: sha512-i7hvVIRk1Ax2uKa9xLRJCT57to08OhFMhFXXjWN07rmx5pWQYQ23MfX1xgggv9drnWTNhqEiD+u4EJeHoS5+Ww==}
engines: {node: '>= 14'}
'@intlify/shared@9.14.0':
resolution: {integrity: sha512-r+N8KRQL7LgN1TMTs1A2svfuAU0J94Wu9wWdJVJqYsoMMLIeJxrPjazihfHpmJqfgZq0ah3Y9Q4pgWV2O90Fyg==}
'@intlify/shared@9.14.5':
resolution: {integrity: sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==}
engines: {node: '>= 16'}
'@intlify/shared@9.3.0-beta.17':
@@ -2307,6 +2307,10 @@ packages:
resolution: {integrity: sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==}
engines: {node: '>= 6.0.0'}
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
aggregate-error@3.1.0:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
@@ -2436,8 +2440,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
axios@0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
axios@1.18.1:
resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==}
babel-core@7.0.0-bridge.0:
resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==}
@@ -2557,6 +2561,10 @@ packages:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
@@ -2946,6 +2954,10 @@ packages:
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
@@ -3008,6 +3020,10 @@ packages:
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
engines: {node: '>= 0.4'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
@@ -3022,10 +3038,18 @@ packages:
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.2:
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.0.3:
resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
es-shim-unscopables@1.0.2:
resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
@@ -3396,8 +3420,8 @@ packages:
resolution: {integrity: sha512-WHRizzSrWFTcKo7cVcbP3wzZVhzsoYxoWqbnH4z+JXGqrjVmnsld6kBZWVlB200PwD5ur8r+HV3KUDxv3cHhOQ==}
engines: {node: '>=0.4.0'}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
follow-redirects@1.16.0:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
@@ -3416,6 +3440,10 @@ packages:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
form-data@4.0.6:
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
engines: {node: '>= 6'}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@@ -3472,6 +3500,10 @@ packages:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
@@ -3491,6 +3523,10 @@ packages:
resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==}
engines: {node: '>=8'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
@@ -3548,6 +3584,10 @@ packages:
gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -3592,6 +3632,10 @@ packages:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
@@ -3603,6 +3647,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hasown@2.0.4:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
@@ -3629,6 +3677,10 @@ packages:
resolution: {integrity: sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==}
engines: {node: '>= 6.0.0'}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
@@ -4161,6 +4213,10 @@ packages:
peerDependencies:
react: '>= 0.14.0'
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
md5-hex@3.0.1:
resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==}
engines: {node: '>=8'}
@@ -4683,6 +4739,10 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
proxy-from-env@2.1.0:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
pug-attrs@3.0.0:
resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==}
@@ -5797,8 +5857,8 @@ packages:
vue-component-type-helpers@2.1.6:
resolution: {integrity: sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==}
vue-component-type-helpers@3.2.5:
resolution: {integrity: sha512-tkvNr+bU8+xD/onAThIe7CHFvOJ/BO6XCOrxMzeytJq40nTfpGDJuVjyCM8ccGZKfAbGk2YfuZyDMXM56qheZQ==}
vue-component-type-helpers@3.3.6:
resolution: {integrity: sha512-FkljacAwJ9BUoSUdpFe3VDy0sGigNlTH9+2zcXUWmZOjN8swiCkl3t48wOJun0OsUd2cEIda1l04tsxMiKIIrQ==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -5822,9 +5882,10 @@ packages:
peerDependencies:
eslint: '>=6.0.0'
vue-i18n@9.14.0:
resolution: {integrity: sha512-LxmpRuCt2rI8gqU+kxeflRZMQn4D5+4M3oP3PWZdowW/ePJraHqhF7p4CuaME52mUxdw3Mmy2yAUKgfZYgCRjA==}
vue-i18n@9.14.5:
resolution: {integrity: sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==}
engines: {node: '>= 16'}
deprecated: v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html
peerDependencies:
vue: ^3.0.0
@@ -7173,7 +7234,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@intlify/bundle-utils@6.0.1(vue-i18n@9.14.0(vue@3.5.8(typescript@4.9.5)))':
'@intlify/bundle-utils@6.0.1(vue-i18n@9.14.5(vue@3.5.8(typescript@4.9.5)))':
dependencies:
'@intlify/message-compiler': 9.3.0-beta.17
'@intlify/shared': 9.3.0-beta.17
@@ -7186,16 +7247,16 @@ snapshots:
source-map: 0.6.1
yaml-eslint-parser: 0.3.2
optionalDependencies:
vue-i18n: 9.14.0(vue@3.5.8(typescript@4.9.5))
vue-i18n: 9.14.5(vue@3.5.8(typescript@4.9.5))
'@intlify/core-base@9.14.0':
'@intlify/core-base@9.14.5':
dependencies:
'@intlify/message-compiler': 9.14.0
'@intlify/shared': 9.14.0
'@intlify/message-compiler': 9.14.5
'@intlify/shared': 9.14.5
'@intlify/message-compiler@9.14.0':
'@intlify/message-compiler@9.14.5':
dependencies:
'@intlify/shared': 9.14.0
'@intlify/shared': 9.14.5
source-map-js: 1.2.1
'@intlify/message-compiler@9.3.0-beta.17':
@@ -7203,13 +7264,13 @@ snapshots:
'@intlify/shared': 9.3.0-beta.17
source-map: 0.6.1
'@intlify/shared@9.14.0': {}
'@intlify/shared@9.14.5': {}
'@intlify/shared@9.3.0-beta.17': {}
'@intlify/unplugin-vue-i18n@0.11.0(rollup@2.79.1)(vue-i18n@9.14.0(vue@3.5.8(typescript@4.9.5)))(webpack-sources@3.2.3)':
'@intlify/unplugin-vue-i18n@0.11.0(rollup@2.79.1)(vue-i18n@9.14.5(vue@3.5.8(typescript@4.9.5)))(webpack-sources@3.2.3)':
dependencies:
'@intlify/bundle-utils': 6.0.1(vue-i18n@9.14.0(vue@3.5.8(typescript@4.9.5)))
'@intlify/bundle-utils': 6.0.1(vue-i18n@9.14.5(vue@3.5.8(typescript@4.9.5)))
'@intlify/shared': 9.3.0-beta.17
'@rollup/pluginutils': 5.1.1(rollup@2.79.1)
'@vue/compiler-sfc': 3.5.8
@@ -7222,7 +7283,7 @@ snapshots:
source-map: 0.6.1
unplugin: 1.14.1(webpack-sources@3.2.3)
optionalDependencies:
vue-i18n: 9.14.0(vue@3.5.8(typescript@4.9.5))
vue-i18n: 9.14.5(vue@3.5.8(typescript@4.9.5))
transitivePeerDependencies:
- rollup
- supports-color
@@ -8325,7 +8386,7 @@ snapshots:
ts-dedent: 2.2.0
type-fest: 2.19.0
vue: 3.5.8(typescript@4.9.5)
vue-component-type-helpers: 3.2.5
vue-component-type-helpers: 3.3.6
transitivePeerDependencies:
- encoding
- supports-color
@@ -9009,6 +9070,12 @@ snapshots:
agent-base@5.1.1: {}
agent-base@6.0.2:
dependencies:
debug: 4.3.7
transitivePeerDependencies:
- supports-color
aggregate-error@3.1.0:
dependencies:
clean-stack: 2.2.0
@@ -9159,12 +9226,15 @@ snapshots:
dependencies:
possible-typed-array-names: 1.0.0
axios@0.27.2:
axios@1.18.1:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.0
follow-redirects: 1.16.0
form-data: 4.0.6
https-proxy-agent: 5.0.1
proxy-from-env: 2.1.0
transitivePeerDependencies:
- debug
- supports-color
babel-core@7.0.0-bridge.0(@babel/core@7.25.2):
dependencies:
@@ -9304,6 +9374,11 @@ snapshots:
cac@6.7.14: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
call-bind@1.0.7:
dependencies:
es-define-property: 1.0.0
@@ -9701,6 +9776,12 @@ snapshots:
dotenv@16.4.5: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
duplexer@0.1.2: {}
duplexify@3.7.1:
@@ -9800,6 +9881,8 @@ snapshots:
dependencies:
get-intrinsic: 1.2.4
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-get-iterator@1.1.3:
@@ -9820,12 +9903,23 @@ snapshots:
dependencies:
es-errors: 1.3.0
es-object-atoms@1.1.2:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.0.3:
dependencies:
get-intrinsic: 1.2.4
has-tostringtag: 1.0.2
hasown: 2.0.2
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.4
es-shim-unscopables@1.0.2:
dependencies:
hasown: 2.0.2
@@ -10352,7 +10446,7 @@ snapshots:
flow-parser@0.246.0: {}
follow-redirects@1.15.9: {}
follow-redirects@1.16.0: {}
for-each@0.3.3:
dependencies:
@@ -10369,6 +10463,14 @@ snapshots:
combined-stream: 1.0.8
mime-types: 2.1.35
form-data@4.0.6:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.4
mime-types: 2.1.35
forwarded@0.2.0: {}
fresh@0.5.2: {}
@@ -10426,6 +10528,19 @@ snapshots:
has-symbols: 1.0.3
hasown: 2.0.2
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.2
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.4
math-intrinsics: 1.1.0
get-nonce@1.0.1: {}
get-npm-tarball-url@2.1.0: {}
@@ -10436,6 +10551,11 @@ snapshots:
get-port@5.1.1: {}
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.2
get-stream@6.0.1: {}
get-stream@8.0.1: {}
@@ -10511,6 +10631,8 @@ snapshots:
dependencies:
get-intrinsic: 1.2.4
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
graphemer@1.4.0: {}
@@ -10560,6 +10682,8 @@ snapshots:
has-symbols@1.0.3: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.0.3
@@ -10570,6 +10694,10 @@ snapshots:
dependencies:
function-bind: 1.1.2
hasown@2.0.4:
dependencies:
function-bind: 1.1.2
he@1.2.0: {}
highlight.js@11.10.0: {}
@@ -10600,6 +10728,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.3.7
transitivePeerDependencies:
- supports-color
human-signals@2.1.0: {}
human-signals@5.0.0: {}
@@ -11105,6 +11240,8 @@ snapshots:
dependencies:
react: 18.2.0
math-intrinsics@1.1.0: {}
md5-hex@3.0.1:
dependencies:
blueimp-md5: 2.19.0
@@ -11605,6 +11742,8 @@ snapshots:
proxy-from-env@1.1.0: {}
proxy-from-env@2.1.0: {}
pug-attrs@3.0.0:
dependencies:
constantinople: 4.0.1
@@ -12827,7 +12966,7 @@ snapshots:
vue-component-type-helpers@2.1.6: {}
vue-component-type-helpers@3.2.5: {}
vue-component-type-helpers@3.3.6: {}
vue-demi@0.14.10(vue@3.5.8(typescript@4.9.5)):
dependencies:
@@ -12862,10 +13001,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
vue-i18n@9.14.0(vue@3.5.8(typescript@4.9.5)):
vue-i18n@9.14.5(vue@3.5.8(typescript@4.9.5)):
dependencies:
'@intlify/core-base': 9.14.0
'@intlify/shared': 9.14.0
'@intlify/core-base': 9.14.5
'@intlify/shared': 9.14.5
'@vue/devtools-api': 6.6.4
vue: 3.5.8(typescript@4.9.5)

View File

@@ -175,7 +175,7 @@ async function autoDetectOffset() {
offsetReason.value = result.reason;
} catch (e) {
console.error('Failed to detect offset:', e);
message.error('Failed to detect offset');
message.error(t('offset.detect_failed'));
} finally {
offsetLoading.value = false;
}

View File

@@ -129,7 +129,7 @@ async function autoDetectOffset() {
}
} catch (e) {
console.error('Failed to detect offset:', e);
message.error('Failed to detect offset');
message.error(t('offset.detect_failed'));
} finally {
offsetLoading.value = false;
}
@@ -146,7 +146,7 @@ async function dismissReview() {
message.success(t('offset.review_dismissed'));
} catch (e) {
console.error('Failed to dismiss review:', e);
message.error('Failed to dismiss review');
message.error(t('offset.dismiss_failed'));
} finally {
dismissingReview.value = false;
}

View File

@@ -7,12 +7,16 @@ const props = withDefaults(
size?: 'big' | 'normal' | 'small';
link?: string | null;
loading?: boolean;
/** Native `type` attribute when rendered as a <button>. Defaults to
* "button" so ab-button never triggers an implicit form submit. */
nativeType?: 'button' | 'submit' | 'reset';
}>(),
{
type: 'primary',
size: 'normal',
link: null,
loading: false,
nativeType: 'button',
}
);
@@ -34,6 +38,7 @@ const buttonSize = computed(() => {
<Component
:is="link !== null ? 'a' : 'button'"
:href="link"
:type="link !== null ? undefined : nativeType"
class="btn"
:class="[`btn--${type}`, buttonSize]"
@click="$emit('click')"

View File

@@ -19,9 +19,32 @@ const props = withDefaults(
const emit = defineEmits(['update:modelValue']);
const selected = ref<SelectItem | string>(
props.modelValue || (props.items?.[0] ?? '')
);
// Find an object item whose `.value` matches a plain string modelValue.
// Lets callers bind a string v-model against object items ({id,label,value})
// without having to round-trip the whole object.
function findByValue(items: Array<SelectItem | string>, value: string) {
return items.find((item) => isObject(item) && item.value === value);
}
// Writable computed (instead of a ref seeded once from props.modelValue) so
// external updates to modelValue (e.g. after async getConfig()) stay in sync.
const selected = computed<SelectItem | string>({
get() {
if (isString(props.modelValue)) {
return findByValue(props.items, props.modelValue) ?? props.modelValue;
}
return props.modelValue ?? props.items?.[0] ?? '';
},
set(item) {
// Object item selected while the caller's v-model is a plain string:
// emit item.value so the binding stays string <-> string.
if (isObject(item) && isString(props.modelValue)) {
emit('update:modelValue', item.value);
} else {
emit('update:modelValue', item);
}
},
});
const otherItems = computed(() => {
return (
@@ -56,10 +79,6 @@ function getLabel(item: SelectItem | string) {
function getDisabled(item: SelectItem | string) {
return isString(item) ? false : item.disabled;
}
watch(selected, (val) => {
emit('update:modelValue', val);
});
</script>
<template>

View File

@@ -13,6 +13,7 @@ const emit = defineEmits<{
}>();
const message = useMessage();
const { t } = useMyI18n();
// Local deep copy of bangumi for editing (prevents mutation of original prop)
const localBangumi = ref<BangumiRule>(JSON.parse(JSON.stringify(props.bangumi)));
@@ -130,7 +131,7 @@ async function autoDetectOffset() {
localBangumi.value.episode_offset = result.suggested_offset;
} catch (e) {
console.error('Failed to detect offset:', e);
message.error('Failed to detect offset');
message.error(t('offset.detect_failed'));
} finally {
offsetLoading.value = false;
}

View File

@@ -11,6 +11,7 @@ const emit = defineEmits<{
}>();
const message = useMessage();
const { t } = useMyI18n();
const { getAll } = useBangumiStore();
const {
@@ -67,8 +68,11 @@ onBeforeUnmount(() => {
closeSearch();
});
// Close on Escape
// Close on Escape (this component is always mounted, so the listener must
// no-op while the modal itself is closed — otherwise Escape anywhere in the
// app would toggle the modal open via the `close` -> toggleModal() handler).
onKeyStroke('Escape', () => {
if (!showModal.value) return;
if (selectedResult.value) {
clearSelectedResult();
} else {
@@ -109,20 +113,20 @@ async function handleConfirm(bangumi: BangumiRule) {
name: bangumi.official_title,
url: bangumi.rss_link?.[0] || '',
aggregate: false,
parser: 'mikan',
parser: provider.value,
enabled: true,
connection_status: null,
last_checked_at: null,
last_error: null,
};
await apiDownload.subscribe(bangumi, rss);
message.success('订阅成功');
message.success(t('search.subscribe_success'));
getAll();
clearSelectedResult();
emit('close');
} catch (e) {
console.error('Subscribe failed:', e);
message.error('订阅失败');
message.error(t('search.subscribe_failed'));
} finally {
subscribing.value = false;
}

View File

@@ -3,7 +3,7 @@ import type { NotificationProviderConfig, NotificationType } from '#/config';
import type { TupleToUnion } from '#/utils';
import { apiNotification } from '@/api/notification';
const { t } = useMyI18n();
const { t, returnUserLangText } = useMyI18n();
const { getSettingGroup } = useConfigStore();
const notificationRef = getSettingGroup('notification');
@@ -179,12 +179,15 @@ async function testProvider(index: number) {
const response = await apiNotification.testProvider({ provider_index: index });
testResult.value = {
success: response.data.success,
message: response.data.message_zh || response.data.message,
message: returnUserLangText({
en: response.data.message_en,
'zh-CN': response.data.message_zh,
}),
};
} catch (error: any) {
} catch {
testResult.value = {
success: false,
message: error.message || 'Test failed',
message: t('config.notification_set.test_failed'),
};
} finally {
testingIndex.value = -1;
@@ -200,12 +203,15 @@ async function testNewProvider() {
);
testResult.value = {
success: response.data.success,
message: response.data.message_zh || response.data.message,
message: returnUserLangText({
en: response.data.message_en,
'zh-CN': response.data.message_zh,
}),
};
} catch (error: any) {
} catch {
testResult.value = {
success: false,
message: error.message || 'Test failed',
message: t('config.notification_set.test_failed'),
};
} finally {
testingIndex.value = -1;

View File

@@ -7,6 +7,7 @@ interface SearchProvider {
}
const { t } = useMyI18n();
const message = useMessage();
// State
const providers = ref<SearchProvider[]>([]);
@@ -30,16 +31,16 @@ onMounted(() => {
async function loadProviders() {
loading.value = true;
try {
const response = await fetch('/api/v1/search/provider/config');
if (response.ok) {
const data = await response.json();
providers.value = Object.entries(data).map(([name, url]) => ({
name,
url: url as string,
}));
}
const { data } = await axios.get<Record<string, string>>(
'api/v1/search/provider/config'
);
providers.value = Object.entries(data).map(([name, url]) => ({
name,
url,
}));
} catch (error) {
console.error('Failed to load providers:', error);
message.error(t('config.search_provider_set.load_failed'));
} finally {
loading.value = false;
}
@@ -52,16 +53,10 @@ async function saveProviders() {
});
try {
const response = await fetch('/api/v1/search/provider/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(providerObj),
});
if (!response.ok) {
throw new Error('Failed to save providers');
}
await axios.put('api/v1/search/provider/config', providerObj);
} catch (error) {
console.error('Failed to save providers:', error);
message.error(t('config.search_provider_set.save_failed'));
}
}

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import { markSetupComplete } from '@/router';
const { t } = useMyI18n();
const setupStore = useSetupStore();
const { accountData, downloaderData, rssData, notificationData, isLoading } =
@@ -13,6 +15,7 @@ async function completeSetup() {
await apiSetup.complete(request);
message.success(t('setup.review.success'));
setupStore.$reset();
markSetupComplete();
router.push({ name: 'Login' });
} catch (e) {
message.error(t('setup.review.failed'));

View File

@@ -7,10 +7,17 @@ export const useAppInfo = createSharedComposable(() => {
function getStatus() {
if (isLoggedIn.value) {
apiProgram.status().then((res) => {
running.value = res.status;
version.value = res.version;
});
apiProgram
.status()
.then((res) => {
running.value = res.status;
version.value = res.version;
})
.catch(() => {
// Errors are already surfaced (throttled) by the axios interceptor;
// swallow here so a transient backend restart doesn't spam an
// unhandled rejection on every 3s poll tick.
});
}
}

View File

@@ -100,7 +100,9 @@
"url_missing_placeholder": "URL must contain %s as search keyword placeholder",
"default": "Default",
"no_providers": "No search providers configured",
"delete_confirm": "Are you sure you want to delete this provider?"
"delete_confirm": "Are you sure you want to delete this provider?",
"load_failed": "Failed to load search providers",
"save_failed": "Failed to save search providers"
}
},
"downloader": {
@@ -406,10 +408,14 @@
"dismiss": "Dismiss",
"suggestion_applied": "Offset suggestion applied",
"no_mismatch": "No season/episode mismatch detected",
"review_dismissed": "Review reminder dismissed"
"review_dismissed": "Review reminder dismissed",
"detect_failed": "Failed to detect offset",
"dismiss_failed": "Failed to dismiss review"
},
"search": {
"subscribe": "Subscribe",
"subscribe_success": "Subscribed successfully",
"subscribe_failed": "Subscription failed",
"no_results": "No results found. Try different keywords.",
"start_typing": "Enter keywords to start searching",
"filter": {

View File

@@ -100,7 +100,9 @@
"url_missing_placeholder": "URL 必须包含 %s 作为搜索关键词占位符",
"default": "默认",
"no_providers": "暂无搜索源",
"delete_confirm": "确定删除此搜索源?"
"delete_confirm": "确定删除此搜索源?",
"load_failed": "加载搜索源失败",
"save_failed": "保存搜索源失败"
}
},
"downloader": {
@@ -406,10 +408,14 @@
"dismiss": "忽略",
"suggestion_applied": "偏移量建议已应用",
"no_mismatch": "未检测到季度/集数不匹配",
"review_dismissed": "检查提醒已忽略"
"review_dismissed": "检查提醒已忽略",
"detect_failed": "检测偏移量失败",
"dismiss_failed": "忽略检查提醒失败"
},
"search": {
"subscribe": "订阅",
"subscribe_success": "订阅成功",
"subscribe_failed": "订阅失败",
"no_results": "未找到相关结果,试试其他关键词",
"start_typing": "输入关键词开始搜索",
"filter": {

View File

@@ -26,8 +26,9 @@ async function refreshCalendar() {
}
onActivated(() => {
if (refreshing.value) return;
refreshCalendar();
// Re-read local state on tab activation; the bangumi.tv scrape + DB write
// in refreshCalendar() is reserved for the explicit refresh button.
getAll();
});
const DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const;

View File

@@ -7,6 +7,13 @@ const router = createRouter({
let setupChecked = false;
let needSetup = false;
// Called once the setup wizard finishes, so the next navigation isn't
// bounced back into /setup by the guard below.
export function markSetupComplete() {
needSetup = false;
setupChecked = true;
}
router.beforeEach(async (to) => {
const { isLoggedIn } = useAuth();
const { type, url } = storeToRefs(usePlayerStore());

View File

@@ -1,4 +1,4 @@
import type { BangumiRule, SearchResult } from '#/bangumi';
import type { BangumiRule } from '#/bangumi';
// Filter types
export interface SearchFilters {
@@ -24,55 +24,6 @@ export interface GroupedBangumi {
variants: BangumiRule[];
}
// Helper to parse metadata from title/bangumi
function parseResolution(bangumi: BangumiRule): string {
// Check dpi field first
if (bangumi.dpi) {
return bangumi.dpi;
}
// Parse from title_raw
const title = bangumi.title_raw || '';
const resMatch = title.match(/\b(4K|2160p|1080p|720p|480p)\b/i);
return resMatch ? resMatch[1].toLowerCase() : '';
}
function parseSubtitleType(bangumi: BangumiRule): string {
// Check subtitle field first
if (bangumi.subtitle) {
const sub = bangumi.subtitle.toLowerCase();
if (sub.includes('简') || sub.includes('chs') || sub.includes('sc')) return '简中';
if (sub.includes('繁') || sub.includes('cht') || sub.includes('tc')) return '繁中';
if (sub.includes('双语') || sub.includes('dual')) return '双语';
}
// Parse from title_raw
const title = bangumi.title_raw || '';
if (/简体|简中|CHS|SC/i.test(title)) return '简中';
if (/繁體|繁中|CHT|TC/i.test(title)) return '繁中';
if (/双语|Dual/i.test(title)) return '双语';
if (/内嵌|内封/i.test(title)) return '内嵌';
if (/外挂|ASS|SRT/i.test(title)) return '外挂';
return '';
}
function parseSeason(bangumi: BangumiRule): string {
if (bangumi.season_raw) {
return bangumi.season_raw;
}
if (bangumi.season) {
return `S${bangumi.season}`;
}
// Parse from title
const title = bangumi.title_raw || '';
if (/剧场版|Movie|劇場版/i.test(title)) return '剧场版';
if (/OVA/i.test(title)) return 'OVA';
const seasonMatch = title.match(/S(\d+)|Season\s*(\d+)|第(\d+)季/i);
if (seasonMatch) {
const num = seasonMatch[1] || seasonMatch[2] || seasonMatch[3];
return `S${num}`;
}
return '';
}
export const useSearchStore = defineStore('search', () => {
const providers = ref<string[]>(['mikan', 'dmhy', 'nyaa']);
@@ -90,23 +41,9 @@ export const useSearchStore = defineStore('search', () => {
// Modal state
const showModal = ref(false);
const selectedResult = ref<BangumiRule | null>(null);
const selectedGroup = ref<GroupedBangumi | null>(null);
// Filter state for selected group variants
const variantFilters = ref<SearchFilters>({
group: [],
resolution: [],
subtitleType: [],
season: [],
});
const loading = computed(() => status.value !== 'CLOSED');
// Raw bangumi list with order
const bangumiList = computed<SearchResult[]>(() =>
searchData.value.map((item, index) => ({ order: index, value: item }))
);
// Group results by official_title
const groupedResults = computed<GroupedBangumi[]>(() => {
const map = new Map<string, BangumiRule[]>();
@@ -135,86 +72,6 @@ export const useSearchStore = defineStore('search', () => {
return groups;
});
// Extract filter options from selected group's variants
const variantFilterOptions = computed<FilterOptions>(() => {
if (!selectedGroup.value) {
return { group: [], resolution: [], subtitleType: [], season: [] };
}
const groups = new Set<string>();
const resolutions = new Set<string>();
const subtitleTypes = new Set<string>();
const seasons = new Set<string>();
for (const item of selectedGroup.value.variants) {
if (item.group_name) groups.add(item.group_name);
const res = parseResolution(item);
if (res) resolutions.add(res);
const subType = parseSubtitleType(item);
if (subType) subtitleTypes.add(subType);
const season = parseSeason(item);
if (season) seasons.add(season);
}
return {
group: Array.from(groups).sort(),
resolution: Array.from(resolutions).sort((a, b) => {
const order = ['4k', '2160p', '1080p', '720p', '480p'];
return order.indexOf(a.toLowerCase()) - order.indexOf(b.toLowerCase());
}),
subtitleType: Array.from(subtitleTypes),
season: Array.from(seasons).sort(),
};
});
// Filtered variants based on selected filters
const filteredVariants = computed<BangumiRule[]>(() => {
if (!selectedGroup.value) return [];
const hasFilters = Object.values(variantFilters.value).some((arr) => arr.length > 0);
if (!hasFilters) {
return selectedGroup.value.variants;
}
return selectedGroup.value.variants.filter((bangumi) => {
// Group filter
if (variantFilters.value.group.length > 0) {
if (!bangumi.group_name || !variantFilters.value.group.includes(bangumi.group_name)) {
return false;
}
}
// Resolution filter
if (variantFilters.value.resolution.length > 0) {
const res = parseResolution(bangumi);
if (!res || !variantFilters.value.resolution.includes(res)) {
return false;
}
}
// Subtitle type filter
if (variantFilters.value.subtitleType.length > 0) {
const subType = parseSubtitleType(bangumi);
if (!subType || !variantFilters.value.subtitleType.includes(subType)) {
return false;
}
}
// Season filter
if (variantFilters.value.season.length > 0) {
const season = parseSeason(bangumi);
if (!season || !variantFilters.value.season.includes(season)) {
return false;
}
}
return true;
});
});
async function getProviders() {
providers.value = await apiSearch.getProvider();
provider.value = providers.value[0];
@@ -241,35 +98,9 @@ export const useSearchStore = defineStore('search', () => {
function clearSearch() {
keyword.value = '';
searchData.value = [];
variantFilters.value = { group: [], resolution: [], subtitleType: [], season: [] };
selectedGroup.value = null;
closeSearch();
}
function toggleVariantFilter(category: keyof SearchFilters, value: string) {
const arr = variantFilters.value[category];
const index = arr.indexOf(value);
if (index === -1) {
arr.push(value);
} else {
arr.splice(index, 1);
}
}
function clearVariantFilters() {
variantFilters.value = { group: [], resolution: [], subtitleType: [], season: [] };
}
function selectGroup(group: GroupedBangumi) {
selectedGroup.value = group;
variantFilters.value = { group: [], resolution: [], subtitleType: [], season: [] };
}
function clearSelectedGroup() {
selectedGroup.value = null;
variantFilters.value = { group: [], resolution: [], subtitleType: [], season: [] };
}
function selectResult(bangumi: BangumiRule) {
selectedResult.value = bangumi;
}
@@ -291,12 +122,7 @@ export const useSearchStore = defineStore('search', () => {
loading,
provider,
providers,
bangumiList,
groupedResults,
selectedGroup,
variantFilters,
variantFilterOptions,
filteredVariants,
showModal,
selectedResult,
@@ -308,10 +134,6 @@ export const useSearchStore = defineStore('search', () => {
openModal,
closeModal,
toggleModal,
toggleVariantFilter,
clearVariantFilters,
selectGroup,
clearSelectedGroup,
selectResult,
clearSelectedResult,
};

View File

@@ -6,6 +6,20 @@ export const axios = Axios.create({
withCredentials: true,
});
// Throttle repeated identical error toasts (e.g. background polling during a
// backend restart would otherwise fire the same "Network error" toast once
// per failed request and produce a toast storm).
const recentToastAt = new Map<string, number>();
const TOAST_THROTTLE_MS = 4000;
function showErrorThrottled(msg: string) {
const now = Date.now();
const last = recentToastAt.get(msg) ?? 0;
if (now - last < TOAST_THROTTLE_MS) return;
recentToastAt.set(msg, now);
useMessage().error(msg);
}
axios.interceptors.response.use(
(res: AxiosResponse) => res,
(err: AxiosError<ApiSuccess>) => {
@@ -13,7 +27,6 @@ axios.interceptors.response.use(
const msg_en = err.response?.data?.msg_en ?? '';
const msg_zh = err.response?.data?.msg_zh ?? '';
const message = useMessage();
const { returnUserLangText } = useMyI18n();
const errorMsg = returnUserLangText({
@@ -25,7 +38,7 @@ axios.interceptors.response.use(
// Handle network errors (no response from server)
if (!err.response) {
message.error(
showErrorThrottled(
returnUserLangText({
en: 'Network error. Please check your connection.',
'zh-CN': '网络错误,请检查连接。',
@@ -43,15 +56,15 @@ axios.interceptors.response.use(
/** token 过期 - only logout on auth errors */
case 401:
isLoggedIn.value = false;
if (errorMsg) message.error(errorMsg);
if (errorMsg) showErrorThrottled(errorMsg);
break;
/** 执行失败 */
case 406:
if (errorMsg) message.error(errorMsg);
if (errorMsg) showErrorThrottled(errorMsg);
break;
/** 服务器错误 - don't logout, just show error */
case 500:
message.error(
showErrorThrottled(
errorMsg ||
returnUserLangText({
en: 'Server error. Please try again later.',
@@ -59,6 +72,17 @@ axios.interceptors.response.use(
})
);
break;
/** Anything else (400/404/422/...) - still surface it instead of
* failing silently */
default:
showErrorThrottled(
errorMsg ||
returnUserLangText({
en: 'Request failed. Please try again.',
'zh-CN': '请求失败,请重试。',
})
);
break;
}
const error = {