Compare commits

...

4 Commits

Author SHA1 Message Date
c119fbaf7e Update: 修改 README.md,增添更多 TODOs 2025-09-10 22:06:49 +08:00
0cf2e1cf15 Feat: 添加托盘功能和响应的托盘菜单 2025-09-10 19:35:19 +08:00
14148ee1ec Fix: 解决打包后的应用找不到素材的问题 2025-09-10 17:09:13 +08:00
51c66cd498 Feat: 素材追加
Fix: 修正打包文件路径错误
2025-09-09 20:58:57 +08:00
14 changed files with 139 additions and 57 deletions

View File

@ -53,4 +53,6 @@ npm run start
- [x] 更美观的说的道理,优化 UI - [x] 更美观的说的道理,优化 UI
- [x] 更多的哇袄 - [x] 更多的哇袄
- [ ] 自定义更换不同的道理 - [x] 更换不同的道理
- [ ] 更完善的托盘
- [ ] 实用功能,更贴心的道理

View File

@ -1,10 +1,13 @@
const { app, BrowserWindow, ipcMain, Menu } = require("electron"); const { app, BrowserWindow, ipcMain, Menu, Tray, nativeImage } = require("electron");
const path = require("path"); const path = require("path");
const { spawn } = require("child_process"); const { spawn } = require("child_process");
const fs = require("fs"); const fs = require("fs");
app.disableHardwareAcceleration(); // 高 DPI 缩放修复 app.disableHardwareAcceleration(); // 高 DPI 缩放修复
let tray = null;
let isQuiting = false;
// 音效播放器 // 音效播放器
function playAudioFile(filePath) { function playAudioFile(filePath) {
if (process.platform === "win32") { if (process.platform === "win32") {
@ -123,9 +126,9 @@ ipcMain.handle('set-pet-selection', async (_, petAsset) => {
}); });
ipcMain.on("show-context-menu", async () => { ipcMain.on("show-context-menu", async () => {
// 尝试动态读取 renderer 下的 assets 目录,开发/生产路径均尝试 // 尝试动态读取 renderer 下的 public/pets 目录,开发/生产路径均尝试
const devAssets = path.join(__dirname, "../renderer/src/assets"); const devAssets = path.join(__dirname, "../renderer/public/pets");
const prodAssets = path.join(__dirname, "../renderer/dist/assets"); const prodAssets = path.join(__dirname, "../renderer/dist/pets");
let assetsDir = null; let assetsDir = null;
if (fs.existsSync(devAssets)) assetsDir = devAssets; if (fs.existsSync(devAssets)) assetsDir = devAssets;
else if (fs.existsSync(prodAssets)) assetsDir = prodAssets; else if (fs.existsSync(prodAssets)) assetsDir = prodAssets;
@ -204,7 +207,7 @@ ipcMain.on("show-context-menu", async () => {
{ type: 'separator' }, { type: 'separator' },
{ {
label: "退出", label: "退出",
click: () => { app.quit(); }, click: () => { isQuiting = true; app.quit(); },
}, },
]; ];
@ -212,6 +215,35 @@ ipcMain.on("show-context-menu", async () => {
menu.popup({ window: mainWindow }); menu.popup({ window: mainWindow });
}); });
// 提供给渲染进程:列出 public/pets 中的素材文件(开发/生产路径)
ipcMain.handle('get-pet-files', async () => {
const devDir = path.join(__dirname, '../renderer/public/pets');
const prodDir = path.join(__dirname, '../renderer/dist/pets');
const dir = fs.existsSync(devDir) ? devDir : (fs.existsSync(prodDir) ? prodDir : null);
if (!dir) return [];
try {
const files = await fs.promises.readdir(dir);
return files.filter(f => /\.(png|jpg|jpeg|gif)$/i.test(f));
} catch (e) {
console.error('读取 pets 目录失败:', e);
return [];
}
});
ipcMain.handle('get-pet-url', (_, fileName) => {
// 在开发时public 文件由 dev server 以根路径提供
if (process.env.NODE_ENV === 'development') {
return `http://localhost:5173/pets/${encodeURIComponent(fileName)}`;
}
// 生产时,从 dist/pets 返回 file:// URL
const prodPath = path.join(__dirname, '../renderer/dist/pets', fileName);
if (fs.existsSync(prodPath)) return `file://${prodPath}`;
// fallback: try public path in source tree
const devPath = path.join(__dirname, '../renderer/public/pets', fileName);
if (fs.existsSync(devPath)) return `file://${devPath}`;
return null;
});
let isAlwaysOnTop = true; let isAlwaysOnTop = true;
let mainWindow; let mainWindow;
@ -222,8 +254,9 @@ function createWindow() {
transparent: true, // 开启透明窗口 transparent: true, // 开启透明窗口
frame: false, // 无边框窗口 frame: false, // 无边框窗口
resizable: false, // 禁止调整大小 resizable: false, // 禁止调整大小
title: "说的道理桌", title: "说的道理桌面宠物(前端)",
alwaysOnTop: isAlwaysOnTop, // 窗口始终在最上层 alwaysOnTop: isAlwaysOnTop, // 窗口始终在最上层
skipTaskbar: true, // 不在任务栏显示
icon: path.join(__dirname, "../build/icon.png"), icon: path.join(__dirname, "../build/icon.png"),
webPreferences: { webPreferences: {
preload: path.join(__dirname, "preload.js"), preload: path.join(__dirname, "preload.js"),
@ -232,6 +265,23 @@ function createWindow() {
}, },
}); });
// 创建托盘图标
try {
// 修改托盘图标路径以确保在开发和生产环境中正确加载
const iconPath = process.env.NODE_ENV === "development"
? path.join(__dirname, "../assets/icon.ico") // 开发环境路径
: path.join(__dirname, "../renderer/public/images/icon.ico"); // 生产环境路径
const trayIcon = nativeImage.createFromPath(iconPath);
if (trayIcon.isEmpty()) {
console.error("托盘图标加载失败,路径:", iconPath);
} else {
tray = new Tray(trayIcon);
}
} catch (e) {
console.error('创建托盘失败:', e);
}
// 当渲染进程传来这个事件时,就移动窗口 // 当渲染进程传来这个事件时,就移动窗口
ipcMain.on("move-window", (event, { x, y }) => { ipcMain.on("move-window", (event, { x, y }) => {
// 使用 Math.round 避免非整数坐标可能带来的问题 // 使用 Math.round 避免非整数坐标可能带来的问题
@ -252,6 +302,23 @@ function createWindow() {
} else { } else {
mainWindow.loadFile(path.join(__dirname, "../renderer/dist/index.html")); mainWindow.loadFile(path.join(__dirname, "../renderer/dist/index.html"));
} }
// 启动时显示窗口(允许随后最小化到托盘)
try { mainWindow.show(); } catch (e) {}
// 最小化时隐藏到托盘
mainWindow.on('minimize', (event) => {
event.preventDefault();
mainWindow.hide();
});
// 关闭窗口时隐藏到托盘,除非用户选择真正退出
mainWindow.on('close', (event) => {
if (!isQuiting) {
event.preventDefault();
mainWindow.hide();
}
});
} }
app.whenReady().then(createWindow); app.whenReady().then(createWindow);

View File

@ -15,6 +15,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
onPetSelectionChanged: (callback) => { onPetSelectionChanged: (callback) => {
ipcRenderer.on('pet-selection-changed', (_, fileName) => callback(fileName)); ipcRenderer.on('pet-selection-changed', (_, fileName) => callback(fileName));
}, },
getPetFiles: () => ipcRenderer.invoke('get-pet-files'),
getPetUrl: (fileName) => ipcRenderer.invoke('get-pet-url', fileName),
showTooltip: (text) => ipcRenderer.send('show-tooltip', text), showTooltip: (text) => ipcRenderer.send('show-tooltip', text),
onUpdatePosition: (callback) => { onUpdatePosition: (callback) => {
ipcRenderer.on('update-position', (_, position) => callback(position)) ipcRenderer.on('update-position', (_, position) => callback(position))

View File

@ -1,6 +1,6 @@
{ {
"name": "shuodedaoli-deskpet", "name": "shuodedaoli-deskpet",
"version": "0.2.0", "version": "0.3.0",
"description": "A cute desktop pet of 'Shuodedaoli' built with Electron and Vue 3.", "description": "A cute desktop pet of 'Shuodedaoli' built with Electron and Vue 3.",
"main": "main/main.js", "main": "main/main.js",
"scripts": { "scripts": {
@ -25,9 +25,9 @@
"allowToChangeInstallationDirectory": true, "allowToChangeInstallationDirectory": true,
"perMachine": false, "perMachine": false,
"allowElevation": false, "allowElevation": false,
"installerIcon": "build/icon.ico", "installerIcon": "assets/icon.ico",
"uninstallerIcon": "build/icon.ico", "uninstallerIcon": "assets/icon.ico",
"installerHeaderIcon": "build/icon.ico", "installerHeaderIcon": "assets/icon.ico",
"installerLanguages": ["zh_CN", "en_US"], "installerLanguages": ["zh_CN", "en_US"],
"language": "2052" "language": "2052"
}, },
@ -43,15 +43,15 @@
}, },
"win": { "win": {
"target": "nsis", "target": "nsis",
"icon": "build/icon.png" "icon": "assets/icon.png"
}, },
"mac": { "mac": {
"target": "dmg", "target": "dmg",
"icon": "build/icon.png" "icon": "assets/icon.png"
}, },
"linux": { "linux": {
"target": "AppImage", "target": "AppImage",
"icon": "build/icon.png" "icon": "assets/icon.png"
} }
}, },
"repository": { "repository": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 677 KiB

After

Width:  |  Height:  |  Size: 677 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

Before

Width:  |  Height:  |  Size: 10 MiB

After

Width:  |  Height:  |  Size: 10 MiB

View File

Before

Width:  |  Height:  |  Size: 6.5 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -2,13 +2,7 @@
import { ref, onMounted, reactive, computed } from "vue"; import { ref, onMounted, reactive, computed } from "vue";
import { Howl } from "howler"; import { Howl } from "howler";
// 默认图片(打包时位于 assets import defaultPet from "/public/pets/普通型道理.gif";
import defaultPet from "./assets/普通型道理.gif";
// 列出 renderer/src/assets 下的图片资源Vite 特性)
// 只匹配常见的图片后缀
const modules = import.meta.glob('./assets/*.{png,jpg,jpeg,gif}', { as: 'url' });
const assetEntries = Object.entries(modules);
// 状态 // 状态
const soundFiles = ref([]); const soundFiles = ref([]);
@ -18,40 +12,53 @@ const isLoading = ref(true);
const isPlaying = ref(false); const isPlaying = ref(false);
// 处理宠物素材选择 // 处理宠物素材选择
const assetList = assetEntries.map(([path, resolver]) => ({ path, resolver })); const assetPreviews = ref([]); // { name, fileName, url }
const assetPreviews = ref([]); // { path, url }
const selectedAsset = ref(null); // 将保存为 URL const selectedAsset = ref(null); // 将保存为 URL
const showSettings = ref(false); const showSettings = ref(false);
const selectedAssetName = computed(() => { const selectedAssetName = computed(() => {
if (!selectedAsset.value) return null; if (!selectedAsset.value) return null;
// 从路径中取文件名 const match = assetPreviews.value.find(p => p.url === selectedAsset.value);
if (match) return match.name;
// fallback: 从 URL 中取最后一段
const parts = selectedAsset.value.split('/'); const parts = selectedAsset.value.split('/');
return parts[parts.length - 1]; return parts[parts.length - 1];
}); });
async function loadSavedSelection() { async function loadSavedSelection() {
// 优先从主进程读取持久化选择 // 优先从主进程读取持久化选择(返回 fileName
if (window.electronAPI && typeof window.electronAPI.getPetSelection === 'function') {
try { try {
const assetPath = await window.electronAPI.getPetSelection(); if (window.electronAPI && typeof window.electronAPI.getPetSelection === 'function') {
if (assetPath) { const fileName = await window.electronAPI.getPetSelection();
// assetPath 存储为相对于 assets 的文件名,例如 "pet2.gif" if (fileName) {
const match = assetList.find(a => a.path.endsWith('/' + assetPath)); const match = assetPreviews.value.find(p => p.fileName === fileName);
if (match) { if (match) {
selectedAsset.value = await match.resolver(); selectedAsset.value = match.url;
return; return;
} }
// 如果 assetPreviews 中没有,但文件名存在,尝试直接请求 URL
if (window.electronAPI && typeof window.electronAPI.getPetUrl === 'function') {
const url = await window.electronAPI.getPetUrl(fileName);
if (url) { selectedAsset.value = url; return; }
}
}
} }
} catch (e) { } catch (e) {
console.error('读取保存的宠物素材失败:', e); console.error('读取保存的宠物素材失败:', e);
} }
}
// fallback: localStorage // fallback: localStorage
try {
const ls = localStorage.getItem('petAsset'); const ls = localStorage.getItem('petAsset');
if (ls) { if (ls) {
const match = assetList.find(a => a.path.endsWith('/' + ls)); const match = assetPreviews.value.find(p => p.fileName === ls);
if (match) selectedAsset.value = await match.resolver(); if (match) { selectedAsset.value = match.url; return; }
if (window.electronAPI && typeof window.electronAPI.getPetUrl === 'function') {
const url = await window.electronAPI.getPetUrl(ls);
if (url) selectedAsset.value = url;
}
}
} catch (e) {
// ignore
} }
} }
@ -81,19 +88,29 @@ onMounted(async () => {
isLoading.value = false; isLoading.value = false;
} }
} }
await loadSavedSelection(); // 从主进程读取 public/pets 下的素材文件名,然后为每个文件请求可用 URL
// 预解析所有资源的 URL用于设置面板的缩略图显示
try { try {
const previews = await Promise.all(assetList.map(async (a) => ({ path: a.path, url: await a.resolver() }))); if (window.electronAPI && typeof window.electronAPI.getPetFiles === 'function') {
const files = await window.electronAPI.getPetFiles();
const previews = await Promise.all(files.map(async (file) => {
const url = await window.electronAPI.getPetUrl(file);
const name = file.replace(/\.[^.]+$/, '');
const displayName = name + (/\.gif$/i.test(file) ? '(可动)' : '');
return { fileName: file, url, name: displayName };
}));
assetPreviews.value = previews; assetPreviews.value = previews;
} catch (e) {
console.error('解析素材预览失败:', e);
} }
} catch (e) {
console.error('获取 pets 列表失败:', e);
}
// 加载并应用保存的选择(依赖于 assetPreviews 已构建)
await loadSavedSelection();
// 监听主进程通过右键菜单发来的选择变更 // 监听主进程通过右键菜单发来的选择变更
if (window.electronAPI && typeof window.electronAPI.onPetSelectionChanged === 'function') { if (window.electronAPI && typeof window.electronAPI.onPetSelectionChanged === 'function') {
window.electronAPI.onPetSelectionChanged(async (fileName) => { window.electronAPI.onPetSelectionChanged(async (fileName) => {
const match = assetPreviews.value.find(p => p.path.endsWith('/' + fileName)); const match = assetPreviews.value.find(p => p.fileName === fileName);
if (match) selectedAsset.value = match.url; if (match) selectedAsset.value = match.url;
}); });
} }
@ -170,15 +187,9 @@ function handleMouseUp() {
function handleRightClick() { window.electronAPI.showContextMenu(); } function handleRightClick() { window.electronAPI.showContextMenu(); }
async function chooseAsset(entry) { async function chooseAsset(entry) {
// entry can be either {path, resolver} or a preview {path, url} // entry is { fileName, url, name }
if (entry.url) {
selectedAsset.value = entry.url; selectedAsset.value = entry.url;
await saveSelection(entry.path); await saveSelection(entry.fileName);
} else {
const url = await entry.resolver();
selectedAsset.value = url;
await saveSelection(entry.path);
}
showSettings.value = false; showSettings.value = false;
} }
</script> </script>