20261104 #17
@ -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_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
|
||||
|
||||
# 测试站可选:填同一 GA4 或单独测试媒体资源;空则不加载
|
||||
# VITE_GA_MEASUREMENT_ID=
|
||||
|
||||
@ -11,3 +11,7 @@ VITE_API_ASSET_ORIGIN=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
|
||||
|
||||
# Google Analytics 4(官网 https://ideship.com)
|
||||
# 地域/设备/来源自动有;首页点击见事件 home_*
|
||||
VITE_GA_MEASUREMENT_ID=G-1XWZ7LL1PW
|
||||
|
||||
@ -49,6 +49,7 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.39.17",
|
||||
"@fontsource/zcool-qingke-huangyou": "^5.3.0",
|
||||
"@glass-sdk/liquid-glass": "~0.0.2",
|
||||
"@handsontable/vue3": "^16.2.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@vueuse/core": "^9.3.0",
|
||||
|
||||
275
pnpm-lock.yaml
generated
275
pnpm-lock.yaml
generated
@ -39,6 +39,9 @@ importers:
|
||||
'@fontsource/zcool-qingke-huangyou':
|
||||
specifier: ^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':
|
||||
specifier: ^16.2.0
|
||||
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==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@8.1.0':
|
||||
resolution: {integrity: sha512-MQye/pU7gzXerMzlhScFNH1u4Je6rR8cqJWSgAfD+rIwbbtwG8tVDegoI7sWfcXkfVRW09G7vk2vjxGIuKFfwg==}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -455,6 +461,33 @@ packages:
|
||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||
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':
|
||||
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
|
||||
|
||||
@ -929,15 +962,36 @@ packages:
|
||||
'@floating-ui/core@1.7.4':
|
||||
resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
|
||||
|
||||
'@floating-ui/core@1.8.0':
|
||||
resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
|
||||
|
||||
'@floating-ui/dom@1.7.5':
|
||||
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':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@floating-ui/utils@0.2.12':
|
||||
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
|
||||
|
||||
'@fontsource/zcool-qingke-huangyou@5.3.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-1VN6N38t5/DcjJ7y7XUYrDx1LuzvvzlrFdBdMG90Qo1xc8+LXHqbWbsTEm5Ec5gXTEbDEO53vUT35R+2COmOyg==}
|
||||
|
||||
@ -1027,12 +1081,24 @@ packages:
|
||||
'@lezer/xml@1.0.6':
|
||||
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':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@mermaid-js/parser@1.2.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==}
|
||||
engines: {node: '>=4'}
|
||||
@ -1841,6 +1907,26 @@ packages:
|
||||
'@upsetjs/venn.js@2.0.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-JgDhxstQlwnHBvZ1BSnU5mbmyQ14/t5JhREc6YH5kWyu2QdAAOsLF6xgHoIWarj8tddaiwFrNzLbWJPudpXKYA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@ -1947,6 +2033,9 @@ packages:
|
||||
'@vueuse/shared@9.13.0':
|
||||
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
|
||||
|
||||
'@webgpu/types@0.1.74':
|
||||
resolution: {integrity: sha512-lgiI4hbuLcI9unnm2cL/tvCaQU45dp0xcLWJh5uB/9MBGvIA4F8XIk7nSiBeg+K4xWGYwgZKn80LmERI5CxoTA==}
|
||||
|
||||
'@wtto00/html2canvas@1.4.3':
|
||||
resolution: {integrity: sha512-jwsb+xL8N+gjrSNABSaFdxmWtE4c7RNFjP20lo1G7gs63Qqo1phhxVBTzxc/apDVh6LgXsU2l5bwKtXd9uz65w==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@ -5469,6 +5558,10 @@ packages:
|
||||
resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pixelmatch@7.2.0:
|
||||
resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==}
|
||||
hasBin: true
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
@ -5479,6 +5572,10 @@ packages:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
pngjs@7.0.0:
|
||||
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
|
||||
engines: {node: '>=14.19.0'}
|
||||
|
||||
pngquant-bin@6.0.1:
|
||||
resolution: {integrity: sha512-Q3PUyolfktf+hYio6wsg3SanQzEU/v8aICg/WpzxXcuCMRb7H2Q81okfpcEztbMvw25ILjd3a87doj2N9kvbpQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -5679,6 +5776,15 @@ packages:
|
||||
raf@3.4.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -5791,6 +5897,9 @@ packages:
|
||||
require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
reselect@5.3.0:
|
||||
resolution: {integrity: sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==}
|
||||
|
||||
reserved-words@0.1.2:
|
||||
resolution: {integrity: sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==}
|
||||
|
||||
@ -6044,6 +6153,9 @@ packages:
|
||||
sax@1.4.1:
|
||||
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
||||
|
||||
scheduler@0.28.0:
|
||||
resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
|
||||
|
||||
screenfull@5.2.0:
|
||||
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -6919,6 +7031,11 @@ packages:
|
||||
resolution: {integrity: sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -6966,6 +7083,15 @@ packages:
|
||||
resolution: {integrity: sha512-KoJRp8V2EsmeW2pg33CVEOEfGDTmcVLL0wk0fOQJjgXfAunciPylOueKovuzlbWSrTQap1UzgNn8xr5zbBsnPA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
|
||||
peerDependencies:
|
||||
@ -7152,12 +7278,18 @@ packages:
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
webgpu@0.4.0:
|
||||
resolution: {integrity: sha512-F5pimn3Aoi0zWjuRdiVs5TnrUwSzD2lESBohsIUsqyitWkGRQlXU2fhV6ycXlQTa1bvAf3sjqiUpBEpmSQ5ptA==}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
wgpu-matrix@3.4.2:
|
||||
resolution: {integrity: sha512-IfZFbG7olEYBD76VCZy2TzqM57GKy3LT++AZh37tMHPPByO6R2h61zXGBZFNPi2IzxNivNi8y5dvze5PjaczjA==}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
@ -7325,6 +7457,9 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zod@4.6.7:
|
||||
resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==}
|
||||
|
||||
zrender@5.6.1:
|
||||
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
|
||||
|
||||
@ -7517,6 +7652,8 @@ snapshots:
|
||||
|
||||
'@babel/runtime@7.28.6': {}
|
||||
|
||||
'@babel/runtime@8.1.0': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
@ -7540,6 +7677,25 @@ snapshots:
|
||||
'@babel/helper-string-parser': 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': {}
|
||||
|
||||
'@bufbuild/protobuf@2.10.1': {}
|
||||
@ -7955,15 +8111,46 @@ snapshots:
|
||||
dependencies:
|
||||
'@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':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.4
|
||||
'@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.12': {}
|
||||
|
||||
'@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/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/lr': 1.4.8
|
||||
|
||||
'@lisse/core@0.7.3': {}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@mermaid-js/parser@1.2.0':
|
||||
dependencies:
|
||||
'@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':
|
||||
dependencies:
|
||||
call-me-maybe: 1.0.2
|
||||
@ -8907,6 +9105,33 @@ snapshots:
|
||||
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))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.0
|
||||
@ -9090,6 +9315,8 @@ snapshots:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@webgpu/types@0.1.74': {}
|
||||
|
||||
'@wtto00/html2canvas@1.4.3':
|
||||
dependencies:
|
||||
css-line-break: 2.1.0
|
||||
@ -12931,6 +13158,10 @@ snapshots:
|
||||
|
||||
pinkie@2.0.4: {}
|
||||
|
||||
pixelmatch@7.2.0:
|
||||
dependencies:
|
||||
pngjs: 7.0.0
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
@ -12945,6 +13176,8 @@ snapshots:
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
pngjs@7.0.0: {}
|
||||
|
||||
pngquant-bin@6.0.1:
|
||||
dependencies:
|
||||
bin-build: 3.0.0
|
||||
@ -13129,6 +13362,13 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
find-up: 1.1.2
|
||||
@ -13293,6 +13533,8 @@ snapshots:
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
reselect@5.3.0: {}
|
||||
|
||||
reserved-words@0.1.2: {}
|
||||
|
||||
resize-detector@0.3.0: {}
|
||||
@ -13535,6 +13777,8 @@ snapshots:
|
||||
sax@1.4.1:
|
||||
optional: true
|
||||
|
||||
scheduler@0.28.0: {}
|
||||
|
||||
screenfull@5.2.0: {}
|
||||
|
||||
scroll-into-view-if-needed@2.2.31:
|
||||
@ -14610,6 +14854,10 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
@ -14680,6 +14928,22 @@ snapshots:
|
||||
- typescript
|
||||
- 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)):
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
@ -14893,10 +15157,19 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
wgpu-matrix@3.4.2: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
@ -15076,6 +15349,8 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod@4.6.7: {}
|
||||
|
||||
zrender@5.6.1:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
|
||||
@ -3,8 +3,10 @@
|
||||
*/
|
||||
import { axiosInstance } from '@/api/rbac'
|
||||
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
|
||||
|
||||
@ -154,7 +156,7 @@ export const CustomerService = {
|
||||
balance: data.accountBalance ?? data.balance ?? 0,
|
||||
totalQuota: data.totalQuota ?? 0,
|
||||
usedQuota: data.usedQuota ?? 0,
|
||||
status: data.status ?? 1,
|
||||
status: data.status ?? CUSTOMER_STATUS.normal,
|
||||
remark: data.remark,
|
||||
salesman: data.salesman ?? data.salesPerson,
|
||||
customerService: data.customerService,
|
||||
@ -186,7 +188,8 @@ export const CustomerService = {
|
||||
balance: data.accountBalance ?? data.balance,
|
||||
totalQuota: data.totalQuota,
|
||||
usedQuota: data.usedQuota,
|
||||
status: data.status ?? 1,
|
||||
// 后端:1=Disabled,2=Normal;勿默认 1,否则公开注册账号无法登录
|
||||
status: data.status ?? CUSTOMER_STATUS.normal,
|
||||
role: data.role,
|
||||
remark: data.remark,
|
||||
salesman: data.salesman ?? data.salesPerson,
|
||||
|
||||
@ -167,8 +167,37 @@ export async function sendBatchEmailOne(body: BatchEmailSendOneBody) {
|
||||
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 */
|
||||
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 page = unwrapPageResult(res.data)
|
||||
return {
|
||||
@ -178,3 +207,15 @@ export async function listBatchEmailRecords(params?: { page?: number; pageSize?:
|
||||
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
4
src/env.d.ts
vendored
@ -53,4 +53,8 @@ interface ImportMetaEnv {
|
||||
readonly VITE_API_ORIGIN?: string;
|
||||
/** 静态资源 origin;本地默认测试 admin */
|
||||
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;
|
||||
}
|
||||
|
||||
@ -4399,11 +4399,24 @@ export default {
|
||||
'system.batchEmail.errLoadTemplates': 'Failed to load templates',
|
||||
'system.batchEmail.errLoadRecords': 'Failed to load 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.subject': 'Subject',
|
||||
'system.batchEmail.record.content': 'Content',
|
||||
'system.batchEmail.record.sendTime': 'Sent at',
|
||||
'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.warnEmptyName': 'Template name is required',
|
||||
'system.batchEmail.confirmOverwriteTitle': 'Overwrite?',
|
||||
@ -4451,6 +4464,33 @@ export default {
|
||||
'system.batchEmail.logSendFail': 'failed - {msg}',
|
||||
'system.batchEmail.logSendDone': 'Done: {total} total, {successCount} ok, {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.search.placeholder': 'Search title or content',
|
||||
|
||||
@ -4423,11 +4423,24 @@ export default {
|
||||
'system.batchEmail.errLoadTemplates': '加载模板失败',
|
||||
'system.batchEmail.errLoadRecords': '加载邮件记录失败',
|
||||
'system.batchEmail.recordList': '邮件记录',
|
||||
'system.batchEmail.recordSearch': '查询',
|
||||
'system.batchEmail.recordSearchPlaceholder': '邮箱 / 主题 / 内容 / 发送人',
|
||||
'system.batchEmail.record.toEmail': '邮箱',
|
||||
'system.batchEmail.record.subject': '主题',
|
||||
'system.batchEmail.record.content': '内容',
|
||||
'system.batchEmail.record.sendTime': '发送时间',
|
||||
'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.warnEmptyName': '模板名称不能为空',
|
||||
'system.batchEmail.confirmOverwriteTitle': '确认覆盖',
|
||||
@ -4475,6 +4488,33 @@ export default {
|
||||
'system.batchEmail.logSendFail': '收件人, 发送失败 - {msg}',
|
||||
'system.batchEmail.logSendDone': '结束发送: 共发送{total}封邮箱, 成功{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': '搜索标题或正文',
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
import { initAuthKeepAlive } from '@/utils/auth-keepalive';
|
||||
import { installWeChatH5HistoryGuard } from '@/utils/device';
|
||||
import { installChunkReloadHandlers, reloadOnChunkLoadError } from '@/utils/chunkReload';
|
||||
import { initGa } from '@/utils/ga';
|
||||
import { setAuthToken } from './api/rbac';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import { applyRbacUserToUserStore } from '@/utils/enter-system';
|
||||
@ -138,6 +139,8 @@ async function bootstrap() {
|
||||
;(window as any).__APP_MOUNTED__ = true;
|
||||
installWeChatH5HistoryGuard(router);
|
||||
initAuthKeepAlive();
|
||||
// GA4:有 VITE_GA_MEASUREMENT_ID 才加载;地域/设备等自动采集
|
||||
initGa();
|
||||
|
||||
// 登录页勿预热 VXE/Element:体积大且会注入全局 CSS,和首屏抢带宽
|
||||
if (isHomePath(entryPath) && !isH5LikePath(entryPath) && !light) {
|
||||
|
||||
@ -10,7 +10,12 @@ function setupPageGuard(router: Router) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@ -318,7 +318,11 @@ export const useRbacUserStore = defineStore('rbacUser', () => {
|
||||
enableKeepAlive()
|
||||
await hydrateUserAfterLogin(result)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
80
src/utils/ga.ts
Normal file
80
src/utils/ga.ts
Normal 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 });
|
||||
}
|
||||
@ -350,11 +350,19 @@ async function handleSubmit() {
|
||||
loading.value = true;
|
||||
errorBox.value = '';
|
||||
|
||||
const success = await rbacUserStore.login(
|
||||
let success = false;
|
||||
try {
|
||||
success = await rbacUserStore.login(
|
||||
loginForm.account,
|
||||
loginForm.password,
|
||||
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) {
|
||||
const userInfoData = rbacUserStore.userInfo;
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
IconCopy,
|
||||
IconSettings,
|
||||
} from '@arco-design/web-vue/es/icon';
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue';
|
||||
import ExcelFileIcon from '@/components/icons/ExcelFileIcon.vue';
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue';
|
||||
import CustomTemplateModal from './components/CustomTemplateModal.vue';
|
||||
import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vue';
|
||||
import FormLabelWithPreview from '@/views/channel/salesProducts/components/FormLabelWithPreview.vue';
|
||||
@ -5777,6 +5777,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -5787,6 +5788,22 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
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 {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
@ -325,6 +325,8 @@ void main() {
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.display = 'block';
|
||||
canvas.style.pointerEvents = 'none';
|
||||
canvas.style.opacity = '0';
|
||||
canvas.style.transition = 'opacity 0.55s ease';
|
||||
container.appendChild(canvas);
|
||||
|
||||
const geometry = new Triangle(gl);
|
||||
@ -367,6 +369,9 @@ void main() {
|
||||
const w = Math.max(1, Math.floor(rect.width * scale));
|
||||
const h = Math.max(1, Math.floor(rect.height * scale));
|
||||
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;
|
||||
res[0] = gl.drawingBufferWidth;
|
||||
res[1] = gl.drawingBufferHeight;
|
||||
@ -454,6 +459,11 @@ void main() {
|
||||
|
||||
tryStart();
|
||||
syncUniforms();
|
||||
// 先画一帧再淡入,盖在静态海天底上,避免灰/白闪
|
||||
requestAnimationFrame(() => {
|
||||
paint(performance.now());
|
||||
canvas.style.opacity = '1';
|
||||
});
|
||||
|
||||
teardown = () => {
|
||||
tryStop();
|
||||
@ -491,5 +501,13 @@ void main() {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.gradient-waves :deep(canvas) {
|
||||
transition: none !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,33 +1,8 @@
|
||||
<template>
|
||||
<div id="ai-advisor-shell" :class="{ 'ai-history-open': historyOpen }">
|
||||
<div class="ai-shell-bg" aria-hidden="true">
|
||||
<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
|
||||
id="ai-advisor-shell"
|
||||
:class="{ 'ai-history-open': historyOpen }"
|
||||
>
|
||||
<div class="ai-advisor-layout">
|
||||
<div class="ai-advisor-app">
|
||||
<div v-if="chatting" class="ai-stage-tools">
|
||||
@ -57,7 +32,7 @@
|
||||
<div class="ai-welcome__intro">
|
||||
<h2 class="ai-welcome-title" :aria-label="shortTitle ? copy.titleShort : copy.title">
|
||||
<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>
|
||||
<TextType
|
||||
v-if="titleAnimate"
|
||||
@ -65,7 +40,8 @@
|
||||
as="span"
|
||||
class-name="ai-welcome-title__type"
|
||||
:text="shortTitle ? copy.titleRestShort : copy.titleRest"
|
||||
:typing-speed="36"
|
||||
:typing-speed="48"
|
||||
:initial-delay="280"
|
||||
:pause-duration="900"
|
||||
:show-cursor="false"
|
||||
:loop="false"
|
||||
@ -128,7 +104,11 @@
|
||||
v-for="door in primaryDoors"
|
||||
:key="door.id"
|
||||
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"
|
||||
@click="onHomeDoor(door)"
|
||||
>
|
||||
@ -149,25 +129,25 @@
|
||||
v-for="door in growthDoors"
|
||||
:key="door.id"
|
||||
type="button"
|
||||
class="ai-intent-door"
|
||||
class="ai-intent-door lg-surface"
|
||||
:class="{
|
||||
'is-agent-tall': door.id === 'agent',
|
||||
'is-growth-wide': door.id === 'newcomer',
|
||||
'is-glass-ready': glassReady,
|
||||
'is-glass-fallback': glassFallback,
|
||||
}"
|
||||
:disabled="busy"
|
||||
@click="onHomeDoor(door)"
|
||||
>
|
||||
<span class="ai-intent-door__head">
|
||||
<span class="ai-intent-door__icon">
|
||||
<SiteIllusIcon :name="door.icon" />
|
||||
</span>
|
||||
<span class="ai-intent-door__copy">
|
||||
<span class="ai-intent-door__title">{{
|
||||
isZh ? door.zh.title : door.en.title
|
||||
}}</span>
|
||||
</span>
|
||||
<span class="ai-intent-door__hint">{{
|
||||
isZh ? door.zh.hint : door.en.hint
|
||||
}}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -287,7 +267,7 @@
|
||||
</template>
|
||||
|
||||
<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 SitePartnerMarquee from './SitePartnerMarquee.vue';
|
||||
import SiteMessageCard from './SiteMessageCard.vue';
|
||||
@ -295,8 +275,7 @@
|
||||
import TextType from './TextType.vue';
|
||||
import { HOME_DOORS, HERO_STATS, SITE_FOLLOWUP_INTENTS, WELCOME_COPY } from './lib/site-welcome';
|
||||
import { ROUTE_DEST_PRESETS } from './lib/portal-catalog';
|
||||
|
||||
const GradientWaves = defineAsyncComponent(() => import('./GradientWaves.vue'));
|
||||
import { SITE_GLASS_PANEL_OPTS, useSiteLiquidGlass } from './lib/site-liquid-glass';
|
||||
|
||||
export type SiteChatMessage = {
|
||||
role: 'user' | 'assistant';
|
||||
@ -342,11 +321,35 @@
|
||||
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 homeDests = ROUTE_DEST_PRESETS.filter((item) => item.id !== 'all');
|
||||
const primaryDoors = HOME_DOORS.filter((d) => d.primary);
|
||||
const growthDoors = HOME_DOORS.filter((d) => !d.primary);
|
||||
// 首屏即按视口决定短标题,避免挂载后再切导致 TextType 卡住只剩「IDE.」
|
||||
// 首屏即按视口决定短标题,避免挂载后再切导致 TextType 卡住只剩品牌前缀
|
||||
const shortTitle = ref(
|
||||
typeof window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches,
|
||||
);
|
||||
@ -397,16 +400,30 @@
|
||||
titleAnimate.value = !mqReduce?.matches;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.chatting,
|
||||
async (chatting) => {
|
||||
if (chatting) {
|
||||
teardownLiquidGlass();
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
void setupLiquidGlass();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
mqNarrow = window.matchMedia('(max-width: 640px)');
|
||||
mqReduce = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
syncMediaPrefs();
|
||||
mqNarrow.addEventListener('change', syncMediaPrefs);
|
||||
mqReduce.addEventListener('change', syncMediaPrefs);
|
||||
void setupLiquidGlass();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mqNarrow?.removeEventListener('change', syncMediaPrefs);
|
||||
mqReduce?.removeEventListener('change', syncMediaPrefs);
|
||||
teardownLiquidGlass();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -29,12 +29,7 @@
|
||||
<ul>
|
||||
<li ref="dockRef" class="site-nav-dock">
|
||||
<span class="site-nav-dock__glass" aria-hidden="true"></span>
|
||||
<span
|
||||
class="site-nav-dock__slider"
|
||||
:class="{ 'is-ready': sliderReady }"
|
||||
:style="sliderStyle"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span ref="sliderRef" class="site-nav-dock__slider" aria-hidden="true"></span>
|
||||
<a
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
@ -210,61 +205,61 @@
|
||||
];
|
||||
|
||||
const dockRef = ref<HTMLElement | null>(null);
|
||||
const sliderReady = ref(false);
|
||||
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 sliderRef = ref<HTMLElement | null>(null);
|
||||
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.w = w;
|
||||
sliderStyle.value = {
|
||||
left: `${x}px`,
|
||||
width: `${w}px`,
|
||||
opacity,
|
||||
};
|
||||
sliderPos.visible = visible;
|
||||
|
||||
if (!sliderReady && visible) {
|
||||
requestAnimationFrame(() => {
|
||||
sliderReady = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateSlider(animate: boolean) {
|
||||
const dock = dockRef.value;
|
||||
const active = dock?.querySelector<HTMLElement>('.site-nav-dock__item.is-active');
|
||||
if (!dock || !active || window.matchMedia('(max-width: 1279px)').matches) {
|
||||
sliderStyle.value = { ...sliderStyle.value, opacity: '0' };
|
||||
if (!dock || !active || mqNarrow?.matches) {
|
||||
paintSlider(sliderPos.x, sliderPos.w, false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const x = active.offsetLeft + SLIDER_INSET;
|
||||
const w = Math.max(active.offsetWidth - SLIDER_INSET * 2, 0);
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
// 首帧或关动画:先贴到位再开 transition,避免初次跳动
|
||||
if (!animate || !sliderReady.value || sliderPos.w <= 0 || reduceMotion) {
|
||||
sliderReady.value = false;
|
||||
applySlider(x, w);
|
||||
requestAnimationFrame(() => {
|
||||
sliderReady.value = true;
|
||||
});
|
||||
if (
|
||||
animate &&
|
||||
Math.abs(sliderPos.x - x) < 0.5 &&
|
||||
Math.abs(sliderPos.w - w) < 0.5 &&
|
||||
sliderPos.visible
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(sliderPos.x - x) < 0.5 && Math.abs(sliderPos.w - w) < 0.5) {
|
||||
applySlider(x, w);
|
||||
return;
|
||||
paintSlider(x, w, true, animate);
|
||||
}
|
||||
|
||||
// 单段滑移:left/width 一次到位,交给 CSS 缓动
|
||||
applySlider(x, w);
|
||||
}
|
||||
|
||||
async function syncSlider(animate: boolean) {
|
||||
await nextTick();
|
||||
function syncSlider(animate: boolean) {
|
||||
if (sliderRaf) cancelAnimationFrame(sliderRaf);
|
||||
sliderRaf = requestAnimationFrame(() => {
|
||||
sliderRaf = 0;
|
||||
@ -273,17 +268,20 @@
|
||||
}
|
||||
|
||||
function onViewportResize() {
|
||||
updateSlider(false);
|
||||
sliderReady = false;
|
||||
syncSlider(false);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void hydrateSession();
|
||||
void syncSlider(false);
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
mqNarrow = window.matchMedia('(max-width: 1279px)');
|
||||
mqReduce = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
void nextTick(() => syncSlider(false));
|
||||
if (typeof ResizeObserver !== 'undefined' && dockRef.value) {
|
||||
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(() => {
|
||||
@ -301,8 +299,9 @@
|
||||
|
||||
watch(
|
||||
() => [props.nav, props.isZh] as const,
|
||||
() => {
|
||||
void syncSlider(true);
|
||||
async () => {
|
||||
await nextTick();
|
||||
syncSlider(true);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
@ -1,13 +1,55 @@
|
||||
<template>
|
||||
<div
|
||||
class="official-site"
|
||||
ref="glassSceneEl"
|
||||
class="official-site lg-scene"
|
||||
:class="{
|
||||
'is-routes-open': panel === 'routes',
|
||||
'is-services-open': panel === 'services',
|
||||
'is-topic-open': panel === 'pricing' || panel === 'about' || panel === 'contact',
|
||||
'is-glass-ready': glass.ready.value,
|
||||
'is-glass-fallback': glass.fallback.value,
|
||||
}"
|
||||
@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
|
||||
:nav="nav"
|
||||
:is-zh="isZh"
|
||||
@ -34,7 +76,7 @@
|
||||
:active-thread-id="activeThreadId"
|
||||
:routes-pending="routesPending"
|
||||
@submit="onSubmit"
|
||||
@intent="advisor.askIntent"
|
||||
@intent="onIntent"
|
||||
@human="onNav('contact')"
|
||||
@wechat="openWecom"
|
||||
@prefill="draft = $event"
|
||||
@ -91,7 +133,7 @@
|
||||
</template>
|
||||
|
||||
<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 SiteHeader from './SiteHeader.vue';
|
||||
import SiteAdvisorShell from './SiteAdvisorShell.vue';
|
||||
@ -99,18 +141,25 @@
|
||||
import SiteServicesPanel from './SiteServicesPanel.vue';
|
||||
import SiteTopicPanel from './SiteTopicPanel.vue';
|
||||
import SiteWecomModal from './SiteWecomModal.vue';
|
||||
import GradientWaves from './GradientWaves.vue';
|
||||
import {
|
||||
goOfficialLoginFromHref,
|
||||
officialLoginUrl,
|
||||
warmupOfficialLogin,
|
||||
} from './lib/site-urls';
|
||||
import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom';
|
||||
import { provideSiteLiquidGlass } from './lib/site-liquid-glass';
|
||||
import { useSiteAdvisor } from './useSiteAdvisor';
|
||||
import { trackSiteEvent } from '@/utils/ga';
|
||||
import './styles/vendor.css';
|
||||
import './styles/icons.css';
|
||||
import './styles/main.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;
|
||||
const NAV_PANELS: PanelId[] = ['routes', 'services', 'pricing', 'about', 'contact'];
|
||||
|
||||
@ -133,9 +182,17 @@
|
||||
function onOfficialLoginCapture(e: MouseEvent) {
|
||||
const target = e.target as Element | 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') || '';
|
||||
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);
|
||||
}
|
||||
|
||||
@ -244,14 +301,22 @@
|
||||
panel.value = id;
|
||||
nav.value = id;
|
||||
syncHash(id);
|
||||
trackSiteEvent('home_panel_open', { panel_id: id });
|
||||
}
|
||||
|
||||
function openRoutes(dest?: string) {
|
||||
const destId = dest || 'all';
|
||||
advisor.clearRoutesPending();
|
||||
routesDest.value = dest || 'all';
|
||||
routesDest.value = destId;
|
||||
trackSiteEvent('home_dest_click', { dest_id: destId });
|
||||
openPanel('routes');
|
||||
}
|
||||
|
||||
function onIntent(intentId: string) {
|
||||
trackSiteEvent('home_intent_click', { intent_id: intentId });
|
||||
advisor.askIntent(intentId);
|
||||
}
|
||||
|
||||
function closePanel(id?: string) {
|
||||
if (id && panel.value && panel.value !== id) return;
|
||||
if (!panel.value) return;
|
||||
@ -266,6 +331,7 @@
|
||||
historyOpen.value = false;
|
||||
nav.value = 'home';
|
||||
syncHash('home');
|
||||
trackSiteEvent('home_nav', { nav_id: 'home' });
|
||||
advisor.startNewChat();
|
||||
}
|
||||
|
||||
@ -275,6 +341,7 @@
|
||||
onHome();
|
||||
return;
|
||||
}
|
||||
trackSiteEvent('home_nav', { nav_id: id });
|
||||
if (NAV_PANELS.includes(id as PanelId)) {
|
||||
openPanel(id as Exclude<PanelId, null>);
|
||||
return;
|
||||
@ -288,6 +355,10 @@
|
||||
const fromRoutes = panel.value === 'routes';
|
||||
closePanel();
|
||||
draft.value = '';
|
||||
trackSiteEvent('home_ask', {
|
||||
intent_id: payload.intentId,
|
||||
from_routes: fromRoutes,
|
||||
});
|
||||
advisor.ask(payload.text, { intentId: payload.intentId, fromRoutes });
|
||||
}
|
||||
|
||||
@ -295,11 +366,16 @@
|
||||
const text = draft.value.trim();
|
||||
if (!text) return;
|
||||
draft.value = '';
|
||||
trackSiteEvent('home_ask', { intent_id: 'free_text' });
|
||||
advisor.ask(text);
|
||||
}
|
||||
|
||||
function onChoice(payload: { id: string; label: string; flowId: string }) {
|
||||
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 });
|
||||
}
|
||||
|
||||
@ -309,6 +385,7 @@
|
||||
}
|
||||
|
||||
function openWecom() {
|
||||
trackSiteEvent('home_wecom_click', { entry: 'advisor' });
|
||||
if (shouldOpenWecomDirect()) {
|
||||
window.location.href = WECOM_KF_URL;
|
||||
return;
|
||||
@ -317,6 +394,7 @@
|
||||
}
|
||||
|
||||
function onTopicWechat() {
|
||||
trackSiteEvent('home_wecom_click', { entry: 'topic' });
|
||||
if (shouldOpenWecomDirect()) {
|
||||
window.location.href = WECOM_KF_URL;
|
||||
return;
|
||||
@ -328,6 +406,7 @@
|
||||
const next = lang.value === 'zh' ? 'en' : 'zh';
|
||||
lang.value = next;
|
||||
localStorage.setItem('ide-language', next);
|
||||
trackSiteEvent('home_lang_toggle', { lang: next });
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
@ -378,9 +457,15 @@
|
||||
if (hash === '9610' || hash === 'services') openPanel('services');
|
||||
else if (NAV_PANELS.includes(hash as PanelId)) openPanel(hash as Exclude<PanelId, null>);
|
||||
else if (hash) nav.value = hash;
|
||||
void nextTick(() => {
|
||||
if (glassSceneEl.value && glassContentEl.value) {
|
||||
glass.init(glassSceneEl.value, glassContentEl.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
glass.dispose();
|
||||
window.removeEventListener('keydown', onKeydown);
|
||||
window.removeEventListener('resize', syncHeaderOffset);
|
||||
window.removeEventListener('resize', syncVisualViewport);
|
||||
@ -395,7 +480,9 @@
|
||||
|
||||
<style scoped>
|
||||
.official-site {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
isolation: isolate;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -419,7 +506,7 @@
|
||||
|
||||
.site-fade-enter-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,
|
||||
@ -430,9 +517,8 @@
|
||||
.site-sheet-enter-active,
|
||||
.site-sheet-leave-active {
|
||||
transition:
|
||||
opacity 0.34s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.4s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
filter 0.34s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
opacity 0.24s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.site-sheet-leave-active {
|
||||
@ -441,25 +527,21 @@
|
||||
|
||||
.site-sheet-enter-from {
|
||||
opacity: 0;
|
||||
filter: blur(2px);
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
transform: translateX(-50%) translateY(-8px);
|
||||
}
|
||||
|
||||
.site-sheet-leave-to {
|
||||
opacity: 0;
|
||||
filter: blur(1px);
|
||||
transform: translateX(-50%) translateY(-6px);
|
||||
transform: translateX(-50%) translateY(-4px);
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.site-sheet-enter-from {
|
||||
filter: none;
|
||||
transform: translateY(16px);
|
||||
transform: translateY(12px);
|
||||
}
|
||||
|
||||
.site-sheet-leave-to {
|
||||
filter: none;
|
||||
transform: translateY(10px);
|
||||
transform: translateY(8px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
231
src/views/site/lib/site-liquid-glass.ts
Normal file
231
src/views/site/lib/site-liquid-glass.ts
Normal 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);
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
export const WELCOME_COPY = {
|
||||
en: {
|
||||
title: 'IDE. Your cross-border logistics advisor',
|
||||
titleShort: 'IDE. Logistics advisor',
|
||||
titleRest: ' Your cross-border logistics advisor',
|
||||
title: 'IDE · One-stop cross-border logistics advisor',
|
||||
titleShort: 'IDE · Logistics advisor',
|
||||
titleRest: 'One-stop cross-border logistics advisor',
|
||||
titleRestShort: 'Logistics advisor',
|
||||
placeholder: 'e.g. 5 kg general cargo to the US, how many days?',
|
||||
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.',
|
||||
},
|
||||
zh: {
|
||||
title: 'IDE.您身边的跨境物流顾问',
|
||||
titleShort: 'IDE.跨境物流顾问',
|
||||
titleRest: '您身边的跨境物流顾问',
|
||||
title: 'IDE · 一站式跨境物流顾问',
|
||||
titleShort: 'IDE · 跨境物流顾问',
|
||||
titleRest: '一站式跨境物流顾问',
|
||||
titleRestShort: '跨境物流顾问',
|
||||
placeholder: '例如:发美国普货,5kg,要几天',
|
||||
sendLabel: '提问',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user