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/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@codemirror/view": "^6.38.2",
|
"@codemirror/view": "^6.38.2",
|
||||||
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@tauri-apps/api": "^2.8.0",
|
"@tauri-apps/api": "^2.8.0",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
|
"lucide-react": "^0.542.0",
|
||||||
"react": "^18.2.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':
|
'@codemirror/view':
|
||||||
specifier: ^6.38.2
|
specifier: ^6.38.2
|
||||||
version: 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':
|
'@tauri-apps/api':
|
||||||
specifier: ^2.8.0
|
specifier: ^2.8.0
|
||||||
version: 2.8.0
|
version: 2.8.0
|
||||||
codemirror:
|
codemirror:
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
|
lucide-react:
|
||||||
|
specifier: ^0.542.0
|
||||||
|
version: 0.542.0(react@18.3.1)
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
|
tailwindcss:
|
||||||
|
specifier: ^4.1.13
|
||||||
|
version: 4.1.13
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: ^2.8.0
|
specifier: ^2.8.0
|
||||||
@@ -47,7 +56,7 @@ importers:
|
|||||||
version: 18.3.7(@types/react@18.3.23)
|
version: 18.3.7(@types/react@18.3.23)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.2.0
|
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:
|
prettier:
|
||||||
specifier: ^3.6.2
|
specifier: ^3.6.2
|
||||||
version: 3.6.2
|
version: 3.6.2
|
||||||
@@ -56,7 +65,7 @@ importers:
|
|||||||
version: 5.9.2
|
version: 5.9.2
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.0.0
|
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:
|
packages:
|
||||||
|
|
||||||
@@ -312,9 +321,16 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
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':
|
'@jridgewell/gen-mapping@0.3.12':
|
||||||
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
|
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
|
||||||
|
|
||||||
|
'@jridgewell/remapping@2.3.5':
|
||||||
|
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2':
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -322,6 +338,9 @@ packages:
|
|||||||
'@jridgewell/sourcemap-codec@1.5.4':
|
'@jridgewell/sourcemap-codec@1.5.4':
|
||||||
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
|
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':
|
'@jridgewell/trace-mapping@0.3.29':
|
||||||
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
|
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
|
||||||
|
|
||||||
@@ -443,6 +462,96 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
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':
|
'@tauri-apps/api@2.8.0':
|
||||||
resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==}
|
resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==}
|
||||||
|
|
||||||
@@ -560,6 +669,10 @@ packages:
|
|||||||
caniuse-lite@1.0.30001731:
|
caniuse-lite@1.0.30001731:
|
||||||
resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==}
|
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:
|
codemirror@6.0.2:
|
||||||
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
|
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
|
||||||
|
|
||||||
@@ -581,9 +694,17 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
detect-libc@2.0.4:
|
||||||
|
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
electron-to-chromium@1.5.197:
|
electron-to-chromium@1.5.197:
|
||||||
resolution: {integrity: sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==}
|
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:
|
esbuild@0.21.5:
|
||||||
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
|
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -602,6 +723,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
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:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -615,6 +743,70 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
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:
|
loose-envify@1.4.0:
|
||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -622,6 +814,27 @@ packages:
|
|||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
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:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -677,6 +890,17 @@ packages:
|
|||||||
style-mod@4.1.2:
|
style-mod@4.1.2:
|
||||||
resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
|
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:
|
typescript@5.9.2:
|
||||||
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@@ -728,6 +952,10 @@ packages:
|
|||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
|
yallist@5.0.0:
|
||||||
|
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
@@ -974,15 +1202,26 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.21.5':
|
'@esbuild/win32-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
minipass: 7.1.2
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.12':
|
'@jridgewell/gen-mapping@0.3.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.4
|
'@jridgewell/sourcemap-codec': 1.5.4
|
||||||
'@jridgewell/trace-mapping': 0.3.29
|
'@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/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.4': {}
|
'@jridgewell/sourcemap-codec@1.5.4': {}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.29':
|
'@jridgewell/trace-mapping@0.3.29':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
@@ -1068,6 +1307,77 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.46.2':
|
'@rollup/rollup-win32-x64-msvc@4.46.2':
|
||||||
optional: true
|
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/api@2.8.0': {}
|
||||||
|
|
||||||
'@tauri-apps/cli-darwin-arm64@2.8.1':
|
'@tauri-apps/cli-darwin-arm64@2.8.1':
|
||||||
@@ -1155,7 +1465,7 @@ snapshots:
|
|||||||
'@types/prop-types': 15.7.15
|
'@types/prop-types': 15.7.15
|
||||||
csstype: 3.1.3
|
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:
|
dependencies:
|
||||||
'@babel/core': 7.28.0
|
'@babel/core': 7.28.0
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@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
|
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -1176,6 +1486,8 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001731: {}
|
caniuse-lite@1.0.30001731: {}
|
||||||
|
|
||||||
|
chownr@3.0.0: {}
|
||||||
|
|
||||||
codemirror@6.0.2:
|
codemirror@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/autocomplete': 6.18.7
|
'@codemirror/autocomplete': 6.18.7
|
||||||
@@ -1196,8 +1508,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
detect-libc@2.0.4: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.197: {}
|
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:
|
esbuild@0.21.5:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.21.5
|
'@esbuild/aix-ppc64': 0.21.5
|
||||||
@@ -1231,12 +1550,61 @@ snapshots:
|
|||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
jiti@2.5.1: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
json5@2.2.3: {}
|
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:
|
loose-envify@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
@@ -1245,6 +1613,22 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
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: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
@@ -1309,6 +1693,19 @@ snapshots:
|
|||||||
|
|
||||||
style-mod@4.1.2: {}
|
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: {}
|
typescript@5.9.2: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
@@ -1319,7 +1716,7 @@ snapshots:
|
|||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
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:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@@ -1327,7 +1724,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 20.19.9
|
'@types/node': 20.19.9
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
lightningcss: 1.30.1
|
||||||
|
|
||||||
w3c-keyname@2.2.8: {}
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
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_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tauri = { version = "2.8.2", features = [] }
|
tauri = { version = "2.8.2", features = ["tray-icon"] }
|
||||||
tauri-plugin-log = "2"
|
tauri-plugin-log = "2"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
|
|||||||
@@ -2,12 +2,220 @@ mod app_config;
|
|||||||
mod codex_config;
|
mod codex_config;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod migration;
|
||||||
mod provider;
|
mod provider;
|
||||||
mod store;
|
mod store;
|
||||||
mod migration;
|
|
||||||
|
|
||||||
use store::AppState;
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
@@ -71,6 +279,36 @@ pub fn run() {
|
|||||||
// 保存配置
|
// 保存配置
|
||||||
let _ = app_state.save();
|
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);
|
app.manage(app_state);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -88,6 +326,7 @@ pub fn run() {
|
|||||||
commands::get_claude_code_config_path,
|
commands::get_claude_code_config_path,
|
||||||
commands::open_config_folder,
|
commands::open_config_folder,
|
||||||
commands::open_external,
|
commands::open_external,
|
||||||
|
update_tray_menu,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.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 EditProviderModal from "./components/EditProviderModal";
|
||||||
import { ConfirmDialog } from "./components/ConfirmDialog";
|
import { ConfirmDialog } from "./components/ConfirmDialog";
|
||||||
import { AppSwitcher } from "./components/AppSwitcher";
|
import { AppSwitcher } from "./components/AppSwitcher";
|
||||||
import "./App.css";
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
||||||
@@ -18,7 +18,7 @@ function App() {
|
|||||||
path: string;
|
path: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
||||||
null,
|
null
|
||||||
);
|
);
|
||||||
const [notification, setNotification] = useState<{
|
const [notification, setNotification] = useState<{
|
||||||
message: string;
|
message: string;
|
||||||
@@ -37,7 +37,7 @@ function App() {
|
|||||||
const showNotification = (
|
const showNotification = (
|
||||||
message: string,
|
message: string,
|
||||||
type: "success" | "error",
|
type: "success" | "error",
|
||||||
duration = 3000,
|
duration = 3000
|
||||||
) => {
|
) => {
|
||||||
// 清除之前的定时器
|
// 清除之前的定时器
|
||||||
if (timeoutRef.current) {
|
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 loadProviders = async () => {
|
||||||
const loadedProviders = await window.api.getProviders(activeApp);
|
const loadedProviders = await window.api.getProviders(activeApp);
|
||||||
const currentId = await window.api.getCurrentProvider(activeApp);
|
const currentId = await window.api.getCurrentProvider(activeApp);
|
||||||
@@ -107,6 +136,8 @@ function App() {
|
|||||||
await window.api.addProvider(newProvider, activeApp);
|
await window.api.addProvider(newProvider, activeApp);
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditProvider = async (provider: Provider) => {
|
const handleEditProvider = async (provider: Provider) => {
|
||||||
@@ -116,6 +147,8 @@ function App() {
|
|||||||
setEditingProviderId(null);
|
setEditingProviderId(null);
|
||||||
// 显示编辑成功提示
|
// 显示编辑成功提示
|
||||||
showNotification("供应商配置已保存", "success", 2000);
|
showNotification("供应商配置已保存", "success", 2000);
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("更新供应商失败:", error);
|
console.error("更新供应商失败:", error);
|
||||||
setEditingProviderId(null);
|
setEditingProviderId(null);
|
||||||
@@ -134,6 +167,8 @@ function App() {
|
|||||||
await loadProviders();
|
await loadProviders();
|
||||||
setConfirmDialog(null);
|
setConfirmDialog(null);
|
||||||
showNotification("供应商删除成功", "success");
|
showNotification("供应商删除成功", "success");
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -147,8 +182,10 @@ function App() {
|
|||||||
showNotification(
|
showNotification(
|
||||||
`切换成功!请重启 ${appName} 终端以生效`,
|
`切换成功!请重启 ${appName} 终端以生效`,
|
||||||
"success",
|
"success",
|
||||||
2000,
|
2000
|
||||||
);
|
);
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
} else {
|
} else {
|
||||||
showNotification("切换失败,请检查配置", "error");
|
showNotification("切换失败,请检查配置", "error");
|
||||||
}
|
}
|
||||||
@@ -162,6 +199,8 @@ function App() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
showNotification("已从现有配置创建默认供应商", "success", 3000);
|
showNotification("已从现有配置创建默认供应商", "success", 3000);
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
}
|
}
|
||||||
// 如果导入失败(比如没有现有配置),静默处理,不显示错误
|
// 如果导入失败(比如没有现有配置),静默处理,不显示错误
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -175,29 +214,39 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="min-h-screen flex flex-col bg-[var(--color-bg-primary)]">
|
||||||
<header className="app-header">
|
{/* Linear 风格的顶部导航 */}
|
||||||
<h1>CC Switch</h1>
|
<header className="bg-white border-b border-[var(--color-border)] px-6 py-4">
|
||||||
<div className="app-tabs">
|
<div className="flex items-center justify-between">
|
||||||
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
|
<h1 className="text-xl font-semibold text-[var(--color-text-primary)]">
|
||||||
</div>
|
CC Switch
|
||||||
<div className="header-actions">
|
</h1>
|
||||||
<button className="add-btn" onClick={() => setIsAddModalOpen(true)}>
|
|
||||||
添加供应商
|
<div className="flex items-center gap-4">
|
||||||
</button>
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="app-main">
|
{/* 主内容区域 */}
|
||||||
<div className="provider-section">
|
<main className="flex-1 p-6">
|
||||||
{/* 浮动通知组件 */}
|
<div className="max-w-4xl mx-auto">
|
||||||
|
{/* 通知组件 */}
|
||||||
{notification && (
|
{notification && (
|
||||||
<div
|
<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.type === "error"
|
||||||
? "notification-error"
|
? "bg-[var(--color-error)] text-white"
|
||||||
: "notification-success"
|
: "bg-[var(--color-success)] text-white"
|
||||||
} ${isNotificationVisible ? "fade-in" : "fade-out"}`}
|
} ${isNotificationVisible ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2"}`}
|
||||||
>
|
>
|
||||||
{notification.message}
|
{notification.message}
|
||||||
</div>
|
</div>
|
||||||
@@ -210,23 +259,36 @@ function App() {
|
|||||||
onDelete={handleDeleteProvider}
|
onDelete={handleDeleteProvider}
|
||||||
onEdit={setEditingProviderId}
|
onEdit={setEditingProviderId}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{configStatus && (
|
{/* 配置文件路径信息 */}
|
||||||
<div className="config-path">
|
{configStatus && (
|
||||||
<span>
|
<div className="mt-8 p-4 bg-white rounded-lg border border-[var(--color-border)]">
|
||||||
配置文件位置: {configStatus.path}
|
<div className="flex items-center justify-between">
|
||||||
{!configStatus.exists ? "(未创建,切换或保存时会自动创建)" : ""}
|
<div className="text-sm text-[var(--color-text-secondary)]">
|
||||||
</span>
|
<span className="font-medium">
|
||||||
<button
|
{activeApp === "claude" ? "Claude Code" : "Codex"}{" "}
|
||||||
className="browse-btn"
|
配置文件位置:
|
||||||
onClick={handleOpenConfigFolder}
|
</span>
|
||||||
title="打开配置文件夹"
|
<span className="ml-2 font-mono text-xs">
|
||||||
>
|
{configStatus.path}
|
||||||
打开
|
</span>
|
||||||
</button>
|
{!configStatus.exists && (
|
||||||
</div>
|
<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>
|
</main>
|
||||||
|
|
||||||
{isAddModalOpen && (
|
{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 { AppType } from "../lib/tauri-api";
|
||||||
import "./AppSwitcher.css";
|
import { Terminal, Code2 } from "lucide-react";
|
||||||
|
|
||||||
interface AppSwitcherProps {
|
interface AppSwitcherProps {
|
||||||
activeApp: AppType;
|
activeApp: AppType;
|
||||||
@@ -13,22 +13,30 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="switcher-pills">
|
<div className="inline-flex bg-[var(--color-bg-tertiary)] rounded-lg p-1 gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`switcher-pill ${activeApp === "claude" ? "active" : ""}`}
|
|
||||||
onClick={() => handleSwitch("claude")}
|
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>
|
<span>Claude Code</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="pills-divider" />
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`switcher-pill ${activeApp === "codex" ? "active" : ""}`}
|
|
||||||
onClick={() => handleSwitch("codex")}
|
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>
|
<span>Codex</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 React from "react";
|
||||||
import "./ConfirmDialog.css";
|
import { AlertTriangle, X } from "lucide-react";
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
interface ConfirmDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -23,25 +23,52 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="confirm-overlay">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
<div className="confirm-dialog">
|
{/* Backdrop */}
|
||||||
<div className="confirm-header">
|
<div
|
||||||
<h3>{title}</h3>
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||||
</div>
|
onClick={onCancel}
|
||||||
<div className="confirm-content">
|
/>
|
||||||
<p>{message}</p>
|
|
||||||
</div>
|
{/* Dialog */}
|
||||||
<div className="confirm-actions">
|
<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
|
<button
|
||||||
className="confirm-btn cancel-btn"
|
|
||||||
onClick={onCancel}
|
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
|
autoFocus
|
||||||
>
|
>
|
||||||
{cancelText}
|
{cancelText}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="confirm-btn confirm-btn-primary"
|
|
||||||
onClick={onConfirm}
|
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}
|
{confirmText}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
} from "../utils/providerConfigUtils";
|
} from "../utils/providerConfigUtils";
|
||||||
import { providerPresets } from "../config/providerPresets";
|
import { providerPresets } from "../config/providerPresets";
|
||||||
import { codexProviderPresets } from "../config/codexProviderPresets";
|
import { codexProviderPresets } from "../config/codexProviderPresets";
|
||||||
import "./AddProviderModal.css";
|
|
||||||
import JsonEditor from "./JsonEditor";
|
import JsonEditor from "./JsonEditor";
|
||||||
|
import { X, AlertCircle, Save, Zap } from "lucide-react";
|
||||||
|
|
||||||
interface ProviderFormProps {
|
interface ProviderFormProps {
|
||||||
appType?: AppType;
|
appType?: AppType;
|
||||||
@@ -49,7 +49,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [codexApiKey, setCodexApiKey] = useState("");
|
const [codexApiKey, setCodexApiKey] = useState("");
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
||||||
showPresets && isCodex ? -1 : null,
|
showPresets && isCodex ? -1 : null
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化 Codex 配置
|
// 初始化 Codex 配置
|
||||||
@@ -74,7 +74,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||||
showPresets ? -1 : null,
|
showPresets ? -1 : null
|
||||||
);
|
);
|
||||||
const [apiKey, setApiKey] = useState("");
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
) => {
|
) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 更新JSON配置
|
// 更新JSON配置
|
||||||
const updatedConfig = updateCoAuthoredSetting(
|
const updatedConfig = updateCoAuthoredSetting(
|
||||||
formData.settingsConfig,
|
formData.settingsConfig,
|
||||||
checked,
|
checked
|
||||||
);
|
);
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
@@ -231,7 +231,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// Codex: 应用预设
|
// Codex: 应用预设
|
||||||
const applyCodexPreset = (
|
const applyCodexPreset = (
|
||||||
preset: (typeof codexProviderPresets)[0],
|
preset: (typeof codexProviderPresets)[0],
|
||||||
index: number,
|
index: number
|
||||||
) => {
|
) => {
|
||||||
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
||||||
setCodexAuth(authString);
|
setCodexAuth(authString);
|
||||||
@@ -269,7 +269,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const configString = setApiKeyInConfig(
|
const configString = setApiKeyInConfig(
|
||||||
formData.settingsConfig,
|
formData.settingsConfig,
|
||||||
key.trim(),
|
key.trim(),
|
||||||
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 },
|
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新表单配置
|
// 更新表单配置
|
||||||
@@ -329,7 +329,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
const parsedKey = getApiKeyFromConfig(
|
const parsedKey = getApiKeyFromConfig(
|
||||||
JSON.stringify(initialData.settingsConfig),
|
JSON.stringify(initialData.settingsConfig)
|
||||||
);
|
);
|
||||||
if (parsedKey) setApiKey(parsedKey);
|
if (parsedKey) setApiKey(parsedKey);
|
||||||
}
|
}
|
||||||
@@ -350,130 +350,156 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="modal-overlay"
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
if (e.target === e.currentTarget) onClose();
|
if (e.target === e.currentTarget) onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="modal-content">
|
{/* Backdrop */}
|
||||||
<div className="modal-titlebar">
|
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
||||||
<div className="modal-spacer" />
|
|
||||||
<div className="modal-title" title={title}>
|
{/* 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}
|
{title}
|
||||||
</div>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="modal-close-btn"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={onClose}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="modal-form">
|
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
|
||||||
<div className="modal-body">
|
<div className="flex-1 overflow-auto p-6 space-y-6">
|
||||||
{error && <div className="error-message">{error}</div>}
|
{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 && (
|
{showPresets && !isCodex && (
|
||||||
<div className="presets">
|
<div className="space-y-4">
|
||||||
<label>选择配置类型</label>
|
<div>
|
||||||
<div className="preset-buttons">
|
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
||||||
<button
|
选择配置类型
|
||||||
type="button"
|
</label>
|
||||||
className={`preset-btn ${
|
<div className="flex flex-wrap gap-2">
|
||||||
selectedPreset === -1 ? "selected" : ""
|
<button
|
||||||
}`}
|
type="button"
|
||||||
onClick={handleCustomClick}
|
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"
|
||||||
</button>
|
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||||
{providerPresets.map((preset, index) => {
|
}`}
|
||||||
return (
|
onClick={handleCustomClick}
|
||||||
|
>
|
||||||
|
自定义
|
||||||
|
</button>
|
||||||
|
{providerPresets.map((preset, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
type="button"
|
type="button"
|
||||||
className={`preset-btn ${
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
selectedPreset === index ? "selected" : ""
|
selectedPreset === index
|
||||||
} ${preset.isOfficial ? "official" : ""}`}
|
? 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)}
|
onClick={() => applyPreset(preset, index)}
|
||||||
>
|
>
|
||||||
|
{preset.isOfficial && <Zap size={14} />}
|
||||||
{preset.name}
|
{preset.name}
|
||||||
</button>
|
</button>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selectedPreset === -1 && (
|
{selectedPreset === -1 && (
|
||||||
<small
|
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||||
className="field-hint"
|
|
||||||
style={{ marginTop: "8px", display: "block" }}
|
|
||||||
>
|
|
||||||
手动配置供应商,需要填写完整的配置信息
|
手动配置供应商,需要填写完整的配置信息
|
||||||
</small>
|
</p>
|
||||||
)}
|
)}
|
||||||
{selectedPreset !== -1 && selectedPreset !== null && (
|
{selectedPreset !== -1 && selectedPreset !== null && (
|
||||||
<small
|
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||||
className="field-hint"
|
|
||||||
style={{ marginTop: "8px", display: "block" }}
|
|
||||||
>
|
|
||||||
{isOfficialPreset
|
{isOfficialPreset
|
||||||
? "Claude 官方登录,不需要填写 API Key"
|
? "Claude 官方登录,不需要填写 API Key"
|
||||||
: "使用预设配置,只需填写 API Key"}
|
: "使用预设配置,只需填写 API Key"}
|
||||||
</small>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showPresets && isCodex && (
|
{showPresets && isCodex && (
|
||||||
<div className="presets">
|
<div className="space-y-4">
|
||||||
<label>选择配置类型</label>
|
<div>
|
||||||
<div className="preset-buttons">
|
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
||||||
<button
|
选择配置类型
|
||||||
type="button"
|
</label>
|
||||||
className={`preset-btn ${
|
<div className="flex flex-wrap gap-2">
|
||||||
selectedCodexPreset === -1 ? "selected" : ""
|
|
||||||
}`}
|
|
||||||
onClick={handleCodexCustomClick}
|
|
||||||
>
|
|
||||||
自定义
|
|
||||||
</button>
|
|
||||||
{codexProviderPresets.map((preset, index) => (
|
|
||||||
<button
|
<button
|
||||||
key={index}
|
|
||||||
type="button"
|
type="button"
|
||||||
className={`preset-btn ${
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
selectedCodexPreset === index ? "selected" : ""
|
selectedCodexPreset === -1
|
||||||
} ${preset.isOfficial ? "official" : ""}`}
|
? "bg-[var(--color-primary)] text-white"
|
||||||
onClick={() => applyCodexPreset(preset, index)}
|
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||||
|
}`}
|
||||||
|
onClick={handleCodexCustomClick}
|
||||||
>
|
>
|
||||||
{preset.name}
|
自定义
|
||||||
</button>
|
</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>
|
</div>
|
||||||
{selectedCodexPreset === -1 && (
|
{selectedCodexPreset === -1 && (
|
||||||
<small
|
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||||
className="field-hint"
|
|
||||||
style={{ marginTop: "8px", display: "block" }}
|
|
||||||
>
|
|
||||||
手动配置供应商,需要填写完整的配置信息
|
手动配置供应商,需要填写完整的配置信息
|
||||||
</small>
|
</p>
|
||||||
)}
|
)}
|
||||||
{selectedCodexPreset !== -1 && selectedCodexPreset !== null && (
|
{selectedCodexPreset !== -1 && selectedCodexPreset !== null && (
|
||||||
<small
|
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||||
className="field-hint"
|
|
||||||
style={{ marginTop: "8px", display: "block" }}
|
|
||||||
>
|
|
||||||
{isCodexOfficialPreset
|
{isCodexOfficialPreset
|
||||||
? "Codex 官方登录,不需要填写 API Key"
|
? "Codex 官方登录,不需要填写 API Key"
|
||||||
: "使用预设配置,只需填写 API Key"}
|
: "使用预设配置,只需填写 API Key"}
|
||||||
</small>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="space-y-2">
|
||||||
<label htmlFor="name">供应商名称 *</label>
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
供应商名称 *
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
@@ -483,14 +509,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
placeholder="例如:Anthropic 官方"
|
placeholder="例如:Anthropic 官方"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
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>
|
</div>
|
||||||
|
|
||||||
{!isCodex && (
|
{!isCodex && showApiKey && (
|
||||||
<div
|
<div className="space-y-2">
|
||||||
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
|
<label
|
||||||
>
|
htmlFor="apiKey"
|
||||||
<label htmlFor="apiKey">API Key *</label>
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
API Key *
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="apiKey"
|
id="apiKey"
|
||||||
@@ -503,24 +533,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}
|
}
|
||||||
disabled={isOfficialPreset}
|
disabled={isOfficialPreset}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
style={
|
className={`w-full px-3 py-2 border rounded-lg text-sm transition-colors ${
|
||||||
isOfficialPreset
|
isOfficialPreset
|
||||||
? {
|
? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||||
backgroundColor: "#f5f5f5",
|
: "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
|
||||||
cursor: "not-allowed",
|
}`}
|
||||||
color: "#999",
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCodex && (
|
{isCodex && showCodexApiKey && (
|
||||||
<div
|
<div className="space-y-2">
|
||||||
className={`form-group api-key-group ${!showCodexApiKey ? "hidden" : ""}`}
|
<label
|
||||||
>
|
htmlFor="codexApiKey"
|
||||||
<label htmlFor="codexApiKey">API Key *</label>
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
API Key *
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="codexApiKey"
|
id="codexApiKey"
|
||||||
@@ -538,21 +567,22 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
!isCodexOfficialPreset
|
!isCodexOfficialPreset
|
||||||
}
|
}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
style={
|
className={`w-full px-3 py-2 border rounded-lg text-sm transition-colors ${
|
||||||
isCodexOfficialPreset
|
isCodexOfficialPreset
|
||||||
? {
|
? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||||
backgroundColor: "#f5f5f5",
|
: "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
|
||||||
cursor: "not-allowed",
|
}`}
|
||||||
color: "#999",
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="space-y-2">
|
||||||
<label htmlFor="websiteUrl">官网地址</label>
|
<label
|
||||||
|
htmlFor="websiteUrl"
|
||||||
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
官网地址
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
id="websiteUrl"
|
id="websiteUrl"
|
||||||
@@ -561,15 +591,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="https://example.com(可选)"
|
placeholder="https://example.com(可选)"
|
||||||
autoComplete="off"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Claude 或 Codex 的配置部分 */}
|
{/* Claude 或 Codex 的配置部分 */}
|
||||||
{isCodex ? (
|
{isCodex ? (
|
||||||
// Codex: 双编辑器
|
// Codex: 双编辑器
|
||||||
<>
|
<div className="space-y-6">
|
||||||
<div className="form-group">
|
<div className="space-y-2">
|
||||||
<label htmlFor="codexAuth">auth.json (JSON) *</label>
|
<label
|
||||||
|
htmlFor="codexAuth"
|
||||||
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
auth.json (JSON) *
|
||||||
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="codexAuth"
|
id="codexAuth"
|
||||||
value={codexAuth}
|
value={codexAuth}
|
||||||
@@ -591,47 +627,61 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
"OPENAI_API_KEY": "sk-your-api-key-here"
|
"OPENAI_API_KEY": "sk-your-api-key-here"
|
||||||
}`}
|
}`}
|
||||||
rows={6}
|
rows={6}
|
||||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
|
||||||
required
|
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>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="space-y-2">
|
||||||
<label htmlFor="codexConfig">config.toml (TOML)</label>
|
<label
|
||||||
|
htmlFor="codexConfig"
|
||||||
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
config.toml (TOML)
|
||||||
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="codexConfig"
|
id="codexConfig"
|
||||||
value={codexConfig}
|
value={codexConfig}
|
||||||
onChange={(e) => setCodexConfig(e.target.value)}
|
onChange={(e) => setCodexConfig(e.target.value)}
|
||||||
placeholder={``}
|
placeholder=""
|
||||||
rows={8}
|
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 配置内容
|
Codex config.toml 配置内容
|
||||||
</small>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Claude: 原有的单编辑器
|
// Claude: 原有的单编辑器
|
||||||
<div className="form-group">
|
<div className="space-y-2">
|
||||||
<div className="label-with-checkbox">
|
<div className="flex items-center justify-between">
|
||||||
<label htmlFor="settingsConfig">
|
<label
|
||||||
|
htmlFor="settingsConfig"
|
||||||
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
Claude Code 配置 (JSON) *
|
Claude Code 配置 (JSON) *
|
||||||
</label>
|
</label>
|
||||||
<label className="checkbox-label">
|
<label className="inline-flex items-center gap-2 text-sm text-[var(--color-text-secondary)] cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={disableCoAuthored}
|
checked={disableCoAuthored}
|
||||||
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
|
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 签名
|
禁止 Claude Code 签名
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
value={formData.settingsConfig}
|
value={formData.settingsConfig}
|
||||||
onChange={(value) => handleChange({
|
onChange={(value) =>
|
||||||
target: { name: "settingsConfig", value }
|
handleChange({
|
||||||
} as React.ChangeEvent<HTMLTextAreaElement>)}
|
target: { name: "settingsConfig", value },
|
||||||
|
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||||
|
}
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
"env": {
|
"env": {
|
||||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||||
@@ -640,18 +690,27 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
rows={12}
|
rows={12}
|
||||||
/>
|
/>
|
||||||
<small className="field-hint">
|
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||||
完整的 Claude Code settings.json 配置内容
|
完整的 Claude Code settings.json 配置内容
|
||||||
</small>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-footer">
|
{/* Footer */}
|
||||||
<button type="button" className="cancel-btn" onClick={onClose}>
|
<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>
|
||||||
<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}
|
{submitText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 React from "react";
|
||||||
import { Provider } from "../types";
|
import { Provider } from "../types";
|
||||||
import "./ProviderList.css";
|
import {
|
||||||
|
Play,
|
||||||
|
Edit3,
|
||||||
|
Trash2,
|
||||||
|
ExternalLink,
|
||||||
|
CheckCircle2,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface ProviderListProps {
|
interface ProviderListProps {
|
||||||
providers: Record<string, Provider>;
|
providers: Record<string, Provider>;
|
||||||
@@ -39,71 +46,104 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="provider-list">
|
<div className="space-y-4">
|
||||||
{Object.values(providers).length === 0 ? (
|
{Object.values(providers).length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="text-center py-12">
|
||||||
<p>还没有添加任何供应商</p>
|
<div className="w-16 h-16 mx-auto mb-4 bg-[var(--color-bg-tertiary)] rounded-full flex items-center justify-center">
|
||||||
<p>点击右上角的"添加供应商"按钮开始</p>
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="provider-items">
|
<div className="space-y-3">
|
||||||
{Object.values(providers).map((provider) => {
|
{Object.values(providers).map((provider) => {
|
||||||
const isCurrent = provider.id === currentProviderId;
|
const isCurrent = provider.id === currentProviderId;
|
||||||
|
const apiUrl = getApiUrl(provider);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={provider.id}
|
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="flex items-start justify-between">
|
||||||
<div className="provider-name">
|
<div className="flex-1">
|
||||||
<span>{provider.name}</span>
|
<div className="flex items-center gap-3 mb-2">
|
||||||
{isCurrent && (
|
<h3 className="font-medium text-[var(--color-text-primary)]">
|
||||||
<span className="current-badge">当前使用</span>
|
{provider.name}
|
||||||
)}
|
</h3>
|
||||||
</div>
|
{isCurrent && (
|
||||||
<div className="provider-url">
|
<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">
|
||||||
{provider.websiteUrl ? (
|
<CheckCircle2 size={12} />
|
||||||
<a
|
当前使用
|
||||||
href="#"
|
</div>
|
||||||
onClick={(e) => {
|
)}
|
||||||
e.preventDefault();
|
</div>
|
||||||
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="provider-actions">
|
<div className="flex items-center gap-2 text-sm text-[var(--color-text-secondary)]">
|
||||||
<button
|
{provider.websiteUrl ? (
|
||||||
className="enable-btn"
|
<button
|
||||||
onClick={() => onSwitch(provider.id)}
|
onClick={(e) => {
|
||||||
disabled={isCurrent}
|
e.preventDefault();
|
||||||
>
|
handleUrlClick(provider.websiteUrl!);
|
||||||
启用
|
}}
|
||||||
</button>
|
className="inline-flex items-center gap-1 hover:text-[var(--color-primary)] transition-colors"
|
||||||
<button
|
title={`访问 ${provider.websiteUrl}`}
|
||||||
className="edit-btn"
|
>
|
||||||
onClick={() => onEdit(provider.id)}
|
<ExternalLink size={14} />
|
||||||
>
|
{provider.websiteUrl}
|
||||||
编辑
|
</button>
|
||||||
</button>
|
) : (
|
||||||
<button
|
<span className="font-mono" title={apiUrl}>
|
||||||
className="delete-btn"
|
{apiUrl}
|
||||||
onClick={() => onDelete(provider.id)}
|
</span>
|
||||||
disabled={isCurrent}
|
)}
|
||||||
>
|
</div>
|
||||||
删除
|
</div>
|
||||||
</button>
|
|
||||||
|
<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>
|
||||||
</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;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
html {
|
||||||
font-family:
|
font-family: var(--font-family);
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
line-height: 1.5;
|
||||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
body {
|
||||||
height: 100vh;
|
margin: 0;
|
||||||
display: flex;
|
padding: 0;
|
||||||
flex-direction: column;
|
background-color: var(--color-bg-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 仅在 macOS 下为顶部预留交通灯空间 */
|
/* 滚动条样式 */
|
||||||
/* 保持 mac 下与内容区域左对齐(不额外偏移) */
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
/* 在 macOS 下稍微增加 banner 高度,拉开与交通灯的垂直距离 */
|
height: 6px;
|
||||||
body.is-mac .app-header {
|
}
|
||||||
padding-top: 1.4rem;
|
|
||||||
padding-bottom: 1.4rem;
|
::-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 { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { Provider } from "../types";
|
import { Provider } from "../types";
|
||||||
|
|
||||||
// 应用类型
|
// 应用类型
|
||||||
@@ -52,7 +53,7 @@ export const tauriAPI = {
|
|||||||
// 更新供应商
|
// 更新供应商
|
||||||
updateProvider: async (
|
updateProvider: async (
|
||||||
provider: Provider,
|
provider: Provider,
|
||||||
app?: AppType,
|
app?: AppType
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
return await invoke("update_provider", { provider, app_type: app, app });
|
return await invoke("update_provider", { provider, app_type: app, app });
|
||||||
@@ -75,7 +76,7 @@ export const tauriAPI = {
|
|||||||
// 切换供应商
|
// 切换供应商
|
||||||
switchProvider: async (
|
switchProvider: async (
|
||||||
providerId: string,
|
providerId: string,
|
||||||
app?: AppType,
|
app?: AppType
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
return await invoke("switch_provider", {
|
return await invoke("switch_provider", {
|
||||||
@@ -91,7 +92,7 @@ export const tauriAPI = {
|
|||||||
|
|
||||||
// 导入当前配置为默认供应商
|
// 导入当前配置为默认供应商
|
||||||
importCurrentConfigAsDefault: async (
|
importCurrentConfigAsDefault: async (
|
||||||
app?: AppType,
|
app?: AppType
|
||||||
): Promise<ImportResult> => {
|
): Promise<ImportResult> => {
|
||||||
try {
|
try {
|
||||||
const success = await invoke<boolean>("import_default_config", {
|
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 暂不实现,保留接口兼容性)
|
// 选择配置文件(Tauri 暂不实现,保留接口兼容性)
|
||||||
|
|||||||
5
src/vite-env.d.ts
vendored
5
src/vite-env.d.ts
vendored
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Provider } from "./types";
|
import { Provider } from "./types";
|
||||||
import { AppType } from "./lib/tauri-api";
|
import { AppType } from "./lib/tauri-api";
|
||||||
|
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
interface ImportResult {
|
interface ImportResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -30,6 +31,10 @@ declare global {
|
|||||||
selectConfigFile: () => Promise<string | null>;
|
selectConfigFile: () => Promise<string | null>;
|
||||||
openConfigFolder: (app?: AppType) => Promise<void>;
|
openConfigFolder: (app?: AppType) => Promise<void>;
|
||||||
openExternal: (url: string) => Promise<void>;
|
openExternal: (url: string) => Promise<void>;
|
||||||
|
updateTrayMenu: () => Promise<boolean>;
|
||||||
|
onProviderSwitched: (
|
||||||
|
callback: (data: { appType: string; providerId: string }) => void
|
||||||
|
) => Promise<UnlistenFn>;
|
||||||
};
|
};
|
||||||
platform: {
|
platform: {
|
||||||
isMac: boolean;
|
isMac: boolean;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"module": "CommonJS",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@@ -11,5 +11,5 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"types": ["node"]
|
"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