diff --git a/.env.ideship-test b/.env.ideship-test index 898d3eb..458f63b 100644 --- a/.env.ideship-test +++ b/.env.ideship-test @@ -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= diff --git a/.env.production b/.env.production index c828691..32d4a93 100644 --- a/.env.production +++ b/.env.production @@ -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 diff --git a/package.json b/package.json index 11063c5..236349f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 966f6f0..8ca5495 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/api/customer/index.ts b/src/api/customer/index.ts index 43b44e9..6cd2dd8 100644 --- a/src/api/customer/index.ts +++ b/src/api/customer/index.ts @@ -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, diff --git a/src/api/system/batchEmail.ts b/src/api/system/batchEmail.ts index e8e1f45..9150063 100644 --- a/src/api/system/batchEmail.ts +++ b/src/api/system/batchEmail.ts @@ -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 + 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 { + 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) +} diff --git a/src/env.d.ts b/src/env.d.ts index 6e251ef..fdadd38 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -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; } diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 2d2074c..513f5c0 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -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', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index aa3e1c3..9002781 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -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': '搜索标题或正文', diff --git a/src/main.ts b/src/main.ts index 05d3af2..d76aa58 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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) { diff --git a/src/router/guard/index.ts b/src/router/guard/index.ts index 25722b5..52b4bba 100644 --- a/src/router/guard/index.ts +++ b/src/router/guard/index.ts @@ -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(); }); } diff --git a/src/store/modules/rbac-user.ts b/src/store/modules/rbac-user.ts index 352a528..605196c 100644 --- a/src/store/modules/rbac-user.ts +++ b/src/store/modules/rbac-user.ts @@ -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 } } diff --git a/src/utils/ga.ts b/src/utils/ga.ts new file mode 100644 index 0000000..7e77bb0 --- /dev/null +++ b/src/utils/ga.ts @@ -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, +): void { + if (!isGaEnabled() || !window.gtag) return; + const cleaned: Record = {}; + 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, +): void { + trackEvent(name, { surface: 'official_site', ...params }); +} diff --git a/src/views/login/components/pwd-login/index.vue b/src/views/login/components/pwd-login/index.vue index 3d5bdf1..f554177 100644 --- a/src/views/login/components/pwd-login/index.vue +++ b/src/views/login/components/pwd-login/index.vue @@ -350,11 +350,19 @@ async function handleSubmit() { loading.value = true; errorBox.value = ''; - const success = await rbacUserStore.login( - loginForm.account, - loginForm.password, - props.loginType, - ); + 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; diff --git a/src/views/order/placeOrder/index.vue b/src/views/order/placeOrder/index.vue index 8980d7c..02e3943 100644 --- a/src/views/order/placeOrder/index.vue +++ b/src/views/order/placeOrder/index.vue @@ -11,10 +11,10 @@ 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 SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vue'; import FormLabelWithPreview from '@/views/channel/salesProducts/components/FormLabelWithPreview.vue'; import CustomerGuideDrawer from '@/views/order/priceCheck/components/CustomerGuideDrawer.vue'; import ExportCustoms9610GuideModal from './components/ExportCustoms9610GuideModal.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; diff --git a/src/views/site/GradientWaves.vue b/src/views/site/GradientWaves.vue index 1e3998b..643bcf2 100644 --- a/src/views/site/GradientWaves.vue +++ b/src/views/site/GradientWaves.vue @@ -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; + } } diff --git a/src/views/site/SiteAdvisorShell.vue b/src/views/site/SiteAdvisorShell.vue index 91a9e4b..8c20198 100644 --- a/src/views/site/SiteAdvisorShell.vue +++ b/src/views/site/SiteAdvisorShell.vue @@ -1,33 +1,8 @@ diff --git a/src/views/site/SiteHeader.vue b/src/views/site/SiteHeader.vue index 803f410..18f8aea 100644 --- a/src/views/site/SiteHeader.vue +++ b/src/views/site/SiteHeader.vue @@ -29,12 +29,7 @@