@tombcato/ai-selector-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +14 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +139 -0
- package/dist/api.js.map +1 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/i18n.d.ts +53 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +51 -0
- package/dist/i18n.js.map +1 -0
- package/dist/icons.d.ts +10 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +22 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +11 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +199 -0
- package/dist/models.js.map +1 -0
- package/dist/providers.d.ts +42 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +229 -0
- package/dist/providers.js.map +1 -0
- package/dist/storage.d.ts +31 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +65 -0
- package/dist/storage.js.map +1 -0
- package/dist/strategies.d.ts +54 -0
- package/dist/strategies.d.ts.map +1 -0
- package/dist/strategies.js +184 -0
- package/dist/strategies.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +4 -0
- package/dist/utils.js.map +1 -0
- package/package.json +36 -0
- package/src/api.ts +154 -0
- package/src/config.ts +104 -0
- package/src/i18n.ts +54 -0
- package/src/index.ts +76 -0
- package/src/models.ts +202 -0
- package/src/providers.ts +239 -0
- package/src/storage.ts +78 -0
- package/src/strategies.ts +251 -0
- package/src/styles.css +244 -0
- package/src/types.ts +151 -0
package/src/storage.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Provider Selector - Storage Utilities
|
|
3
|
+
* Abstraction layer for config persistence
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AIConfig, StorageAdapter } from './types';
|
|
7
|
+
|
|
8
|
+
const STORAGE_KEY = 'ai_provider_config';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default localStorage adapter
|
|
12
|
+
*/
|
|
13
|
+
export const localStorageAdapter: StorageAdapter = {
|
|
14
|
+
get: (key: string) => {
|
|
15
|
+
if (typeof window === 'undefined') return null;
|
|
16
|
+
return localStorage.getItem(key);
|
|
17
|
+
},
|
|
18
|
+
set: (key: string, value: string) => {
|
|
19
|
+
if (typeof window === 'undefined') return;
|
|
20
|
+
localStorage.setItem(key, value);
|
|
21
|
+
},
|
|
22
|
+
remove: (key: string) => {
|
|
23
|
+
if (typeof window === 'undefined') return;
|
|
24
|
+
localStorage.removeItem(key);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a config storage instance
|
|
30
|
+
*/
|
|
31
|
+
export interface StorageOptions {
|
|
32
|
+
serialize?: (data: any) => string;
|
|
33
|
+
deserialize?: (data: string) => any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createConfigStorage(
|
|
37
|
+
adapter: StorageAdapter = localStorageAdapter,
|
|
38
|
+
options: StorageOptions = {}
|
|
39
|
+
) {
|
|
40
|
+
const serialize = options.serialize || JSON.stringify;
|
|
41
|
+
const deserialize = options.deserialize || JSON.parse;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
/**
|
|
45
|
+
* Save AI config
|
|
46
|
+
*/
|
|
47
|
+
save(config: AIConfig): void {
|
|
48
|
+
try {
|
|
49
|
+
const serialized = serialize(config);
|
|
50
|
+
adapter.set(STORAGE_KEY, serialized);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error('Failed to save config:', e);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load AI config
|
|
58
|
+
*/
|
|
59
|
+
load(): AIConfig | null {
|
|
60
|
+
const raw = adapter.get(STORAGE_KEY);
|
|
61
|
+
if (!raw) return null;
|
|
62
|
+
try {
|
|
63
|
+
return deserialize(raw) as AIConfig;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('Failed to load config:', e);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Clear AI config
|
|
72
|
+
*/
|
|
73
|
+
clear(): void {
|
|
74
|
+
adapter.remove(STORAGE_KEY);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Provider, Model } from './types';
|
|
2
|
+
|
|
3
|
+
export interface ProviderStrategy {
|
|
4
|
+
format: string;
|
|
5
|
+
// 模型列表
|
|
6
|
+
getModelsEndpoint?: (baseUrl: string, apiKey: string) => string;
|
|
7
|
+
parseModelsResponse?: (data: any) => Model[];
|
|
8
|
+
// 聊天 (也用于连接测试)
|
|
9
|
+
getChatEndpoint: (baseUrl: string, apiKey: string, model: string) => string;
|
|
10
|
+
buildChatPayload: (model: string, messages: Array<{ role: string; content: string }>, maxTokens: number) => Record<string, unknown>;
|
|
11
|
+
parseChatResponse: (data: any) => string;
|
|
12
|
+
// Headers
|
|
13
|
+
buildHeaders: (apiKey: string) => Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const defaultParseModelsResponse = (data: any): Model[] => {
|
|
17
|
+
if (Array.isArray(data?.data)) {
|
|
18
|
+
return data.data
|
|
19
|
+
.filter((m: any) => m.id)
|
|
20
|
+
.map((m: any) => {
|
|
21
|
+
const name =
|
|
22
|
+
m.name ||
|
|
23
|
+
(m.id.split('/').pop() ?? '')
|
|
24
|
+
.replace(/[-_]/g, ' ')
|
|
25
|
+
.replace(/\b\w/g, (c: string) => c.toUpperCase());
|
|
26
|
+
return {
|
|
27
|
+
id: m.id,
|
|
28
|
+
name,
|
|
29
|
+
created: m.created || 0,
|
|
30
|
+
} as any;
|
|
31
|
+
})
|
|
32
|
+
.sort((a: any, b: any) => (b.created || 0) - (a.created || 0)); // 最新的排在前面
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const openaiStrategy: ProviderStrategy = {
|
|
38
|
+
format: 'openai',
|
|
39
|
+
getModelsEndpoint: (baseUrl) => `${baseUrl}/models`,
|
|
40
|
+
getChatEndpoint: (baseUrl) => `${baseUrl}/chat/completions`,
|
|
41
|
+
buildHeaders: (apiKey) => ({
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
44
|
+
}),
|
|
45
|
+
buildChatPayload: (model, messages, maxTokens) => ({
|
|
46
|
+
model,
|
|
47
|
+
messages,
|
|
48
|
+
max_tokens: maxTokens,
|
|
49
|
+
}),
|
|
50
|
+
parseChatResponse: (data) => {
|
|
51
|
+
return data.choices?.[0]?.message?.content || '';
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const anthropicStrategy: ProviderStrategy = {
|
|
56
|
+
format: 'anthropic',
|
|
57
|
+
getChatEndpoint: (baseUrl) => `${baseUrl}/messages`,
|
|
58
|
+
buildHeaders: (apiKey) => ({
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'x-api-key': apiKey,
|
|
61
|
+
'anthropic-version': '2023-06-01',
|
|
62
|
+
}),
|
|
63
|
+
buildChatPayload: (model, messages, maxTokens) => ({
|
|
64
|
+
model,
|
|
65
|
+
messages,
|
|
66
|
+
max_tokens: maxTokens,
|
|
67
|
+
}),
|
|
68
|
+
parseChatResponse: (data) => {
|
|
69
|
+
return data.content?.[0]?.text || '';
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const geminiStrategy: ProviderStrategy = {
|
|
74
|
+
format: 'gemini',
|
|
75
|
+
getModelsEndpoint: (baseUrl, apiKey) => `${baseUrl}/models?key=${apiKey}`,
|
|
76
|
+
getChatEndpoint: (baseUrl, apiKey, model) => `${baseUrl}/models/${model}:generateContent?key=${apiKey}`,
|
|
77
|
+
buildHeaders: () => ({
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
}),
|
|
80
|
+
buildChatPayload: (_model, messages, maxTokens) => {
|
|
81
|
+
// 转换 OpenAI 格式的 messages 为 Gemini 格式
|
|
82
|
+
const contents = messages.map(m => ({
|
|
83
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
84
|
+
parts: [{ text: m.content }],
|
|
85
|
+
}));
|
|
86
|
+
return {
|
|
87
|
+
contents,
|
|
88
|
+
generationConfig: { maxOutputTokens: maxTokens },
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
parseChatResponse: (data) => {
|
|
92
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
93
|
+
},
|
|
94
|
+
// Gemini 返回格式: { models: [{ name: "models/gemini-pro", ... }] }
|
|
95
|
+
parseModelsResponse: (data) => {
|
|
96
|
+
if (Array.isArray(data.models)) {
|
|
97
|
+
return data.models
|
|
98
|
+
.filter((m: any) => m.supportedGenerationMethods?.includes('generateContent'))
|
|
99
|
+
.map((m: any) => ({
|
|
100
|
+
id: m.name.replace('models/', ''), // "models/gemini-pro" -> "gemini-pro"
|
|
101
|
+
name: m.displayName || m.name.replace('models/', ''),
|
|
102
|
+
created: m.created || 0,
|
|
103
|
+
}))
|
|
104
|
+
.sort((a: any, b: any) => (b.created || 0) - (a.created || 0));
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const cohereStrategy: ProviderStrategy = {
|
|
111
|
+
format: 'cohere',
|
|
112
|
+
getModelsEndpoint: (baseUrl) => `${baseUrl}/models`,
|
|
113
|
+
getChatEndpoint: (baseUrl) => `${baseUrl}/chat`,
|
|
114
|
+
buildHeaders: (apiKey) => ({
|
|
115
|
+
'Content-Type': 'application/json',
|
|
116
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
117
|
+
}),
|
|
118
|
+
buildChatPayload: (model, messages, maxTokens) => {
|
|
119
|
+
// Cohere 使用不同的格式: message 是当前消息, chat_history 是历史
|
|
120
|
+
const lastMessage = messages[messages.length - 1];
|
|
121
|
+
const chatHistory = messages.slice(0, -1).map(m => ({
|
|
122
|
+
role: m.role === 'assistant' ? 'CHATBOT' : 'USER',
|
|
123
|
+
message: m.content,
|
|
124
|
+
}));
|
|
125
|
+
return {
|
|
126
|
+
model,
|
|
127
|
+
message: lastMessage.content,
|
|
128
|
+
chat_history: chatHistory,
|
|
129
|
+
max_tokens: maxTokens,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
parseChatResponse: (data) => {
|
|
133
|
+
return data.text || '';
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const strategyRegistry: Record<string, ProviderStrategy> = {
|
|
138
|
+
openai: openaiStrategy,
|
|
139
|
+
anthropic: anthropicStrategy,
|
|
140
|
+
gemini: geminiStrategy,
|
|
141
|
+
cohere: cohereStrategy,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export function getStrategy(format: string): ProviderStrategy {
|
|
145
|
+
return strategyRegistry[format] || openaiStrategy;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// 纯前端直连聊天函数
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
export interface DirectChatOptions {
|
|
153
|
+
apiFormat: string;
|
|
154
|
+
baseUrl: string;
|
|
155
|
+
apiKey: string;
|
|
156
|
+
model: string;
|
|
157
|
+
messages: Array<{ role: string; content: string }>;
|
|
158
|
+
maxTokens?: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface DirectChatResult {
|
|
162
|
+
success: boolean;
|
|
163
|
+
content?: string;
|
|
164
|
+
message?: string;
|
|
165
|
+
latencyMs?: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 纯前端直连 AI 厂商进行聊天
|
|
170
|
+
* 注意: 这会将 API Key 暴露在浏览器中,仅适用于 Demo/测试场景
|
|
171
|
+
*/
|
|
172
|
+
export async function sendDirectChat(options: DirectChatOptions): Promise<DirectChatResult> {
|
|
173
|
+
const { apiFormat, baseUrl, apiKey, model, messages, maxTokens = 2048 } = options;
|
|
174
|
+
const strategy = getStrategy(apiFormat);
|
|
175
|
+
|
|
176
|
+
const endpoint = strategy.getChatEndpoint(baseUrl, apiKey, model);
|
|
177
|
+
const headers = strategy.buildHeaders(apiKey);
|
|
178
|
+
const payload = strategy.buildChatPayload(model, messages, maxTokens);
|
|
179
|
+
|
|
180
|
+
const startTime = performance.now();
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(endpoint, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers,
|
|
186
|
+
body: JSON.stringify(payload),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const latencyMs = Math.round(performance.now() - startTime);
|
|
190
|
+
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const errorData = await response.json().catch(() => ({}));
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
message: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
196
|
+
latencyMs,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const data = await response.json();
|
|
201
|
+
const content = strategy.parseChatResponse(data);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
content,
|
|
206
|
+
latencyMs,
|
|
207
|
+
};
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
message: e instanceof Error ? e.message : '网络错误',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// 连接测试函数 (复用聊天接口)
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
export interface TestConnectionOptions {
|
|
221
|
+
apiFormat: string;
|
|
222
|
+
baseUrl: string;
|
|
223
|
+
apiKey: string;
|
|
224
|
+
model: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface TestConnectionResult {
|
|
228
|
+
success: boolean;
|
|
229
|
+
latencyMs?: number;
|
|
230
|
+
message?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 测试 AI 厂商连接 (通过发送一个简单的聊天请求)
|
|
235
|
+
*/
|
|
236
|
+
export async function testDirectConnection(options: TestConnectionOptions): Promise<TestConnectionResult> {
|
|
237
|
+
const result = await sendDirectChat({
|
|
238
|
+
apiFormat: options.apiFormat,
|
|
239
|
+
baseUrl: options.baseUrl,
|
|
240
|
+
apiKey: options.apiKey,
|
|
241
|
+
model: options.model,
|
|
242
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
243
|
+
maxTokens: 5, // 最小 token 数,节省成本
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
success: result.success,
|
|
248
|
+
latencyMs: result.latencyMs,
|
|
249
|
+
message: result.success ? undefined : result.message,
|
|
250
|
+
};
|
|
251
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer components {
|
|
6
|
+
|
|
7
|
+
/* Scoped Preflight / Reset for apmsu-root */
|
|
8
|
+
.apmsu-root {
|
|
9
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
line-height: 1.5;
|
|
11
|
+
-webkit-text-size-adjust: 100%;
|
|
12
|
+
-moz-tab-size: 4;
|
|
13
|
+
tab-size: 4;
|
|
14
|
+
@apply mx-auto box-border;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* 按钮 */
|
|
18
|
+
.apmsu-btn {
|
|
19
|
+
@apply inline-flex items-center justify-center rounded-md text-xs font-medium transition-all duration-200 h-10 px-4 py-2 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.apmsu-btn-primary {
|
|
23
|
+
@apply bg-zinc-900 text-zinc-50 hover:bg-zinc-900/90 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/90;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.apmsu-btn-success {
|
|
27
|
+
@apply bg-green-600 text-white hover:bg-green-700;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.apmsu-btn-ghost {
|
|
31
|
+
@apply bg-transparent hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* 输入框 */
|
|
35
|
+
.apmsu-input {
|
|
36
|
+
@apply flex h-11 w-full rounded-lg border bg-gray-100/30 dark:bg-zinc-800/50 px-4 py-2.5 text-sm shadow-sm placeholder:text-gray-400 dark:placeholder:text-zinc-500 focus:outline-none transition-colors;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.apmsu-input-default {
|
|
40
|
+
@apply border-gray-200 dark:border-zinc-700 focus:border-zinc-500 dark:focus:border-zinc-500 hover:border-gray-300 dark:hover:border-zinc-600;
|
|
41
|
+
border-width: 1px;
|
|
42
|
+
/* Ensure border width as preflight is disabled */
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.apmsu-input-error {
|
|
46
|
+
@apply border-red-500 dark:border-red-500/50 focus-visible:ring-red-500 dark:bg-red-500/5;
|
|
47
|
+
border-width: 1px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.apmsu-input-success {
|
|
51
|
+
@apply border-green-500 dark:border-green-500/50 focus-visible:ring-green-500 dark:bg-green-500/5;
|
|
52
|
+
border-width: 1px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* 下拉框 */
|
|
56
|
+
.apmsu-select-trigger {
|
|
57
|
+
@apply flex h-11 w-full min-w-0 items-center justify-between whitespace-nowrap rounded-lg border border-gray-200 dark:border-zinc-700 bg-gray-100/20 dark:bg-zinc-800/50 px-4 py-2.5 text-sm shadow-sm focus:outline-none focus:border-zinc-500 dark:focus:border-zinc-500 hover:border-gray-300 dark:hover:border-zinc-600 transition-colors;
|
|
58
|
+
border-width: 1px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.apmsu-dropdown {
|
|
62
|
+
@apply absolute z-50 mt-1 w-full overflow-hidden rounded-md border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 shadow-xl;
|
|
63
|
+
border-width: 1px;
|
|
64
|
+
|
|
65
|
+
/* 动画初始状态 */
|
|
66
|
+
max-height: 0;
|
|
67
|
+
opacity: 0;
|
|
68
|
+
transform-origin: top;
|
|
69
|
+
transform: translateY(-8px) scale(0.98);
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
visibility: hidden;
|
|
72
|
+
|
|
73
|
+
/* 过渡效果 */
|
|
74
|
+
transition:
|
|
75
|
+
max-height 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
76
|
+
opacity 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
77
|
+
transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
78
|
+
visibility 0s linear 0.4s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.apmsu-dropdown-open {
|
|
82
|
+
max-height: 480px;
|
|
83
|
+
/* 足够大的高度以容纳内容 */
|
|
84
|
+
opacity: 1;
|
|
85
|
+
transform: translateY(0) scale(1);
|
|
86
|
+
pointer-events: auto;
|
|
87
|
+
visibility: visible;
|
|
88
|
+
transition-delay: 0s;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.apmsu-dropdown-item {
|
|
92
|
+
@apply w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-zinc-800 transition-all cursor-pointer active:scale-[0.98];
|
|
93
|
+
background: transparent;
|
|
94
|
+
border: none;
|
|
95
|
+
text-align: left;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.apmsu-dropdown-item-active {
|
|
99
|
+
@apply bg-zinc-100 dark:bg-zinc-800;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* 标签 */
|
|
103
|
+
.apmsu-label {
|
|
104
|
+
@apply text-xs font-medium leading-none block mb-2 uppercase tracking-wider text-gray-500 dark:text-zinc-500;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* 文本 */
|
|
108
|
+
.apmsu-error-text {
|
|
109
|
+
@apply text-xs text-red-500 dark:text-red-400;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.apmsu-hint-text {
|
|
113
|
+
@apply text-xs text-gray-400 dark:text-zinc-500;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.apmsu-success-text {
|
|
117
|
+
@apply text-xs text-green-600 dark:text-green-400;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* 卡片 */
|
|
121
|
+
.apmsu-card {
|
|
122
|
+
@apply bg-white dark:bg-zinc-900 rounded-xl border border-gray-200 dark:border-zinc-800 shadow-sm p-6 space-y-5;
|
|
123
|
+
border-width: 1px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* 图标 */
|
|
127
|
+
.apmsu-icon-spin {
|
|
128
|
+
@apply w-4 h-4 animate-spin text-gray-400;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.apmsu-icon-success {
|
|
132
|
+
@apply w-4 h-4 text-green-500;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.apmsu-icon-error {
|
|
136
|
+
@apply w-4 h-4 text-red-500;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Provider 图标 - dark mode 反转 */
|
|
140
|
+
.apmsu-provider-icon {
|
|
141
|
+
@apply w-4 h-4 flex-shrink-0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* 下拉箭头 */
|
|
145
|
+
.apmsu-chevron {
|
|
146
|
+
@apply w-4 h-4 opacity-50 flex-shrink-0 transition-transform duration-200;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Placeholder 文本 */
|
|
150
|
+
.apmsu-placeholder {
|
|
151
|
+
@apply text-gray-400 dark:text-zinc-500;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* 表单字段容器 */
|
|
155
|
+
.apmsu-field {
|
|
156
|
+
@apply space-y-2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* 分割线 */
|
|
160
|
+
.apmsu-divider {
|
|
161
|
+
@apply border-b border-gray-100 dark:border-zinc-800;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Dark mode 下 provider 图标反转颜色 */
|
|
166
|
+
.dark .apmsu-provider-icon {
|
|
167
|
+
filter: invert(1) hue-rotate(180deg);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* 自定义滚动条 */
|
|
171
|
+
.apmsu-root ::-webkit-scrollbar {
|
|
172
|
+
width: 8px;
|
|
173
|
+
height: 8px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.apmsu-root ::-webkit-scrollbar-track {
|
|
177
|
+
background: transparent;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.apmsu-root ::-webkit-scrollbar-thumb {
|
|
181
|
+
background: rgba(156, 163, 175, 0.5);
|
|
182
|
+
border-radius: 4px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.apmsu-root ::-webkit-scrollbar-thumb:hover {
|
|
186
|
+
background: rgba(156, 163, 175, 0.7);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.apmsu-root.dark ::-webkit-scrollbar-thumb {
|
|
190
|
+
background: rgba(82, 82, 91, 0.6);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.apmsu-root.dark ::-webkit-scrollbar-thumb:hover {
|
|
194
|
+
background: rgba(82, 82, 91, 0.8);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* 下拉框滚动条 */
|
|
198
|
+
.apmsu-dropdown::-webkit-scrollbar,
|
|
199
|
+
.apmsu-dropdown *::-webkit-scrollbar {
|
|
200
|
+
width: 6px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.apmsu-dropdown::-webkit-scrollbar-thumb,
|
|
204
|
+
.apmsu-dropdown *::-webkit-scrollbar-thumb {
|
|
205
|
+
background: rgba(156, 163, 175, 0.4);
|
|
206
|
+
border-radius: 3px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.apmsu-dropdown::-webkit-scrollbar-track,
|
|
210
|
+
.apmsu-dropdown *::-webkit-scrollbar-track {
|
|
211
|
+
background: transparent;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.dark .apmsu-dropdown::-webkit-scrollbar-thumb,
|
|
215
|
+
.dark .apmsu-dropdown *::-webkit-scrollbar-thumb {
|
|
216
|
+
background: rgba(82, 82, 91, 0.5);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@keyframes apmsu-fade-in {
|
|
220
|
+
from {
|
|
221
|
+
opacity: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
to {
|
|
225
|
+
opacity: 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.apmsu-fade-in {
|
|
230
|
+
animation: apmsu-fade-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Vue Transition Classes */
|
|
234
|
+
.apmsu-fade-enter-active {
|
|
235
|
+
transition: opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.apmsu-fade-enter-from {
|
|
239
|
+
opacity: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.apmsu-fade-leave-active {
|
|
243
|
+
display: none;
|
|
244
|
+
}
|