294 lines
8.1 KiB
TypeScript
294 lines
8.1 KiB
TypeScript
|
// electron/main.ts
|
|||
|
import { app, BrowserWindow, Menu, globalShortcut, net, ipcMain } from 'electron';
|
|||
|
import path from 'path';
|
|||
|
import http from 'http';
|
|||
|
import { URL } from 'url';
|
|||
|
//import { apiRoutes } from './api'; // 导入API路由配置
|
|||
|
import dotenv from 'dotenv';
|
|||
|
dotenv.config({ path: path.join(__dirname, '../.env') }); // 根据实际路径调整
|
|||
|
|
|||
|
// 确保在news.ts中能读取到
|
|||
|
console.log('JUHE_KEY:', process.env.JUHE_NEWS_KEY?.substring(0, 3) + '***'); // 打印前3位验证
|
|||
|
console.log('CAIYUN_KEY:', process.env.CAIYUN_API_KEY?.substring(0, 3) + '***'); // 打印前3位验证
|
|||
|
|
|||
|
dotenv.config({ path: path.join(__dirname, '../.env') });
|
|||
|
|
|||
|
// 环境配置
|
|||
|
const isDev = process.env.NODE_ENV === 'development';
|
|||
|
|
|||
|
// 注册 IPC 处理器
|
|||
|
function registerIpcHandlers() {
|
|||
|
// 天气接口(需要修改)
|
|||
|
ipcMain.handle('get-weather', async (_, { lon, lat }) => {
|
|||
|
console.log('[IPC] 开始获取天气数据', { lon, lat });
|
|||
|
|
|||
|
// 检查缓存有效性
|
|||
|
const now = Date.now();
|
|||
|
if (now - apiCache.weather.timestamp < apiCache.weather.ttl) {
|
|||
|
console.log('[Cache] 返回缓存的天气数据');
|
|||
|
return apiCache.weather.data;
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
const apiKey = process.env.CAIYUN_API_KEY!;
|
|||
|
const apiUrl = `https://api.caiyunapp.com/v2.6/${apiKey}/${lon},${lat}/weather`;
|
|||
|
|
|||
|
const response = await net.fetch(apiUrl);
|
|||
|
const jsonData = await response.json();
|
|||
|
|
|||
|
if (jsonData.status !== 'ok') {
|
|||
|
throw new Error(jsonData.message || '天气数据获取失败');
|
|||
|
}
|
|||
|
|
|||
|
// 详细数据格式化
|
|||
|
const formattedData = {
|
|||
|
realtime: formatRealtime(jsonData.result.realtime),
|
|||
|
forecast: formatDailyForecast(jsonData.result.daily)
|
|||
|
};
|
|||
|
|
|||
|
console.log('[IPC] 格式化天气数据:', formattedData);
|
|||
|
// 更新缓存
|
|||
|
apiCache.weather.data = formattedData;
|
|||
|
apiCache.weather.timestamp = now;
|
|||
|
|
|||
|
return formattedData;
|
|||
|
} catch (error: any) {
|
|||
|
console.error('[IPC] 天气请求失败:', error);
|
|||
|
return {
|
|||
|
error: true,
|
|||
|
message: error.message
|
|||
|
};
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// 实时天气格式化
|
|||
|
const formatRealtime = (data: any) => ({
|
|||
|
temperature: data.temperature.toFixed(1),
|
|||
|
humidity: (data.humidity * 100).toFixed(1) + '%',
|
|||
|
wind: {
|
|||
|
speed: (data.wind.speed * 3.6).toFixed(1) + 'km/h',
|
|||
|
direction: getWindDirection(data.wind.direction)
|
|||
|
},
|
|||
|
airQuality: {
|
|||
|
aqi: data.air_quality.aqi.chn,
|
|||
|
description: data.air_quality.description.chn
|
|||
|
},
|
|||
|
skycon: data.skycon,
|
|||
|
apparentTemperature: data.apparent_temperature.toFixed(1)
|
|||
|
});
|
|||
|
|
|||
|
// 天气预报格式化
|
|||
|
const formatDailyForecast = (data: any) => ({
|
|||
|
temperature: data.temperature.map((item: any) => ({
|
|||
|
date: item.date,
|
|||
|
max: item.max,
|
|||
|
min: item.min
|
|||
|
})),
|
|||
|
skycon: data.skycon,
|
|||
|
precipitation: data.precipitation
|
|||
|
});
|
|||
|
|
|||
|
// 风向转换
|
|||
|
const getWindDirection = (degrees: number) => {
|
|||
|
const directions = ['北风', '东北风', '东风', '东南风', '南风', '西南风', '西风', '西北风'];
|
|||
|
return directions[Math.round(degrees % 360 / 45) % 8];
|
|||
|
};
|
|||
|
|
|||
|
// 新闻接口
|
|||
|
ipcMain.handle('get-news', async () => {
|
|||
|
console.log('[IPC] 开始获取新闻数据');
|
|||
|
const now = Date.now();
|
|||
|
|
|||
|
if (now - apiCache.news.timestamp < apiCache.news.ttl) {
|
|||
|
console.log('[Cache] 返回缓存的新闻数据');
|
|||
|
return apiCache.news.data;
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
const apiKey = process.env.JUHE_NEWS_KEY!;
|
|||
|
const response = await net.fetch(
|
|||
|
`https://v.juhe.cn/toutiao/index?type=top&key=${apiKey}`
|
|||
|
);
|
|||
|
const json = await response.json();
|
|||
|
console.log('[IPC] 新闻响应:', json); // 添加原始响应日志
|
|||
|
const formatted = formatNews(json.result?.data || []);
|
|||
|
// 更新缓存
|
|||
|
apiCache.news.data = formatted;
|
|||
|
apiCache.news.timestamp = now;
|
|||
|
return formatted;
|
|||
|
//return formatNews(json.result?.data || []);
|
|||
|
} catch (error) {
|
|||
|
console.error('[IPC] 新闻请求失败:', error);
|
|||
|
throw error;
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
const formatNews = (items: any[]) => items.map(item => ({
|
|||
|
uniquekey: item.uniquekey,
|
|||
|
title: item.title,
|
|||
|
date: item.date,
|
|||
|
category: item.category || "头条新闻",
|
|||
|
author_name: item.author_name || "未知作者",
|
|||
|
url: item.url,
|
|||
|
thumbnail_pic_s: item.thumbnail_pic_s,
|
|||
|
is_content: item.is_content
|
|||
|
}));
|
|||
|
|
|||
|
|
|||
|
// 添加缓存对象
|
|||
|
const apiCache = {
|
|||
|
weather: {
|
|||
|
data: null as any,
|
|||
|
timestamp: 0,
|
|||
|
//ttl: 1800000 // 30分钟缓存
|
|||
|
//6小时缓存
|
|||
|
ttl: 21600000
|
|||
|
},
|
|||
|
news: {
|
|||
|
data: null as any,
|
|||
|
timestamp: 0,
|
|||
|
//ttl: 3600000 // 1小时缓存
|
|||
|
//2小时缓存
|
|||
|
ttl: 7200000
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
// 禁用默认菜单(提升安全性和专业性)
|
|||
|
Menu.setApplicationMenu(null);
|
|||
|
|
|||
|
/**
|
|||
|
* 创建主窗口
|
|||
|
*/
|
|||
|
async function createWindow() {
|
|||
|
const mainWindow = new BrowserWindow({
|
|||
|
width: 1200,
|
|||
|
height: 800,
|
|||
|
webPreferences: {
|
|||
|
preload: path.join(__dirname, 'preload.js'),
|
|||
|
contextIsolation: true, // 启用上下文隔离(安全必需)
|
|||
|
nodeIntegration: false, // 禁用Node集成(安全要求)
|
|||
|
webSecurity: !isDev, // 生产环境启用安全策略
|
|||
|
sandbox: true // 启用沙箱模式
|
|||
|
},
|
|||
|
show: false // 先隐藏窗口直到内容加载完成
|
|||
|
});
|
|||
|
|
|||
|
// 优化加载体验
|
|||
|
mainWindow.once('ready-to-show', () => {
|
|||
|
mainWindow.show();
|
|||
|
if (isDev) {
|
|||
|
//mainWindow.webContents.openDevTools({ mode: 'detach' }); // 打开开发者工具
|
|||
|
console.log('Developer tools opened in detached mode');
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// 新增权限处理代码(必须在loadURL之前)
|
|||
|
const ses = mainWindow.webContents.session;
|
|||
|
|
|||
|
// 自动处理权限请求
|
|||
|
ses.setPermissionRequestHandler((webContents, permission, callback) => {
|
|||
|
const allowedPermissions = new Set(['media', 'microphone', 'audioCapture']);
|
|||
|
callback(allowedPermissions.has(permission));
|
|||
|
});
|
|||
|
|
|||
|
// 自动授予设备权限
|
|||
|
// 替换原来的setDevicePermissionHandler
|
|||
|
ses.setPermissionRequestHandler((webContents, permission, callback) => {
|
|||
|
// 统一允许所有媒体相关权限
|
|||
|
if (['microphone', 'audioCapture', 'media'].includes(permission)) {
|
|||
|
return callback(true);
|
|||
|
}
|
|||
|
callback(false);
|
|||
|
});
|
|||
|
|
|||
|
// 加载内容
|
|||
|
const loadURL = isDev
|
|||
|
? 'http://localhost:5173'
|
|||
|
: `file://${path.join(__dirname, '../dist/index.html')}`;
|
|||
|
|
|||
|
console.log(`Loading URL: ${loadURL}`);
|
|||
|
await mainWindow.loadURL(loadURL);
|
|||
|
|
|||
|
registerGlobalShortcuts(mainWindow);
|
|||
|
return mainWindow;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 统一响应格式(带安全头)
|
|||
|
*/
|
|||
|
function sendResponse(
|
|||
|
res: http.ServerResponse,
|
|||
|
status: number,
|
|||
|
data: unknown,
|
|||
|
headers: Record<string, string> = {}
|
|||
|
) {
|
|||
|
const securityHeaders = {
|
|||
|
'Content-Security-Policy': "default-src 'self'",
|
|||
|
'X-Content-Type-Options': 'nosniff',
|
|||
|
'X-Frame-Options': 'DENY'
|
|||
|
};
|
|||
|
|
|||
|
const responseHeaders = {
|
|||
|
'Content-Type': 'application/json',
|
|||
|
'Access-Control-Allow-Origin': '*',
|
|||
|
...securityHeaders,
|
|||
|
...headers
|
|||
|
};
|
|||
|
|
|||
|
res.writeHead(status, responseHeaders);
|
|||
|
res.end(JSON.stringify(data));
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 注册全局快捷键
|
|||
|
*/
|
|||
|
function registerGlobalShortcuts(win: BrowserWindow) {
|
|||
|
const shortcut = process.platform === 'darwin' ? 'Command+Q' : 'Ctrl+Q';
|
|||
|
|
|||
|
const ret = globalShortcut.register(shortcut, () => {
|
|||
|
console.log('Gracefully shutting down...');
|
|||
|
win.destroy();
|
|||
|
app.quit();
|
|||
|
});
|
|||
|
|
|||
|
if (!ret) {
|
|||
|
console.error('Failed to register shortcut');
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 应用启动流程
|
|||
|
*/
|
|||
|
app.whenReady()
|
|||
|
.then(() => {
|
|||
|
// 注册 IPC 处理器的代码
|
|||
|
registerIpcHandlers();
|
|||
|
console.log('Creating browser window...');
|
|||
|
return createWindow();
|
|||
|
})
|
|||
|
.catch((error) => {
|
|||
|
console.error('App initialization failed:', error);
|
|||
|
app.quit();
|
|||
|
});
|
|||
|
|
|||
|
/**
|
|||
|
* 生命周期管理
|
|||
|
*/
|
|||
|
app.on('window-all-closed', () => {
|
|||
|
if (process.platform !== 'darwin') {
|
|||
|
app.quit();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
app.on('will-quit', () => {
|
|||
|
globalShortcut.unregisterAll();
|
|||
|
});
|