mirror of
https://github.com/handsomezhuzhu/AeroStart.git
synced 2026-02-20 12:00:15 +00:00
♻️ refactor: reorganize project structure and centralize search engine config
- Move all source files from root to src/ directory for better organization - Create src/config/searchEngines.ts to centralize search engine configurations - Define unified SearchEngineConfig interface - Support both JSONP and Fetch request methods - Implement response parsers for Google, Baidu, Bing, DuckDuckGo, and Bilibili - Refactor src/utils/suggestions.ts to use centralized config - Simplify code from 126 lines to 81 lines - Support hybrid JSONP/Fetch mode (Bilibili uses Fetch via Vite proxy) - Remove duplicate URL construction and parsing logic - Update path alias configuration - Change @/* from ./* to ./src/* in tsconfig.json - Update vite.config.ts alias to point to ./src - Add Bilibili proxy configuration in vite.config.ts for development - Remove Bilibili rewrites from vercel.json (use Vite proxy instead) - Add @vercel/node to devDependencies - Remove unused files: README.md, i18n/README.md, metadata.json, vite-env.d.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
28
README.md
28
README.md
@@ -29,16 +29,12 @@ Click the button above to deploy your own instance of AeroStart to Vercel in min
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install
|
pnpm install
|
||||||
# or
|
|
||||||
npm install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run Development Server
|
### Run Development Server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev
|
pnpm dev
|
||||||
# or
|
|
||||||
npm run dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Visit `http://localhost:3000` to view the application.
|
Visit `http://localhost:3000` to view the application.
|
||||||
@@ -47,8 +43,6 @@ Visit `http://localhost:3000` to view the application.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm build
|
pnpm build
|
||||||
# or
|
|
||||||
npm run build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎯 Usage Guide
|
## 🎯 Usage Guide
|
||||||
@@ -97,28 +91,6 @@ AeroStart/
|
|||||||
└── index.tsx # Application entry point
|
└── index.tsx # Application entry point
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Customization
|
|
||||||
|
|
||||||
### Add Search Engine
|
|
||||||
|
|
||||||
Edit the `SEARCH_ENGINES` array in `constants.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
name: 'Engine Name',
|
|
||||||
urlPattern: 'https://example.com/search?q=',
|
|
||||||
icon: 'SVG icon string'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add Theme Color
|
|
||||||
|
|
||||||
Edit the `THEMES` array in `constants.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{ name: 'Theme Name', hex: '#colorcode' }
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
|
|||||||
228
i18n/README.md
228
i18n/README.md
@@ -1,228 +0,0 @@
|
|||||||
# 多语言架构使用指南 (i18n Architecture Guide)
|
|
||||||
|
|
||||||
## 📁 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
i18n/
|
|
||||||
├── types.ts # 类型定义
|
|
||||||
├── locales/
|
|
||||||
│ ├── en.ts # 英文翻译
|
|
||||||
│ └── zh.ts # 中文翻译
|
|
||||||
├── I18nContext.tsx # Context 和 Provider
|
|
||||||
├── index.ts # 导出文件
|
|
||||||
└── README.md # 本文档
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 1. 在组件中使用翻译
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { useTranslation } from '../i18n';
|
|
||||||
|
|
||||||
const MyComponent = () => {
|
|
||||||
const { t, language, setLanguage } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>{t.settings}</h1>
|
|
||||||
<p>{t.appearance}</p>
|
|
||||||
<button onClick={() => setLanguage('zh')}>切换到中文</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 添加新的翻译键
|
|
||||||
|
|
||||||
#### 步骤 1: 在 `types.ts` 中添加类型定义
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface Translation {
|
|
||||||
// ... 现有的键
|
|
||||||
myNewKey: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 步骤 2: 在 `locales/en.ts` 中添加英文翻译
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const en: Translation = {
|
|
||||||
// ... 现有的翻译
|
|
||||||
myNewKey: 'My New Text',
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 步骤 3: 在 `locales/zh.ts` 中添加中文翻译
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const zh: Translation = {
|
|
||||||
// ... 现有的翻译
|
|
||||||
myNewKey: '我的新文本',
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 在组件中使用新的翻译键
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const MyComponent = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return <div>{t.myNewKey}</div>;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 已支持的语言
|
|
||||||
|
|
||||||
- **English (en)** - 英语
|
|
||||||
- **简体中文 (zh)** - Simplified Chinese
|
|
||||||
|
|
||||||
## 🎯 已翻译的组件
|
|
||||||
|
|
||||||
以下组件已经集成了多语言支持:
|
|
||||||
|
|
||||||
- ✅ **ThemeSettings** - 主题设置(包含语言切换器)
|
|
||||||
- ⏳ **SettingsModal** - 设置模态框(待更新)
|
|
||||||
- ⏳ **SearchBox** - 搜索框(待更新)
|
|
||||||
- ⏳ **SearchEngineManager** - 搜索引擎管理器(待更新)
|
|
||||||
- ⏳ **WallpaperManager** - 壁纸管理器(待更新)
|
|
||||||
- ⏳ **GlobalContextMenu** - 全局右键菜单(待更新)
|
|
||||||
- ⏳ **ErrorBoundary** - 错误边界(待更新)
|
|
||||||
- ⏳ **Clock** - 时钟(待更新)
|
|
||||||
|
|
||||||
## 🔧 API 参考
|
|
||||||
|
|
||||||
### `useTranslation()` Hook
|
|
||||||
|
|
||||||
返回一个包含以下属性的对象:
|
|
||||||
|
|
||||||
- **`t`**: `Translation` - 当前语言的翻译对象
|
|
||||||
- **`language`**: `Language` - 当前语言 ('en' | 'zh')
|
|
||||||
- **`setLanguage`**: `(lang: Language) => void` - 切换语言的函数
|
|
||||||
|
|
||||||
### `I18nProvider` Component
|
|
||||||
|
|
||||||
Props:
|
|
||||||
- **`language`**: `Language` - 当前语言
|
|
||||||
- **`onLanguageChange`**: `(lang: Language) => void` - 语言变化回调
|
|
||||||
- **`children`**: `ReactNode` - 子组件
|
|
||||||
|
|
||||||
## 💡 最佳实践
|
|
||||||
|
|
||||||
### 1. 保持翻译键的一致性
|
|
||||||
|
|
||||||
使用描述性的键名,例如:
|
|
||||||
- ✅ `searchEngineDeleted`
|
|
||||||
- ❌ `msg1`
|
|
||||||
|
|
||||||
### 2. 避免在翻译中使用 HTML
|
|
||||||
|
|
||||||
如果需要格式化文本,使用多个翻译键:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// ❌ 不推荐
|
|
||||||
myKey: '<strong>Bold</strong> text'
|
|
||||||
|
|
||||||
// ✅ 推荐
|
|
||||||
myKeyBold: 'Bold'
|
|
||||||
myKeyText: 'text'
|
|
||||||
|
|
||||||
// 在组件中使用
|
|
||||||
<div><strong>{t.myKeyBold}</strong> {t.myKeyText}</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 为长文本使用描述性键名
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ 好的命名
|
|
||||||
errorMessage: 'The application encountered an unexpected error...'
|
|
||||||
|
|
||||||
// ❌ 不好的命名
|
|
||||||
error: 'The application encountered an unexpected error...'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 组织相关的翻译键
|
|
||||||
|
|
||||||
在 `Translation` 接口中使用注释分组:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface Translation {
|
|
||||||
// Common
|
|
||||||
settings: string;
|
|
||||||
appearance: string;
|
|
||||||
|
|
||||||
// Theme Settings
|
|
||||||
themeColor: string;
|
|
||||||
showSeconds: string;
|
|
||||||
|
|
||||||
// Error Messages
|
|
||||||
errorMessage: string;
|
|
||||||
somethingWentWrong: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌍 添加新语言
|
|
||||||
|
|
||||||
### 步骤 1: 在 `types.ts` 中添加语言类型
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export type Language = 'en' | 'zh' | 'ja'; // 添加 'ja' 日语
|
|
||||||
```
|
|
||||||
|
|
||||||
### 步骤 2: 创建新的语言文件
|
|
||||||
|
|
||||||
创建 `locales/ja.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Translation } from '../types';
|
|
||||||
|
|
||||||
export const ja: Translation = {
|
|
||||||
settings: '設定',
|
|
||||||
appearance: '外観',
|
|
||||||
// ... 其他翻译
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 步骤 3: 在 `I18nContext.tsx` 中注册新语言
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const translations: Record<Language, Translation> = {
|
|
||||||
en,
|
|
||||||
zh,
|
|
||||||
ja, // 添加日语
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 步骤 4: 在 `ThemeSettings` 中添加语言选项
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<button onClick={() => handleLanguageChange('ja')}>
|
|
||||||
日本語
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐛 故障排除
|
|
||||||
|
|
||||||
### 问题:翻译不显示
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
1. 确保组件在 `I18nProvider` 内部
|
|
||||||
2. 检查是否正确导入了 `useTranslation`
|
|
||||||
3. 验证翻译键在所有语言文件中都存在
|
|
||||||
|
|
||||||
### 问题:TypeScript 错误
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
1. 确保所有语言文件都实现了 `Translation` 接口
|
|
||||||
2. 运行 `npx tsc --noEmit` 检查类型错误
|
|
||||||
3. 确保新添加的键在 `types.ts` 中有定义
|
|
||||||
|
|
||||||
## 📚 相关文件
|
|
||||||
|
|
||||||
- `types.ts` - UserSettings 类型(包含 language 字段)
|
|
||||||
- `App.tsx` - I18nProvider 集成
|
|
||||||
- `utils/storage.ts` - 语言偏好持久化
|
|
||||||
|
|
||||||
## 🎉 完成!
|
|
||||||
|
|
||||||
现在你已经了解了如何使用和扩展多语言架构。如果有任何问题,请参考现有组件的实现或查看本文档。
|
|
||||||
@@ -10,6 +10,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "AeroStart",
|
|
||||||
"description": "A high-fidelity, advanced material design homepage featuring a precision clock and search interface.",
|
|
||||||
"requestFramePermissions": []
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
"@vercel/node": "^5.5.15",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
|||||||
1081
pnpm-lock.yaml
generated
1081
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
108
src/config/searchEngines.ts
Normal file
108
src/config/searchEngines.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Search Engine Configuration
|
||||||
|
* Centralized management of API endpoints, parameters, and response parsing for all search engines
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type SearchEngineType = 'Google' | 'Baidu' | 'Bing' | 'DuckDuckGo' | 'Bilibili';
|
||||||
|
|
||||||
|
export type RequestMethod = 'jsonp' | 'fetch';
|
||||||
|
|
||||||
|
export interface SearchEngineConfig {
|
||||||
|
/** Engine name */
|
||||||
|
name: SearchEngineType;
|
||||||
|
/** Request method */
|
||||||
|
method: RequestMethod;
|
||||||
|
/** API endpoint URL template ({query} will be replaced with search term, {callback} with callback function name) */
|
||||||
|
urlTemplate?: string;
|
||||||
|
/** Local proxy path (for fetch method) */
|
||||||
|
proxyPath?: string;
|
||||||
|
/** Response data parser */
|
||||||
|
parseResponse: (data: any) => string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search engine configuration list
|
||||||
|
*/
|
||||||
|
export const searchEngineConfigs: Record<SearchEngineType, SearchEngineConfig> = {
|
||||||
|
Google: {
|
||||||
|
name: 'Google',
|
||||||
|
method: 'jsonp',
|
||||||
|
urlTemplate: 'https://suggestqueries.google.com/complete/search?client=youtube&q={query}&jsonp={callback}',
|
||||||
|
parseResponse: (data: any) => {
|
||||||
|
// Google (client=youtube) returns: ["query", ["sug1", "sug2"], ...]
|
||||||
|
return data[1].map((item: any) => Array.isArray(item) ? item[0] : item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Baidu: {
|
||||||
|
name: 'Baidu',
|
||||||
|
method: 'jsonp',
|
||||||
|
urlTemplate: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd={query}&cb={callback}',
|
||||||
|
parseResponse: (data: any) => {
|
||||||
|
// Baidu returns: {q: "query", s: ["sug1", "sug2"]}
|
||||||
|
return data.s;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Bing: {
|
||||||
|
name: 'Bing',
|
||||||
|
method: 'jsonp',
|
||||||
|
urlTemplate: 'https://api.bing.com/osjson.aspx?query={query}&JsonType=callback&JsonCallback={callback}',
|
||||||
|
parseResponse: (data: any) => {
|
||||||
|
// Bing returns: ["query", ["sug1", "sug2"]]
|
||||||
|
return data[1].map((item: any) => item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
DuckDuckGo: {
|
||||||
|
name: 'DuckDuckGo',
|
||||||
|
method: 'jsonp',
|
||||||
|
urlTemplate: 'https://duckduckgo.com/ac/?q={query}&callback={callback}&type=list',
|
||||||
|
parseResponse: (data: any) => {
|
||||||
|
// DuckDuckGo returns: [{phrase: "sug1"}, ...]
|
||||||
|
return data.map((item: any) => item.phrase);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Bilibili: {
|
||||||
|
name: 'Bilibili',
|
||||||
|
method: 'jsonp',
|
||||||
|
urlTemplate: 'https://s.search.bilibili.com/main/suggest?term={query}&func={callback}',
|
||||||
|
parseResponse: (data: any) => {
|
||||||
|
// Bilibili returns: {code: 0, result: {tag: [{value: "suggestion text"}, ...]}}
|
||||||
|
if (data.code === 0 && data.result && data.result.tag) {
|
||||||
|
return data.result.tag.map((item: any) => item.value);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get search engine configuration
|
||||||
|
*/
|
||||||
|
export const getSearchEngineConfig = (engine: SearchEngineType): SearchEngineConfig | undefined => {
|
||||||
|
return searchEngineConfigs[engine];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build JSONP URL
|
||||||
|
*/
|
||||||
|
export const buildJsonpUrl = (config: SearchEngineConfig, query: string, callbackName: string): string => {
|
||||||
|
if (!config.urlTemplate) {
|
||||||
|
throw new Error(`Engine ${config.name} does not have a URL template`);
|
||||||
|
}
|
||||||
|
return config.urlTemplate
|
||||||
|
.replace('{query}', encodeURIComponent(query))
|
||||||
|
.replace('{callback}', callbackName);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Fetch URL
|
||||||
|
*/
|
||||||
|
export const buildFetchUrl = (config: SearchEngineConfig, query: string): string => {
|
||||||
|
if (!config.proxyPath) {
|
||||||
|
throw new Error(`Engine ${config.name} does not have a proxy path`);
|
||||||
|
}
|
||||||
|
return `${config.proxyPath}?term=${encodeURIComponent(query)}`;
|
||||||
|
};
|
||||||
81
src/utils/suggestions.ts
Normal file
81
src/utils/suggestions.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
getSearchEngineConfig,
|
||||||
|
buildJsonpUrl,
|
||||||
|
type SearchEngineType
|
||||||
|
} from '@/config/searchEngines';
|
||||||
|
|
||||||
|
let callbackCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch search suggestions (supports hybrid JSONP and Fetch mode)
|
||||||
|
*/
|
||||||
|
export const fetchSuggestions = (engine: string, query: string): Promise<string[]> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!query || !query.trim()) {
|
||||||
|
resolve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getSearchEngineConfig(engine as SearchEngineType);
|
||||||
|
if (!config) {
|
||||||
|
resolve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bilibili uses fetch (via Vite proxy)
|
||||||
|
if (engine === 'Bilibili') {
|
||||||
|
const url = `/bilibili?term=${encodeURIComponent(query)}`;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
try {
|
||||||
|
const suggestions = config.parseResponse(data);
|
||||||
|
resolve(suggestions);
|
||||||
|
} catch (e) {
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other engines use JSONP
|
||||||
|
const callbackName = `jsonp_cb_${Date.now()}_${callbackCount++}`;
|
||||||
|
const script = document.createElement('script');
|
||||||
|
let timeoutId: any;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if ((window as any)[callbackName]) delete (window as any)[callbackName];
|
||||||
|
if (document.body.contains(script)) document.body.removeChild(script);
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3 seconds timeout
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
resolve([]);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Set global callback function
|
||||||
|
(window as any)[callbackName] = (data: any) => {
|
||||||
|
cleanup();
|
||||||
|
try {
|
||||||
|
const suggestions = config.parseResponse(data);
|
||||||
|
resolve(suggestions);
|
||||||
|
} catch (e) {
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build URL and make request
|
||||||
|
script.src = buildJsonpUrl(config, query, callbackName);
|
||||||
|
script.onerror = () => {
|
||||||
|
cleanup();
|
||||||
|
resolve([]);
|
||||||
|
};
|
||||||
|
document.body.appendChild(script);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
|
|
||||||
let callbackCount = 0;
|
|
||||||
|
|
||||||
// Store and manage AbortController
|
|
||||||
const abortControllers = new Map<string, AbortController>();
|
|
||||||
|
|
||||||
export const fetchSuggestions = (engine: string, query: string): Promise<string[]> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!query || !query.trim()) {
|
|
||||||
resolve([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bilibili uses fetch API (via Vite proxy)
|
|
||||||
if (engine === 'Bilibili') {
|
|
||||||
const encodedQuery = encodeURIComponent(query);
|
|
||||||
const url = `/bilibili?term=${encodedQuery}`;
|
|
||||||
|
|
||||||
// Cancel previous request
|
|
||||||
const requestKey = `${engine}-${query}`;
|
|
||||||
if (abortControllers.has(requestKey)) {
|
|
||||||
abortControllers.get(requestKey)?.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new AbortController
|
|
||||||
const controller = new AbortController();
|
|
||||||
abortControllers.set(requestKey, controller);
|
|
||||||
|
|
||||||
fetch(url, { signal: controller.signal })
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
// Clean up AbortController
|
|
||||||
abortControllers.delete(requestKey);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Bilibili return format: {code: 0, result: {tag: [{value: "suggestion text"}, ...]}}
|
|
||||||
if (data.code === 0 && data.result && data.result.tag) {
|
|
||||||
const suggestions = data.result.tag.map((item: any) => item.value);
|
|
||||||
resolve(suggestions);
|
|
||||||
} else {
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
// Clean up AbortController
|
|
||||||
abortControllers.delete(requestKey);
|
|
||||||
|
|
||||||
// If request was cancelled, do nothing
|
|
||||||
if (error.name === 'AbortError') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve([]);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other search engines use JSONP
|
|
||||||
const callbackName = `jsonp_cb_${Date.now()}_${callbackCount++}`;
|
|
||||||
const script = document.createElement('script');
|
|
||||||
let timeoutId: any;
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
if ((window as any)[callbackName]) delete (window as any)[callbackName];
|
|
||||||
if (document.body.contains(script)) document.body.removeChild(script);
|
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
|
||||||
};
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
cleanup();
|
|
||||||
// Resolve empty on timeout to avoid breaking UI
|
|
||||||
resolve([]);
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
(window as any)[callbackName] = (data: any) => {
|
|
||||||
cleanup();
|
|
||||||
try {
|
|
||||||
if (engine === 'Google') {
|
|
||||||
// Google (client=youtube) returns ["query", ["sug1", "sug2"], ...]
|
|
||||||
resolve(data[1].map((item: any) => Array.isArray(item) ? item[0] : item));
|
|
||||||
} else if (engine === 'Baidu') {
|
|
||||||
// Baidu returns {q: "query", s: ["sug1", "sug2"]}
|
|
||||||
resolve(data.s);
|
|
||||||
} else if (engine === 'Bing') {
|
|
||||||
// Bing returns ["query", ["sug1", "sug2"]]
|
|
||||||
resolve(data[1].map((item: any) => item));
|
|
||||||
} else if (engine === 'DuckDuckGo') {
|
|
||||||
// DDG returns [{phrase: "sug1"}, ...]
|
|
||||||
resolve(data.map((item: any) => item.phrase));
|
|
||||||
} else {
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = '';
|
|
||||||
const encodedQuery = encodeURIComponent(query);
|
|
||||||
|
|
||||||
if (engine === 'Google') {
|
|
||||||
// client=youtube supports JSONP
|
|
||||||
url = `https://suggestqueries.google.com/complete/search?client=youtube&q=${encodedQuery}&jsonp=${callbackName}`;
|
|
||||||
} else if (engine === 'Baidu') {
|
|
||||||
url = `https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=${encodedQuery}&cb=${callbackName}`;
|
|
||||||
} else if (engine === 'Bing') {
|
|
||||||
url = `https://api.bing.com/osjson.aspx?query=${encodedQuery}&JsonType=callback&JsonCallback=${callbackName}`;
|
|
||||||
} else if (engine === 'DuckDuckGo') {
|
|
||||||
url = `https://duckduckgo.com/ac/?q=${encodedQuery}&callback=${callbackName}&type=list`;
|
|
||||||
} else {
|
|
||||||
// Unsupported engine
|
|
||||||
cleanup();
|
|
||||||
resolve([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
script.src = url;
|
|
||||||
script.onerror = () => {
|
|
||||||
cleanup();
|
|
||||||
resolve([]);
|
|
||||||
};
|
|
||||||
document.body.appendChild(script);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -4,12 +4,6 @@
|
|||||||
"devCommand": "pnpm dev",
|
"devCommand": "pnpm dev",
|
||||||
"installCommand": "pnpm install",
|
"installCommand": "pnpm install",
|
||||||
"framework": "vite",
|
"framework": "vite",
|
||||||
"rewrites": [
|
|
||||||
{
|
|
||||||
"source": "/bilibili/:path*",
|
|
||||||
"destination": "https://s.search.bilibili.com/main/suggest/:path*"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"source": "/(.*)",
|
"source": "/(.*)",
|
||||||
|
|||||||
9
vite-env.d.ts
vendored
9
vite-env.d.ts
vendored
@@ -1,9 +0,0 @@
|
|||||||
declare module '*.svg?raw' {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.svg' {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, '.'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user