From 5af476d3762d3757f1ffab4d589ba70292d6bd17 Mon Sep 17 00:00:00 2001 From: TinsFox Date: Sat, 6 Sep 2025 16:21:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E6=89=98=E7=9B=98=20?= =?UTF-8?q?(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 系统托盘 1. 添加系统托盘 2. 托盘添加切换供应商功能 3. 整理组件目录 * feat: 优化系统托盘菜单结构 - 扁平化Claude和Codex的菜单结构,直接将所有供应商添加到主菜单,简化用户交互。 - 添加无供应商时的提示信息,提升用户体验。 - 更新分隔符文本以增强可读性。 * feat: integrate Tailwind CSS and Lucide icons - Added Tailwind CSS for styling and layout improvements. - Integrated Lucide icons for enhanced UI elements. - Updated project structure by removing unused CSS files and components. - Refactored configuration files to support new styling and component structure. - Introduced new components for managing providers with improved UI interactions. * fix: 修复类型声明和分隔符实现问题 - 修复 updateTrayMenu 返回类型不一致(Promise -> Promise) - 添加缺失的 UnlistenFn 类型导入 - 使用 MenuBuilder.separator() 替代文本分隔符 --------- Co-authored-by: farion1231 =18.0.0'} + '@jridgewell/gen-mapping@0.3.12': resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -322,6 +338,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.4': resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} @@ -443,6 +462,96 @@ packages: cpu: [x64] os: [win32] + '@tailwindcss/node@4.1.13': + resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==} + + '@tailwindcss/oxide-android-arm64@4.1.13': + resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.13': + resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.13': + resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.13': + resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': + resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': + resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.13': + resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.13': + resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.13': + resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.13': + resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': + resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.13': + resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.13': + resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.13': + resolution: {integrity: sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@tauri-apps/api@2.8.0': resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==} @@ -560,6 +669,10 @@ packages: caniuse-lite@1.0.30001731: resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + codemirror@6.0.2: resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} @@ -581,9 +694,17 @@ packages: supports-color: optional: true + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + electron-to-chromium@1.5.197: resolution: {integrity: sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -602,6 +723,13 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -615,6 +743,70 @@ packages: engines: {node: '>=6'} hasBin: true + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -622,6 +814,27 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.542.0: + resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.18: + resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -677,6 +890,17 @@ packages: style-mod@4.1.2: resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + tailwindcss@4.1.13: + resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} + + tapable@2.2.3: + resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -728,6 +952,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + snapshots: '@ampproject/remapping@2.3.0': @@ -974,15 +1202,26 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.12': dependencies: '@jridgewell/sourcemap-codec': 1.5.4 '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.4': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -1068,6 +1307,77 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true + '@tailwindcss/node@4.1.13': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.18 + source-map-js: 1.2.1 + tailwindcss: 4.1.13 + + '@tailwindcss/oxide-android-arm64@4.1.13': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.13': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.13': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.13': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.13': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.13': + optional: true + + '@tailwindcss/oxide@4.1.13': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.13 + '@tailwindcss/oxide-darwin-arm64': 4.1.13 + '@tailwindcss/oxide-darwin-x64': 4.1.13 + '@tailwindcss/oxide-freebsd-x64': 4.1.13 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.13 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.13 + '@tailwindcss/oxide-linux-x64-musl': 4.1.13 + '@tailwindcss/oxide-wasm32-wasi': 4.1.13 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 + + '@tailwindcss/vite@4.1.13(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))': + dependencies: + '@tailwindcss/node': 4.1.13 + '@tailwindcss/oxide': 4.1.13 + tailwindcss: 4.1.13 + vite: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1) + '@tauri-apps/api@2.8.0': {} '@tauri-apps/cli-darwin-arm64@2.8.1': @@ -1155,7 +1465,7 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.1.3 - '@vitejs/plugin-react@4.7.0(vite@5.4.19(@types/node@20.19.9))': + '@vitejs/plugin-react@4.7.0(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) @@ -1163,7 +1473,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.19(@types/node@20.19.9) + vite: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1) transitivePeerDependencies: - supports-color @@ -1176,6 +1486,8 @@ snapshots: caniuse-lite@1.0.30001731: {} + chownr@3.0.0: {} + codemirror@6.0.2: dependencies: '@codemirror/autocomplete': 6.18.7 @@ -1196,8 +1508,15 @@ snapshots: dependencies: ms: 2.1.3 + detect-libc@2.0.4: {} + electron-to-chromium@1.5.197: {} + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.3 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -1231,12 +1550,61 @@ snapshots: gensync@1.0.0-beta.2: {} + graceful-fs@4.2.11: {} + + jiti@2.5.1: {} + js-tokens@4.0.0: {} jsesc@3.1.0: {} json5@2.2.3: {} + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -1245,6 +1613,22 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.542.0(react@18.3.1): + dependencies: + react: 18.3.1 + + magic-string@0.30.18: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -1309,6 +1693,19 @@ snapshots: style-mod@4.1.2: {} + tailwindcss@4.1.13: {} + + tapable@2.2.3: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + typescript@5.9.2: {} undici-types@6.21.0: {} @@ -1319,7 +1716,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - vite@5.4.19(@types/node@20.19.9): + vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -1327,7 +1724,10 @@ snapshots: optionalDependencies: '@types/node': 20.19.9 fsevents: 2.3.3 + lightningcss: 1.30.1 w3c-keyname@2.2.8: {} yallist@3.1.1: {} + + yallist@5.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..fd050a4 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - '@tailwindcss/oxide' diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3fbbdaa..017f78e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,7 +21,7 @@ tauri-build = { version = "2.4.0", features = [] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" -tauri = { version = "2.8.2", features = [] } +tauri = { version = "2.8.2", features = ["tray-icon"] } tauri-plugin-log = "2" tauri-plugin-opener = "2" dirs = "5.0" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 077b029..eb1c37f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,12 +2,220 @@ mod app_config; mod codex_config; mod commands; mod config; +mod migration; mod provider; mod store; -mod migration; use store::AppState; -use tauri::Manager; +use tauri::{ + menu::{CheckMenuItem, Menu, MenuBuilder, MenuItem}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, +}; +use tauri::{Emitter, Manager}; + +/// 创建动态托盘菜单 +fn create_tray_menu( + app: &tauri::AppHandle, + app_state: &AppState, +) -> Result, String> { + let config = app_state + .config + .lock() + .map_err(|e| format!("获取锁失败: {}", e))?; + + let mut menu_builder = MenuBuilder::new(app); + + // 直接添加所有供应商到主菜单(扁平化结构,更简单可靠) + if let Some(claude_manager) = config.get_manager(&crate::app_config::AppType::Claude) { + // 添加Claude标题(禁用状态,仅作为分组标识) + let claude_header = + MenuItem::with_id(app, "claude_header", "─── Claude ───", false, None::<&str>) + .map_err(|e| format!("创建Claude标题失败: {}", e))?; + menu_builder = menu_builder.item(&claude_header); + + if !claude_manager.providers.is_empty() { + for (id, provider) in &claude_manager.providers { + let is_current = claude_manager.current == *id; + let item = CheckMenuItem::with_id( + app, + format!("claude_{}", id), + &provider.name, + true, + is_current, + None::<&str>, + ) + .map_err(|e| format!("创建菜单项失败: {}", e))?; + menu_builder = menu_builder.item(&item); + } + } else { + // 没有供应商时显示提示 + let empty_hint = MenuItem::with_id( + app, + "claude_empty", + " (无供应商,请在主界面添加)", + false, + None::<&str>, + ) + .map_err(|e| format!("创建Claude空提示失败: {}", e))?; + menu_builder = menu_builder.item(&empty_hint); + } + } + + if let Some(codex_manager) = config.get_manager(&crate::app_config::AppType::Codex) { + // 添加Codex标题(禁用状态,仅作为分组标识) + let codex_header = + MenuItem::with_id(app, "codex_header", "─── Codex ───", false, None::<&str>) + .map_err(|e| format!("创建Codex标题失败: {}", e))?; + menu_builder = menu_builder.item(&codex_header); + + if !codex_manager.providers.is_empty() { + for (id, provider) in &codex_manager.providers { + let is_current = codex_manager.current == *id; + let item = CheckMenuItem::with_id( + app, + format!("codex_{}", id), + &provider.name, + true, + is_current, + None::<&str>, + ) + .map_err(|e| format!("创建菜单项失败: {}", e))?; + menu_builder = menu_builder.item(&item); + } + } else { + // 没有供应商时显示提示 + let empty_hint = MenuItem::with_id( + app, + "codex_empty", + " (无供应商,请在主界面添加)", + false, + None::<&str>, + ) + .map_err(|e| format!("创建Codex空提示失败: {}", e))?; + menu_builder = menu_builder.item(&empty_hint); + } + } + + // 分隔符和退出菜单 + let quit_item = MenuItem::with_id(app, "quit", "退出", true, None::<&str>) + .map_err(|e| format!("创建退出菜单失败: {}", e))?; + + menu_builder = menu_builder.separator().item(&quit_item); + + menu_builder + .build() + .map_err(|e| format!("构建菜单失败: {}", e)) +} + +/// 处理托盘菜单事件 +fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) { + println!("处理托盘菜单事件: {}", event_id); + + match event_id { + "quit" => { + println!("退出应用"); + app.exit(0); + } + id if id.starts_with("claude_") => { + let provider_id = id.strip_prefix("claude_").unwrap(); + println!("切换到Claude供应商: {}", provider_id); + + // 执行切换 + let app_handle = app.clone(); + let provider_id = provider_id.to_string(); + tauri::async_runtime::spawn(async move { + if let Err(e) = switch_provider_internal( + &app_handle, + crate::app_config::AppType::Claude, + provider_id, + ) + .await + { + eprintln!("切换Claude供应商失败: {}", e); + } + }); + } + id if id.starts_with("codex_") => { + let provider_id = id.strip_prefix("codex_").unwrap(); + println!("切换到Codex供应商: {}", provider_id); + + // 执行切换 + let app_handle = app.clone(); + let provider_id = provider_id.to_string(); + tauri::async_runtime::spawn(async move { + if let Err(e) = switch_provider_internal( + &app_handle, + crate::app_config::AppType::Codex, + provider_id, + ) + .await + { + eprintln!("切换Codex供应商失败: {}", e); + } + }); + } + _ => { + println!("未处理的菜单事件: {}", event_id); + } + } +} + +/// 内部切换供应商函数 +async fn switch_provider_internal( + app: &tauri::AppHandle, + app_type: crate::app_config::AppType, + provider_id: String, +) -> Result<(), String> { + if let Some(app_state) = app.try_state::() { + // 在使用前先保存需要的值 + let app_type_str = app_type.as_str().to_string(); + let provider_id_clone = provider_id.clone(); + + crate::commands::switch_provider( + app_state.clone().into(), + Some(app_type), + None, + None, + provider_id, + ) + .await?; + + // 切换成功后重新创建托盘菜单 + if let Ok(new_menu) = create_tray_menu(app, app_state.inner()) { + if let Some(tray) = app.tray_by_id("main") { + if let Err(e) = tray.set_menu(Some(new_menu)) { + eprintln!("更新托盘菜单失败: {}", e); + } + } + } + + // 发射事件到前端,通知供应商已切换 + let event_data = serde_json::json!({ + "appType": app_type_str, + "providerId": provider_id_clone + }); + if let Err(e) = app.emit("provider-switched", event_data) { + eprintln!("发射供应商切换事件失败: {}", e); + } + } + Ok(()) +} + +/// 更新托盘菜单的Tauri命令 +#[tauri::command] +async fn update_tray_menu( + app: tauri::AppHandle, + state: tauri::State<'_, AppState>, +) -> Result { + if let Ok(new_menu) = create_tray_menu(&app, state.inner()) { + if let Some(tray) = app.tray_by_id("main") { + tray.set_menu(Some(new_menu)) + .map_err(|e| format!("更新托盘菜单失败: {}", e))?; + return Ok(true); + } + } + Ok(false) +} #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -71,6 +279,36 @@ pub fn run() { // 保存配置 let _ = app_state.save(); + // 创建动态托盘菜单 + let menu = create_tray_menu(&app.handle(), &app_state)?; + + let _tray = TrayIconBuilder::with_id("main") + .on_tray_icon_event(|tray, event| match event { + TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } => { + println!("left click pressed and released"); + // 在这个例子中,当点击托盘图标时,将展示并聚焦于主窗口 + let app = tray.app_handle(); + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } + } + _ => { + println!("unhandled event {event:?}"); + } + }) + .menu(&menu) + .on_menu_event(|app, event| { + handle_tray_menu_event(app, &event.id.0); + }) + .icon(app.default_window_icon().unwrap().clone()) + .show_menu_on_left_click(true) + .build(app)?; // 将同一个实例注入到全局状态,避免重复创建导致的不一致 app.manage(app_state); Ok(()) @@ -88,6 +326,7 @@ pub fn run() { commands::get_claude_code_config_path, commands::open_config_folder, commands::open_external, + update_tray_menu, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.css b/src/App.css deleted file mode 100644 index bb29173..0000000 --- a/src/App.css +++ /dev/null @@ -1,242 +0,0 @@ -.app { - height: 100vh; - display: flex; - flex-direction: column; -} - -.app-header { - background: linear-gradient(180deg, #3498db 0%, #2d89c7 100%); - color: white; - padding: 0.35rem 2rem 0.45rem; - display: grid; - grid-template-columns: 1fr auto 1fr; - grid-template-rows: auto auto; - grid-template-areas: - ". title ." - "tabs . actions"; - align-items: center; - row-gap: 0.6rem; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); - user-select: none; -} - -.app-tabs { - grid-area: tabs; -} - -/* Segmented control */ -.segmented { - --seg-bg: rgba(255, 255, 255, 0.16); - --seg-thumb: #ffffff; - --seg-color: rgba(255, 255, 255, 0.85); - --seg-active: #2d89c7; - position: relative; - display: grid; - grid-template-columns: 1fr 1fr; - width: 280px; - background: var(--seg-bg); - border-radius: 999px; - padding: 4px; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); - backdrop-filter: saturate(140%) blur(2px); -} - -.segmented-thumb { - position: absolute; - top: 4px; - left: 4px; - width: calc(50% - 4px); - height: calc(100% - 8px); - background: var(--seg-thumb); - border-radius: 999px; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); - transition: - transform 220ms ease, - width 220ms ease; - will-change: transform; -} - -.segmented-item { - position: relative; - z-index: 1; - background: transparent; - border: none; - border-radius: 999px; - padding: 6px 16px; /* 更紧凑的高度 */ - color: var(--seg-color); - font-size: 0.95rem; - font-weight: 600; - letter-spacing: 0.2px; - cursor: pointer; - transition: color 200ms ease; -} - -.segmented-item.active { - color: var(--seg-active); -} - -.segmented-item:focus-visible { - outline: 2px solid rgba(255, 255, 255, 0.8); - outline-offset: 2px; -} - -.app-header h1 { - font-size: 1.5rem; - font-weight: 500; - margin: 0; - grid-area: title; - text-align: center; -} - -.header-actions { - display: flex; - gap: 1rem; - grid-area: actions; - justify-self: end; -} - -.refresh-btn, -.add-btn { - padding: 0.5rem 1rem; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.9rem; - transition: all 0.2s; -} - -.refresh-btn { - background: #3498db; - color: white; -} - -.refresh-btn:hover:not(:disabled) { - background: #2980b9; -} - -.refresh-btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.import-btn { - background: rgba(255, 255, 255, 0.2); - color: white; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.import-btn:hover { - background: rgba(255, 255, 255, 0.3); -} - -.import-btn:focus { - outline: none; -} - -.add-btn { - background: #27ae60; - color: white; - border: none; -} - -.add-btn:hover { - background: #229954; -} - -.add-btn:focus { - outline: none; -} - -.app-main { - flex: 1; - padding: 2rem; - overflow-y: auto; -} - -.config-path { - margin-top: 2rem; - padding: 1rem; - background: #ecf0f1; - border-radius: 4px; - font-size: 0.9rem; - color: #7f8c8d; - display: flex; - justify-content: space-between; - align-items: center; -} - -.browse-btn { - padding: 0.5rem 1rem; - border: none; - border-radius: 4px; - background: #3498db; - color: white; - cursor: pointer; - font-size: 0.9rem; - transition: all 0.2s; - margin-left: 1rem; -} - -.browse-btn:hover { - background: #2980b9; -} - -/* 供应商列表区域 - 相对定位容器 */ -.provider-section { - position: relative; -} - -/* 浮动通知 - 绝对定位,不占据空间 */ -.notification-floating { - position: absolute; - top: -10px; - left: 50%; - transform: translateX(-50%); - z-index: 100; - - padding: 0.75rem 1.25rem; - border-radius: 6px; - font-size: 0.9rem; - font-weight: 500; - - width: fit-content; - white-space: nowrap; -} - -.fade-in { - animation: fadeIn 0.3s ease-out; -} - -.fade-out { - animation: fadeOut 0.3s ease-out; -} - -.notification-success { - background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); - color: white; - box-shadow: 0 4px 12px rgba(39, 174, 96, 0.3); -} - -.notification-error { - background: linear-gradient(135deg, #e74c3c 0%, #ec7063 100%); - color: white; - box-shadow: 0 4px 12px rgba(231, 76, 60, 0.3); -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes fadeOut { - from { - opacity: 1; - } - to { - opacity: 0; - } -} diff --git a/src/App.tsx b/src/App.tsx index 1010920..a133d20 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import AddProviderModal from "./components/AddProviderModal"; import EditProviderModal from "./components/EditProviderModal"; import { ConfirmDialog } from "./components/ConfirmDialog"; import { AppSwitcher } from "./components/AppSwitcher"; -import "./App.css"; +import { Plus } from "lucide-react"; function App() { const [activeApp, setActiveApp] = useState("claude"); @@ -18,7 +18,7 @@ function App() { path: string; } | null>(null); const [editingProviderId, setEditingProviderId] = useState( - null, + null ); const [notification, setNotification] = useState<{ message: string; @@ -37,7 +37,7 @@ function App() { const showNotification = ( message: string, type: "success" | "error", - duration = 3000, + duration = 3000 ) => { // 清除之前的定时器 if (timeoutRef.current) { @@ -74,6 +74,35 @@ function App() { }; }, []); + // 监听托盘切换事件 + useEffect(() => { + let unlisten: (() => void) | null = null; + + const setupListener = async () => { + try { + unlisten = await window.api.onProviderSwitched(async (data) => { + console.log("收到供应商切换事件:", data); + + // 如果当前应用类型匹配,则重新加载数据 + if (data.appType === activeApp) { + await loadProviders(); + } + }); + } catch (error) { + console.error("设置供应商切换监听器失败:", error); + } + }; + + setupListener(); + + // 清理监听器 + return () => { + if (unlisten) { + unlisten(); + } + }; + }, [activeApp]); // 依赖activeApp,切换应用时重新设置监听器 + const loadProviders = async () => { const loadedProviders = await window.api.getProviders(activeApp); const currentId = await window.api.getCurrentProvider(activeApp); @@ -107,6 +136,8 @@ function App() { await window.api.addProvider(newProvider, activeApp); await loadProviders(); setIsAddModalOpen(false); + // 更新托盘菜单 + await window.api.updateTrayMenu(); }; const handleEditProvider = async (provider: Provider) => { @@ -116,6 +147,8 @@ function App() { setEditingProviderId(null); // 显示编辑成功提示 showNotification("供应商配置已保存", "success", 2000); + // 更新托盘菜单 + await window.api.updateTrayMenu(); } catch (error) { console.error("更新供应商失败:", error); setEditingProviderId(null); @@ -134,6 +167,8 @@ function App() { await loadProviders(); setConfirmDialog(null); showNotification("供应商删除成功", "success"); + // 更新托盘菜单 + await window.api.updateTrayMenu(); }, }); }; @@ -147,8 +182,10 @@ function App() { showNotification( `切换成功!请重启 ${appName} 终端以生效`, "success", - 2000, + 2000 ); + // 更新托盘菜单 + await window.api.updateTrayMenu(); } else { showNotification("切换失败,请检查配置", "error"); } @@ -162,6 +199,8 @@ function App() { if (result.success) { await loadProviders(); showNotification("已从现有配置创建默认供应商", "success", 3000); + // 更新托盘菜单 + await window.api.updateTrayMenu(); } // 如果导入失败(比如没有现有配置),静默处理,不显示错误 } catch (error) { @@ -175,29 +214,39 @@ function App() { }; return ( -
-
-

CC Switch

-
- -
-
- +
+ {/* Linear 风格的顶部导航 */} +
+
+

+ CC Switch +

+ +
+ + + +
-
-
- {/* 浮动通知组件 */} + {/* 主内容区域 */} +
+
+ {/* 通知组件 */} {notification && (
{notification.message}
@@ -210,23 +259,36 @@ function App() { onDelete={handleDeleteProvider} onEdit={setEditingProviderId} /> -
- {configStatus && ( -
- - 配置文件位置: {configStatus.path} - {!configStatus.exists ? "(未创建,切换或保存时会自动创建)" : ""} - - -
- )} + {/* 配置文件路径信息 */} + {configStatus && ( +
+
+
+ + {activeApp === "claude" ? "Claude Code" : "Codex"}{" "} + 配置文件位置: + + + {configStatus.path} + + {!configStatus.exists && ( + + (未创建,切换或保存时会自动创建) + + )} +
+ +
+
+ )} +
{isAddModalOpen && ( diff --git a/src/components/AddProviderModal.css b/src/components/AddProviderModal.css deleted file mode 100644 index 60da719..0000000 --- a/src/components/AddProviderModal.css +++ /dev/null @@ -1,268 +0,0 @@ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modal-content { - background: white; - border-radius: 10px; - padding: 0; - width: 90%; - max-width: 640px; - max-height: 90vh; - overflow: hidden; /* 由 body 滚动,标题栏固定 */ - box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); - position: relative; - z-index: 1001; - display: flex; /* 纵向布局,便于底栏固定 */ - flex-direction: column; -} - -/* 模拟窗口标题栏 */ -.modal-titlebar { - display: flex; - align-items: center; - justify-content: space-between; - height: 3rem; /* 与主窗口标题栏一致 */ - padding: 0 12px; /* 接近主头部的水平留白 */ - background: #3498db; /* 与 .app-header 相同 */ - color: #fff; - border-top-left-radius: 10px; - border-top-right-radius: 10px; -} - -/* 左侧占位以保证标题居中(与右侧关闭按钮宽度相当) */ -.modal-spacer { - width: 32px; - flex: 0 0 32px; -} - -.modal-title { - flex: 1; - text-align: center; - color: #fff; - font-weight: 600; - font-size: 1rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.modal-close-btn { - background: transparent; - border: none; - color: #fff; - font-size: 20px; - line-height: 1; - padding: 2px 6px; - border-radius: 6px; - cursor: pointer; -} - -.modal-close-btn:hover { - background: rgba(255, 255, 255, 0.18); - color: #fff; -} - -.modal-form { - /* 表单外层包裹 body + footer */ - display: flex; - flex-direction: column; - flex: 1 1 auto; - min-height: 0; /* 允许子元素正确计算高度 */ -} - -.modal-body { - padding: 1.25rem 1.5rem 1.5rem; - overflow: auto; /* 仅内容区滚动 */ - flex: 1 1 auto; - min-height: 0; -} - -.error-message { - background: #fee; - color: #c33; - padding: 0.75rem; - border-radius: 4px; - margin-bottom: 1rem; - border: 1px solid #fcc; - font-size: 0.9rem; -} - -.presets { - margin-bottom: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid #ecf0f1; -} - -.presets label { - display: block; - margin-bottom: 0.5rem; - color: #555; - font-size: 0.9rem; -} - -.preset-buttons { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.preset-btn { - padding: 0.375rem 0.75rem; - border: 1px solid #3498db; - background: white; - color: #3498db; - border-radius: 4px; - cursor: pointer; - font-size: 0.85rem; - transition: all 0.2s; -} - -.preset-btn:hover, -.preset-btn.selected { - background: #3498db; - color: white; -} - -/* 官方按钮橙色主题(Anthropic 风格) */ -.preset-btn.official { - border: 1px solid #d97706; - color: #d97706; -} - -.preset-btn.official:hover, -.preset-btn.official.selected { - background: #d97706; - color: white; -} - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - color: #555; - font-weight: 500; -} - -/* API Key 输入框容器 - 预留空间避免抖动 */ -.form-group.api-key-group { - min-height: 88px; /* 固定高度:label + input + 间距 */ - transition: opacity 0.2s ease; -} - -.form-group.api-key-group.hidden { - opacity: 0; - visibility: hidden; - pointer-events: none; -} - -.form-group input, -.form-group textarea { - width: 100%; - padding: 0.625rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 0.95rem; - transition: border-color 0.2s; - background: white; - box-sizing: border-box; -} - -.form-group textarea { - resize: vertical; - min-height: 200px; -} - -.form-group input:focus, -.form-group textarea:focus { - outline: none; - border-color: #3498db; -} - -.modal-footer { - /* 固定在弹窗底部(非滚动区) */ - display: flex; - gap: 1rem; - justify-content: flex-end; - padding: 0.75rem 1.5rem; - border-top: 1px solid #ecf0f1; - background: #fff; -} - -.cancel-btn, -.submit-btn { - padding: 0.625rem 1.25rem; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.95rem; - transition: all 0.2s; -} - -.cancel-btn { - background: #ecf0f1; - color: #555; -} - -.cancel-btn:hover { - background: #bdc3c7; -} - -.submit-btn { - background: #27ae60; - color: white; -} - -.submit-btn:hover { - background: #229954; -} - -.field-hint { - display: block; - margin-top: 0.25rem; - color: #7f8c8d; - font-size: 0.8rem; - line-height: 1.3; -} - -/* 添加标签和选择框的样式 */ -.label-with-checkbox { - display: flex; - justify-content: space-between; - align-items: baseline; - margin-bottom: 0.5rem; -} - -.label-with-checkbox label:first-child { - margin-bottom: 0; -} - -.checkbox-label { - display: flex; - align-items: center; - gap: 0.3rem; - font-size: 0.85rem; - color: #666; - font-weight: normal; - margin-bottom: 0; - cursor: pointer; -} - -.checkbox-label input[type="checkbox"] { - width: auto; - margin: 2px; - cursor: pointer; - transform: translateY(2px); -} diff --git a/src/components/AppSwitcher.css b/src/components/AppSwitcher.css deleted file mode 100644 index 8dc9906..0000000 --- a/src/components/AppSwitcher.css +++ /dev/null @@ -1,73 +0,0 @@ -/* 药丸式切换按钮 */ -.switcher-pills { - display: inline-flex; - align-items: center; - gap: 12px; - background: rgba(255, 255, 255, 0.08); - padding: 6px 8px; - border-radius: 50px; - backdrop-filter: blur(10px); -} - -.switcher-pill { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 8px 16px; - background: transparent; - border: none; - border-radius: 50px; - color: rgba(255, 255, 255, 0.6); - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: all 200ms ease; - min-width: 120px; -} - -.switcher-pill:hover:not(.active) { - color: rgba(255, 255, 255, 0.8); - background: rgba(255, 255, 255, 0.05); -} - -.switcher-pill.active { - background: rgba(255, 255, 255, 0.15); - color: white; - box-shadow: - inset 0 1px 3px rgba(0, 0, 0, 0.1), - 0 1px 0 rgba(255, 255, 255, 0.1); -} - -.pill-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: currentColor; - opacity: 0.4; - transition: all 200ms ease; -} - -.switcher-pill.active .pill-dot { - opacity: 1; - box-shadow: 0 0 8px currentColor; - animation: pulse 2s infinite; -} - -.pills-divider { - width: 1px; - height: 20px; - background: rgba(255, 255, 255, 0.2); -} - -@keyframes pulse { - 0%, - 100% { - transform: scale(1); - opacity: 1; - } - 50% { - transform: scale(1.2); - opacity: 0.8; - } -} diff --git a/src/components/AppSwitcher.tsx b/src/components/AppSwitcher.tsx index e70b3cf..833a8a5 100644 --- a/src/components/AppSwitcher.tsx +++ b/src/components/AppSwitcher.tsx @@ -1,5 +1,5 @@ import { AppType } from "../lib/tauri-api"; -import "./AppSwitcher.css"; +import { Terminal, Code2 } from "lucide-react"; interface AppSwitcherProps { activeApp: AppType; @@ -13,22 +13,30 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) { }; return ( -
+
-
+
diff --git a/src/components/ConfirmDialog.css b/src/components/ConfirmDialog.css deleted file mode 100644 index d93721a..0000000 --- a/src/components/ConfirmDialog.css +++ /dev/null @@ -1,107 +0,0 @@ -.confirm-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - backdrop-filter: blur(2px); -} - -.confirm-dialog { - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); - min-width: 300px; - max-width: 400px; - animation: confirmSlideIn 0.2s ease-out; -} - -@keyframes confirmSlideIn { - from { - opacity: 0; - transform: scale(0.9) translateY(-20px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -.confirm-header { - padding: 1.5rem 1.5rem 1rem; - border-bottom: 1px solid #eee; -} - -.confirm-header h3 { - margin: 0; - font-size: 1.1rem; - color: #333; - font-weight: 600; -} - -.confirm-content { - padding: 1rem 1.5rem; -} - -.confirm-content p { - margin: 0; - color: #666; - line-height: 1.5; -} - -.confirm-actions { - display: flex; - gap: 0.75rem; - padding: 1rem 1.5rem 1.5rem; - justify-content: flex-end; -} - -.confirm-btn { - padding: 0.5rem 1rem; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.9rem; - font-weight: 500; - transition: - background-color 0.2s, - transform 0.1s; - min-width: 70px; -} - -.confirm-btn:hover { - transform: translateY(-1px); -} - -.confirm-btn:active { - transform: translateY(0); -} - -.cancel-btn { - background: #f8f9fa; - color: #6c757d; - border: 1px solid #dee2e6; -} - -.cancel-btn:hover { - background: #e9ecef; -} - -.confirm-btn-primary { - background: #dc3545; - color: white; -} - -.confirm-btn-primary:hover { - background: #c82333; -} - -.confirm-btn:focus { - outline: 2px solid #007bff; - outline-offset: 2px; -} diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx index 6981fef..fde1d7b 100644 --- a/src/components/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog.tsx @@ -1,5 +1,5 @@ import React from "react"; -import "./ConfirmDialog.css"; +import { AlertTriangle, X } from "lucide-react"; interface ConfirmDialogProps { isOpen: boolean; @@ -23,25 +23,52 @@ export const ConfirmDialog: React.FC = ({ if (!isOpen) return null; return ( -
-
-
-

{title}

-
-
-

{message}

-
-
+
+ {/* Backdrop */} +
+ + {/* Dialog */} +
+ {/* Header */} +
+
+
+ +
+

+ {title} +

+
+
+ + {/* Content */} +
+

+ {message} +

+
+ + {/* Actions */} +
+ diff --git a/src/components/ProviderForm.tsx b/src/components/ProviderForm.tsx index 600fcb0..83dfeac 100644 --- a/src/components/ProviderForm.tsx +++ b/src/components/ProviderForm.tsx @@ -10,8 +10,8 @@ import { } from "../utils/providerConfigUtils"; import { providerPresets } from "../config/providerPresets"; import { codexProviderPresets } from "../config/codexProviderPresets"; -import "./AddProviderModal.css"; import JsonEditor from "./JsonEditor"; +import { X, AlertCircle, Save, Zap } from "lucide-react"; interface ProviderFormProps { appType?: AppType; @@ -49,7 +49,7 @@ const ProviderForm: React.FC = ({ const [codexApiKey, setCodexApiKey] = useState(""); // -1 表示自定义,null 表示未选择,>= 0 表示预设索引 const [selectedCodexPreset, setSelectedCodexPreset] = useState( - showPresets && isCodex ? -1 : null, + showPresets && isCodex ? -1 : null ); // 初始化 Codex 配置 @@ -74,7 +74,7 @@ const ProviderForm: React.FC = ({ const [disableCoAuthored, setDisableCoAuthored] = useState(false); // -1 表示自定义,null 表示未选择,>= 0 表示预设索引 const [selectedPreset, setSelectedPreset] = useState( - showPresets ? -1 : null, + showPresets ? -1 : null ); const [apiKey, setApiKey] = useState(""); @@ -155,7 +155,7 @@ const ProviderForm: React.FC = ({ }; const handleChange = ( - e: React.ChangeEvent, + e: React.ChangeEvent ) => { const { name, value } = e.target; @@ -188,7 +188,7 @@ const ProviderForm: React.FC = ({ // 更新JSON配置 const updatedConfig = updateCoAuthoredSetting( formData.settingsConfig, - checked, + checked ); setFormData({ ...formData, @@ -231,7 +231,7 @@ const ProviderForm: React.FC = ({ // Codex: 应用预设 const applyCodexPreset = ( preset: (typeof codexProviderPresets)[0], - index: number, + index: number ) => { const authString = JSON.stringify(preset.auth || {}, null, 2); setCodexAuth(authString); @@ -269,7 +269,7 @@ const ProviderForm: React.FC = ({ const configString = setApiKeyInConfig( formData.settingsConfig, key.trim(), - { createIfMissing: selectedPreset !== null && selectedPreset !== -1 }, + { createIfMissing: selectedPreset !== null && selectedPreset !== -1 } ); // 更新表单配置 @@ -329,7 +329,7 @@ const ProviderForm: React.FC = ({ useEffect(() => { if (initialData) { const parsedKey = getApiKeyFromConfig( - JSON.stringify(initialData.settingsConfig), + JSON.stringify(initialData.settingsConfig) ); if (parsedKey) setApiKey(parsedKey); } @@ -350,130 +350,156 @@ const ProviderForm: React.FC = ({ return (
{ if (e.target === e.currentTarget) onClose(); }} > -
-
-
-
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+

{title} -

+
-
-
- {error &&
{error}
} + +
+ {error && ( +
+ +

+ {error} +

+
+ )} {showPresets && !isCodex && ( -
- -
- - {providerPresets.map((preset, index) => { - return ( +
+
+ +
+ + {providerPresets.map((preset, index) => ( - ); - })} + ))} +
{selectedPreset === -1 && ( - +

手动配置供应商,需要填写完整的配置信息 - +

)} {selectedPreset !== -1 && selectedPreset !== null && ( - +

{isOfficialPreset ? "Claude 官方登录,不需要填写 API Key" : "使用预设配置,只需填写 API Key"} - +

)}
)} {showPresets && isCodex && ( -
- -
- - {codexProviderPresets.map((preset, index) => ( +
+
+ +
- ))} + {codexProviderPresets.map((preset, index) => ( + + ))} +
{selectedCodexPreset === -1 && ( - +

手动配置供应商,需要填写完整的配置信息 - +

)} {selectedCodexPreset !== -1 && selectedCodexPreset !== null && ( - +

{isCodexOfficialPreset ? "Codex 官方登录,不需要填写 API Key" : "使用预设配置,只需填写 API Key"} - +

)}
)} -
- +
+ = ({ placeholder="例如:Anthropic 官方" required autoComplete="off" + className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors" />
- {!isCodex && ( -
- + {!isCodex && showApiKey && ( +
+ = ({ } disabled={isOfficialPreset} autoComplete="off" - style={ + className={`w-full px-3 py-2 border rounded-lg text-sm transition-colors ${ isOfficialPreset - ? { - backgroundColor: "#f5f5f5", - cursor: "not-allowed", - color: "#999", - } - : {} - } + ? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed" + : "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]" + }`} />
)} - {isCodex && ( -
- + {isCodex && showCodexApiKey && ( +
+ = ({ !isCodexOfficialPreset } autoComplete="off" - style={ + className={`w-full px-3 py-2 border rounded-lg text-sm transition-colors ${ isCodexOfficialPreset - ? { - backgroundColor: "#f5f5f5", - cursor: "not-allowed", - color: "#999", - } - : {} - } + ? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed" + : "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]" + }`} />
)} -
- +
+ = ({ onChange={handleChange} placeholder="https://example.com(可选)" autoComplete="off" + className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors" />
{/* Claude 或 Codex 的配置部分 */} {isCodex ? ( // Codex: 双编辑器 - <> -
- +
+
+