flat: 暂存

This commit is contained in:
Apcallover
2025-12-22 16:32:42 +08:00
parent e9c90d759b
commit 92ec6504f9
18 changed files with 275 additions and 187 deletions

View File

@@ -1,11 +1,18 @@
import { app, shell, BrowserWindow } from "electron";
import { setupLiveHandlers } from "./ipc/live";
import { setupPromptHandlers } from "./ipc/prompt";
import { setupWorkflowHandlers } from "./ipc/workflow";
import { preload, indexHtml, ELECTRON_RENDERER_URL } from "./config";
// 必须在 app ready 之前调用
app.commandLine.appendSwitch("disable-site-isolation-trials");
app.commandLine.appendSwitch(
"disable-features",
"IsolateOrigins,site-per-process",
);
// 简单的开发环境检测
const isDev = process.env.NODE_ENV === 'development';
const isDev = process.env.NODE_ENV === "development";
/**
* 创建主窗口
@@ -15,6 +22,7 @@ function createWindow(): void {
width: 1080,
height: 670,
show: false,
titleBarStyle: "hidden",
autoHideMenuBar: true,
webPreferences: {
preload,
@@ -59,8 +67,6 @@ app.whenReady().then(() => {
// 直播相关处理
setupLiveHandlers();
setupPromptHandlers();
setupWorkflowHandlers();
createWindow();
@@ -84,7 +90,7 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => {
// 清理所有资源
const windows = BrowserWindow.getAllWindows();
windows.forEach(window => {
windows.forEach((window) => {
window.removeAllListeners();
window.close();
});

View File

@@ -91,7 +91,7 @@ export function setupLiveHandlers() {
}
// 3. 等待网页加载完成
// await waitForPageLoad();
await waitForIframeVideoPlaying(liveState.liveWindow!.webContents);
// 4. 获取 sessionId
console.log("获取 sessionId");
@@ -115,7 +115,7 @@ export function setupLiveHandlers() {
interrupt: true,
});
showPrompt("直播已开始", "info");
// showPrompt("直播已开始", "info");
return { success: true, sessionId: liveState.sessionId };
} catch (error: any) {
console.error("Start live error:", error);
@@ -140,7 +140,7 @@ export function setupLiveHandlers() {
liveState.cameraActive = false;
liveState.audioActive = false;
showPrompt("直播已结束", "info");
// showPrompt("直播已结束", "info");
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
@@ -212,7 +212,7 @@ export function setupLiveHandlers() {
// 创建直播窗口
async function createLiveWindow() {
const width = 375;
const height = 690;
const height = 665;
let liveUrl = `${ELECTRON_RENDERER_URL}/#/live`;
if (liveState.userId) {
liveUrl += `?userId=${liveState.userId}`;
@@ -225,7 +225,11 @@ export function setupLiveHandlers() {
minimizable: false,
maximizable: false,
closable: true,
frame: false,
trafficLightPosition: { x: -100, y: -100 },
alwaysOnTop: true,
titleBarStyle: "hidden",
autoHideMenuBar: true,
webPreferences: {
preload,
nodeIntegration: true,
@@ -234,7 +238,7 @@ export function setupLiveHandlers() {
allowRunningInsecureContent: true, // 允许运行本地内容
},
});
// liveState.liveWindow.webContents.openDevTools();
liveState.liveWindow.on("closed", () => {
liveState.liveWindow = null;
liveState.isLiveOn = false;
@@ -248,49 +252,55 @@ export function setupLiveHandlers() {
}
// 等待页面加载完成
// @ts-ignore - 暂时未使用但保留以备将来使用
async function waitForPageLoad() {
if (!liveState.liveWindow) {
throw new Error("直播窗口不存在");
}
async function waitForIframeVideoPlaying(
webContents: Electron.WebContents,
timeoutMs = 20000,
) {
console.log("开始检测 iframe 内视频的实际播放状态...");
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("页面加载超时"));
}, 30000); // 30秒超时
// 监听页面加载完成事件
liveState.liveWindow!.webContents.once("did-finish-load", () => {
clearTimeout(timeout);
console.log("页面加载完成");
// 额外等待一下确保iframe也加载完成
setTimeout(() => {
// 通知所有渲染进程状态已更新
const allWindows = BrowserWindow.getAllWindows();
allWindows.forEach((window: BrowserWindow) => {
if (window.webContents && !window.isDestroyed()) {
window.webContents.send("live-status-updated", {
hasLiveWindow: true,
windowLoaded: true,
});
try {
await webContents.executeJavaScript(`
new Promise((resolve) => {
const start = Date.now();
const check = setInterval(() => {
const iframe = document.querySelector('iframe');
if (!iframe) return;
const iframeDoc = iframe.contentDocument;
if (!iframeDoc) {
return;
}
const video = iframeDoc.querySelector('video');
if (video) {
const isReady = video.readyState >= 3;
const hasProgress = video.currentTime > 0;
const isPlaying = !video.paused;
const isVisible = video.videoWidth > 0 && video.videoHeight > 0;
if (isReady && hasProgress && isPlaying && isVisible) {
console.log("Success: Iframe 内视频已开始播放!当前进度:", video.currentTime);
clearInterval(check);
resolve(true);
} else {
// 可以打印状态方便调试 (可选)
// console.log(\`等待播放: Ready=\${video.readyState}, Time=\${video.currentTime}, Paused=\${video.paused}\`);
}
});
resolve(true);
}, 2000);
});
// 监听加载失败事件
liveState.liveWindow!.webContents.once(
"did-fail-load",
(_, errorCode, errorDesc) => {
clearTimeout(timeout);
reject(
new Error(`页面加载失败: ${errorDesc} (${errorCode})`),
);
},
}
if (Date.now() - start > ${timeoutMs}) {
clearInterval(check);
console.warn("Iframe 视频播放检测超时");
resolve(false);
}
}, 500); // 每 0.5 秒检查一次
})
`);
return true;
} catch (error) {
console.error(
"Iframe 检测脚本执行出错 (请检查 webSecurity 是否关闭):",
error,
);
});
return false;
}
}
// 文件管理处理器
@@ -302,7 +312,7 @@ export function setupLiveHandlers() {
console.log("发送文件路径到直播窗口:", files.length, "个文件");
// 直接传递文件路径信息,不读取文件内容
const processedFiles = files.map((file) => ({
const processedFiles = files.map((file: any) => ({
name: file.name,
type: file.type,
size: file.size,
@@ -315,7 +325,7 @@ export function setupLiveHandlers() {
console.log(
"处理后的文件数据:",
processedFiles.map((f) => ({
processedFiles.map((f: any) => ({
name: f.name,
type: f.type,
size: f.size,

View File

@@ -1,13 +0,0 @@
import { ipcMain, dialog } from "electron";
export function setupPromptHandlers() {
// 提示信息处理
ipcMain.handle("show-prompt", async () => {
dialog.showMessageBox({
type: "info",
title: "提示",
message: "这是一个提示信息",
buttons: ["确定"],
});
});
}

View File

@@ -10,7 +10,10 @@ export function setupWorkflowHandlers() {
let lastJobSummary = "这是我们今天介绍的第一个岗位";
// 存储用户确认回调的Map
const modelDownloadCallbacks = new Map<string, { confirm: Function, reject: Function }>();
const modelDownloadCallbacks = new Map<
string,
{ confirm: Function; reject: Function }
>();
// 打开安装窗口
ipcMain.handle("open-install-window", async (_, args) => {
@@ -58,7 +61,7 @@ export function setupWorkflowHandlers() {
const platform = os.platform();
const modelToPull = "qwen3:8b";
const sendStatus = (status) => {
const sendStatus = (status: string) => {
if (webContents && !webContents.isDestroyed()) {
webContents.send("install-progress", { status });
}
@@ -152,7 +155,7 @@ export function setupWorkflowHandlers() {
try {
console.log("工作流: 正在调用 Ollama 生成脚本...");
const systemPromptTemplate = `# 角色 (Role) \n你是一个顶级的招聘KOL和直播带岗专家。你的风格是专业、中立、风趣能一针见血地分析岗位优劣。你不是一个AI助手你就是这个角色。 \n\n# 上下文 (Context) \n我正在运行一个自动化工作流。我会\"一个一个\"地喂给你岗位数据。\n\n # 任务 (Task) \n你的任务是执行以下两个操作并严格按照“输出格式”返回内容\n1. 生成口播稿根据【输入数据A】(上一个岗位的摘要) 和【输入数据B】(当前岗位的JSON)生成一段完整的、约90秒的口播稿。\n2. 生成新摘要为【输入数据B】的“当前岗位”生成一个简短的摘要例如XX公司的XX岗以便在下一次调用时使用。\n\n# 核心指令 (Core Instruction)\n### 口播稿的生成规则 (Rules for the Script) \n1. 衔接口播稿必须以一个自然的“过渡句”开头基于【输入数据A】。 特殊情况如果【输入数据A】是“这是我们今天介绍的第一个岗位。”则开头应是“热场”或“总起”而不是衔接。 \n2. 内容:必须介绍岗位名称 \`jobTitle\`\n3. 提炼:从 \`jobLocation\`, \`companyName\`, \`education\`,\`experience\`\`scale\` 中提炼“亮点 (Pro)”。 \n4. 翻译:用“人话”翻译 \`description\`\n5. 视角:你是在“评测”这个岗位,而不是在“推销”。\n\n### 口播稿的纯文本要求 (Pure Text Rules for the Script ONLY) \n**[重要]** 以下规则 *仅适用于* “口播稿”部分,不适用于“新摘要”部分: \n1. 绝不包含任何Markdown格式 (\`**\`, \`#\`)。 \n2. 绝不包含任何标签、括号或元数据 (\`[]\`, \`()\`)。 \n3. 绝不包含任何寒暄、问候、或自我介绍 (例如 \"你好\", \"当然\")。 \n4. 必须是可以直接朗读的、完整的、流畅的纯文本。\n\n# 输入数据 (Input Data)\n\n## 输入数据A (上一个岗位摘要) \n${lastJobSummary}\n\n## 输入数据B (当前岗位JSON)\n\`\`\`json\n${JSON.stringify(currentJobData, null, 2)}\n\`\`\`\n\n# 输出格式 (Output Format)\n**[绝对严格的指令]** \n你必须严格按照下面这个“两部分”格式输出使用 \`---NEXT_SUMMARY---\` 作为唯一的分隔符。 绝不在分隔符之外添加任何多余的文字、解释或Markdown。 \n\n[这里是AI生成的、符合上述所有“纯文本要求”的完整口播稿] \n---NEXT_SUMMARY--- \n[这里是AI为“当前岗位”生成的简短新摘要]`;
const systemPromptTemplate = `# 角色 (Role) \n你是一个顶级的招聘KOL和直播带岗专家。你的风格是专业、中立、风趣能一针见血地分析岗位优劣。你不是一个AI助手你就是这个角色。 \n\n# 上下文 (Context) \n我正在运行一个自动化工作流。我会\"一个一个\"地喂给你岗位数据。\n\n # 任务 (Task) \n你的任务是执行以下两个操作并严格按照“输出格式”返回内容\n1. 生成口播稿根据【输入数据A】(上一个岗位的摘要) 和【输入数据B】(当前岗位的JSON)生成一段完整的、约90秒的口播稿。\n2. 生成新摘要为【输入数据B】的“当前岗位”生成一个简短的摘要例如XX公司的XX岗以便在下一次调用时使用。\n\n# 核心指令 (Core Instruction)\n### 口播稿的生成规则 (Rules for the Script) \n1. 衔接口播稿必须以一个自然的“过渡句”开头基于【输入数据A】。 特殊情况如果【输入数据A】是“这是我们今天介绍的第一个岗位。”则开头应是“热场”或“总起”而不是衔接。 \n2. 内容:必须介绍岗位名称 \`jobTitle\`\n3. 提炼:从 \`jobLocation\`, \`companyName\`, \`education\`,\`experience\`,\`scale\` 中提炼“亮点 (Pro)”。 \n4. 翻译:用“人话”翻译 \`description\`\n5. 视角:你是在“评测”这个岗位,而不是在“推销”。\n\n### 口播稿的纯文本要求 (Pure Text Rules for the Script ONLY) \n**[重要]** 以下规则 *仅适用于* “口播稿”部分,不适用于“新摘要”部分: \n1. 绝不包含任何Markdown格式 (\`**\`, \`#\`)。 \n2. 绝不包含任何标签、括号或元数据 (\`[]\`, \`()\`)。 \n3. 绝不包含任何寒暄、问候、或自我介绍 (例如 \"你好\", \"当然\")。 \n4. 必须是可以直接朗读的、完整的、流畅的纯文本。\n\n# 输入数据 (Input Data)\n\n## 输入数据A (上一个岗位摘要) \n${lastJobSummary}\n\n## 输入数据B (当前岗位JSON)\n\`\`\`json\n${JSON.stringify(currentJobData, null, 2)}\n\`\`\`\n\n# 输出格式 (Output Format)\n**[绝对严格的指令]** \n你必须严格按照下面这个“两部分”格式输出使用 \`---NEXT_SUMMARY---\` 作为唯一的分隔符。 绝不在分隔符之外添加任何多余的文字、解释或Markdown。 \n\n[这里是AI生成的、符合上述所有“纯文本要求”的完整口播稿] \n---NEXT_SUMMARY--- \n[这里是AI为“当前岗位”生成的简短新摘要]`;
answerText = await runOllamaNonStream(
systemPromptTemplate,
@@ -216,33 +219,39 @@ export function setupWorkflowHandlers() {
const models = data.models || [];
// 检查模型是否存在于本地
const modelExists = models.some((model: any) => model.name === modelName);
const modelExists = models.some(
(model: any) => model.name === modelName,
);
return {
success: true,
exists: modelExists,
models: models.map((m: any) => ({ name: m.name, size: m.size, modified_at: m.modified_at }))
models: models.map((m: any) => ({
name: m.name,
size: m.size,
modified_at: m.modified_at,
})),
};
} catch (error: any) {
console.error("Check model error:", error);
return {
success: false,
exists: false,
error: error.message
error: error.message,
};
}
});
// 加载模型检查ollama状<61><E78AB6><EFBFBD>下载模型如果不存在
ipcMain.handle("load-model", async (_, modelName = "qwen3:8b") => {
const webContents = BrowserWindow.getFocusedWindow()?.webContents;
ipcMain.handle("load-model", async (event, modelName = "qwen3:8b") => {
const webContents = event.sender;
const sendStatus = (status: string, type = "info") => {
if (webContents && !webContents.isDestroyed()) {
webContents.send("model-load-progress", {
status,
type,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
}
};
@@ -253,7 +262,7 @@ export function setupWorkflowHandlers() {
webContents.send("model-download-confirm", {
modelName,
message: `模型 ${modelName} 不存在,是否下载?`,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
}
};
@@ -269,7 +278,7 @@ export function setupWorkflowHandlers() {
try {
// 尝试启动Ollama服务
await runCommand("ollama", ["ps"]);
await new Promise(resolve => setTimeout(resolve, 3000));
await new Promise((resolve) => setTimeout(resolve, 3000));
const isRunningNow = await checkOllamaServer();
if (!isRunningNow) {
@@ -281,7 +290,7 @@ export function setupWorkflowHandlers() {
return {
success: false,
message: `Ollama服务启动失败: ${error.message}`,
downloaded: false
downloaded: false,
};
}
} else {
@@ -291,18 +300,23 @@ export function setupWorkflowHandlers() {
// 2. 检查模型是否存在
sendStatus(`正在检查模型 ${modelName} 是否存在...`, "info");
const modelCheckResult = await new Promise<{ exists: boolean, models: any[] }>((resolve, reject) => {
const modelCheckResult = await new Promise<{
exists: boolean;
models: any[];
}>((resolve, reject) => {
fetch("http://127.0.0.1:11434/api/tags", {
method: "GET",
headers: { "Content-Type": "application/json" },
})
.then(response => response.json())
.then(data => {
const models = data.models || [];
const modelExists = models.some((model: any) => model.name === modelName);
resolve({ exists: modelExists, models });
})
.catch(reject);
.then((response) => response.json())
.then((data) => {
const models = data.models || [];
const modelExists = models.some(
(model: any) => model.name === modelName,
);
resolve({ exists: modelExists, models });
})
.catch(reject);
});
if (modelCheckResult.exists) {
@@ -310,7 +324,7 @@ export function setupWorkflowHandlers() {
return {
success: true,
message: `模型 ${modelName} 已就绪`,
downloaded: false
downloaded: false,
};
}
@@ -318,46 +332,54 @@ export function setupWorkflowHandlers() {
askUserToDownload();
// 等待用户确认
const userConfirmed = await new Promise<boolean>((resolve, reject) => {
const timeout = setTimeout(() => {
modelDownloadCallbacks.delete(modelName);
resolve(false); // 30秒超时自动取消
}, 30000);
const userConfirmed = await new Promise<boolean>(
(resolve, reject) => {
const timeout = setTimeout(() => {
if (modelDownloadCallbacks.has(modelName)) {
modelDownloadCallbacks.delete(modelName);
resolve(false); // 30秒超时自动取消
}
}, 30000);
// 存储回调函数
modelDownloadCallbacks.set(modelName, {
confirm: () => {
clearTimeout(timeout);
resolve(true);
},
reject: (error: any) => {
clearTimeout(timeout);
reject(error);
}
});
});
modelDownloadCallbacks.set(modelName, {
confirm: () => {
clearTimeout(timeout);
resolve(true);
},
reject: (error: any) => {
clearTimeout(timeout);
reject(error);
},
});
},
);
if (!userConfirmed) {
sendStatus("用户取消了模型下载", "info");
return {
success: false,
message: `用户取消了 ${modelName} 模型的下载`,
downloaded: false
downloaded: false,
};
}
// 4. 用户确认,开始下载模型
sendStatus(`开始下载模型 ${modelName},这可能需要一些时间...`, "info");
sendStatus(
`开始下载模型 ${modelName},这可能需要一些时间...`,
"info",
);
await new Promise<void>((resolve, reject) => {
const process = spawn("ollama", ["pull", modelName], { shell: true });
const process = spawn("ollama", ["pull", modelName], {
shell: true,
});
const sendProgress = (data: any) => {
if (webContents && !webContents.isDestroyed()) {
webContents.send("model-load-progress", {
status: data.toString().trim(),
type: "download",
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
}
};
@@ -383,16 +405,15 @@ export function setupWorkflowHandlers() {
return {
success: true,
message: `模型 ${modelName} 下载并加载成功`,
downloaded: true
downloaded: true,
};
} catch (error: any) {
console.error("Load model error:", error);
sendStatus(`加载模型失败: ${error.message}`, "error");
return {
success: false,
message: error.message,
downloaded: false
downloaded: false,
};
}
});
@@ -415,10 +436,10 @@ export function setupWorkflowHandlers() {
// 润色文本的处理器
ipcMain.handle("polish-text", async (_, text) => {
try {
if (!text || typeof text !== 'string' || text.trim() === '') {
if (!text || typeof text !== "string" || text.trim() === "") {
return {
success: false,
error: "输入文本不能为空"
error: "输入文本不能为空",
};
}
@@ -433,7 +454,10 @@ export function setupWorkflowHandlers() {
原文:${text.trim()}`;
const polishedText = await runOllamaNonStream(systemPrompt, "qwen3:8b");
const polishedText = await runOllamaNonStream(
systemPrompt,
"qwen3:8b",
);
if (!polishedText) {
throw new Error("AI模型返回为空");
@@ -441,18 +465,17 @@ export function setupWorkflowHandlers() {
return {
success: true,
data: polishedText.trim()
data: polishedText.trim(),
};
} catch (error: any) {
console.error("润色文本失败:", error);
return {
success: false,
error: error.message || "润色服务出现错误"
error: error.message || "润色服务出现错误",
};
}
});
// 处理器:检查服务,如果没运行,就用一个轻量命令唤醒它
ipcMain.handle("ensure-ollama-running", async () => {
let isRunning = await checkOllamaServer();
@@ -513,7 +536,7 @@ function checkOllamaServer() {
}
// 辅助函数:运行一个简单的命令并等待它完成
function runCommand(command, args) {
function runCommand(command: string, args: string[]) {
return new Promise((resolve, reject) => {
const process = spawn(command, args, { shell: true });
process.on("close", (code) => {
@@ -527,7 +550,7 @@ function runCommand(command, args) {
});
}
// 这是一个非流式的 Ollama 助手函数
async function runOllamaNonStream(prompt, model = "qwen3:8b") {
async function runOllamaNonStream(prompt: string, model: string = "qwen3:8b") {
try {
const response = await fetch("http://127.0.0.1:11434/api/chat", {
method: "POST",
@@ -553,11 +576,16 @@ async function runOllamaNonStream(prompt, model = "qwen3:8b") {
}
}
function streamCommand(command, args, webContents, eventName) {
function streamCommand(
command: string,
args: string[],
webContents: any,
eventName: string,
) {
return new Promise((resolve, reject) => {
const process = spawn(command, args, { shell: true });
const send = (channel, data) => {
const send = (channel: string, data: any) => {
if (webContents && !webContents.isDestroyed()) {
webContents.send(channel, data);
}

View File

@@ -13,7 +13,7 @@ export function showPrompt(
});
}
export function convertJobData(originalData) {
export function convertJobData(originalData: any) {
// 检查输入是否为对象
if (typeof originalData !== "object" || originalData === null) {
console.error("输入必须是一个有效的对象。");

View File

@@ -1,4 +1,14 @@
interface ProcessAPI {
versions: {
electron: string;
chrome: string;
node: string;
[key: string]: any;
};
}
interface ElectronAPI {
process: ProcessAPI;
ipcRenderer: {
invoke: (channel: string, ...args: any[]) => Promise<any>;
on: (channel: string, listener: (...args: any[]) => void) => void;

View File

@@ -1,6 +1,9 @@
import { contextBridge, ipcRenderer } from "electron";
const electronAPI = {
process: {
versions: process.versions
},
ipcRenderer: {
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args),
on: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.on(channel, listener),
@@ -14,7 +17,7 @@ const api = {
installOllama: () => ipcRenderer.invoke("install-ollama-and-model"),
// 监听进度 (Send/On)
onInstallProgress: (callback) => {
onInstallProgress: (callback: any) => {
ipcRenderer.on("install-progress", (_event, value) => callback(value));
},

View File

@@ -1,13 +0,0 @@
<script setup lang="ts">
import { reactive } from 'vue'
const versions = reactive({ ...window.electron.process.versions })
</script>
<template>
<ul class="versions">
<li class="electron-version">Electron v{{ versions.electron }}</li>
<li class="chrome-version">Chromium v{{ versions.chrome }}</li>
<li class="node-version">Node v{{ versions.node }}</li>
</ul>
</template>

View File

@@ -3,8 +3,8 @@
<a-layout-header class="header">
<a-flex justify="space-between" align="center" style="height: 100%">
<div class="logo-title">
<WifiOutlined />
直播中台控制系统
<!-- <WifiOutlined />
直播中台控制系统 -->
</div>
<a-space :size="20">
<SettingOutlined class="action-icon" />
@@ -172,7 +172,7 @@
<span class="file-name">{{ file.name }}</span>
<a-tag :color="getFileTagColor(file.type)" size="small">{{
getFileTypeText(file.type)
}}</a-tag>
}}</a-tag>
</div>
<a-button size="small" danger @click="removeFile(index)">
<template #icon>
@@ -708,53 +708,76 @@ const handleLiveStatusUpdated = (event, data) => {
}
};
// 处理模型加载进度
const handleModelLoadProgress = (event, data) => {
console.log('模型加载进度:', data);
// 模型加载进度优化
const modelProgressTimer = ref(null);
const lastProgressMessage = ref('');
const progressDebounceDelay = 1000; // 1秒内最多显示一次进度更新
// 根据进度消息类型显示不同的提示
if (data.type === 'error') {
message.error(`模型加载错误: ${data.status}`);
} else if (data.type === 'success') {
message.success(data.status);
} else if (data.type === 'warning') {
message.warning(data.status);
} else {
// 普通信息,显示更详细的进度
message.info(data.status, 3);
const handleModelLoadProgress = (event, data) => {
// 清除之前的定时器
if (modelProgressTimer.value) {
clearTimeout(modelProgressTimer.value);
}
const messageType = data.type || 'info';
const currentMessage = data.status;
// 错误、成功、警告消息立即显示
if (messageType === 'error') {
message.error(`模型加载错误: ${currentMessage}`);
return;
}
if (messageType === 'success') {
message.success(currentMessage);
lastProgressMessage.value = '';
return;
}
if (messageType === 'warning') {
message.warning(currentMessage);
return;
}
// 下载进度和普通进度消息处理
if (currentMessage === lastProgressMessage.value) {
return;
}
const delay = messageType === 'download' ? 200 : progressDebounceDelay;
const duration = messageType === 'download' ? 3 : 2;
modelProgressTimer.value = setTimeout(() => {
message.info(currentMessage, duration);
lastProgressMessage.value = currentMessage;
}, delay);
};
// 处理模型下载确认请求
const handleModelDownloadConfirm = async (_event, data) => {
console.log('收到模型下载确认请求:', data);
try {
await Modal.confirm({
title: '模型下载确认',
content: data.message,
okText: '确认下载',
cancelText: '取消',
centered: true,
width: 400,
maskClosable: false,
});
// 用户点击确认
window.electron.ipcRenderer.send('model-download-confirm-response', {
modelName: data.modelName,
confirmed: true,
timestamp: new Date().toISOString()
});
} catch (error) {
// 用户点击了取消
console.log('用户取消了模型下载');
window.electron.ipcRenderer.send('model-download-confirm-response', {
modelName: data.modelName,
confirmed: false,
timestamp: new Date().toISOString()
});
}
Modal.confirm({
title: '模型下载确认',
content: data.message,
okText: '确认下载',
cancelText: '取消',
centered: true,
width: 400,
maskClosable: false,
onOk() {
window.electron.ipcRenderer.send('model-download-confirm-response', {
modelName: data.modelName,
confirmed: true,
timestamp: new Date().toISOString()
});
},
onCancel() {
window.electron.ipcRenderer.send('model-download-confirm-response', {
modelName: data.modelName,
confirmed: false,
timestamp: new Date().toISOString()
});
}
});
};
// 监听直播状态变化
@@ -778,6 +801,9 @@ onMounted(() => {
// 清理定时器
onUnmounted(() => {
clearInterval(statusInterval);
if (modelProgressTimer.value) {
clearTimeout(modelProgressTimer.value);
}
window.electron.ipcRenderer.off('live-status-updated', handleLiveStatusUpdated);
window.electron.ipcRenderer.off('model-load-progress', handleModelLoadProgress);
window.electron.ipcRenderer.off('model-download-confirm', handleModelDownloadConfirm);
@@ -806,6 +832,7 @@ onMounted(() => {
line-height: 60px;
flex-shrink: 0;
/* 防止顶部栏被压缩 */
-webkit-app-region: drag;
}
.logo-title {

View File

@@ -1,5 +1,6 @@
<template>
<div class="livePage">
<div class="live-header"></div>
<!-- 主画面 -->
<iframe ref="liveIframe" :src="liveUrl" frameborder="0" allowfullscreen class="main-iframe"></iframe>
<!-- 插播画面 -->
@@ -14,7 +15,7 @@
<div class="files-container">
<div v-for="(file, index) in displayedFiles" :key="index" class="file-display-item">
<!-- 关闭按钮 -->
<button class="file-close-btn" @click="removeFileFromDisplay(index)">×</button>
<!-- <button class="file-close-btn" @click="removeFileFromDisplay(index)">×</button> -->
<!-- 图片文件展示 -->
<div v-if="file.type.startsWith('image/')" class="image-display">
@@ -436,7 +437,7 @@ function handleForceCleanup() {
/* 文件悬浮展示样式 */
.files-display-overlay {
position: absolute;
top: 20px;
bottom: 40px;
left: 20px;
z-index: 1000;
max-width: 80vw;
@@ -554,4 +555,10 @@ function handleForceCleanup() {
opacity: 0.8;
}
}
.live-header {
height: 44px;
width: 100%;
-webkit-app-region: drag;
}
</style>

View File

@@ -38,7 +38,7 @@ const handleLogin = () => {
if (username.value === 'admin' && password.value === '123456') {
userStore.setUserInfo(userInfo);
userStore.setToken('token_abcdefg1234567');
router.push('/home');
router.push('/');
} else {
alert('用户名或密码错误!');
}
@@ -47,6 +47,7 @@ const handleLogin = () => {
<style scoped>
.login-container {
-webkit-app-region: drag;
display: flex;
justify-content: center;
align-items: center;
@@ -64,6 +65,7 @@ const handleLogin = () => {
width: 100%;
max-width: 400px;
backdrop-filter: blur(10px);
-webkit-app-region: none;
}
h1 {