Feat: 允许切换不同形态的道理

This commit is contained in:
2025-09-09 20:46:03 +08:00
parent 2fea99a7e2
commit 27c65820e6
7 changed files with 223 additions and 65 deletions

View File

@ -52,5 +52,5 @@ npm run start
## TODO ## TODO
- [x] 更美观的说的道理,优化 UI - [x] 更美观的说的道理,优化 UI
- [ ] 更多的哇袄 - [x] 更多的哇袄
- [ ] 自定义更换不同的道理 - [ ] 自定义更换不同的道理

View File

@ -88,27 +88,128 @@ ipcMain.handle("get-sound-files", async () => {
} }
}); });
ipcMain.on("show-context-menu", () => { // 持久化用户设置 (用于保存所选宠物素材文件名)
const settingsFile = path.join(app.getPath('userData') || __dirname, 'settings.json');
ipcMain.handle('get-pet-selection', async () => {
try {
if (fs.existsSync(settingsFile)) {
const data = JSON.parse(await fs.promises.readFile(settingsFile, 'utf8'));
return data.petAsset || null;
}
} catch (err) {
console.error('读取设置失败:', err);
}
return null;
});
ipcMain.handle('set-pet-selection', async (_, petAsset) => {
try {
let data = {};
if (fs.existsSync(settingsFile)) {
try {
data = JSON.parse(await fs.promises.readFile(settingsFile, 'utf8')) || {};
} catch (e) {
data = {};
}
}
data.petAsset = petAsset;
await fs.promises.writeFile(settingsFile, JSON.stringify(data, null, 2), 'utf8');
return true;
} catch (err) {
console.error('写入设置失败:', err);
return false;
}
});
ipcMain.on("show-context-menu", async () => {
// 尝试动态读取 renderer 下的 assets 目录,开发/生产路径均尝试
const devAssets = path.join(__dirname, "../renderer/src/assets");
const prodAssets = path.join(__dirname, "../renderer/dist/assets");
let assetsDir = null;
if (fs.existsSync(devAssets)) assetsDir = devAssets;
else if (fs.existsSync(prodAssets)) assetsDir = prodAssets;
// 读取当前保存的选择(文件名)
let currentSelection = null;
try {
if (fs.existsSync(settingsFile)) {
const data = JSON.parse(await fs.promises.readFile(settingsFile, 'utf8')) || {};
currentSelection = data.petAsset || null;
}
} catch (e) {
console.error('读取当前选择失败:', e);
}
// 构建素材菜单项,如果无法读取目录,则提供一个默认值
let assetItems = [];
try {
if (assetsDir) {
const files = await fs.promises.readdir(assetsDir);
const imgs = files.filter(f => /\.(png|jpg|jpeg|gif)$/i.test(f));
assetItems = imgs.map((f) => {
const nameWithoutExt = f.replace(/\.[^.]+$/, '');
const displayLabel = nameWithoutExt + (/\.gif$/i.test(f) ? '(可动)' : '');
return ({
label: displayLabel,
type: 'radio',
checked: f === currentSelection,
click: async () => {
try {
// 写入 settings.json
let data = {};
if (fs.existsSync(settingsFile)) {
try { data = JSON.parse(await fs.promises.readFile(settingsFile, 'utf8')) || {}; } catch (e) { data = {}; }
}
data.petAsset = f;
await fs.promises.writeFile(settingsFile, JSON.stringify(data, null, 2), 'utf8');
// 通知渲染进程更新
if (mainWindow && mainWindow.webContents) {
mainWindow.webContents.send('pet-selection-changed', f);
}
} catch (err) {
console.error('写入选择失败:', err);
}
}
});
});
}
} catch (err) {
console.error('读取 assets 目录失败:', err);
assetItems = [];
}
// 如果没有任何可用素材,提供占位项
if (assetItems.length === 0) {
assetItems = [
{ label: '(无可用素材)', enabled: false }
];
}
const template = [ const template = [
{ {
label: "置顶显示", label: "置顶显示",
type: "checkbox", type: "checkbox",
checked: isAlwaysOnTop, // 菜单项的选中状态与变量同步 checked: isAlwaysOnTop,
click: () => { click: () => {
isAlwaysOnTop = !isAlwaysOnTop; // 点击时切换状态 isAlwaysOnTop = !isAlwaysOnTop;
mainWindow.setAlwaysOnTop(isAlwaysOnTop); // 并应用到窗口 if (mainWindow) mainWindow.setAlwaysOnTop(isAlwaysOnTop);
}, },
}, },
{ type: "separator" }, // 分隔线 { type: 'separator' },
{
label: '选择素材',
submenu: assetItems,
},
{ type: 'separator' },
{ {
label: "退出", label: "退出",
click: () => { click: () => { app.quit(); },
app.quit(); // 点击时退出应用
},
}, },
]; ];
const menu = Menu.buildFromTemplate(template); const menu = Menu.buildFromTemplate(template);
menu.popup({ window: mainWindow }); // 在主窗口上弹出菜单 menu.popup({ window: mainWindow });
}); });
let isAlwaysOnTop = true; let isAlwaysOnTop = true;

View File

@ -10,6 +10,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
}, },
getSoundPath: (soundFile) => ipcRenderer.invoke('get-sound-path', soundFile), getSoundPath: (soundFile) => ipcRenderer.invoke('get-sound-path', soundFile),
getSoundFiles: () => ipcRenderer.invoke('get-sound-files'), getSoundFiles: () => ipcRenderer.invoke('get-sound-files'),
getPetSelection: () => ipcRenderer.invoke('get-pet-selection'),
setPetSelection: (petAsset) => ipcRenderer.invoke('set-pet-selection', petAsset),
onPetSelectionChanged: (callback) => {
ipcRenderer.on('pet-selection-changed', (_, fileName) => callback(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,133 +1,185 @@
<script setup> <script setup>
import { ref, onMounted, reactive } from "vue"; import { ref, onMounted, reactive, computed } from "vue";
import petGif from "./assets/pet.gif";
import { Howl } from "howler"; import { Howl } from "howler";
// 状态管理 // 默认图片(打包时位于 assets
const soundFiles = ref([]); // 存储从主进程获取的声音文件名列表 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 showTooltip = ref(false); const showTooltip = ref(false);
const currentTooltip = ref(""); const currentTooltip = ref("");
const isLoading = ref(true); // 跟踪文件列表是否已加载 const isLoading = ref(true);
const isPlaying = ref(false); // 是否正在播放音效,用作锁 const isPlaying = ref(false);
// 在组件挂载后,从主进程获取声音文件列表 // 处理宠物素材选择
const assetList = assetEntries.map(([path, resolver]) => ({ path, resolver }));
const assetPreviews = ref([]); // { path, url }
const selectedAsset = ref(null); // 将保存为 URL
const showSettings = ref(false);
const selectedAssetName = computed(() => {
if (!selectedAsset.value) return null;
// 从路径中取文件名
const parts = selectedAsset.value.split('/');
return parts[parts.length - 1];
});
async function loadSavedSelection() {
// 优先从主进程读取持久化选择
if (window.electronAPI && typeof window.electronAPI.getPetSelection === 'function') {
try {
const assetPath = await window.electronAPI.getPetSelection();
if (assetPath) {
// assetPath 存储为相对于 assets 的文件名,例如 "pet2.gif"
const match = assetList.find(a => a.path.endsWith('/' + assetPath));
if (match) {
selectedAsset.value = await match.resolver();
return;
}
}
} catch (e) {
console.error('读取保存的宠物素材失败:', e);
}
}
// fallback: localStorage
const ls = localStorage.getItem('petAsset');
if (ls) {
const match = assetList.find(a => a.path.endsWith('/' + ls));
if (match) selectedAsset.value = await match.resolver();
}
}
async function saveSelection(assetPath) {
const fileName = assetPath.replace(/^.*\//, '');
// 保存到主进程
if (window.electronAPI && typeof window.electronAPI.setPetSelection === 'function') {
try {
await window.electronAPI.setPetSelection(fileName);
} catch (e) {
console.error('保存宠物素材失败:', e);
}
}
// fallback: localStorage
try { localStorage.setItem('petAsset', fileName); } catch (_) {}
}
// 初始化
onMounted(async () => { onMounted(async () => {
// 加载声音文件列表
if (window.electronAPI && typeof window.electronAPI.getSoundFiles === 'function') { if (window.electronAPI && typeof window.electronAPI.getSoundFiles === 'function') {
try { try {
soundFiles.value = await window.electronAPI.getSoundFiles(); soundFiles.value = await window.electronAPI.getSoundFiles();
} catch (error) { } catch (err) {
console.error("获取声音文件列表失败:", error); console.error("获取声音文件列表失败:", err);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
await loadSavedSelection();
// 预解析所有资源的 URL用于设置面板的缩略图显示
try {
const previews = await Promise.all(assetList.map(async (a) => ({ path: a.path, url: await a.resolver() })));
assetPreviews.value = previews;
} catch (e) {
console.error('解析素材预览失败:', e);
}
// 监听主进程通过右键菜单发来的选择变更
if (window.electronAPI && typeof window.electronAPI.onPetSelectionChanged === 'function') {
window.electronAPI.onPetSelectionChanged(async (fileName) => {
const match = assetPreviews.value.find(p => p.path.endsWith('/' + fileName));
if (match) selectedAsset.value = match.url;
}); });
}
});
const petGifUrl = computed(() => selectedAsset.value || defaultPet);
const playRandomSound = async () => { const playRandomSound = async () => {
if (isPlaying.value) return; if (isPlaying.value) return;
if (isLoading.value || soundFiles.value.length === 0) return; if (isLoading.value || soundFiles.value.length === 0) return;
const randomSoundFile = soundFiles.value[Math.floor(Math.random() * soundFiles.value.length)]; const randomSoundFile = soundFiles.value[Math.floor(Math.random() * soundFiles.value.length)];
isPlaying.value = true; // 加锁 isPlaying.value = true;
try { try {
const audioUrl = await window.electronAPI.getSoundPath(randomSoundFile); const audioUrl = await window.electronAPI.getSoundPath(randomSoundFile);
if (audioUrl) { if (audioUrl) {
new Howl({ new Howl({
src: [audioUrl], src: [audioUrl],
format: ["mp3"], format: ["mp3"],
// 当音频播放结束时
onend: function() { onend: function() {
// 隐藏提示框
showTooltip.value = false; showTooltip.value = false;
// 解锁,允许下一次点击
isPlaying.value = false; isPlaying.value = false;
} }
}).play(); }).play();
} else { } else {
// 如果音频路径获取失败,也要解锁
isPlaying.value = false; isPlaying.value = false;
} }
} catch (err) { } catch (err) {
// 如果播放过程出错,也要解锁
isPlaying.value = false; isPlaying.value = false;
console.error("播放失败:", err); console.error("播放失败:", err);
} }
if (randomSoundFile === "哇袄.mp3") {
currentTooltip.value = "哇袄!!!";
} else {
currentTooltip.value = randomSoundFile.replace(/\.mp3$/, ''); currentTooltip.value = randomSoundFile.replace(/\.mp3$/, '');
}
showTooltip.value = true; showTooltip.value = true;
}; };
const dragState = reactive({ const dragState = reactive({
isDragging: false, isDragging: false,
hasMoved: false, hasMoved: false,
// 分别记录鼠标和窗口的起始位置
mouseStartX: 0, mouseStartX: 0,
mouseStartY: 0, mouseStartY: 0,
windowStartX: 0, windowStartX: 0,
windowStartY: 0, windowStartY: 0,
}); });
// 鼠标按下事件 (改为异步函数)
async function handleMouseDown(event) { async function handleMouseDown(event) {
// 如果按下的不是鼠标左键,则不执行任何操作 if (event.button !== 0) return;
if (event.button !== 0) {
return;
}
// 在拖动开始时,先获取窗口的当前位置
const { x, y } = await window.electronAPI.getWindowPosition(); const { x, y } = await window.electronAPI.getWindowPosition();
dragState.windowStartX = x; dragState.windowStartX = x;
dragState.windowStartY = y; dragState.windowStartY = y;
// 记录鼠标的初始位置
dragState.mouseStartX = event.screenX; dragState.mouseStartX = event.screenX;
dragState.mouseStartY = event.screenY; dragState.mouseStartY = event.screenY;
dragState.isDragging = true; dragState.isDragging = true;
dragState.hasMoved = false; dragState.hasMoved = false;
window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp); window.addEventListener('mouseup', handleMouseUp);
} }
// 鼠标移动事件
function handleMouseMove(event) { function handleMouseMove(event) {
if (!dragState.isDragging) return; if (!dragState.isDragging) return;
// 计算鼠标从起点移动的距离(偏移量)
const deltaX = event.screenX - dragState.mouseStartX; const deltaX = event.screenX - dragState.mouseStartX;
const deltaY = event.screenY - dragState.mouseStartY; const deltaY = event.screenY - dragState.mouseStartY;
if (!dragState.hasMoved && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) dragState.hasMoved = true;
if (!dragState.hasMoved && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) {
dragState.hasMoved = true;
}
// 计算窗口的新位置 = 窗口初始位置 + 鼠标偏移量
const newWindowX = dragState.windowStartX + deltaX; const newWindowX = dragState.windowStartX + deltaX;
const newWindowY = dragState.windowStartY + deltaY; const newWindowY = dragState.windowStartY + deltaY;
// 将计算出的正确位置发送给主进程
window.electronAPI.moveWindow({ x: newWindowX, y: newWindowY }); window.electronAPI.moveWindow({ x: newWindowX, y: newWindowY });
} }
// 鼠标抬起事件
function handleMouseUp() { function handleMouseUp() {
// 如果鼠标按下后没有真正移动过,就认为这是一次点击 if (dragState.isDragging && !dragState.hasMoved) playRandomSound();
if (dragState.isDragging && !dragState.hasMoved) {
playRandomSound();
}
// 状态重置
dragState.isDragging = false; dragState.isDragging = false;
// 移除全局监听器(非常重要)
window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp); window.removeEventListener('mouseup', handleMouseUp);
} }
function handleRightClick() { function handleRightClick() { window.electronAPI.showContextMenu(); }
window.electronAPI.showContextMenu();
async function chooseAsset(entry) {
// entry can be either {path, resolver} or a preview {path, url}
if (entry.url) {
selectedAsset.value = entry.url;
await saveSelection(entry.path);
} else {
const url = await entry.resolver();
selectedAsset.value = url;
await saveSelection(entry.path);
}
showSettings.value = false;
} }
</script> </script>
@ -142,7 +194,7 @@ function handleRightClick() {
{{ currentTooltip }} {{ currentTooltip }}
</div> </div>
</transition> </transition>
<img :src="petGif" class="pet-gif" /> <img :src="petGifUrl" class="pet-gif" />
</div> </div>
</template> </template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

View File

Before

Width:  |  Height:  |  Size: 6.5 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB