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();
|
||
}); |