Merge pull request '20261104' (#17) from test into master
All checks were successful
Build and Deploy Front / deploy (push) Successful in 2m25s

Reviewed-on: #17
This commit is contained in:
zzh 2026-11-04 17:36:08 +08:00
commit 8af2bba07c
23 changed files with 2926 additions and 862 deletions

View File

@ -10,3 +10,6 @@ VITE_API_ASSET_ORIGIN=https://api.test.ideship.com/admin
VITE_OMS_ADMIN_URL=https://api.test.ideship.com/admin VITE_OMS_ADMIN_URL=https://api.test.ideship.com/admin
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
# 测试站可选:填同一 GA4 或单独测试媒体资源;空则不加载
# VITE_GA_MEASUREMENT_ID=

View File

@ -11,3 +11,7 @@ VITE_API_ASSET_ORIGIN=https://api.ideship.com/admin
VITE_OMS_ADMIN_URL=https://api.ideship.com/admin VITE_OMS_ADMIN_URL=https://api.ideship.com/admin
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
# Google Analytics 4(官网 https://ideship.com)
# 地域/设备/来源自动有;首页点击见事件 home_*
VITE_GA_MEASUREMENT_ID=G-1XWZ7LL1PW

View File

@ -49,6 +49,7 @@
"@codemirror/theme-one-dark": "^6.1.3", "@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.39.17", "@codemirror/view": "^6.39.17",
"@fontsource/zcool-qingke-huangyou": "^5.3.0", "@fontsource/zcool-qingke-huangyou": "^5.3.0",
"@glass-sdk/liquid-glass": "~0.0.2",
"@handsontable/vue3": "^16.2.0", "@handsontable/vue3": "^16.2.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@vueuse/core": "^9.3.0", "@vueuse/core": "^9.3.0",

275
pnpm-lock.yaml generated
View File

@ -39,6 +39,9 @@ importers:
'@fontsource/zcool-qingke-huangyou': '@fontsource/zcool-qingke-huangyou':
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.3.0 version: 5.3.0
'@glass-sdk/liquid-glass':
specifier: ~0.0.2
version: 0.0.2(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
'@handsontable/vue3': '@handsontable/vue3':
specifier: ^16.2.0 specifier: ^16.2.0
version: 16.2.0(handsontable@16.2.0)(vue@3.5.18(typescript@4.9.5)) version: 16.2.0(handsontable@16.2.0)(vue@3.5.18(typescript@4.9.5))
@ -443,6 +446,9 @@ packages:
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/runtime@8.1.0':
resolution: {integrity: sha512-MQye/pU7gzXerMzlhScFNH1u4Je6rR8cqJWSgAfD+rIwbbtwG8tVDegoI7sWfcXkfVRW09G7vk2vjxGIuKFfwg==}
'@babel/template@7.27.2': '@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -455,6 +461,33 @@ packages:
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@base-ui/react@1.9.0':
resolution: {integrity: sha512-S8CSYosASBKtpL8SWUfS78rFElfLijiCgA7oHOK22jCJ95FULsYyKwBNFOgudt0RA+/7yNc7F81bOvQnRgbBiQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@date-fns/tz': ^1.2.0
'@types/react': ^17 || ^18 || ^19
date-fns: ^4.0.0
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@date-fns/tz':
optional: true
'@types/react':
optional: true
date-fns:
optional: true
'@base-ui/utils@0.5.0':
resolution: {integrity: sha512-dYGGcEttjkYiMoa5nHm8vLaJ2UcuTWxo3BFAwOYXeRg/q7UVVvA6cacEIXEs3FgBfK1CGtW1ptElqs7wsx+t9A==}
peerDependencies:
'@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@types/react':
optional: true
'@braintree/sanitize-url@7.1.2': '@braintree/sanitize-url@7.1.2':
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
@ -929,15 +962,36 @@ packages:
'@floating-ui/core@1.7.4': '@floating-ui/core@1.7.4':
resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
'@floating-ui/core@1.8.0':
resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
'@floating-ui/dom@1.7.5': '@floating-ui/dom@1.7.5':
resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==}
'@floating-ui/dom@1.8.0':
resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
'@floating-ui/react-dom@2.1.9':
resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.10': '@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@fontsource/zcool-qingke-huangyou@5.3.0': '@fontsource/zcool-qingke-huangyou@5.3.0':
resolution: {integrity: sha512-6/ld8x/knq1NjDQxLSyg9pDnBPOmtWDb30ZTFk+n8OxfkBoUvPAxh3aV/6+UFGmA5CekJfcgpUwd2/x/weLvSQ==} resolution: {integrity: sha512-6/ld8x/knq1NjDQxLSyg9pDnBPOmtWDb30ZTFk+n8OxfkBoUvPAxh3aV/6+UFGmA5CekJfcgpUwd2/x/weLvSQ==}
'@glass-sdk/liquid-glass@0.0.2':
resolution: {integrity: sha512-bDVDBXj6Cw++U0k9fcrSPEexnVnj/S4BjxF1t7NVQqW8La1gSVtxzPl9dG2VQxKs7Ik9kWR8Zi32JzyggI5Seg==}
peerDependencies:
react: '>=19.0.0'
react-dom: '>=19.0.0'
'@handsontable/pikaday@1.0.0': '@handsontable/pikaday@1.0.0':
resolution: {integrity: sha512-1VN6N38t5/DcjJ7y7XUYrDx1LuzvvzlrFdBdMG90Qo1xc8+LXHqbWbsTEm5Ec5gXTEbDEO53vUT35R+2COmOyg==} resolution: {integrity: sha512-1VN6N38t5/DcjJ7y7XUYrDx1LuzvvzlrFdBdMG90Qo1xc8+LXHqbWbsTEm5Ec5gXTEbDEO53vUT35R+2COmOyg==}
@ -1027,12 +1081,24 @@ packages:
'@lezer/xml@1.0.6': '@lezer/xml@1.0.6':
resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==}
'@lisse/core@0.7.3':
resolution: {integrity: sha512-WJWGskrOInlIs+DqK1CfAHNIjWBJ8t4AtM1cPn5RZfu2OmQumBdNGAhFuaIg+tgpibqP17GSwEiMq/fT+NCoIw==}
engines: {node: '>=18'}
'@marijn/find-cluster-break@1.0.2': '@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
'@mermaid-js/parser@1.2.0': '@mermaid-js/parser@1.2.0':
resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==}
'@modelcontextprotocol/core@2.0.0':
resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==}
engines: {node: '>=20'}
'@modelcontextprotocol/server@2.0.0':
resolution: {integrity: sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==}
engines: {node: '>=20'}
'@mrmlnc/readdir-enhanced@2.2.1': '@mrmlnc/readdir-enhanced@2.2.1':
resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -1841,6 +1907,26 @@ packages:
'@upsetjs/venn.js@2.0.0': '@upsetjs/venn.js@2.0.0':
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
'@vgpu/adapter-mock@0.5.0':
resolution: {integrity: sha512-YtgXOFn1qI+8oTJenG2wWbQPA28V0CfE5UJs5Xqbt6qyR/xaZ5LHH2XWTwFcjnqn2b1xQCyTatJCMyyOdweUnw==}
'@vgpu/adapter-node@0.5.0':
resolution: {integrity: sha512-lgzqFcLDejwRgwlaNStzkwLNU9b4aN2OoPpWqT1UyPmK521BSw2OIQKzAuDIkzNlREla6KNC12G6bthhOzZKzw==}
'@vgpu/core@0.5.0':
resolution: {integrity: sha512-z80DHRHViv6+FKKkPzggEtT1ZZs/WCKRQbzJuwYdA135DZaSbCMoRK4anhM+I/nOHuDpf7H3xjkwE6rfolWZUw==}
'@vgpu/wgsl-std@0.5.0':
resolution: {integrity: sha512-NjMyJxwy4evFNCSqFQy4EqAbZ7iU+NUyJYyG9tLUhbdDLzBmfZlk1S3PyRZ9olBFFfEiVW2JNVw10maPKLEs1A==}
'@vgpu/wgsl@0.5.0':
resolution: {integrity: sha512-PDlDJem7J28C9bk+vPy2kYZEoW8+nYIx2oN4nu7/WF2V7ETx32BtmWcawwMbhiu/CVsobFafML0h0eqLRBNa3Q==}
peerDependencies:
'@vgpu/adapter-node': '>=0.1.6 <1.0.0 || ^0.5.0-rc.0'
peerDependenciesMeta:
'@vgpu/adapter-node':
optional: true
'@vitejs/plugin-vue-jsx@2.1.1': '@vitejs/plugin-vue-jsx@2.1.1':
resolution: {integrity: sha512-JgDhxstQlwnHBvZ1BSnU5mbmyQ14/t5JhREc6YH5kWyu2QdAAOsLF6xgHoIWarj8tddaiwFrNzLbWJPudpXKYA==} resolution: {integrity: sha512-JgDhxstQlwnHBvZ1BSnU5mbmyQ14/t5JhREc6YH5kWyu2QdAAOsLF6xgHoIWarj8tddaiwFrNzLbWJPudpXKYA==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
@ -1947,6 +2033,9 @@ packages:
'@vueuse/shared@9.13.0': '@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
'@webgpu/types@0.1.74':
resolution: {integrity: sha512-lgiI4hbuLcI9unnm2cL/tvCaQU45dp0xcLWJh5uB/9MBGvIA4F8XIk7nSiBeg+K4xWGYwgZKn80LmERI5CxoTA==}
'@wtto00/html2canvas@1.4.3': '@wtto00/html2canvas@1.4.3':
resolution: {integrity: sha512-jwsb+xL8N+gjrSNABSaFdxmWtE4c7RNFjP20lo1G7gs63Qqo1phhxVBTzxc/apDVh6LgXsU2l5bwKtXd9uz65w==} resolution: {integrity: sha512-jwsb+xL8N+gjrSNABSaFdxmWtE4c7RNFjP20lo1G7gs63Qqo1phhxVBTzxc/apDVh6LgXsU2l5bwKtXd9uz65w==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
@ -5469,6 +5558,10 @@ packages:
resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
pixelmatch@7.2.0:
resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==}
hasBin: true
pkg-types@1.3.1: pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
@ -5479,6 +5572,10 @@ packages:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
pngjs@7.0.0:
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
engines: {node: '>=14.19.0'}
pngquant-bin@6.0.1: pngquant-bin@6.0.1:
resolution: {integrity: sha512-Q3PUyolfktf+hYio6wsg3SanQzEU/v8aICg/WpzxXcuCMRb7H2Q81okfpcEztbMvw25ILjd3a87doj2N9kvbpQ==} resolution: {integrity: sha512-Q3PUyolfktf+hYio6wsg3SanQzEU/v8aICg/WpzxXcuCMRb7H2Q81okfpcEztbMvw25ILjd3a87doj2N9kvbpQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -5679,6 +5776,15 @@ packages:
raf@3.4.1: raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
react-dom@19.3.1:
resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==}
peerDependencies:
react: ^19.3.1
react@19.3.1:
resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==}
engines: {node: '>=0.10.0'}
read-pkg-up@1.0.1: read-pkg-up@1.0.1:
resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==} resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -5791,6 +5897,9 @@ packages:
require-main-filename@2.0.0: require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
reselect@5.3.0:
resolution: {integrity: sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==}
reserved-words@0.1.2: reserved-words@0.1.2:
resolution: {integrity: sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==} resolution: {integrity: sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==}
@ -6044,6 +6153,9 @@ packages:
sax@1.4.1: sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
scheduler@0.28.0:
resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
screenfull@5.2.0: screenfull@5.2.0:
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -6919,6 +7031,11 @@ packages:
resolution: {integrity: sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==} resolution: {integrity: sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
use-sync-external-store@1.7.0:
resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
use@3.1.1: use@3.1.1:
resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -6966,6 +7083,15 @@ packages:
resolution: {integrity: sha512-KoJRp8V2EsmeW2pg33CVEOEfGDTmcVLL0wk0fOQJjgXfAunciPylOueKovuzlbWSrTQap1UzgNn8xr5zbBsnPA==} resolution: {integrity: sha512-KoJRp8V2EsmeW2pg33CVEOEfGDTmcVLL0wk0fOQJjgXfAunciPylOueKovuzlbWSrTQap1UzgNn8xr5zbBsnPA==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
vgpu@0.5.0:
resolution: {integrity: sha512-isQfMHRTysYFfT8UWTzqQI/ROw2L7lVkj7xoUJlvIF6FjYxBNS/YNG7qp3tZ7ZvZZ3uwYjso+geQv/bEq5Hl3w==}
hasBin: true
peerDependencies:
three: '>=0.180.0 <0.200.0'
peerDependenciesMeta:
three:
optional: true
vite-plugin-compression@0.5.1: vite-plugin-compression@0.5.1:
resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
peerDependencies: peerDependencies:
@ -7152,12 +7278,18 @@ packages:
w3c-keyname@2.2.8: w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
webgpu@0.4.0:
resolution: {integrity: sha512-F5pimn3Aoi0zWjuRdiVs5TnrUwSzD2lESBohsIUsqyitWkGRQlXU2fhV6ycXlQTa1bvAf3sjqiUpBEpmSQ5ptA==}
webidl-conversions@3.0.1: webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
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==}
wgpu-matrix@3.4.2:
resolution: {integrity: sha512-IfZFbG7olEYBD76VCZy2TzqM57GKy3LT++AZh37tMHPPByO6R2h61zXGBZFNPi2IzxNivNi8y5dvze5PjaczjA==}
whatwg-url@5.0.0: whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@ -7325,6 +7457,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zod@4.6.7:
resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==}
zrender@5.6.1: zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
@ -7517,6 +7652,8 @@ snapshots:
'@babel/runtime@7.28.6': {} '@babel/runtime@7.28.6': {}
'@babel/runtime@8.1.0': {}
'@babel/template@7.27.2': '@babel/template@7.27.2':
dependencies: dependencies:
'@babel/code-frame': 7.27.1 '@babel/code-frame': 7.27.1
@ -7540,6 +7677,25 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1 '@babel/helper-validator-identifier': 7.27.1
'@base-ui/react@1.9.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1)':
dependencies:
'@babel/runtime': 8.1.0
'@base-ui/utils': 0.5.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
'@floating-ui/react-dom': 2.1.9(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
'@floating-ui/utils': 0.2.12
react: 19.3.1
react-dom: 19.3.1(react@19.3.1)
use-sync-external-store: 1.7.0(react@19.3.1)
'@base-ui/utils@0.5.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1)':
dependencies:
'@babel/runtime': 8.1.0
'@floating-ui/utils': 0.2.12
react: 19.3.1
react-dom: 19.3.1(react@19.3.1)
reselect: 5.3.0
use-sync-external-store: 1.7.0(react@19.3.1)
'@braintree/sanitize-url@7.1.2': {} '@braintree/sanitize-url@7.1.2': {}
'@bufbuild/protobuf@2.10.1': {} '@bufbuild/protobuf@2.10.1': {}
@ -7955,15 +8111,46 @@ snapshots:
dependencies: dependencies:
'@floating-ui/utils': 0.2.10 '@floating-ui/utils': 0.2.10
'@floating-ui/core@1.8.0':
dependencies:
'@floating-ui/utils': 0.2.12
'@floating-ui/dom@1.7.5': '@floating-ui/dom@1.7.5':
dependencies: dependencies:
'@floating-ui/core': 1.7.4 '@floating-ui/core': 1.7.4
'@floating-ui/utils': 0.2.10 '@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.8.0':
dependencies:
'@floating-ui/core': 1.8.0
'@floating-ui/utils': 0.2.12
'@floating-ui/react-dom@2.1.9(react-dom@19.3.1(react@19.3.1))(react@19.3.1)':
dependencies:
'@floating-ui/dom': 1.8.0
react: 19.3.1
react-dom: 19.3.1(react@19.3.1)
'@floating-ui/utils@0.2.10': {} '@floating-ui/utils@0.2.10': {}
'@floating-ui/utils@0.2.12': {}
'@fontsource/zcool-qingke-huangyou@5.3.0': {} '@fontsource/zcool-qingke-huangyou@5.3.0': {}
'@glass-sdk/liquid-glass@0.0.2(react-dom@19.3.1(react@19.3.1))(react@19.3.1)':
dependencies:
'@base-ui/react': 1.9.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
'@lisse/core': 0.7.3
react: 19.3.1
react-dom: 19.3.1(react@19.3.1)
vgpu: 0.5.0
transitivePeerDependencies:
- '@date-fns/tz'
- '@types/react'
- date-fns
- supports-color
- three
'@handsontable/pikaday@1.0.0': {} '@handsontable/pikaday@1.0.0': {}
'@handsontable/vue3@16.2.0(handsontable@16.2.0)(vue@3.5.18(typescript@4.9.5))': '@handsontable/vue3@16.2.0(handsontable@16.2.0)(vue@3.5.18(typescript@4.9.5))':
@ -8078,12 +8265,23 @@ snapshots:
'@lezer/highlight': 1.2.3 '@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8 '@lezer/lr': 1.4.8
'@lisse/core@0.7.3': {}
'@marijn/find-cluster-break@1.0.2': {} '@marijn/find-cluster-break@1.0.2': {}
'@mermaid-js/parser@1.2.0': '@mermaid-js/parser@1.2.0':
dependencies: dependencies:
'@chevrotain/types': 11.1.2 '@chevrotain/types': 11.1.2
'@modelcontextprotocol/core@2.0.0':
dependencies:
zod: 4.6.7
'@modelcontextprotocol/server@2.0.0':
dependencies:
'@modelcontextprotocol/core': 2.0.0
zod: 4.6.7
'@mrmlnc/readdir-enhanced@2.2.1': '@mrmlnc/readdir-enhanced@2.2.1':
dependencies: dependencies:
call-me-maybe: 1.0.2 call-me-maybe: 1.0.2
@ -8907,6 +9105,33 @@ snapshots:
d3-selection: 3.0.0 d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0) d3-transition: 3.0.1(d3-selection@3.0.0)
'@vgpu/adapter-mock@0.5.0(@vgpu/adapter-node@0.5.0)':
dependencies:
'@vgpu/core': 0.5.0(@vgpu/adapter-node@0.5.0)
transitivePeerDependencies:
- '@vgpu/adapter-node'
'@vgpu/adapter-node@0.5.0':
dependencies:
'@vgpu/core': 0.5.0(@vgpu/adapter-node@0.5.0)
webgpu: 0.4.0
transitivePeerDependencies:
- supports-color
'@vgpu/core@0.5.0(@vgpu/adapter-node@0.5.0)':
dependencies:
'@vgpu/wgsl': 0.5.0(@vgpu/adapter-node@0.5.0)
transitivePeerDependencies:
- '@vgpu/adapter-node'
'@vgpu/wgsl-std@0.5.0': {}
'@vgpu/wgsl@0.5.0(@vgpu/adapter-node@0.5.0)':
dependencies:
'@vgpu/wgsl-std': 0.5.0
optionalDependencies:
'@vgpu/adapter-node': 0.5.0
'@vitejs/plugin-vue-jsx@2.1.1(vite@6.3.5(@types/node@20.5.1)(jiti@1.21.7)(less@4.4.0)(sass-embedded@1.93.3)(sass@1.93.3)(sugarss@2.0.0)(tsx@4.20.3))(vue@3.5.18(typescript@4.9.5))': '@vitejs/plugin-vue-jsx@2.1.1(vite@6.3.5(@types/node@20.5.1)(jiti@1.21.7)(less@4.4.0)(sass-embedded@1.93.3)(sass@1.93.3)(sugarss@2.0.0)(tsx@4.20.3))(vue@3.5.18(typescript@4.9.5))':
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
@ -9090,6 +9315,8 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
'@webgpu/types@0.1.74': {}
'@wtto00/html2canvas@1.4.3': '@wtto00/html2canvas@1.4.3':
dependencies: dependencies:
css-line-break: 2.1.0 css-line-break: 2.1.0
@ -12931,6 +13158,10 @@ snapshots:
pinkie@2.0.4: {} pinkie@2.0.4: {}
pixelmatch@7.2.0:
dependencies:
pngjs: 7.0.0
pkg-types@1.3.1: pkg-types@1.3.1:
dependencies: dependencies:
confbox: 0.1.8 confbox: 0.1.8
@ -12945,6 +13176,8 @@ snapshots:
pngjs@5.0.0: {} pngjs@5.0.0: {}
pngjs@7.0.0: {}
pngquant-bin@6.0.1: pngquant-bin@6.0.1:
dependencies: dependencies:
bin-build: 3.0.0 bin-build: 3.0.0
@ -13129,6 +13362,13 @@ snapshots:
dependencies: dependencies:
performance-now: 2.1.0 performance-now: 2.1.0
react-dom@19.3.1(react@19.3.1):
dependencies:
react: 19.3.1
scheduler: 0.28.0
react@19.3.1: {}
read-pkg-up@1.0.1: read-pkg-up@1.0.1:
dependencies: dependencies:
find-up: 1.1.2 find-up: 1.1.2
@ -13293,6 +13533,8 @@ snapshots:
require-main-filename@2.0.0: {} require-main-filename@2.0.0: {}
reselect@5.3.0: {}
reserved-words@0.1.2: {} reserved-words@0.1.2: {}
resize-detector@0.3.0: {} resize-detector@0.3.0: {}
@ -13535,6 +13777,8 @@ snapshots:
sax@1.4.1: sax@1.4.1:
optional: true optional: true
scheduler@0.28.0: {}
screenfull@5.2.0: {} screenfull@5.2.0: {}
scroll-into-view-if-needed@2.2.31: scroll-into-view-if-needed@2.2.31:
@ -14610,6 +14854,10 @@ snapshots:
url-to-options@1.0.1: {} url-to-options@1.0.1: {}
use-sync-external-store@1.7.0(react@19.3.1):
dependencies:
react: 19.3.1
use@3.1.1: {} use@3.1.1: {}
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
@ -14680,6 +14928,22 @@ snapshots:
- typescript - typescript
- utf-8-validate - utf-8-validate
vgpu@0.5.0:
dependencies:
'@modelcontextprotocol/server': 2.0.0
'@vgpu/adapter-mock': 0.5.0(@vgpu/adapter-node@0.5.0)
'@vgpu/adapter-node': 0.5.0
'@vgpu/core': 0.5.0(@vgpu/adapter-node@0.5.0)
'@vgpu/wgsl': 0.5.0(@vgpu/adapter-node@0.5.0)
'@vgpu/wgsl-std': 0.5.0
'@webgpu/types': 0.1.74
pixelmatch: 7.2.0
pngjs: 7.0.0
wgpu-matrix: 3.4.2
zod: 4.6.7
transitivePeerDependencies:
- supports-color
vite-plugin-compression@0.5.1(vite@6.3.5(@types/node@20.5.1)(jiti@1.21.7)(less@4.4.0)(sass-embedded@1.93.3)(sass@1.93.3)(sugarss@2.0.0)(tsx@4.20.3)): vite-plugin-compression@0.5.1(vite@6.3.5(@types/node@20.5.1)(jiti@1.21.7)(less@4.4.0)(sass-embedded@1.93.3)(sass@1.93.3)(sugarss@2.0.0)(tsx@4.20.3)):
dependencies: dependencies:
chalk: 4.1.2 chalk: 4.1.2
@ -14893,10 +15157,19 @@ snapshots:
w3c-keyname@2.2.8: {} w3c-keyname@2.2.8: {}
webgpu@0.4.0:
dependencies:
'@webgpu/types': 0.1.74
debug: 4.4.1
transitivePeerDependencies:
- supports-color
webidl-conversions@3.0.1: {} webidl-conversions@3.0.1: {}
webpack-virtual-modules@0.6.2: {} webpack-virtual-modules@0.6.2: {}
wgpu-matrix@3.4.2: {}
whatwg-url@5.0.0: whatwg-url@5.0.0:
dependencies: dependencies:
tr46: 0.0.3 tr46: 0.0.3
@ -15076,6 +15349,8 @@ snapshots:
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zod@4.6.7: {}
zrender@5.6.1: zrender@5.6.1:
dependencies: dependencies:
tslib: 2.3.0 tslib: 2.3.0

View File

@ -3,8 +3,10 @@
*/ */
import { axiosInstance } from '@/api/rbac' import { axiosInstance } from '@/api/rbac'
import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common' import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common'
import { CUSTOMER_STATUS } from '@/utils/customerStatus'
export type CustomerStatus = 0 | 1 | 2 /** 与后端 CustomerStatus 对齐:0=None,1=Disabled,2=Normal,3=Frozen */
export type CustomerStatus = 0 | 1 | 2 | 3
export type CustomerGroup = 0 | 1 | 2 | 3 | 4 | 5 export type CustomerGroup = 0 | 1 | 2 | 3 | 4 | 5
@ -154,7 +156,7 @@ export const CustomerService = {
balance: data.accountBalance ?? data.balance ?? 0, balance: data.accountBalance ?? data.balance ?? 0,
totalQuota: data.totalQuota ?? 0, totalQuota: data.totalQuota ?? 0,
usedQuota: data.usedQuota ?? 0, usedQuota: data.usedQuota ?? 0,
status: data.status ?? 1, status: data.status ?? CUSTOMER_STATUS.normal,
remark: data.remark, remark: data.remark,
salesman: data.salesman ?? data.salesPerson, salesman: data.salesman ?? data.salesPerson,
customerService: data.customerService, customerService: data.customerService,
@ -186,7 +188,8 @@ export const CustomerService = {
balance: data.accountBalance ?? data.balance, balance: data.accountBalance ?? data.balance,
totalQuota: data.totalQuota, totalQuota: data.totalQuota,
usedQuota: data.usedQuota, usedQuota: data.usedQuota,
status: data.status ?? 1, // 后端:1=Disabled,2=Normal;勿默认 1,否则公开注册账号无法登录
status: data.status ?? CUSTOMER_STATUS.normal,
role: data.role, role: data.role,
remark: data.remark, remark: data.remark,
salesman: data.salesman ?? data.salesPerson, salesman: data.salesman ?? data.salesPerson,

View File

@ -167,8 +167,37 @@ export async function sendBatchEmailOne(body: BatchEmailSendOneBody) {
assertBusinessOk(res.data) assertBusinessOk(res.data)
} }
export interface BatchEmailDuplicatePair {
to: string
subject: string
}
export interface BatchEmailDuplicateHit {
toEmail: string
subject: string
sendTime: string
sender: string
}
function normalizeDuplicateHit(raw: unknown): BatchEmailDuplicateHit | null {
if (raw == null || typeof raw !== 'object') return null
const row = raw as Record<string, unknown>
const toEmail = pickStr(row, 'toEmail', 'ToEmail')
if (!toEmail) return null
return {
toEmail,
subject: pickStr(row, 'subject', 'Subject'),
sendTime: pickStr(row, 'sendTime', 'SendTime'),
sender: pickStr(row, 'sender', 'Sender'),
}
}
/** GET /system/batch-email/records/list */ /** GET /system/batch-email/records/list */
export async function listBatchEmailRecords(params?: { page?: number; pageSize?: number }) { export async function listBatchEmailRecords(params?: {
page?: number
pageSize?: number
keyword?: string
}) {
const res = await axiosInstance.get(omsUrl('/system/batch-email/records/list'), { params }) const res = await axiosInstance.get(omsUrl('/system/batch-email/records/list'), { params })
const page = unwrapPageResult(res.data) const page = unwrapPageResult(res.data)
return { return {
@ -178,3 +207,15 @@ export async function listBatchEmailRecords(params?: { page?: number; pageSize?:
total: page.total, total: page.total,
} }
} }
/** POST /system/batch-email/check-duplicates — 近 N 天同邮箱同主题预检 */
export async function checkBatchEmailDuplicates(body: {
pairs: BatchEmailDuplicatePair[]
withinDays?: number
}): Promise<BatchEmailDuplicateHit[]> {
const res = await axiosInstance.post(omsUrl('/system/batch-email/check-duplicates'), body)
const list = unwrapList(res.data)
return list
.map(normalizeDuplicateHit)
.filter((item): item is BatchEmailDuplicateHit => item != null)
}

4
src/env.d.ts vendored
View File

@ -53,4 +53,8 @@ interface ImportMetaEnv {
readonly VITE_API_ORIGIN?: string; readonly VITE_API_ORIGIN?: string;
/** 静态资源 origin;本地默认测试 admin */ /** 静态资源 origin;本地默认测试 admin */
readonly VITE_API_ASSET_ORIGIN?: string; readonly VITE_API_ASSET_ORIGIN?: string;
/** 企微客服链接 */
readonly VITE_WECOM_KF_URL?: string;
/** Google Analytics 4 Measurement ID,如 G-XXXXXXXX;空则不加载 */
readonly VITE_GA_MEASUREMENT_ID?: string;
} }

View File

@ -4399,11 +4399,24 @@ export default {
'system.batchEmail.errLoadTemplates': 'Failed to load templates', 'system.batchEmail.errLoadTemplates': 'Failed to load templates',
'system.batchEmail.errLoadRecords': 'Failed to load email records', 'system.batchEmail.errLoadRecords': 'Failed to load email records',
'system.batchEmail.recordList': 'Email records', 'system.batchEmail.recordList': 'Email records',
'system.batchEmail.recordSearch': 'Search',
'system.batchEmail.recordSearchPlaceholder': 'Email / subject / body / sender',
'system.batchEmail.record.toEmail': 'Email', 'system.batchEmail.record.toEmail': 'Email',
'system.batchEmail.record.subject': 'Subject', 'system.batchEmail.record.subject': 'Subject',
'system.batchEmail.record.content': 'Content', 'system.batchEmail.record.content': 'Content',
'system.batchEmail.record.sendTime': 'Sent at', 'system.batchEmail.record.sendTime': 'Sent at',
'system.batchEmail.record.sender': 'Sender', 'system.batchEmail.record.sender': 'Sender',
'system.batchEmail.errCheckDup': 'Duplicate check failed. Please try again.',
'system.batchEmail.dupConfirmTitle': 'Duplicates in the last 30 days',
'system.batchEmail.dupConfirmBody': '{count} item(s) share the same email + subject within {days} days (including in-batch duplicates):',
'system.batchEmail.dupConfirmMore': '{n} more not shown',
'system.batchEmail.dupConfirmBatchOnly': '{n} more are duplicates within this batch',
'system.batchEmail.dupBatchOnlyTip': '{n} duplicate group(s) in this batch — only the first of each email+subject will be sent',
'system.batchEmail.dupConfirmHint': 'Duplicates are skipped by default. Click “Send anyway” to include them.',
'system.batchEmail.dupConfirmForce': 'Send anyway',
'system.batchEmail.dupConfirmSkip': 'Skip duplicates',
'system.batchEmail.failDupRecent': 'Already sent with the same subject within {days} days',
'system.batchEmail.failDupInBatch': 'Duplicate in this batch, skipped',
'system.batchEmail.warnEmptyContent': 'Template body is required', 'system.batchEmail.warnEmptyContent': 'Template body is required',
'system.batchEmail.warnEmptyName': 'Template name is required', 'system.batchEmail.warnEmptyName': 'Template name is required',
'system.batchEmail.confirmOverwriteTitle': 'Overwrite?', 'system.batchEmail.confirmOverwriteTitle': 'Overwrite?',
@ -4451,6 +4464,33 @@ export default {
'system.batchEmail.logSendFail': 'failed - {msg}', 'system.batchEmail.logSendFail': 'failed - {msg}',
'system.batchEmail.logSendDone': 'Done: {total} total, {successCount} ok, {failCount} failed', 'system.batchEmail.logSendDone': 'Done: {total} total, {successCount} ok, {failCount} failed',
'system.batchEmail.msgSendDone': 'Done: {successCount} succeeded, {failCount} failed', 'system.batchEmail.msgSendDone': 'Done: {successCount} succeeded, {failCount} failed',
'system.batchEmail.accountReady': 'Mailbox ready · {account}',
'system.batchEmail.accountNotReady': 'Mailbox not configured — set it up before sending',
'system.batchEmail.templateEdit': 'Edit template',
'system.batchEmail.placeholderInsertHint': 'Click to insert a placeholder at the subject/body cursor',
'system.batchEmail.sendProgressTitle': 'Batch send progress',
'system.batchEmail.sendProgressMeta': '{current}/{total} processed · {success} ok · {fail} failed',
'system.batchEmail.sendProgressCurrent': 'Current: {email}',
'system.batchEmail.sendConfirmTitle': 'Confirm batch send',
'system.batchEmail.sendConfirmOk': 'Send now',
'system.batchEmail.sendConfirmAccount': 'From',
'system.batchEmail.sendConfirmCount': 'Recipients',
'system.batchEmail.sendConfirmHint': 'Messages are sent one by one with the current template. You can cancel remaining sends; already sent emails cannot be recalled.',
'system.batchEmail.cancelSend': 'Cancel remaining',
'system.batchEmail.sendFailList': 'Failures ({n})',
'system.batchEmail.copyFailList': 'Copy list',
'system.batchEmail.msgCopyFailOk': 'Failure list copied',
'system.batchEmail.errCopyFail': 'Copy failed — select the text manually',
'system.batchEmail.failInvalidEmail': 'Invalid email',
'system.batchEmail.failEmptyBody': 'Empty body',
'system.batchEmail.msgSendCancelled': 'Cancelled: {successCount} ok, {failCount} failed',
'system.batchEmail.replacePreview': 'Placeholder preview',
'system.batchEmail.replacePreviewEmpty': 'Add recipients to preview placeholder replacement',
'system.batchEmail.replacePreviewManual': 'Based on {email} (manual emails leave order/track placeholders empty)',
'system.batchEmail.replacePreviewSample': 'Preview using first recipient {email}',
'system.batchEmail.confirmSwitchTplTitle': 'Switch template?',
'system.batchEmail.confirmSwitchTplBody': 'Subject/body were edited. Switching will discard unsaved changes.',
'system.batchEmail.removeRecipient': 'Remove this email',
// System - Announcement // System - Announcement
'system.announcement.search.placeholder': 'Search title or content', 'system.announcement.search.placeholder': 'Search title or content',

View File

@ -4423,11 +4423,24 @@ export default {
'system.batchEmail.errLoadTemplates': '加载模板失败', 'system.batchEmail.errLoadTemplates': '加载模板失败',
'system.batchEmail.errLoadRecords': '加载邮件记录失败', 'system.batchEmail.errLoadRecords': '加载邮件记录失败',
'system.batchEmail.recordList': '邮件记录', 'system.batchEmail.recordList': '邮件记录',
'system.batchEmail.recordSearch': '查询',
'system.batchEmail.recordSearchPlaceholder': '邮箱 / 主题 / 内容 / 发送人',
'system.batchEmail.record.toEmail': '邮箱', 'system.batchEmail.record.toEmail': '邮箱',
'system.batchEmail.record.subject': '主题', 'system.batchEmail.record.subject': '主题',
'system.batchEmail.record.content': '内容', 'system.batchEmail.record.content': '内容',
'system.batchEmail.record.sendTime': '发送时间', 'system.batchEmail.record.sendTime': '发送时间',
'system.batchEmail.record.sender': '发送人', 'system.batchEmail.record.sender': '发送人',
'system.batchEmail.errCheckDup': '查重失败,请稍后重试',
'system.batchEmail.dupConfirmTitle': '发现近 30 天重复发送',
'system.batchEmail.dupConfirmBody': '有 {count} 条「同邮箱 + 同主题」在近 {days} 天内已发送过(含本批重复):',
'system.batchEmail.dupConfirmMore': '另有 {n} 条未展示',
'system.batchEmail.dupConfirmBatchOnly': '另有 {n} 条为本批内重复收件',
'system.batchEmail.dupBatchOnlyTip': '本批有 {n} 组重复收件,同邮箱同主题仅发送首条',
'system.batchEmail.dupConfirmHint': '默认跳过重复项;若仍要发送,请点「仍要发送」。',
'system.batchEmail.dupConfirmForce': '仍要发送',
'system.batchEmail.dupConfirmSkip': '仅发送未重复',
'system.batchEmail.failDupRecent': '近 {days} 天已发送过同主题',
'system.batchEmail.failDupInBatch': '本批内重复,已跳过',
'system.batchEmail.warnEmptyContent': '模板内容不能为空', 'system.batchEmail.warnEmptyContent': '模板内容不能为空',
'system.batchEmail.warnEmptyName': '模板名称不能为空', 'system.batchEmail.warnEmptyName': '模板名称不能为空',
'system.batchEmail.confirmOverwriteTitle': '确认覆盖', 'system.batchEmail.confirmOverwriteTitle': '确认覆盖',
@ -4475,6 +4488,33 @@ export default {
'system.batchEmail.logSendFail': '收件人, 发送失败 - {msg}', 'system.batchEmail.logSendFail': '收件人, 发送失败 - {msg}',
'system.batchEmail.logSendDone': '结束发送: 共发送{total}封邮箱, 成功{successCount}, 失败{failCount}', 'system.batchEmail.logSendDone': '结束发送: 共发送{total}封邮箱, 成功{successCount}, 失败{failCount}',
'system.batchEmail.msgSendDone': '发送完成:成功 {successCount},失败 {failCount}', 'system.batchEmail.msgSendDone': '发送完成:成功 {successCount},失败 {failCount}',
'system.batchEmail.accountReady': '邮箱已配置 · {account}',
'system.batchEmail.accountNotReady': '邮箱未配置,发送前请先完成设置',
'system.batchEmail.templateEdit': '模板编辑',
'system.batchEmail.placeholderInsertHint': '点击插入占位符(插入到主题或正文光标处)',
'system.batchEmail.sendProgressTitle': '批量发送进度',
'system.batchEmail.sendProgressMeta': '已处理 {current}/{total} · 成功 {success} · 失败 {fail}',
'system.batchEmail.sendProgressCurrent': '当前:{email}',
'system.batchEmail.sendConfirmTitle': '确认批量发送',
'system.batchEmail.sendConfirmOk': '确认发送',
'system.batchEmail.sendConfirmAccount': '发件账号',
'system.batchEmail.sendConfirmCount': '收件人数',
'system.batchEmail.sendConfirmHint': '将按当前模板逐一发送,发送过程可取消;已发出的邮件无法撤回。',
'system.batchEmail.cancelSend': '取消剩余',
'system.batchEmail.sendFailList': '失败明细({n})',
'system.batchEmail.copyFailList': '复制明细',
'system.batchEmail.msgCopyFailOk': '失败明细已复制',
'system.batchEmail.errCopyFail': '复制失败,请手动选择文本',
'system.batchEmail.failInvalidEmail': '邮箱无效',
'system.batchEmail.failEmptyBody': '正文为空',
'system.batchEmail.msgSendCancelled': '已取消:成功 {successCount},失败 {failCount}',
'system.batchEmail.replacePreview': '替换预览',
'system.batchEmail.replacePreviewEmpty': '添加收件人后,可预览占位符替换效果',
'system.batchEmail.replacePreviewManual': '基于 {email}(手动邮箱无单号/轨迹时,对应占位符会为空)',
'system.batchEmail.replacePreviewSample': '基于首个收件人 {email} 的替换效果',
'system.batchEmail.confirmSwitchTplTitle': '切换模板?',
'system.batchEmail.confirmSwitchTplBody': '当前主题/正文已修改,切换将覆盖未保存的内容。',
'system.batchEmail.removeRecipient': '移除该邮箱',
// 系统 - 公告通知 // 系统 - 公告通知
'system.announcement.search.placeholder': '搜索标题或正文', 'system.announcement.search.placeholder': '搜索标题或正文',

View File

@ -19,6 +19,7 @@ import {
import { initAuthKeepAlive } from '@/utils/auth-keepalive'; import { initAuthKeepAlive } from '@/utils/auth-keepalive';
import { installWeChatH5HistoryGuard } from '@/utils/device'; import { installWeChatH5HistoryGuard } from '@/utils/device';
import { installChunkReloadHandlers, reloadOnChunkLoadError } from '@/utils/chunkReload'; import { installChunkReloadHandlers, reloadOnChunkLoadError } from '@/utils/chunkReload';
import { initGa } from '@/utils/ga';
import { setAuthToken } from './api/rbac'; import { setAuthToken } from './api/rbac';
import { useRbacUserStore } from '@/store/modules/rbac-user'; import { useRbacUserStore } from '@/store/modules/rbac-user';
import { applyRbacUserToUserStore } from '@/utils/enter-system'; import { applyRbacUserToUserStore } from '@/utils/enter-system';
@ -138,6 +139,8 @@ async function bootstrap() {
;(window as any).__APP_MOUNTED__ = true; ;(window as any).__APP_MOUNTED__ = true;
installWeChatH5HistoryGuard(router); installWeChatH5HistoryGuard(router);
initAuthKeepAlive(); initAuthKeepAlive();
// GA4:有 VITE_GA_MEASUREMENT_ID 才加载;地域/设备等自动采集
initGa();
// 登录页勿预热 VXE/Element:体积大且会注入全局 CSS,和首屏抢带宽 // 登录页勿预热 VXE/Element:体积大且会注入全局 CSS,和首屏抢带宽
if (isHomePath(entryPath) && !isH5LikePath(entryPath) && !light) { if (isHomePath(entryPath) && !isH5LikePath(entryPath) && !light) {

View File

@ -10,7 +10,12 @@ function setupPageGuard(router: Router) {
setRouteEmitter(to); setRouteEmitter(to);
}); });
router.afterEach(() => { router.afterEach((to) => {
// SPA 页面浏览(未配置 VITE_GA_MEASUREMENT_ID 时为空操作)
const fullPath = to.fullPath || to.path || '/';
void import('@/utils/ga').then(({ trackPageView }) => {
trackPageView(fullPath);
});
NProgress.done(); NProgress.done();
}); });
} }

View File

@ -318,7 +318,11 @@ export const useRbacUserStore = defineStore('rbacUser', () => {
enableKeepAlive() enableKeepAlive()
await hydrateUserAfterLogin(result) await hydrateUserAfterLogin(result)
return true return true
} catch { } catch (error: unknown) {
// 交给登录页展示业务文案(如「客户已禁用」),勿一律当成账号密码错误
const anyErr = error as { body?: { message?: string }; message?: string }
const msg = String(anyErr?.body?.message || anyErr?.message || '').trim()
if (msg) throw new Error(msg)
return false return false
} }
} }

80
src/utils/ga.ts Normal file
View File

@ -0,0 +1,80 @@
/**
* Google Analytics 4(gtag)
* 通过 VITE_GA_MEASUREMENT_ID(如 G-XXXXXXXX)启用;未配置则整段空操作。
* 地域分布、设备、来源等由 GA4 自动采集,业务点击用 trackEvent / trackSiteEvent。
*/
declare global {
interface Window {
dataLayer: unknown[];
gtag?: (...args: unknown[]) => void;
}
}
const MEASUREMENT_ID = String(import.meta.env.VITE_GA_MEASUREMENT_ID || '').trim();
let booted = false;
export function isGaEnabled(): boolean {
return /^G-[A-Z0-9]+$/i.test(MEASUREMENT_ID);
}
export function getGaMeasurementId(): string {
return isGaEnabled() ? MEASUREMENT_ID : '';
}
/** 注入 gtag 脚本并 config;可重复调用,仅首次生效 */
export function initGa(): void {
if (!isGaEnabled() || typeof window === 'undefined' || booted) return;
booted = true;
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag(...args: unknown[]) {
window.dataLayer.push(args);
};
window.gtag('js', new Date());
window.gtag('config', MEASUREMENT_ID, {
// SPA 由路由 afterEach 主动发 page_view,避免首屏重复
send_page_view: false,
anonymize_ip: true,
});
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(MEASUREMENT_ID)}`;
document.head.appendChild(script);
}
export function trackPageView(path: string, title?: string): void {
if (!isGaEnabled() || !window.gtag) return;
const pagePath = path || '/';
window.gtag('event', 'page_view', {
page_path: pagePath,
page_title: title || document.title,
page_location: typeof window !== 'undefined' ? window.location.href : undefined,
});
}
export function trackEvent(
name: string,
params?: Record<string, string | number | boolean | undefined | null>,
): void {
if (!isGaEnabled() || !window.gtag) return;
const cleaned: Record<string, string | number | boolean> = {};
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') return;
cleaned[key] = value;
});
}
window.gtag('event', name, cleaned);
}
/** 官网营销事件(统一带 surface,方便在探索里筛选) */
export function trackSiteEvent(
name: string,
params?: Record<string, string | number | boolean | undefined | null>,
): void {
trackEvent(name, { surface: 'official_site', ...params });
}

View File

@ -350,11 +350,19 @@ async function handleSubmit() {
loading.value = true; loading.value = true;
errorBox.value = ''; errorBox.value = '';
const success = await rbacUserStore.login( let success = false;
try {
success = await rbacUserStore.login(
loginForm.account, loginForm.account,
loginForm.password, loginForm.password,
props.loginType, props.loginType,
); );
} catch (loginErr: unknown) {
const msg = loginErr instanceof Error ? loginErr.message.trim() : '';
errorBox.value = msg || t('login.form.login.failed');
loginCharacters.value?.onLoginFailed();
return;
}
if (success && rbacUserStore.userInfo) { if (success && rbacUserStore.userInfo) {
const userInfoData = rbacUserStore.userInfo; const userInfoData = rbacUserStore.userInfo;

View File

@ -11,8 +11,8 @@
IconCopy, IconCopy,
IconSettings, IconSettings,
} from '@arco-design/web-vue/es/icon'; } from '@arco-design/web-vue/es/icon';
import SlidingTabs from '@/components/SlidingTabs/index.vue';
import ExcelFileIcon from '@/components/icons/ExcelFileIcon.vue'; import ExcelFileIcon from '@/components/icons/ExcelFileIcon.vue';
import SlidingTabs from '@/components/SlidingTabs/index.vue';
import CustomTemplateModal from './components/CustomTemplateModal.vue'; import CustomTemplateModal from './components/CustomTemplateModal.vue';
import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vue'; import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vue';
import FormLabelWithPreview from '@/views/channel/salesProducts/components/FormLabelWithPreview.vue'; import FormLabelWithPreview from '@/views/channel/salesProducts/components/FormLabelWithPreview.vue';
@ -5777,6 +5777,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
min-width: 0; min-width: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
overflow: hidden; overflow: hidden;
} }
@ -5787,6 +5788,22 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
max-width: 100%; max-width: 100%;
} }
:deep(.t-tab[aria-selected='true'] .t-tab__label) {
display: inline-flex;
align-items: center;
gap: 6px;
&::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
flex-shrink: 0;
}
}
.place-order-pane { .place-order-pane {
flex: 1; flex: 1;
min-height: 0; min-height: 0;

View File

@ -325,6 +325,8 @@ void main() {
canvas.style.height = '100%'; canvas.style.height = '100%';
canvas.style.display = 'block'; canvas.style.display = 'block';
canvas.style.pointerEvents = 'none'; canvas.style.pointerEvents = 'none';
canvas.style.opacity = '0';
canvas.style.transition = 'opacity 0.55s ease';
container.appendChild(canvas); container.appendChild(canvas);
const geometry = new Triangle(gl); const geometry = new Triangle(gl);
@ -367,6 +369,9 @@ void main() {
const w = Math.max(1, Math.floor(rect.width * scale)); const w = Math.max(1, Math.floor(rect.width * scale));
const h = Math.max(1, Math.floor(rect.height * scale)); const h = Math.max(1, Math.floor(rect.height * scale));
renderer.setSize(w, h); renderer.setSize(w, h);
// OGL setSize writes pixel style width/height; keep CSS 100% so parent resize always fills.
canvas.style.width = '100%';
canvas.style.height = '100%';
const res = mu.iResolution.value; const res = mu.iResolution.value;
res[0] = gl.drawingBufferWidth; res[0] = gl.drawingBufferWidth;
res[1] = gl.drawingBufferHeight; res[1] = gl.drawingBufferHeight;
@ -454,6 +459,11 @@ void main() {
tryStart(); tryStart();
syncUniforms(); syncUniforms();
// 先画一帧再淡入,盖在静态海天底上,避免灰/白闪
requestAnimationFrame(() => {
paint(performance.now());
canvas.style.opacity = '1';
});
teardown = () => { teardown = () => {
tryStop(); tryStop();
@ -491,5 +501,13 @@ void main() {
display: block; display: block;
width: 100% !important; width: 100% !important;
height: 100% !important; height: 100% !important;
will-change: opacity;
}
@media (prefers-reduced-motion: reduce) {
.gradient-waves :deep(canvas) {
transition: none !important;
opacity: 1 !important;
}
} }
</style> </style>

View File

@ -1,33 +1,8 @@
<template> <template>
<div id="ai-advisor-shell" :class="{ 'ai-history-open': historyOpen }"> <div
<div class="ai-shell-bg" aria-hidden="true"> id="ai-advisor-shell"
<GradientWaves :class="{ 'ai-history-open': historyOpen }"
class="ai-shell-bg__waves" >
horizon-color="#023e8a"
wave-color="#0077b6"
crest-color="#caf0f8"
:speed="0.34"
:amplitude="3.3"
:wave-scale="0.72"
:wave-ratio="0.82"
:swell="42"
:turbulence="26"
:tilt="1.08"
:zoom="0.92"
:height="4.8"
:fog-depth="22"
detail="low"
:brightness="1.08"
:opacity="1.0"
:mouse-interaction="true"
:parallax-strength="0.4"
:grain="false"
:fps="30"
:render-scale="0.72"
:max-dpr="1.25"
/>
<div class="ai-shell-bg__veil" aria-hidden="true"></div>
</div>
<div class="ai-advisor-layout"> <div class="ai-advisor-layout">
<div class="ai-advisor-app"> <div class="ai-advisor-app">
<div v-if="chatting" class="ai-stage-tools"> <div v-if="chatting" class="ai-stage-tools">
@ -57,7 +32,7 @@
<div class="ai-welcome__intro"> <div class="ai-welcome__intro">
<h2 class="ai-welcome-title" :aria-label="shortTitle ? copy.titleShort : copy.title"> <h2 class="ai-welcome-title" :aria-label="shortTitle ? copy.titleShort : copy.title">
<span class="ai-welcome-title__brand" aria-hidden="true"> <span class="ai-welcome-title__brand" aria-hidden="true">
IDE<span class="ai-welcome-title__dot">.</span> IDE<span class="ai-welcome-title__dot"> · </span>
</span> </span>
<TextType <TextType
v-if="titleAnimate" v-if="titleAnimate"
@ -65,7 +40,8 @@
as="span" as="span"
class-name="ai-welcome-title__type" class-name="ai-welcome-title__type"
:text="shortTitle ? copy.titleRestShort : copy.titleRest" :text="shortTitle ? copy.titleRestShort : copy.titleRest"
:typing-speed="36" :typing-speed="48"
:initial-delay="280"
:pause-duration="900" :pause-duration="900"
:show-cursor="false" :show-cursor="false"
:loop="false" :loop="false"
@ -128,7 +104,11 @@
v-for="door in primaryDoors" v-for="door in primaryDoors"
:key="door.id" :key="door.id"
type="button" type="button"
class="ai-intent-door is-primary" class="ai-intent-door is-primary lg-surface"
:class="{
'is-glass-ready': glassReady,
'is-glass-fallback': glassFallback,
}"
:disabled="busy" :disabled="busy"
@click="onHomeDoor(door)" @click="onHomeDoor(door)"
> >
@ -149,25 +129,25 @@
v-for="door in growthDoors" v-for="door in growthDoors"
:key="door.id" :key="door.id"
type="button" type="button"
class="ai-intent-door" class="ai-intent-door lg-surface"
:class="{ :class="{
'is-agent-tall': door.id === 'agent', 'is-glass-ready': glassReady,
'is-growth-wide': door.id === 'newcomer', 'is-glass-fallback': glassFallback,
}" }"
:disabled="busy" :disabled="busy"
@click="onHomeDoor(door)" @click="onHomeDoor(door)"
> >
<span class="ai-intent-door__head">
<span class="ai-intent-door__icon"> <span class="ai-intent-door__icon">
<SiteIllusIcon :name="door.icon" /> <SiteIllusIcon :name="door.icon" />
</span> </span>
<span class="ai-intent-door__copy">
<span class="ai-intent-door__title">{{ <span class="ai-intent-door__title">{{
isZh ? door.zh.title : door.en.title isZh ? door.zh.title : door.en.title
}}</span> }}</span>
</span>
<span class="ai-intent-door__hint">{{ <span class="ai-intent-door__hint">{{
isZh ? door.zh.hint : door.en.hint isZh ? door.zh.hint : door.en.hint
}}</span> }}</span>
</span>
</button> </button>
</div> </div>
</div> </div>
@ -287,7 +267,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import SiteComposer from './SiteComposer.vue'; import SiteComposer from './SiteComposer.vue';
import SitePartnerMarquee from './SitePartnerMarquee.vue'; import SitePartnerMarquee from './SitePartnerMarquee.vue';
import SiteMessageCard from './SiteMessageCard.vue'; import SiteMessageCard from './SiteMessageCard.vue';
@ -295,8 +275,7 @@
import TextType from './TextType.vue'; import TextType from './TextType.vue';
import { HOME_DOORS, HERO_STATS, SITE_FOLLOWUP_INTENTS, WELCOME_COPY } from './lib/site-welcome'; import { HOME_DOORS, HERO_STATS, SITE_FOLLOWUP_INTENTS, WELCOME_COPY } from './lib/site-welcome';
import { ROUTE_DEST_PRESETS } from './lib/portal-catalog'; import { ROUTE_DEST_PRESETS } from './lib/portal-catalog';
import { SITE_GLASS_PANEL_OPTS, useSiteLiquidGlass } from './lib/site-liquid-glass';
const GradientWaves = defineAsyncComponent(() => import('./GradientWaves.vue'));
export type SiteChatMessage = { export type SiteChatMessage = {
role: 'user' | 'assistant'; role: 'user' | 'assistant';
@ -342,11 +321,35 @@
choice: [payload: { id: string; label: string; flowId: string }]; choice: [payload: { id: string; label: string; flowId: string }];
}>(); }>();
const siteGlass = useSiteLiquidGlass();
const glassReady = computed(() => !!siteGlass?.ready.value);
const glassFallback = computed(() => !!siteGlass?.fallback.value);
let removeGlassSurfaces: Array<() => void> = [];
function teardownLiquidGlass() {
removeGlassSurfaces.forEach((remove) => remove());
removeGlassSurfaces = [];
}
async function setupLiquidGlass() {
await nextTick();
if (!siteGlass || props.chatting) return;
teardownLiquidGlass();
const doorSurfaces = Array.from(
document.querySelectorAll<HTMLElement>(
'#ai-advisor-shell .ai-welcome__command .ai-intent-door.lg-surface',
),
);
removeGlassSurfaces = doorSurfaces.map((el) =>
siteGlass.registerSurface(el, SITE_GLASS_PANEL_OPTS),
);
}
const copy = computed(() => (props.isZh ? WELCOME_COPY.zh : WELCOME_COPY.en)); const copy = computed(() => (props.isZh ? WELCOME_COPY.zh : WELCOME_COPY.en));
const homeDests = ROUTE_DEST_PRESETS.filter((item) => item.id !== 'all'); const homeDests = ROUTE_DEST_PRESETS.filter((item) => item.id !== 'all');
const primaryDoors = HOME_DOORS.filter((d) => d.primary); const primaryDoors = HOME_DOORS.filter((d) => d.primary);
const growthDoors = HOME_DOORS.filter((d) => !d.primary); const growthDoors = HOME_DOORS.filter((d) => !d.primary);
// 首屏即按视口决定短标题,避免挂载后再切导致 TextType 卡住只剩「IDE.」 // 首屏即按视口决定短标题,避免挂载后再切导致 TextType 卡住只剩品牌前缀
const shortTitle = ref( const shortTitle = ref(
typeof window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches, typeof window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches,
); );
@ -397,16 +400,30 @@
titleAnimate.value = !mqReduce?.matches; titleAnimate.value = !mqReduce?.matches;
} }
watch(
() => props.chatting,
async (chatting) => {
if (chatting) {
teardownLiquidGlass();
return;
}
await nextTick();
void setupLiquidGlass();
},
);
onMounted(() => { onMounted(() => {
mqNarrow = window.matchMedia('(max-width: 640px)'); mqNarrow = window.matchMedia('(max-width: 640px)');
mqReduce = window.matchMedia('(prefers-reduced-motion: reduce)'); mqReduce = window.matchMedia('(prefers-reduced-motion: reduce)');
syncMediaPrefs(); syncMediaPrefs();
mqNarrow.addEventListener('change', syncMediaPrefs); mqNarrow.addEventListener('change', syncMediaPrefs);
mqReduce.addEventListener('change', syncMediaPrefs); mqReduce.addEventListener('change', syncMediaPrefs);
void setupLiquidGlass();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
mqNarrow?.removeEventListener('change', syncMediaPrefs); mqNarrow?.removeEventListener('change', syncMediaPrefs);
mqReduce?.removeEventListener('change', syncMediaPrefs); mqReduce?.removeEventListener('change', syncMediaPrefs);
teardownLiquidGlass();
}); });
</script> </script>

View File

@ -29,12 +29,7 @@
<ul> <ul>
<li ref="dockRef" class="site-nav-dock"> <li ref="dockRef" class="site-nav-dock">
<span class="site-nav-dock__glass" aria-hidden="true"></span> <span class="site-nav-dock__glass" aria-hidden="true"></span>
<span <span ref="sliderRef" class="site-nav-dock__slider" aria-hidden="true"></span>
class="site-nav-dock__slider"
:class="{ 'is-ready': sliderReady }"
:style="sliderStyle"
aria-hidden="true"
></span>
<a <a
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
@ -210,61 +205,61 @@
]; ];
const dockRef = ref<HTMLElement | null>(null); const dockRef = ref<HTMLElement | null>(null);
const sliderReady = ref(false); const sliderRef = ref<HTMLElement | null>(null);
const sliderStyle = ref<Record<string, string>>({
left: '0px',
width: '0px',
opacity: '0',
});
const sliderPos = { x: 0, w: 0 };
let resizeObserver: ResizeObserver | null = null;
let sliderRaf = 0;
const SLIDER_INSET = 4; const SLIDER_INSET = 4;
const sliderPos = { x: 0, w: 0, visible: false };
let sliderReady = false;
let sliderRaf = 0;
let resizeObserver: ResizeObserver | null = null;
let mqNarrow: MediaQueryList | null = null;
let mqReduce: MediaQueryList | null = null;
function paintSlider(x: number, w: number, visible: boolean, animate: boolean) {
const el = sliderRef.value;
if (!el) return;
const reduce = !!mqReduce?.matches;
const canAnimate = animate && sliderReady && sliderPos.visible && !reduce && sliderPos.w > 0;
el.style.transition = canAnimate
? 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.18s ease'
: 'none';
el.style.width = `${w}px`;
el.style.transform = `translate3d(${x}px, 0, 0)`;
el.style.opacity = visible ? '1' : '0';
function applySlider(x: number, w: number, opacity = '1') {
sliderPos.x = x; sliderPos.x = x;
sliderPos.w = w; sliderPos.w = w;
sliderStyle.value = { sliderPos.visible = visible;
left: `${x}px`,
width: `${w}px`, if (!sliderReady && visible) {
opacity, requestAnimationFrame(() => {
}; sliderReady = true;
});
}
} }
function updateSlider(animate: boolean) { function updateSlider(animate: boolean) {
const dock = dockRef.value; const dock = dockRef.value;
const active = dock?.querySelector<HTMLElement>('.site-nav-dock__item.is-active'); const active = dock?.querySelector<HTMLElement>('.site-nav-dock__item.is-active');
if (!dock || !active || window.matchMedia('(max-width: 1279px)').matches) { if (!dock || !active || mqNarrow?.matches) {
sliderStyle.value = { ...sliderStyle.value, opacity: '0' }; paintSlider(sliderPos.x, sliderPos.w, false, false);
return; return;
} }
const x = active.offsetLeft + SLIDER_INSET; const x = active.offsetLeft + SLIDER_INSET;
const w = Math.max(active.offsetWidth - SLIDER_INSET * 2, 0); const w = Math.max(active.offsetWidth - SLIDER_INSET * 2, 0);
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (
animate &&
// 首帧或关动画:先贴到位再开 transition,避免初次跳动 Math.abs(sliderPos.x - x) < 0.5 &&
if (!animate || !sliderReady.value || sliderPos.w <= 0 || reduceMotion) { Math.abs(sliderPos.w - w) < 0.5 &&
sliderReady.value = false; sliderPos.visible
applySlider(x, w); ) {
requestAnimationFrame(() => {
sliderReady.value = true;
});
return; return;
} }
paintSlider(x, w, true, animate);
if (Math.abs(sliderPos.x - x) < 0.5 && Math.abs(sliderPos.w - w) < 0.5) {
applySlider(x, w);
return;
} }
// 单段滑移:left/width 一次到位,交给 CSS 缓动 function syncSlider(animate: boolean) {
applySlider(x, w);
}
async function syncSlider(animate: boolean) {
await nextTick();
if (sliderRaf) cancelAnimationFrame(sliderRaf); if (sliderRaf) cancelAnimationFrame(sliderRaf);
sliderRaf = requestAnimationFrame(() => { sliderRaf = requestAnimationFrame(() => {
sliderRaf = 0; sliderRaf = 0;
@ -273,17 +268,20 @@
} }
function onViewportResize() { function onViewportResize() {
updateSlider(false); sliderReady = false;
syncSlider(false);
} }
onMounted(() => { onMounted(() => {
void hydrateSession(); void hydrateSession();
void syncSlider(false); mqNarrow = window.matchMedia('(max-width: 1279px)');
if (typeof ResizeObserver !== 'undefined') { mqReduce = window.matchMedia('(prefers-reduced-motion: reduce)');
void nextTick(() => syncSlider(false));
if (typeof ResizeObserver !== 'undefined' && dockRef.value) {
resizeObserver = new ResizeObserver(onViewportResize); resizeObserver = new ResizeObserver(onViewportResize);
if (dockRef.value) resizeObserver.observe(dockRef.value); resizeObserver.observe(dockRef.value);
} }
window.addEventListener('resize', onViewportResize); window.addEventListener('resize', onViewportResize, { passive: true });
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -301,8 +299,9 @@
watch( watch(
() => [props.nav, props.isZh] as const, () => [props.nav, props.isZh] as const,
() => { async () => {
void syncSlider(true); await nextTick();
syncSlider(true);
}, },
); );
</script> </script>

View File

@ -1,13 +1,55 @@
<template> <template>
<div <div
class="official-site" ref="glassSceneEl"
class="official-site lg-scene"
:class="{ :class="{
'is-routes-open': panel === 'routes', 'is-routes-open': panel === 'routes',
'is-services-open': panel === 'services', 'is-services-open': panel === 'services',
'is-topic-open': panel === 'pricing' || panel === 'about' || panel === 'contact', 'is-topic-open': panel === 'pricing' || panel === 'about' || panel === 'contact',
'is-glass-ready': glass.ready.value,
'is-glass-fallback': glass.fallback.value,
}" }"
@click.capture="onOfficialLoginCapture" @click.capture="onOfficialLoginCapture"
> >
<!--
lg-content 会被 liquid-glass 在 resize 时改写 left/right/宽高;
波浪放在固定视口 __paint 层,避免缩窗后右侧留白。
-->
<div
ref="glassContentEl"
class="ai-shell-bg lg-content"
data-layout="overlay"
aria-hidden="true"
>
<div class="ai-shell-bg__paint">
<GradientWaves
class="ai-shell-bg__waves"
horizon-color="#023e8a"
wave-color="#0077b6"
crest-color="#caf0f8"
:speed="0.34"
:amplitude="3.3"
:wave-scale="0.72"
:wave-ratio="0.82"
:swell="42"
:turbulence="26"
:tilt="1.08"
:zoom="0.92"
:height="4.8"
:fog-depth="22"
detail="low"
:brightness="1.08"
:opacity="1.0"
:mouse-interaction="true"
:parallax-strength="0.4"
:grain="false"
:fps="30"
:render-scale="0.72"
:max-dpr="1.25"
/>
<div class="ai-shell-bg__veil" aria-hidden="true"></div>
</div>
</div>
<SiteHeader <SiteHeader
:nav="nav" :nav="nav"
:is-zh="isZh" :is-zh="isZh"
@ -34,7 +76,7 @@
:active-thread-id="activeThreadId" :active-thread-id="activeThreadId"
:routes-pending="routesPending" :routes-pending="routesPending"
@submit="onSubmit" @submit="onSubmit"
@intent="advisor.askIntent" @intent="onIntent"
@human="onNav('contact')" @human="onNav('contact')"
@wechat="openWecom" @wechat="openWecom"
@prefill="draft = $event" @prefill="draft = $event"
@ -91,7 +133,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import SiteHeader from './SiteHeader.vue'; import SiteHeader from './SiteHeader.vue';
import SiteAdvisorShell from './SiteAdvisorShell.vue'; import SiteAdvisorShell from './SiteAdvisorShell.vue';
@ -99,18 +141,25 @@
import SiteServicesPanel from './SiteServicesPanel.vue'; import SiteServicesPanel from './SiteServicesPanel.vue';
import SiteTopicPanel from './SiteTopicPanel.vue'; import SiteTopicPanel from './SiteTopicPanel.vue';
import SiteWecomModal from './SiteWecomModal.vue'; import SiteWecomModal from './SiteWecomModal.vue';
import GradientWaves from './GradientWaves.vue';
import { import {
goOfficialLoginFromHref, goOfficialLoginFromHref,
officialLoginUrl, officialLoginUrl,
warmupOfficialLogin, warmupOfficialLogin,
} from './lib/site-urls'; } from './lib/site-urls';
import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom'; import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom';
import { provideSiteLiquidGlass } from './lib/site-liquid-glass';
import { useSiteAdvisor } from './useSiteAdvisor'; import { useSiteAdvisor } from './useSiteAdvisor';
import { trackSiteEvent } from '@/utils/ga';
import './styles/vendor.css'; import './styles/vendor.css';
import './styles/icons.css'; import './styles/icons.css';
import './styles/main.css'; import './styles/main.css';
import './styles/ai-advisor.css'; import './styles/ai-advisor.css';
const glass = provideSiteLiquidGlass();
const glassSceneEl = ref<HTMLElement | null>(null);
const glassContentEl = ref<HTMLElement | null>(null);
type PanelId = 'routes' | 'services' | 'pricing' | 'about' | 'contact' | null; type PanelId = 'routes' | 'services' | 'pricing' | 'about' | 'contact' | null;
const NAV_PANELS: PanelId[] = ['routes', 'services', 'pricing', 'about', 'contact']; const NAV_PANELS: PanelId[] = ['routes', 'services', 'pricing', 'about', 'contact'];
@ -133,9 +182,17 @@
function onOfficialLoginCapture(e: MouseEvent) { function onOfficialLoginCapture(e: MouseEvent) {
const target = e.target as Element | null; const target = e.target as Element | null;
const anchor = target?.closest?.('a[href]') as HTMLAnchorElement | null; const anchor = target?.closest?.('a[href]') as HTMLAnchorElement | null;
if (!anchor || anchor.classList.contains('site-nav-login')) return; if (!anchor) return;
const href = anchor.getAttribute('href') || ''; const href = anchor.getAttribute('href') || '';
if (!href.includes('from=official-site')) return; if (!href.includes('from=official-site')) return;
trackSiteEvent('home_login_click', {
link_role: anchor.classList.contains('site-nav-login')
? 'nav_login'
: anchor.classList.contains('ai-welcome-cta-link')
? 'hero_cta'
: 'other',
});
if (anchor.classList.contains('site-nav-login')) return;
goOfficialLoginFromHref(router, href, e); goOfficialLoginFromHref(router, href, e);
} }
@ -244,14 +301,22 @@
panel.value = id; panel.value = id;
nav.value = id; nav.value = id;
syncHash(id); syncHash(id);
trackSiteEvent('home_panel_open', { panel_id: id });
} }
function openRoutes(dest?: string) { function openRoutes(dest?: string) {
const destId = dest || 'all';
advisor.clearRoutesPending(); advisor.clearRoutesPending();
routesDest.value = dest || 'all'; routesDest.value = destId;
trackSiteEvent('home_dest_click', { dest_id: destId });
openPanel('routes'); openPanel('routes');
} }
function onIntent(intentId: string) {
trackSiteEvent('home_intent_click', { intent_id: intentId });
advisor.askIntent(intentId);
}
function closePanel(id?: string) { function closePanel(id?: string) {
if (id && panel.value && panel.value !== id) return; if (id && panel.value && panel.value !== id) return;
if (!panel.value) return; if (!panel.value) return;
@ -266,6 +331,7 @@
historyOpen.value = false; historyOpen.value = false;
nav.value = 'home'; nav.value = 'home';
syncHash('home'); syncHash('home');
trackSiteEvent('home_nav', { nav_id: 'home' });
advisor.startNewChat(); advisor.startNewChat();
} }
@ -275,6 +341,7 @@
onHome(); onHome();
return; return;
} }
trackSiteEvent('home_nav', { nav_id: id });
if (NAV_PANELS.includes(id as PanelId)) { if (NAV_PANELS.includes(id as PanelId)) {
openPanel(id as Exclude<PanelId, null>); openPanel(id as Exclude<PanelId, null>);
return; return;
@ -288,6 +355,10 @@
const fromRoutes = panel.value === 'routes'; const fromRoutes = panel.value === 'routes';
closePanel(); closePanel();
draft.value = ''; draft.value = '';
trackSiteEvent('home_ask', {
intent_id: payload.intentId,
from_routes: fromRoutes,
});
advisor.ask(payload.text, { intentId: payload.intentId, fromRoutes }); advisor.ask(payload.text, { intentId: payload.intentId, fromRoutes });
} }
@ -295,11 +366,16 @@
const text = draft.value.trim(); const text = draft.value.trim();
if (!text) return; if (!text) return;
draft.value = ''; draft.value = '';
trackSiteEvent('home_ask', { intent_id: 'free_text' });
advisor.ask(text); advisor.ask(text);
} }
function onChoice(payload: { id: string; label: string; flowId: string }) { function onChoice(payload: { id: string; label: string; flowId: string }) {
draft.value = ''; draft.value = '';
trackSiteEvent('home_choice', {
choice_id: payload.id,
flow_id: payload.flowId,
});
advisor.ask(payload.label, { kind: 'choice', flowId: payload.flowId, choice: payload.id }); advisor.ask(payload.label, { kind: 'choice', flowId: payload.flowId, choice: payload.id });
} }
@ -309,6 +385,7 @@
} }
function openWecom() { function openWecom() {
trackSiteEvent('home_wecom_click', { entry: 'advisor' });
if (shouldOpenWecomDirect()) { if (shouldOpenWecomDirect()) {
window.location.href = WECOM_KF_URL; window.location.href = WECOM_KF_URL;
return; return;
@ -317,6 +394,7 @@
} }
function onTopicWechat() { function onTopicWechat() {
trackSiteEvent('home_wecom_click', { entry: 'topic' });
if (shouldOpenWecomDirect()) { if (shouldOpenWecomDirect()) {
window.location.href = WECOM_KF_URL; window.location.href = WECOM_KF_URL;
return; return;
@ -328,6 +406,7 @@
const next = lang.value === 'zh' ? 'en' : 'zh'; const next = lang.value === 'zh' ? 'en' : 'zh';
lang.value = next; lang.value = next;
localStorage.setItem('ide-language', next); localStorage.setItem('ide-language', next);
trackSiteEvent('home_lang_toggle', { lang: next });
} }
function onKeydown(e: KeyboardEvent) { function onKeydown(e: KeyboardEvent) {
@ -378,9 +457,15 @@
if (hash === '9610' || hash === 'services') openPanel('services'); if (hash === '9610' || hash === 'services') openPanel('services');
else if (NAV_PANELS.includes(hash as PanelId)) openPanel(hash as Exclude<PanelId, null>); else if (NAV_PANELS.includes(hash as PanelId)) openPanel(hash as Exclude<PanelId, null>);
else if (hash) nav.value = hash; else if (hash) nav.value = hash;
void nextTick(() => {
if (glassSceneEl.value && glassContentEl.value) {
glass.init(glassSceneEl.value, glassContentEl.value);
}
});
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
glass.dispose();
window.removeEventListener('keydown', onKeydown); window.removeEventListener('keydown', onKeydown);
window.removeEventListener('resize', syncHeaderOffset); window.removeEventListener('resize', syncHeaderOffset);
window.removeEventListener('resize', syncVisualViewport); window.removeEventListener('resize', syncVisualViewport);
@ -395,7 +480,9 @@
<style scoped> <style scoped>
.official-site { .official-site {
position: relative;
min-height: 100vh; min-height: 100vh;
isolation: isolate;
} }
</style> </style>
@ -419,7 +506,7 @@
.site-fade-enter-active, .site-fade-enter-active,
.site-fade-leave-active { .site-fade-leave-active {
transition: opacity 0.32s cubic-bezier(0.22, 1, 0.36, 1); transition: opacity 0.22s ease;
} }
.site-fade-enter-from, .site-fade-enter-from,
@ -430,9 +517,8 @@
.site-sheet-enter-active, .site-sheet-enter-active,
.site-sheet-leave-active { .site-sheet-leave-active {
transition: transition:
opacity 0.34s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.24s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.4s cubic-bezier(0.22, 1, 0.36, 1), transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
filter 0.34s cubic-bezier(0.22, 1, 0.36, 1);
} }
.site-sheet-leave-active { .site-sheet-leave-active {
@ -441,25 +527,21 @@
.site-sheet-enter-from { .site-sheet-enter-from {
opacity: 0; opacity: 0;
filter: blur(2px); transform: translateX(-50%) translateY(-8px);
transform: translateX(-50%) translateY(-10px);
} }
.site-sheet-leave-to { .site-sheet-leave-to {
opacity: 0; opacity: 0;
filter: blur(1px); transform: translateX(-50%) translateY(-4px);
transform: translateX(-50%) translateY(-6px);
} }
@media (max-width: 780px) { @media (max-width: 780px) {
.site-sheet-enter-from { .site-sheet-enter-from {
filter: none; transform: translateY(12px);
transform: translateY(16px);
} }
.site-sheet-leave-to { .site-sheet-leave-to {
filter: none; transform: translateY(8px);
transform: translateY(10px);
} }
} }

View File

@ -0,0 +1,231 @@
import { inject, provide, ref, type InjectionKey, type Ref } from 'vue';
import {
attachSelectionLens,
createGlassScene,
type GlassSceneController,
type SurfaceOptions,
} from '@glass-sdk/liquid-glass/dom';
import '@glass-sdk/liquid-glass/styles.css';
export type SiteGlassSurfaceOpts = SurfaceOptions;
type QueuedJob =
| { kind: 'surface'; el: HTMLElement; opts?: SiteGlassSurfaceOpts; setRemove: (fn: () => void) => void }
| { kind: 'indicator'; el: HTMLElement; opts?: SiteGlassSurfaceOpts; setRemove: (fn: () => void) => void };
export type SiteLiquidGlassApi = {
ready: Ref<boolean>;
fallback: Ref<boolean>;
init: (root: HTMLElement, content: HTMLElement) => void;
registerSurface: (el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) => () => void;
registerTabIndicator: (el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) => () => void;
dispose: () => void;
};
const SITE_LIQUID_GLASS_KEY: InjectionKey<SiteLiquidGlassApi> = Symbol('site-liquid-glass');
/** 履约 / 权益门卡:低折射 + 关闭 fluid,减轻矮卡左右竖边光学校弯 */
export const SITE_GLASS_PANEL_OPTS: SiteGlassSurfaceOpts = {
material: 'clear',
appearance: 'light',
radius: 12,
refraction: 20,
fluid: false,
interactive: false,
};
export const SITE_GLASS_TABS_OPTS: SiteGlassSurfaceOpts = {
material: 'clear',
appearance: 'light',
radius: 'capsule',
refraction: 56,
interactive: true,
};
export const SITE_GLASS_TAB_INDICATOR_OPTS: SiteGlassSurfaceOpts = {
material: 'clear',
appearance: 'light',
radius: 'capsule',
refraction: 40,
interactive: false,
};
/** Welcome input — frosted glass, no tint */
export const SITE_GLASS_COMPOSER_OPTS: SiteGlassSurfaceOpts = {
material: 'regular',
appearance: 'light',
radius: 12,
refraction: 32,
fluid: true,
interactive: false,
};
export function provideSiteLiquidGlass(): SiteLiquidGlassApi {
const ready = ref(false);
const fallback = ref(false);
let scene: GlassSceneController | null = null;
let removers: Array<() => void> = [];
let queue: QueuedJob[] = [];
let readyObserver: MutationObserver | null = null;
let fallbackTimer: number | null = null;
const trackedSurfaces = new Set<HTMLElement>();
function clearFallbackTimer() {
if (fallbackTimer != null) {
window.clearTimeout(fallbackTimer);
fallbackTimer = null;
}
}
function isClipReady(el: HTMLElement) {
return !!el.getAttribute('style')?.includes('clip-path');
}
function syncReady() {
if ([...trackedSurfaces].some(isClipReady)) {
ready.value = true;
fallback.value = false;
clearFallbackTimer();
return true;
}
return false;
}
function observeSurface(el: HTMLElement) {
trackedSurfaces.add(el);
if (!readyObserver) {
readyObserver = new MutationObserver(() => {
syncReady();
});
}
readyObserver.observe(el, { attributes: true, attributeFilter: ['style'] });
}
function bindSurface(el: HTMLElement, opts?: SiteGlassSurfaceOpts) {
if (!scene) return () => undefined;
const remove = scene.addSurface(el, opts);
observeSurface(el);
syncReady();
return () => {
trackedSurfaces.delete(el);
remove();
};
}
function bindIndicator(el: HTMLElement, opts?: SiteGlassSurfaceOpts) {
if (!scene) return () => undefined;
const removeSurface = bindSurface(el, opts ?? SITE_GLASS_TAB_INDICATOR_OPTS);
const removeAnimator = scene.addAnimator(attachSelectionLens(el, () => 'full'));
return () => {
removeAnimator();
removeSurface();
};
}
function enqueue(
kind: 'surface' | 'indicator',
el: HTMLElement,
opts?: SiteGlassSurfaceOpts,
) {
let removeFn = () => undefined;
let flushed = false;
const job: QueuedJob = {
kind,
el,
opts,
setRemove: (fn) => {
flushed = true;
removeFn = fn;
},
};
queue.push(job);
return () => {
if (!flushed) {
queue = queue.filter((item) => item !== job);
return;
}
removeFn();
};
}
function registerSurface(el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) {
if (!el) return () => undefined;
if (scene) {
const remove = bindSurface(el, opts);
removers.push(remove);
return remove;
}
return enqueue('surface', el, opts);
}
function registerTabIndicator(el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) {
if (!el) return () => undefined;
if (scene) {
const remove = bindIndicator(el, opts);
removers.push(remove);
return remove;
}
return enqueue('indicator', el, opts);
}
function init(root: HTMLElement, content: HTMLElement) {
if (scene) return;
fallbackTimer = window.setTimeout(() => {
if (!ready.value) fallback.value = true;
}, 2200);
scene = createGlassScene(root, {
maxSurfaces: 16,
onDiagnostic: (diagnostic) => {
if (diagnostic.error) {
fallback.value = true;
ready.value = false;
} else if (diagnostic.maps > 0) {
syncReady();
}
},
});
scene.setContent(content);
for (const job of queue) {
const remove =
job.kind === 'indicator' ? bindIndicator(job.el, job.opts) : bindSurface(job.el, job.opts);
job.setRemove(remove);
removers.push(remove);
}
queue = [];
syncReady();
}
function dispose() {
clearFallbackTimer();
readyObserver?.disconnect();
readyObserver = null;
removers.forEach((remove) => remove());
removers = [];
queue = [];
trackedSurfaces.clear();
scene?.dispose();
scene = null;
ready.value = false;
fallback.value = false;
}
const api: SiteLiquidGlassApi = {
ready,
fallback,
init,
registerSurface,
registerTabIndicator,
dispose,
};
provide(SITE_LIQUID_GLASS_KEY, api);
return api;
}
export function useSiteLiquidGlass(): SiteLiquidGlassApi | null {
return inject(SITE_LIQUID_GLASS_KEY, null);
}

View File

@ -1,8 +1,8 @@
export const WELCOME_COPY = { export const WELCOME_COPY = {
en: { en: {
title: 'IDE. Your cross-border logistics advisor', title: 'IDE · One-stop cross-border logistics advisor',
titleShort: 'IDE. Logistics advisor', titleShort: 'IDE · Logistics advisor',
titleRest: ' Your cross-border logistics advisor', titleRest: 'One-stop cross-border logistics advisor',
titleRestShort: 'Logistics advisor', titleRestShort: 'Logistics advisor',
placeholder: 'e.g. 5 kg general cargo to the US, how many days?', placeholder: 'e.g. 5 kg general cargo to the US, how many days?',
sendLabel: 'Ask', sendLabel: 'Ask',
@ -26,9 +26,9 @@ export const WELCOME_COPY = {
fallback: 'Quotes are in the customer system after you register. You can also talk to an advisor.', fallback: 'Quotes are in the customer system after you register. You can also talk to an advisor.',
}, },
zh: { zh: {
title: 'IDE.您身边的跨境物流顾问', title: 'IDE · 一站式跨境物流顾问',
titleShort: 'IDE.跨境物流顾问', titleShort: 'IDE · 跨境物流顾问',
titleRest: '您身边的跨境物流顾问', titleRest: '一站式跨境物流顾问',
titleRestShort: '跨境物流顾问', titleRestShort: '跨境物流顾问',
placeholder: '例如:发美国普货,5kg,要几天', placeholder: '例如:发美国普货,5kg,要几天',
sendLabel: '提问', sendLabel: '提问',

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff