feat: 系统托盘 (#12)
* 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<void> -> Promise<boolean>) - 添加缺失的 UnlistenFn 类型导入 - 使用 MenuBuilder.separator() 替代文本分隔符 --------- Co-authored-by: farion1231 <farion1231@gmail.c
This commit is contained in:
@@ -30,9 +30,12 @@
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.38.2",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tauri-apps/api": "^2.8.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"lucide-react": "^0.542.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^4.1.13"
|
||||
}
|
||||
}
|
||||
|
||||
410
pnpm-lock.yaml
generated
410
pnpm-lock.yaml
generated
@@ -20,18 +20,27 @@ importers:
|
||||
'@codemirror/view':
|
||||
specifier: ^6.38.2
|
||||
version: 6.38.2
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.13
|
||||
version: 4.1.13(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.8.0
|
||||
version: 2.8.0
|
||||
codemirror:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
lucide-react:
|
||||
specifier: ^0.542.0
|
||||
version: 0.542.0(react@18.3.1)
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
tailwindcss:
|
||||
specifier: ^4.1.13
|
||||
version: 4.1.13
|
||||
devDependencies:
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^2.8.0
|
||||
@@ -47,7 +56,7 @@ importers:
|
||||
version: 18.3.7(@types/react@18.3.23)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.2.0
|
||||
version: 4.7.0(vite@5.4.19(@types/node@20.19.9))
|
||||
version: 4.7.0(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
@@ -56,7 +65,7 @@ importers:
|
||||
version: 5.9.2
|
||||
vite:
|
||||
specifier: ^5.0.0
|
||||
version: 5.4.19(@types/node@20.19.9)
|
||||
version: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -312,9 +321,16 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||
engines: {node: '>=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: {}
|
||||
|
||||
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@tailwindcss/oxide'
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Menu<tauri::Wry>, 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::<AppState>() {
|
||||
// 在使用前先保存需要的值
|
||||
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<bool, String> {
|
||||
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");
|
||||
|
||||
242
src/App.css
242
src/App.css
@@ -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;
|
||||
}
|
||||
}
|
||||
136
src/App.tsx
136
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<AppType>("claude");
|
||||
@@ -18,7 +18,7 @@ function App() {
|
||||
path: string;
|
||||
} | null>(null);
|
||||
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
||||
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 (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<h1>CC Switch</h1>
|
||||
<div className="app-tabs">
|
||||
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<button className="add-btn" onClick={() => setIsAddModalOpen(true)}>
|
||||
添加供应商
|
||||
</button>
|
||||
<div className="min-h-screen flex flex-col bg-[var(--color-bg-primary)]">
|
||||
{/* Linear 风格的顶部导航 */}
|
||||
<header className="bg-white border-b border-[var(--color-border)] px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-[var(--color-text-primary)]">
|
||||
CC Switch
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
|
||||
|
||||
<button
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-hover)] transition-colors text-sm font-medium"
|
||||
>
|
||||
<Plus size={16} />
|
||||
添加供应商
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="app-main">
|
||||
<div className="provider-section">
|
||||
{/* 浮动通知组件 */}
|
||||
{/* 主内容区域 */}
|
||||
<main className="flex-1 p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* 通知组件 */}
|
||||
{notification && (
|
||||
<div
|
||||
className={`notification-floating ${
|
||||
className={`fixed top-6 left-1/2 transform -translate-x-1/2 z-50 px-4 py-3 rounded-lg shadow-lg transition-all duration-300 ${
|
||||
notification.type === "error"
|
||||
? "notification-error"
|
||||
: "notification-success"
|
||||
} ${isNotificationVisible ? "fade-in" : "fade-out"}`}
|
||||
? "bg-[var(--color-error)] text-white"
|
||||
: "bg-[var(--color-success)] text-white"
|
||||
} ${isNotificationVisible ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2"}`}
|
||||
>
|
||||
{notification.message}
|
||||
</div>
|
||||
@@ -210,23 +259,36 @@ function App() {
|
||||
onDelete={handleDeleteProvider}
|
||||
onEdit={setEditingProviderId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{configStatus && (
|
||||
<div className="config-path">
|
||||
<span>
|
||||
配置文件位置: {configStatus.path}
|
||||
{!configStatus.exists ? "(未创建,切换或保存时会自动创建)" : ""}
|
||||
</span>
|
||||
<button
|
||||
className="browse-btn"
|
||||
onClick={handleOpenConfigFolder}
|
||||
title="打开配置文件夹"
|
||||
>
|
||||
打开
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* 配置文件路径信息 */}
|
||||
{configStatus && (
|
||||
<div className="mt-8 p-4 bg-white rounded-lg border border-[var(--color-border)]">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-[var(--color-text-secondary)]">
|
||||
<span className="font-medium">
|
||||
{activeApp === "claude" ? "Claude Code" : "Codex"}{" "}
|
||||
配置文件位置:
|
||||
</span>
|
||||
<span className="ml-2 font-mono text-xs">
|
||||
{configStatus.path}
|
||||
</span>
|
||||
{!configStatus.exists && (
|
||||
<span className="ml-2 text-[var(--color-warning)]">
|
||||
(未创建,切换或保存时会自动创建)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOpenConfigFolder}
|
||||
className="px-3 py-1.5 text-sm font-medium text-[var(--color-primary)] hover:bg-[var(--color-bg-tertiary)] rounded-md transition-colors"
|
||||
title="打开配置文件夹"
|
||||
>
|
||||
打开文件夹
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{isAddModalOpen && (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="switcher-pills">
|
||||
<div className="inline-flex bg-[var(--color-bg-tertiary)] rounded-lg p-1 gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`switcher-pill ${activeApp === "claude" ? "active" : ""}`}
|
||||
onClick={() => handleSwitch("claude")}
|
||||
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
activeApp === "claude"
|
||||
? "bg-white text-[var(--color-text-primary)] shadow-sm"
|
||||
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-white/50"
|
||||
}`}
|
||||
>
|
||||
<span className="pill-dot" />
|
||||
<Code2 size={16} />
|
||||
<span>Claude Code</span>
|
||||
</button>
|
||||
<div className="pills-divider" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`switcher-pill ${activeApp === "codex" ? "active" : ""}`}
|
||||
onClick={() => handleSwitch("codex")}
|
||||
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
activeApp === "codex"
|
||||
? "bg-white text-[var(--color-text-primary)] shadow-sm"
|
||||
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-white/50"
|
||||
}`}
|
||||
>
|
||||
<span className="pill-dot" />
|
||||
<Terminal size={16} />
|
||||
<span>Codex</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<ConfirmDialogProps> = ({
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="confirm-overlay">
|
||||
<div className="confirm-dialog">
|
||||
<div className="confirm-header">
|
||||
<h3>{title}</h3>
|
||||
</div>
|
||||
<div className="confirm-content">
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
<div className="confirm-actions">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div className="relative bg-white rounded-xl shadow-lg max-w-md w-full mx-4 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-[var(--color-error-light)] rounded-full flex items-center justify-center">
|
||||
<AlertTriangle size={20} className="text-[var(--color-error)]" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--color-text-primary)]">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
className="confirm-btn cancel-btn"
|
||||
onClick={onCancel}
|
||||
className="p-1 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)] rounded-md transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<p className="text-[var(--color-text-secondary)] leading-relaxed">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-3 p-6 border-t border-[var(--color-border)] bg-[var(--color-bg-tertiary)]">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-white rounded-md transition-colors"
|
||||
autoFocus
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
className="confirm-btn confirm-btn-primary"
|
||||
onClick={onConfirm}
|
||||
className="px-4 py-2 text-sm font-medium bg-[var(--color-error)] text-white hover:bg-[var(--color-error)]/90 rounded-md transition-colors"
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
|
||||
@@ -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<ProviderFormProps> = ({
|
||||
const [codexApiKey, setCodexApiKey] = useState("");
|
||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
||||
showPresets && isCodex ? -1 : null,
|
||||
showPresets && isCodex ? -1 : null
|
||||
);
|
||||
|
||||
// 初始化 Codex 配置
|
||||
@@ -74,7 +74,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||
showPresets ? -1 : null,
|
||||
showPresets ? -1 : null
|
||||
);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
|
||||
@@ -155,7 +155,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
@@ -188,7 +188,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 更新JSON配置
|
||||
const updatedConfig = updateCoAuthoredSetting(
|
||||
formData.settingsConfig,
|
||||
checked,
|
||||
checked
|
||||
);
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -231,7 +231,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 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<ProviderFormProps> = ({
|
||||
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<ProviderFormProps> = ({
|
||||
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<ProviderFormProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-overlay"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
onMouseDown={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-titlebar">
|
||||
<div className="modal-spacer" />
|
||||
<div className="modal-title" title={title}>
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white rounded-xl shadow-lg max-w-3xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border)]">
|
||||
<h2 className="text-xl font-semibold text-[var(--color-text-primary)]">
|
||||
{title}
|
||||
</div>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="modal-close-btn"
|
||||
aria-label="关闭"
|
||||
onClick={onClose}
|
||||
title="关闭"
|
||||
className="p-1 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)] rounded-md transition-colors"
|
||||
aria-label="关闭"
|
||||
>
|
||||
×
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="modal-form">
|
||||
<div className="modal-body">
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex-1 overflow-auto p-6 space-y-6">
|
||||
{error && (
|
||||
<div className="flex items-center gap-3 p-4 bg-[var(--color-error-light)] border border-[var(--color-error)]/20 rounded-lg">
|
||||
<AlertCircle
|
||||
size={20}
|
||||
className="text-[var(--color-error)] flex-shrink-0"
|
||||
/>
|
||||
<p className="text-[var(--color-error)] text-sm font-medium">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPresets && !isCodex && (
|
||||
<div className="presets">
|
||||
<label>选择配置类型</label>
|
||||
<div className="preset-buttons">
|
||||
<button
|
||||
type="button"
|
||||
className={`preset-btn ${
|
||||
selectedPreset === -1 ? "selected" : ""
|
||||
}`}
|
||||
onClick={handleCustomClick}
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
{providerPresets.map((preset, index) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
||||
选择配置类型
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedPreset === -1
|
||||
? "bg-[var(--color-primary)] text-white"
|
||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||
}`}
|
||||
onClick={handleCustomClick}
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
{providerPresets.map((preset, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`preset-btn ${
|
||||
selectedPreset === index ? "selected" : ""
|
||||
} ${preset.isOfficial ? "official" : ""}`}
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedPreset === index
|
||||
? preset.isOfficial
|
||||
? "bg-[var(--color-warning)] text-white"
|
||||
: "bg-[var(--color-primary)] text-white"
|
||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||
}`}
|
||||
onClick={() => applyPreset(preset, index)}
|
||||
>
|
||||
{preset.isOfficial && <Zap size={14} />}
|
||||
{preset.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{selectedPreset === -1 && (
|
||||
<small
|
||||
className="field-hint"
|
||||
style={{ marginTop: "8px", display: "block" }}
|
||||
>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
手动配置供应商,需要填写完整的配置信息
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
{selectedPreset !== -1 && selectedPreset !== null && (
|
||||
<small
|
||||
className="field-hint"
|
||||
style={{ marginTop: "8px", display: "block" }}
|
||||
>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
{isOfficialPreset
|
||||
? "Claude 官方登录,不需要填写 API Key"
|
||||
: "使用预设配置,只需填写 API Key"}
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPresets && isCodex && (
|
||||
<div className="presets">
|
||||
<label>选择配置类型</label>
|
||||
<div className="preset-buttons">
|
||||
<button
|
||||
type="button"
|
||||
className={`preset-btn ${
|
||||
selectedCodexPreset === -1 ? "selected" : ""
|
||||
}`}
|
||||
onClick={handleCodexCustomClick}
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
{codexProviderPresets.map((preset, index) => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
||||
选择配置类型
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`preset-btn ${
|
||||
selectedCodexPreset === index ? "selected" : ""
|
||||
} ${preset.isOfficial ? "official" : ""}`}
|
||||
onClick={() => applyCodexPreset(preset, index)}
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedCodexPreset === -1
|
||||
? "bg-[var(--color-primary)] text-white"
|
||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||
}`}
|
||||
onClick={handleCodexCustomClick}
|
||||
>
|
||||
{preset.name}
|
||||
自定义
|
||||
</button>
|
||||
))}
|
||||
{codexProviderPresets.map((preset, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedCodexPreset === index
|
||||
? preset.isOfficial
|
||||
? "bg-[var(--color-warning)] text-white"
|
||||
: "bg-[var(--color-primary)] text-white"
|
||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||
}`}
|
||||
onClick={() => applyCodexPreset(preset, index)}
|
||||
>
|
||||
{preset.isOfficial && <Zap size={14} />}
|
||||
{preset.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{selectedCodexPreset === -1 && (
|
||||
<small
|
||||
className="field-hint"
|
||||
style={{ marginTop: "8px", display: "block" }}
|
||||
>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
手动配置供应商,需要填写完整的配置信息
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
{selectedCodexPreset !== -1 && selectedCodexPreset !== null && (
|
||||
<small
|
||||
className="field-hint"
|
||||
style={{ marginTop: "8px", display: "block" }}
|
||||
>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
{isCodexOfficialPreset
|
||||
? "Codex 官方登录,不需要填写 API Key"
|
||||
: "使用预设配置,只需填写 API Key"}
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">供应商名称 *</label>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
供应商名称 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
@@ -483,14 +509,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isCodex && (
|
||||
<div
|
||||
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
|
||||
>
|
||||
<label htmlFor="apiKey">API Key *</label>
|
||||
{!isCodex && showApiKey && (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="apiKey"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
API Key *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="apiKey"
|
||||
@@ -503,24 +533,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
}
|
||||
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)]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCodex && (
|
||||
<div
|
||||
className={`form-group api-key-group ${!showCodexApiKey ? "hidden" : ""}`}
|
||||
>
|
||||
<label htmlFor="codexApiKey">API Key *</label>
|
||||
{isCodex && showCodexApiKey && (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="codexApiKey"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
API Key *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="codexApiKey"
|
||||
@@ -538,21 +567,22 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
!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)]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="websiteUrl">官网地址</label>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="websiteUrl"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
官网地址
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="websiteUrl"
|
||||
@@ -561,15 +591,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Claude 或 Codex 的配置部分 */}
|
||||
{isCodex ? (
|
||||
// Codex: 双编辑器
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="codexAuth">auth.json (JSON) *</label>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="codexAuth"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
auth.json (JSON) *
|
||||
</label>
|
||||
<textarea
|
||||
id="codexAuth"
|
||||
value={codexAuth}
|
||||
@@ -591,47 +627,61 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
"OPENAI_API_KEY": "sk-your-api-key-here"
|
||||
}`}
|
||||
rows={6}
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors resize-y min-h-[8rem]"
|
||||
/>
|
||||
<small className="field-hint">Codex auth.json 配置内容</small>
|
||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||
Codex auth.json 配置内容
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="codexConfig">config.toml (TOML)</label>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="codexConfig"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
config.toml (TOML)
|
||||
</label>
|
||||
<textarea
|
||||
id="codexConfig"
|
||||
value={codexConfig}
|
||||
onChange={(e) => setCodexConfig(e.target.value)}
|
||||
placeholder={``}
|
||||
placeholder=""
|
||||
rows={8}
|
||||
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors resize-y min-h-[10rem]"
|
||||
/>
|
||||
<small className="field-hint">
|
||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||
Codex config.toml 配置内容
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
// Claude: 原有的单编辑器
|
||||
<div className="form-group">
|
||||
<div className="label-with-checkbox">
|
||||
<label htmlFor="settingsConfig">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="settingsConfig"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
Claude Code 配置 (JSON) *
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<label className="inline-flex items-center gap-2 text-sm text-[var(--color-text-secondary)] cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={disableCoAuthored}
|
||||
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
|
||||
className="w-4 h-4 text-[var(--color-primary)] bg-white border-[var(--color-border)] rounded focus:ring-[var(--color-primary)] focus:ring-2"
|
||||
/>
|
||||
禁止 Claude Code 签名
|
||||
</label>
|
||||
</div>
|
||||
<JsonEditor
|
||||
value={formData.settingsConfig}
|
||||
onChange={(value) => handleChange({
|
||||
target: { name: "settingsConfig", value }
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>)}
|
||||
onChange={(value) =>
|
||||
handleChange({
|
||||
target: { name: "settingsConfig", value },
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||
}
|
||||
placeholder={`{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||
@@ -640,18 +690,27 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
}`}
|
||||
rows={12}
|
||||
/>
|
||||
<small className="field-hint">
|
||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||
完整的 Claude Code settings.json 配置内容
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="cancel-btn" onClick={onClose}>
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 p-6 border-t border-[var(--color-border)] bg-[var(--color-bg-tertiary)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-white rounded-lg transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" className="submit-btn">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-hover)] transition-colors text-sm font-medium"
|
||||
>
|
||||
<Save size={16} />
|
||||
{submitText}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
.provider-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.empty-state p:first-child {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.provider-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border: 2px solid #ecf0f1;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.provider-item:hover {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.provider-item.current {
|
||||
border-color: #27ae60;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.provider-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.provider-name input[type="radio"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.provider-name input[type="radio"]:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.current-badge {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.provider-url {
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.url-link {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.url-link:hover {
|
||||
color: #2980b9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.api-url {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.provider-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.response-time {
|
||||
color: #3498db;
|
||||
font-size: 0.85rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.check-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #f39c12;
|
||||
background: white;
|
||||
color: #f39c12;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.check-btn:hover:not(:disabled) {
|
||||
background: #f39c12;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.check-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.enable-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #27ae60;
|
||||
background: white;
|
||||
color: #27ae60;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.enable-btn:hover:not(:disabled) {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.enable-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.edit-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;
|
||||
}
|
||||
|
||||
.edit-btn:hover:not(:disabled) {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.edit-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #e74c3c;
|
||||
background: white;
|
||||
color: #e74c3c;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.delete-btn:hover:not(:disabled) {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import React from "react";
|
||||
import { Provider } from "../types";
|
||||
import "./ProviderList.css";
|
||||
import {
|
||||
Play,
|
||||
Edit3,
|
||||
Trash2,
|
||||
ExternalLink,
|
||||
CheckCircle2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
|
||||
interface ProviderListProps {
|
||||
providers: Record<string, Provider>;
|
||||
@@ -39,71 +46,104 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="provider-list">
|
||||
<div className="space-y-4">
|
||||
{Object.values(providers).length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>还没有添加任何供应商</p>
|
||||
<p>点击右上角的"添加供应商"按钮开始</p>
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-[var(--color-bg-tertiary)] rounded-full flex items-center justify-center">
|
||||
<Users size={24} className="text-[var(--color-text-tertiary)]" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-[var(--color-text-primary)] mb-2">
|
||||
还没有添加任何供应商
|
||||
</h3>
|
||||
<p className="text-[var(--color-text-secondary)] text-sm">
|
||||
点击右上角的"添加供应商"按钮开始配置您的第一个API供应商
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="provider-items">
|
||||
<div className="space-y-3">
|
||||
{Object.values(providers).map((provider) => {
|
||||
const isCurrent = provider.id === currentProviderId;
|
||||
const apiUrl = getApiUrl(provider);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={provider.id}
|
||||
className={`provider-item ${isCurrent ? "current" : ""}`}
|
||||
className={`bg-white rounded-lg border p-4 transition-all duration-200 ${
|
||||
isCurrent
|
||||
? "border-[var(--color-primary)] ring-1 ring-[var(--color-primary)]/20 bg-[var(--color-primary)]/5"
|
||||
: "border-[var(--color-border)] hover:border-[var(--color-border-hover)] hover:shadow-sm"
|
||||
}`}
|
||||
>
|
||||
<div className="provider-info">
|
||||
<div className="provider-name">
|
||||
<span>{provider.name}</span>
|
||||
{isCurrent && (
|
||||
<span className="current-badge">当前使用</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="provider-url">
|
||||
{provider.websiteUrl ? (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleUrlClick(provider.websiteUrl!);
|
||||
}}
|
||||
className="url-link"
|
||||
title={`访问 ${provider.websiteUrl}`}
|
||||
>
|
||||
{provider.websiteUrl}
|
||||
</a>
|
||||
) : (
|
||||
<span className="api-url" title={getApiUrl(provider)}>
|
||||
{getApiUrl(provider)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="font-medium text-[var(--color-text-primary)]">
|
||||
{provider.name}
|
||||
</h3>
|
||||
{isCurrent && (
|
||||
<div className="inline-flex items-center gap-1 px-2 py-1 bg-[var(--color-success)]/10 text-[var(--color-success)] rounded-md text-xs font-medium">
|
||||
<CheckCircle2 size={12} />
|
||||
当前使用
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="provider-actions">
|
||||
<button
|
||||
className="enable-btn"
|
||||
onClick={() => onSwitch(provider.id)}
|
||||
disabled={isCurrent}
|
||||
>
|
||||
启用
|
||||
</button>
|
||||
<button
|
||||
className="edit-btn"
|
||||
onClick={() => onEdit(provider.id)}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
className="delete-btn"
|
||||
onClick={() => onDelete(provider.id)}
|
||||
disabled={isCurrent}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-text-secondary)]">
|
||||
{provider.websiteUrl ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleUrlClick(provider.websiteUrl!);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 hover:text-[var(--color-primary)] transition-colors"
|
||||
title={`访问 ${provider.websiteUrl}`}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
{provider.websiteUrl}
|
||||
</button>
|
||||
) : (
|
||||
<span className="font-mono" title={apiUrl}>
|
||||
{apiUrl}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<button
|
||||
onClick={() => onSwitch(provider.id)}
|
||||
disabled={isCurrent}
|
||||
className={`inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
isCurrent
|
||||
? "bg-[var(--color-bg-tertiary)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||
: "bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)]"
|
||||
}`}
|
||||
>
|
||||
<Play size={14} />
|
||||
{isCurrent ? "使用中" : "启用"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onEdit(provider.id)}
|
||||
className="p-1.5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)] rounded-md transition-colors"
|
||||
title="编辑供应商"
|
||||
>
|
||||
<Edit3 size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onDelete(provider.id)}
|
||||
disabled={isCurrent}
|
||||
className={`p-1.5 rounded-md transition-colors ${
|
||||
isCurrent
|
||||
? "text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||
: "text-[var(--color-text-secondary)] hover:text-[var(--color-error)] hover:bg-[var(--color-error-light)]"
|
||||
}`}
|
||||
title="删除供应商"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,30 +1,89 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Linear 风格的全局配色和基础样式 */
|
||||
:root {
|
||||
/* Linear 配色方案 */
|
||||
--color-bg-primary: #fafafa;
|
||||
--color-bg-secondary: #ffffff;
|
||||
--color-bg-tertiary: #f4f4f5;
|
||||
--color-border: #e4e4e7;
|
||||
--color-border-hover: #d4d4d8;
|
||||
|
||||
/* 蓝色主色调 */
|
||||
--color-primary: #3498db;
|
||||
--color-primary-hover: #2980b9;
|
||||
--color-primary-light: #5dade2;
|
||||
|
||||
/* 文本颜色 */
|
||||
--color-text-primary: #18181b;
|
||||
--color-text-secondary: #71717a;
|
||||
--color-text-tertiary: #a1a1aa;
|
||||
|
||||
/* 状态颜色 */
|
||||
--color-success: #10b981;
|
||||
--color-success-light: #d1fae5;
|
||||
--color-error: #ef4444;
|
||||
--color-error-light: #fee2e2;
|
||||
--color-warning: #f59e0b;
|
||||
--color-warning-light: #fef3c7;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
|
||||
/* 字体 */
|
||||
--font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue",
|
||||
Arial, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
html {
|
||||
font-family: var(--font-family);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 仅在 macOS 下为顶部预留交通灯空间 */
|
||||
/* 保持 mac 下与内容区域左对齐(不额外偏移) */
|
||||
|
||||
/* 在 macOS 下稍微增加 banner 高度,拉开与交通灯的垂直距离 */
|
||||
body.is-mac .app-header {
|
||||
padding-top: 1.4rem;
|
||||
padding-bottom: 1.4rem;
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border-hover);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* 焦点样式 */
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { Provider } from "../types";
|
||||
|
||||
// 应用类型
|
||||
@@ -52,7 +53,7 @@ export const tauriAPI = {
|
||||
// 更新供应商
|
||||
updateProvider: async (
|
||||
provider: Provider,
|
||||
app?: AppType,
|
||||
app?: AppType
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke("update_provider", { provider, app_type: app, app });
|
||||
@@ -75,7 +76,7 @@ export const tauriAPI = {
|
||||
// 切换供应商
|
||||
switchProvider: async (
|
||||
providerId: string,
|
||||
app?: AppType,
|
||||
app?: AppType
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke("switch_provider", {
|
||||
@@ -91,7 +92,7 @@ export const tauriAPI = {
|
||||
|
||||
// 导入当前配置为默认供应商
|
||||
importCurrentConfigAsDefault: async (
|
||||
app?: AppType,
|
||||
app?: AppType
|
||||
): Promise<ImportResult> => {
|
||||
try {
|
||||
const success = await invoke<boolean>("import_default_config", {
|
||||
@@ -167,6 +168,25 @@ export const tauriAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
// 更新托盘菜单
|
||||
updateTrayMenu: async (): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke("update_tray_menu");
|
||||
} catch (error) {
|
||||
console.error("更新托盘菜单失败:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// 监听供应商切换事件
|
||||
onProviderSwitched: async (
|
||||
callback: (data: { appType: string; providerId: string }) => void
|
||||
): Promise<UnlistenFn> => {
|
||||
return await listen("provider-switched", (event) => {
|
||||
callback(event.payload as { appType: string; providerId: string });
|
||||
});
|
||||
},
|
||||
|
||||
// (保留空位,取消迁移提示)
|
||||
|
||||
// 选择配置文件(Tauri 暂不实现,保留接口兼容性)
|
||||
|
||||
5
src/vite-env.d.ts
vendored
5
src/vite-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Provider } from "./types";
|
||||
import { AppType } from "./lib/tauri-api";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
|
||||
interface ImportResult {
|
||||
success: boolean;
|
||||
@@ -30,6 +31,10 @@ declare global {
|
||||
selectConfigFile: () => Promise<string | null>;
|
||||
openConfigFolder: (app?: AppType) => Promise<void>;
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
updateTrayMenu: () => Promise<boolean>;
|
||||
onProviderSwitched: (
|
||||
callback: (data: { appType: string; providerId: string }) => void
|
||||
) => Promise<UnlistenFn>;
|
||||
};
|
||||
platform: {
|
||||
isMac: boolean;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -11,5 +11,5 @@
|
||||
"strict": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
"include": ["vite.config.mts"]
|
||||
}
|
||||
|
||||
19
vite.config.mts
Normal file
19
vite.config.mts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
root: "src",
|
||||
plugins: [react(), tailwindcss()],
|
||||
base: "./",
|
||||
build: {
|
||||
outDir: "../dist",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
strictPort: true,
|
||||
},
|
||||
clearScreen: false,
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
root: 'src',
|
||||
plugins: [react()],
|
||||
base: './',
|
||||
build: {
|
||||
outDir: '../dist',
|
||||
emptyOutDir: true
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
strictPort: true
|
||||
},
|
||||
clearScreen: false,
|
||||
envPrefix: ['VITE_', 'TAURI_']
|
||||
})
|
||||
Reference in New Issue
Block a user