数字人直播开发

main
xiangcongshuai 1 month ago
parent 025aa15d57
commit 76347132b4

@ -1,24 +1,46 @@
const sqlite3 = require("sqlite3").verbose();
const path = require("path");
const { app } = require("electron");
const { spawn, exec } = require("child_process");
// 获取数据库路径
// 进程引用管理
const processes = {
LiveTalkingProcess: null,
gptsovitsProcess: null,
chatProcess: null
};
// 获取数据库路径
function getDatabasePath() {
const dbPath = path.join(__dirname, "..", "..", "live_chat.db");
return dbPath;
}
if (process.env.NODE_ENV === "development") {
const dbPath = path.join(__dirname, "..", "..", "live_chat.db");
return dbPath;
} else {
const exePath = app.getPath("exe");
// 获取win-unpacked文件夹路径
const winUnpackedDir = path.dirname(exePath);
// 获取win-unpacked的上级目录与win-unpacked同级的目录
const parentDir = path.dirname(winUnpackedDir);
// 更新配置值
function updateConfigValue(key, value) {
// 构建live_chat.db的完整路径
const dbPath = path.join(parentDir, "live_chat.db");
console.log("dbPath", dbPath);
return dbPath;
}
}
// 更新配置值
function updateConfig(tableName, key, value) {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(getDatabasePath(), err => {
if (err) {
console.error("数据库连接错误:", err.message);
console.error("数据库连接错误:", err.message);
return reject(err);
}
});
// 先尝试更新如果影响行数为0则插入新记录
// 先尝试更新如果影响行数为0则插入新记录
db.run(
`UPDATE config SET value = ? WHERE key = ?`,
`UPDATE ${tableName} SET value = ? WHERE key = ?`,
[value, key],
function (err) {
if (err) {
@ -26,10 +48,10 @@ function updateConfigValue(key, value) {
return reject(err);
}
// 如果没有匹配的记录则插入
// 如果没有匹配的记录则插入
if (this.changes === 0) {
db.run(
`INSERT INTO config (key, value) VALUES (?, ?)`,
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)`,
[key, value],
function (err) {
db.close();
@ -50,8 +72,8 @@ function updateConfigValue(key, value) {
});
}
// 获取配置值
function getConfigValue(key) {
// 获取配置值
function getConfigValue(tableName, key) {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(getDatabasePath(), err => {
if (err) {
@ -59,18 +81,23 @@ function getConfigValue(key) {
}
});
db.get(`SELECT value FROM config WHERE key = ?`, [key], (err, row) => {
db.close();
if (err) return reject(err);
resolve(row ? row.value : null);
});
db.get(
`SELECT value FROM ${tableName} WHERE key = ?`,
[key],
(err, row) => {
db.close();
if (err) return reject(err);
resolve(row ? row.value : null);
}
);
});
}
// 批量插入系统消息
// 批量插入系统消息
const bulkInsertSystemMessages = messages => {
console.log("系统消息:", messages);
return new Promise((resolve, reject) => {
if (!Array.isArray(messages) || messages.length === 0) {
reject(new Error("请提供有效的消息数组"));
reject(new Error("请提供有效的消息数组"));
return;
}
@ -80,74 +107,54 @@ const bulkInsertSystemMessages = messages => {
reject(err);
return;
}
// 清空表
db.run("DELETE FROM system_message", function (err) {
// 使用事务进行批量插入,提高性能
db.run("BEGIN TRANSACTION", err => {
if (err) {
db.close();
reject(err);
db.close();
return;
}
// 重置自增计数器SQLite特定
db.run(
"DELETE FROM sqlite_sequence WHERE name = 'system_message'",
err => {
if (err) {
db.close();
reject(err);
return;
} else {
// 使用事务进行批量插入,提高性能
db.run("BEGIN TRANSACTION", err => {
if (err) {
reject(err);
const stmt = db.prepare(
"INSERT INTO system_message (message) VALUES (?)"
);
let completed = 0;
let errorOccurred = null;
messages.forEach(message => {
stmt.run(message, function (err) {
if (err && !errorOccurred) {
errorOccurred = err;
}
completed++;
// 所有消息都处理完毕
if (completed === messages.length) {
stmt.finalize();
if (errorOccurred) {
db.run("ROLLBACK", () => {
reject(errorOccurred);
db.close();
return;
}
const stmt = db.prepare(
"INSERT INTO system_message (message) VALUES (?)"
);
let completed = 0;
let errorOccurred = null;
messages.forEach(message => {
stmt.run(message, function (err) {
if (err && !errorOccurred) {
errorOccurred = err;
}
completed++;
// 所有消息都处理完毕
if (completed === messages.length) {
stmt.finalize();
if (errorOccurred) {
db.run("ROLLBACK", () => {
reject(errorOccurred);
db.close();
});
} else {
db.run("COMMIT", err => {
if (err) {
reject(err);
} else {
resolve({
success: true,
count: messages.length
});
}
db.close();
});
}
}
});
});
});
} else {
db.run("COMMIT", err => {
if (err) {
reject(err);
} else {
resolve({
success: true,
count: messages.length
});
}
db.close();
});
}
}
}
);
});
});
});
});
} catch (error) {
@ -155,8 +162,132 @@ const bulkInsertSystemMessages = messages => {
}
});
};
// 清空系统消息表
const clearSystemMessages = () => {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(getDatabasePath(), err => {
if (err) {
reject(err);
return;
}
// 使用TRUNCATE-like操作清空表同时重置自增ID
db.run("DELETE FROM system_message", err => {
if (err) {
reject(err);
db.close();
return;
}
// 重置自增计数器SQLite特定
db.run(
"DELETE FROM sqlite_sequence WHERE name = 'system_message'",
err => {
if (err) {
reject(err);
} else {
resolve({ success: true });
}
db.close();
}
);
});
});
});
};
function getProcessExePath(fileName, exeName) {
let exePath;
let exeFolder;
// 判断是否为开发环境
if (process.env.NODE_ENV === "development") {
// process.cwd() 通常指向项目根目录
const projectRoot = process.cwd();
const parentDir = path.dirname(projectRoot);
exePath = path.join(parentDir, fileName, exeName);
exeFolder = path.join(parentDir, fileName);
} else {
// 生产环境: 与win-unpacked同级目录下的LiveTalking文件夹
exePath = app.getPath("exe");
// 获取win-unpacked文件夹路径
const winUnpackedDir = path.dirname(exePath);
// 获取win-unpacked的上级目录
const parentDir = path.dirname(winUnpackedDir);
exePath = path.join(parentDir, fileName, exeName);
exeFolder = path.join(parentDir, fileName);
}
console.log("exePath", exePath);
console.log("exeFolder", exeFolder);
// 标准化路径格式
return { exePath, exeFolder };
}
/**
* 启动LiveTalking.exe
*/
async function startProcess(fileName, exeName) {
return new Promise(async (resolve, reject) => {
const processKey = `${fileName}Process`;
if (processes[processKey]) {
reject(`${fileName} is already running`);
return;
}
// 获取程序路径
const obj = getProcessExePath(fileName, exeName);
try {
// 启动进程时通过wmic获取详细信息仅Windows
const newProcess = spawn("cmd.exe", ["/c", obj.exePath], {
// 直接运行bat不使用start命令
cwd: obj.exeFolder,
windowsVerbatimArguments: true,
windowsHide: false,
detached: false // 不分离,便于跟踪子进程
});
// 记录进程ID关键保存实际PID
processes[processKey] = {
process: newProcess,
pid: newProcess.pid // 保存PID用于后续终止
};
newProcess.on("error", err => {
processes[processKey] = null;
reject(`启动失败: ${err.message}`);
});
console.log(`spawned with PID ${newProcess.pid}`);
resolve(`${fileName} 启动成功 (PID: ${newProcess.pid})`);
} catch (err) {
reject(`启动异常: ${err.message}`);
}
});
}
// 停止所有进程
async function stopAllProcesses() {
try {
// 清空所有属性,变成空对象
for (const key in processes) {
if (processes[key]) {
delete processes[key];
}
}
const projectRoot = process.cwd();
const parentDir = path.dirname(projectRoot);
const exPath = path.join(parentDir, "kill-live.bat");
const child = spawn("cmd.exe", ["/c", "start", '""', `"${exPath}"`], {
cwd: parentDir,
windowsVerbatimArguments: true,
windowsHide: false,
detached: true
});
child.unref();
} catch (err) {
console.error(err);
}
}
module.exports = {
updateConfigValue,
updateConfig,
getConfigValue,
bulkInsertSystemMessages
bulkInsertSystemMessages,
clearSystemMessages,
startProcess,
stopAllProcesses
};

@ -1,54 +1,30 @@
const { app, BrowserWindow } = require("electron");
const { ipcMain } = require("electron");
const path = require("path");
const http = require("http");
const httpProxy = require("http-proxy"); // 需要安装npm install http-proxy
const {
updateConfigValue,
updateConfig,
getConfigValue,
bulkInsertSystemMessages
bulkInsertSystemMessages,
stopProcess,
startProcess,
stopAllProcesses
} = require("./dbHandler");
// 创建代理服务器
const proxyServer = httpProxy.createProxyServer({});
// 定义目标服务
const proxyTargets = {
"/live-digital-avatar-manage":
"http://192.168.10.25:9909/live-digital-avatar-manage"
};
// 创建本地 HTTP 服务器
const server = http.createServer((req, res) => {
// 根据请求路径匹配目标服务
for (const pathPrefix in proxyTargets) {
if (req.url.startsWith(pathPrefix)) {
const target = proxyTargets[pathPrefix];
// 重写路径(移除前缀)
req.url = req.url.replace(pathPrefix, "");
// 转发请求
proxyServer.web(req, res, { target });
return;
}
}
res.statusCode = 404;
res.end("Not Found");
});
// 注册IPC通信处理
function registerIpcHandlers() {
// 更新backend_token
ipcMain.handle("update-config", async (event, key, token) => {
ipcMain.handle("update-config", async (event, tableName, key, token) => {
try {
return await updateConfigValue(key, token);
return await updateConfig(tableName, key, token);
} catch (error) {
console.error("更新backend_token失败:", error);
throw error;
}
});
ipcMain.handle("get-config", async (event, key) => {
ipcMain.handle("get-config", async (event, tableName, key) => {
try {
return await getConfigValue(key);
return await getConfigValue(tableName, key);
} catch (error) {
console.error("获取失败:", error);
throw error;
@ -62,12 +38,31 @@ function registerIpcHandlers() {
throw error;
}
});
ipcMain.handle("start-process", async (event, fileName, exeName) => {
try {
return await startProcess(fileName, exeName);
} catch (error) {
console.error("启动失败:", error);
throw error;
}
});
ipcMain.handle("stop-process", async (event, fileName, exeName) => {
try {
return await stopProcess(fileName, exeName);
} catch (error) {
console.error("停止失败:", error);
throw error;
}
});
ipcMain.handle("stop-all-process", async () => {
try {
return await stopAllProcesses();
} catch (error) {
console.error("停止失败:", error);
throw error;
}
});
}
// 启动服务器
server.listen(3000, () => {
console.log("Proxy server running on port 3000");
});
function createWindow() {
const win = new BrowserWindow({
width: 1920,
@ -83,16 +78,17 @@ function createWindow() {
win.webContents.on("did-fail-load", (event, errorCode, errorDescription) => {
console.error("Preload 加载失败:", errorDescription);
});
win.webContents.openDevTools();
// 加载应用
console.log(process.env.NODE_ENV);
if (process.env.NODE_ENV === "development") {
win.loadURL("http://localhost:8848/#/login");
win.webContents.openDevTools();
} else {
win.webContents.openDevTools();
win.loadFile("dist/index.html");
}
// win.loadURL("http://localhost:8848");
// win.webContents.openDevTools();
}
app.whenReady().then(() => {

@ -3,13 +3,18 @@ const { contextBridge, ipcRenderer } = require("electron");
// 安全暴露数据库操作方法
contextBridge.exposeInMainWorld("electronAPI", {
updateConfig: async (key, value) => {
return await ipcRenderer.invoke("update-config", key, value);
updateConfig: async (tableName, key, value) => {
return await ipcRenderer.invoke("update-config", tableName, key, value);
},
getConfig: async (key, value) => {
return await ipcRenderer.invoke("get-config", key, value);
getConfig: async (tableName, key) => {
return await ipcRenderer.invoke("get-config", tableName, key);
},
bulkInsertSystemMessages: async messages => {
return await ipcRenderer.invoke("bulk-insert-system-messages", messages);
}
},
startProcess: (fileName, exeName) =>
ipcRenderer.invoke("start-process", fileName, exeName),
stopProcess: (fileName, exeName) =>
ipcRenderer.invoke("stop-process", fileName, exeName),
stopAllProcesses: () => ipcRenderer.invoke("stop-all-process")
});

@ -5,7 +5,7 @@
"main": "electron/main.js",
"private": true,
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
"dev": "NODE_ENV=development vite",
"serve": "pnpm dev",
"build": "vite build",
"build:staging": "rimraf dist && vite build --mode staging",
@ -24,7 +24,7 @@
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"prepare": "husky install",
"preinstall": "npx only-allow pnpm",
"electron:serve": "concurrently -k \"vite\" \"wait-on http://192.168.10.25:8080/live-digital-avatar-manage && electron .\"",
"electron:serve": "NODE_ENV=development electron .",
"electron:build": "vite build && electron-builder"
},
"build": {
@ -38,9 +38,14 @@
{
"from": "resources/",
"to": "resources/",
"filter": ["!*.asar"]
"filter": [
"!*.asar"
]
}
],
"extraMetadata": {
"NODE_ENV": "production"
},
"files": [
"dist/**/*",
"electron/**/*",
@ -105,6 +110,7 @@
"vue": "^3.3.4",
"vue-router": "^4.2.2",
"vue-types": "^5.1.0",
"vue-webrtc": "^3.0.1",
"vue3-seamless-scroll": "^2.0.1",
"wangeditor": "^4.7.15"
},

@ -110,6 +110,9 @@ importers:
vue-types:
specifier: ^5.1.0
version: 5.1.3(vue@3.5.16(typescript@5.0.4))
vue-webrtc:
specifier: ^3.0.1
version: 3.0.1(typescript@5.0.4)
vue3-seamless-scroll:
specifier: ^2.0.1
version: 2.0.1
@ -287,7 +290,7 @@ importers:
version: 3.3.2
tailwindcss:
specifier: ^3.3.2
version: 3.4.17(ts-node@10.9.2(@types/node@20.19.0)(typescript@5.0.4))
version: 3.4.17(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.8.3))
terser:
specifier: ^5.18.1
version: 5.42.0
@ -1076,6 +1079,9 @@ packages:
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
'@sxzz/popperjs-es@2.11.7':
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
@ -1734,6 +1740,12 @@ packages:
axios@1.9.0:
resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
babel-polyfill@6.26.0:
resolution: {integrity: sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==}
babel-runtime@6.26.0:
resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@ -1784,6 +1796,10 @@ packages:
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
bufferutil@4.0.9:
resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==}
engines: {node: '>=6.14.2'}
builder-util-runtime@9.3.1:
resolution: {integrity: sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==}
engines: {node: '>=12.0.0'}
@ -2052,6 +2068,10 @@ packages:
core-js-pure@3.43.0:
resolution: {integrity: sha512-i/AgxU2+A+BbJdMxh3v7/vxi2SbFqxiFmg6VsDwYB4jkucrd1BZNA9a9gphC0fYMG5IBSgQcbQnk865VCLe7xA==}
core-js@2.6.12:
resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==}
deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
core-js@3.43.0:
resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==}
@ -2185,6 +2205,10 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
cuid@2.1.8:
resolution: {integrity: sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==}
deprecated: Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead.
d@1.0.2:
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
engines: {node: '>=0.12'}
@ -2429,6 +2453,13 @@ packages:
end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
engine.io-client@6.6.3:
resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==}
engine.io-parser@5.2.3:
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
engines: {node: '>=10.0.0'}
enhanced-resolve@5.18.2:
resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
engines: {node: '>=10.13.0'}
@ -2447,6 +2478,9 @@ packages:
err-code@2.0.3:
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
err-code@3.0.1:
resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==}
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@ -2786,6 +2820,9 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
get-browser-rtc@1.1.0:
resolution: {integrity: sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@ -3663,6 +3700,12 @@ packages:
namespace-emitter@2.0.1:
resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==}
nanoassert@1.1.0:
resolution: {integrity: sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==}
nanobus@4.5.0:
resolution: {integrity: sha512-7sBZo9wthqNJ7QXnfVXZL7fkKJLN55GLOdX+RyZT34UOvxxnFtJe/c7K0ZRLAKOvaY1xJThFFn0Usw2H9R6Frg==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -3671,6 +3714,12 @@ packages:
nanopop@2.4.2:
resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==}
nanoscheduler@1.0.3:
resolution: {integrity: sha512-jBbrF3qdU9321r8n9X7yu18DjP31Do2ItJm3mWrt90wJTrnDO+HXpoV7ftaUglAtjgj9s+OaCxGufbvx6pvbEQ==}
nanotiming@7.3.1:
resolution: {integrity: sha512-l3lC7v/PfOuRWQa8vV29Jo6TG10wHtnthLElFXs4Te4Aas57Fo4n1Q8LH9n+NDh9riOzTVvb2QNBhTS4JUKNjw==}
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
@ -3715,6 +3764,10 @@ packages:
encoding:
optional: true
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
node-gyp@8.4.1:
resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==}
engines: {node: '>= 10.12.0'}
@ -4507,6 +4560,15 @@ packages:
resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==}
engines: {node: '>=12'}
regenerator-runtime@0.10.5:
resolution: {integrity: sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==}
regenerator-runtime@0.11.1:
resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==}
remove-array-items@1.1.1:
resolution: {integrity: sha512-MXW/jtHyl5F1PZI7NbpS8SOtympdLuF20aoWJT5lELR1p/HJDd5nqW8Eu9uLh/hCRY3FgvrIT5AwDCgBODklcA==}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@ -4738,6 +4800,12 @@ packages:
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
simple-peer@9.11.1:
resolution: {integrity: sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==}
simple-signal-client@3.0.0:
resolution: {integrity: sha512-eiqR8S+2i1squkIVKx4IgEyGe7do/soMb2Lt06gJov281sLl3USyIn1fKAg/hzcIvevOpuH1BmzFZ6BSugFZsQ==}
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
@ -4777,6 +4845,14 @@ packages:
resolution: {integrity: sha512-ig5qOnCDbugFntKi6c7Xlib8bA6xiJVk8O+WdFrV3wxbMqeHO0hXFQC4nAhPVWfZfi8255lcZkNhtIBINCc4+Q==}
engines: {node: '>=12.17.0'}
socket.io-client@4.8.1:
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
engines: {node: '>=10.0.0'}
socket.io-parser@4.2.4:
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
engines: {node: '>=10.0.0'}
socks-proxy-agent@6.2.1:
resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==}
engines: {node: '>= 10'}
@ -5327,6 +5403,10 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
utf-8-validate@5.0.10:
resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
engines: {node: '>=6.14.2'}
utf8-byte-length@1.0.5:
resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==}
@ -5453,6 +5533,10 @@ packages:
vue:
optional: true
vue-webrtc@3.0.1:
resolution: {integrity: sha512-Wm5QAyuPhQVYHU8RLhGnD6ocaQVxatfYqoIUHF9Fq/Cer19dA7fSiZO+q4Q+XwvN40M4f8STaZRhF1A/rKfeVg==}
engines: {node: '>=12'}
vue3-seamless-scroll@2.0.1:
resolution: {integrity: sha512-mI3BaDU3pjcPUhVSw3/xNKdfPBDABTi/OdZaZqKysx4cSdNfGRbVvGNDzzptBbJ5S7imv5T55l6x/SqgnxKreg==}
@ -5543,6 +5627,18 @@ packages:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xml-name-validator@4.0.0:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
@ -5551,6 +5647,10 @@ packages:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'}
xmlhttprequest-ssl@2.1.2:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -6456,6 +6556,8 @@ snapshots:
'@sindresorhus/is@4.6.0': {}
'@socket.io/component-emitter@3.1.2': {}
'@sxzz/popperjs-es@2.11.7': {}
'@szmarczak/http-timer@4.0.6':
@ -7310,6 +7412,17 @@ snapshots:
transitivePeerDependencies:
- debug
babel-polyfill@6.26.0:
dependencies:
babel-runtime: 6.26.0
core-js: 2.6.12
regenerator-runtime: 0.10.5
babel-runtime@6.26.0:
dependencies:
core-js: 2.6.12
regenerator-runtime: 0.11.1
balanced-match@1.0.2: {}
balanced-match@2.0.0: {}
@ -7362,6 +7475,10 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
bufferutil@4.0.9:
dependencies:
node-gyp-build: 4.8.4
builder-util-runtime@9.3.1:
dependencies:
debug: 4.4.1
@ -7693,6 +7810,8 @@ snapshots:
core-js-pure@3.43.0: {}
core-js@2.6.12: {}
core-js@3.43.0: {}
core-util-is@1.0.2:
@ -7890,6 +8009,8 @@ snapshots:
csstype@3.1.3: {}
cuid@2.1.8: {}
d@1.0.2:
dependencies:
es5-ext: 0.10.64
@ -8183,6 +8304,20 @@ snapshots:
dependencies:
once: 1.4.0
engine.io-client@6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10):
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.4
engine.io-parser: 5.2.3
ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)
xmlhttprequest-ssl: 2.1.2
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
engine.io-parser@5.2.3: {}
enhanced-resolve@5.18.2:
dependencies:
graceful-fs: 4.2.11
@ -8196,6 +8331,8 @@ snapshots:
err-code@2.0.3: {}
err-code@3.0.1: {}
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
@ -8635,6 +8772,8 @@ snapshots:
gensync@1.0.0-beta.2: {}
get-browser-rtc@1.1.0: {}
get-caller-file@2.0.5: {}
get-intrinsic@1.3.0:
@ -9523,10 +9662,27 @@ snapshots:
namespace-emitter@2.0.1: {}
nanoassert@1.1.0: {}
nanobus@4.5.0:
dependencies:
nanoassert: 1.1.0
nanotiming: 7.3.1
remove-array-items: 1.1.1
nanoid@3.3.11: {}
nanopop@2.4.2: {}
nanoscheduler@1.0.3:
dependencies:
nanoassert: 1.1.0
nanotiming@7.3.1:
dependencies:
nanoassert: 1.1.0
nanoscheduler: 1.0.3
napi-build-utils@2.0.0: {}
natural-compare-lite@1.4.0: {}
@ -9561,6 +9717,8 @@ snapshots:
optionalDependencies:
encoding: 0.1.13
node-gyp-build@4.8.4: {}
node-gyp@8.4.1:
dependencies:
env-paths: 2.2.1
@ -9913,7 +10071,7 @@ snapshots:
camelcase-css: 2.0.1
postcss: 8.5.5
postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@20.19.0)(typescript@5.0.4)):
postcss-load-config@4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.8.3)):
dependencies:
lilconfig: 3.1.3
yaml: 2.8.0
@ -10332,6 +10490,12 @@ snapshots:
indent-string: 5.0.0
strip-indent: 4.0.0
regenerator-runtime@0.10.5: {}
regenerator-runtime@0.11.1: {}
remove-array-items@1.1.1: {}
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
@ -10547,6 +10711,28 @@ snapshots:
once: 1.4.0
simple-concat: 1.0.1
simple-peer@9.11.1:
dependencies:
buffer: 6.0.3
debug: 4.4.1
err-code: 3.0.1
get-browser-rtc: 1.1.0
queue-microtask: 1.2.3
randombytes: 2.1.0
readable-stream: 3.6.2
transitivePeerDependencies:
- supports-color
simple-signal-client@3.0.0:
dependencies:
babel-polyfill: 6.26.0
cuid: 2.1.8
inherits: 2.0.4
nanobus: 4.5.0
simple-peer: 9.11.1
transitivePeerDependencies:
- supports-color
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
@ -10590,6 +10776,24 @@ snapshots:
snabbdom@3.6.2: {}
socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10):
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.4
engine.io-client: 6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)
socket.io-parser: 4.2.4
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
socket.io-parser@4.2.4:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.4
transitivePeerDependencies:
- supports-color
socks-proxy-agent@6.2.1:
dependencies:
agent-base: 6.0.2
@ -10938,7 +11142,7 @@ snapshots:
string-width: 4.2.3
strip-ansi: 6.0.1
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.19.0)(typescript@5.0.4)):
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.8.3)):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@ -10957,7 +11161,7 @@ snapshots:
postcss: 8.5.5
postcss-import: 15.1.0(postcss@8.5.5)
postcss-js: 4.0.1(postcss@8.5.5)
postcss-load-config: 4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@20.19.0)(typescript@5.0.4))
postcss-load-config: 4.0.2(postcss@8.5.5)(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.8.3))
postcss-nested: 6.2.0(postcss@8.5.5)
postcss-selector-parser: 6.1.2
resolve: 1.22.10
@ -11236,6 +11440,10 @@ snapshots:
dependencies:
punycode: 2.3.1
utf-8-validate@5.0.10:
dependencies:
node-gyp-build: 4.8.4
utf8-byte-length@1.0.5: {}
util-deprecate@1.0.2: {}
@ -11359,6 +11567,18 @@ snapshots:
optionalDependencies:
vue: 3.5.16(typescript@5.0.4)
vue-webrtc@3.0.1(typescript@5.0.4):
dependencies:
bufferutil: 4.0.9
core-js: 3.43.0
simple-signal-client: 3.0.0
socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)
utf-8-validate: 5.0.10
vue: 3.5.16(typescript@5.0.4)
transitivePeerDependencies:
- supports-color
- typescript
vue3-seamless-scroll@2.0.1:
dependencies:
throttle-debounce: 5.0.0
@ -11483,10 +11703,17 @@ snapshots:
imurmurhash: 0.1.4
signal-exit: 4.1.0
ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10):
optionalDependencies:
bufferutil: 4.0.9
utf-8-validate: 5.0.10
xml-name-validator@4.0.0: {}
xmlbuilder@15.1.1: {}
xmlhttprequest-ssl@2.1.2: {}
y18n@5.0.8: {}
yallist@3.1.1: {}

@ -37,7 +37,7 @@ export default defineComponent({
const route = useRoute();
//
const isLoginPage = computed(() =>
["/login", "/live"].includes(route.path)
["/login", "/Live"].includes(route.path)
);
return { currentLocale: zhCn, isLoginPage };
}

@ -1,9 +1,9 @@
import { http } from "@/utils/http";
import config from "@/utils/httpProxy";
export const getSalespitch = (data?: object) => {
return http.request<any>(
"post",
"/live-digital-avatar-manage/liveDigital/generate/salespitch",
`${config.services.liveDigital}/generate/salespitch`,
{ data }
);
};

@ -1,4 +1,5 @@
import { http } from "@/utils/http";
import config from "@/utils/httpProxy";
export type UserResult = {
success: boolean;
@ -26,7 +27,7 @@ export type RefreshTokenResult = {
export const getLogin = (data?: object) => {
return http.request<UserResult>(
"post",
"/live-digital-avatar-manage/auth/login",
`${config.services.liveDigital}/auth/login`,
{
data
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -1,14 +1,23 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { onMounted, ref, watch } from "vue";
import userImg from "@/assets/home/user.png";
import actUserImg from "@/assets/home/act_user.png";
import liveImg from "@/assets/home/live.png";
import actLiveImg from "@/assets/home/act_live.png";
import settingImg from "@/assets/home/setting.png";
import actSettingImg from "@/assets/home/act_setting.png";
import { useRouter } from "vue-router";
const activedName = ref("user");
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const activedName = ref((route.name as string) || "DigitalHuman");
watch(
() => route.name,
newName => {
activedName.value = newName as string;
}
);
function handleSidebarClick(name: string, path: string) {
activedName.value = name;
router.push(path);
@ -20,28 +29,28 @@ onMounted(() => {});
<div class="sidebar-list">
<div
class="sidebar-item"
:class="[{ active: activedName === 'user' }]"
@click="handleSidebarClick('user', '/digitalHuman')"
:class="[{ active: activedName === 'DigitalHuman' }]"
@click="handleSidebarClick('DigitalHuman', '/digitalHuman')"
>
<img v-if="activedName === 'user'" :src="actUserImg" alt="" />
<img v-if="activedName === 'DigitalHuman'" :src="actUserImg" alt="" />
<img v-else :src="userImg" alt="" />
<span>数字人</span>
</div>
<div
class="sidebar-item"
:class="[{ active: activedName === 'live' }]"
@click="handleSidebarClick('live', '/AiLive')"
:class="[{ active: activedName === 'AiLive' }]"
@click="handleSidebarClick('AiLive', '/AiLive')"
>
<img v-if="activedName === 'live'" :src="actLiveImg" alt="" />
<img v-if="activedName === 'AiLive'" :src="actLiveImg" alt="" />
<img v-else :src="liveImg" alt="" />
<span>AI直播</span>
</div>
<div
class="sidebar-item setting"
:class="[{ active: activedName === 'setting' }]"
@click="handleSidebarClick('setting', '/setting')"
:class="[{ active: activedName === 'Setting' }]"
@click="handleSidebarClick('Setting', '/setting')"
>
<img v-if="activedName === 'setting'" :src="actSettingImg" alt="" />
<img v-if="activedName === 'Setting'" :src="actSettingImg" alt="" />
<img v-else :src="settingImg" alt="" />
<span>设置</span>
</div>

@ -21,7 +21,7 @@ export function useLayout() {
theme: $config?.Theme ?? "default",
darkMode: $config?.DarkMode ?? false,
sidebarStatus: $config?.SidebarStatus ?? true,
epThemeColor: $config?.EpThemeColor ?? "#409EFF"
epThemeColor: $config?.EpThemeColor ?? "#2e80fa"
};
}
/** 灰色模式、色弱模式、隐藏标签页 */

@ -185,18 +185,18 @@
flex: 1;
overflow: auto;
}
.el-button {
border-color: #2e80fa !important;
color: #1d2129;
}
.el-button--primary {
background-color: #2e80fa !important; //
border-color: #2e80fa !important;
color: #ffffff;
}
.el-button--primary:hover,
.el-button--primary:focus {
background-color: #2e80fa !important; // hover
border-color: #2e80fa !important;
color: #ffffff;
}
// .el-button {
// border-color: #2e80fa;
// color: #1d2129;
// }
// .el-button--primary {
// background-color: #2e80fa; //
// border-color: #2e80fa;
// color: #ffffff;
// }
// .el-button--primary:hover,
// .el-button--primary:focus {
// background-color: #2e80fa; // hover
// border-color: #2e80fa;
// color: #ffffff;
// }

@ -4,7 +4,7 @@
:root {
/* 左侧菜单展开、收起动画时长 */
--pure-transition-duration: 0.3s;
--el-color-primary: #1843ad !important;
--el-color-primary: #2e80fa !important;
--el-text-color-regular: #333333 !important;
}

@ -1,8 +1,11 @@
// src/electron-api.d.ts
interface Window {
electronAPI: {
updateConfig: (key: string, value: string) => void;
getConfig: (key: string) => Promise<string>;
updateConfig: (tableName: string, key: string, value: string) => void;
getConfig: (tableName: string, key: string) => Promise<string>;
bulkInsertSystemMessages: (messages: any) => void;
startProcess: (fileName, exeName) => void;
stopProcess: (fileName, exeName) => void;
stopAllProcesses: () => void;
};
}

@ -0,0 +1,10 @@
// config.js
const isDevelopment = process.env.NODE_ENV === "development";
console.log("current environment", process.env.NODE_ENV);
export default {
services: {
liveDigital: isDevelopment
? "/live-digital-avatar-manage"
: "http://192.168.10.25:9909/live-digital-avatar-manage"
}
};

@ -14,7 +14,7 @@ export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
theme: config.Theme ?? "default",
darkMode: config.DarkMode ?? false,
sidebarStatus: config.SidebarStatus ?? true,
epThemeColor: config.EpThemeColor ?? "#409EFF"
epThemeColor: config.EpThemeColor ?? "#2e80fa"
},
configure: Storage.getData("configure", nameSpace) ?? {
grey: config.Grey ?? false,

@ -1,6 +1,6 @@
<script setup lang="ts">
import { FormInstance } from "element-plus/es/components";
import { reactive, ref } from "vue";
import { reactive, ref, onMounted } from "vue";
import headPicImg from "@/assets/live/head_pic.png";
import rotateImg from "@/assets/live/rotate.png";
import tipsImg from "@/assets/svg/live/tips.svg";
@ -13,16 +13,43 @@ const formData = reactive({
isEnter: true,
isGlobal: true,
randomTriggerProbability: "",
replyMethod: "1"
reply_prob_enter_live_room: "",
reply_prob_follow: "",
reply_prob_like: "",
reply_prob_gift: ""
});
const keyList = [
"reply_prob_enter_live_room",
"reply_prob_follow",
"reply_prob_like",
"reply_prob_gift"
];
const rules = {
liveTime: [{ required: true, message: "请选择直播时间", trigger: "change" }]
};
const getDbInfo = async key => {
formData[key] = await window.electronAPI.getConfig("config", key);
};
const updateDbInfo = async (key: string, val: string) => {
window.electronAPI.updateConfig("config", key, val);
};
const save = () => {
keyList.forEach(key => {
updateDbInfo(key, formData[key]);
});
};
onMounted(async () => {
keyList.forEach(key => {
getDbInfo(key);
});
});
</script>
<template>
<div class="BasicSettings">
<div class="live">1111</div>
<div class="live">
<video src="@/assets/live/girl.mp4" class="video" autoplay muted loop />
</div>
<div class="main">
<div class="title">基础设置</div>
<div class="main-content">
@ -45,7 +72,7 @@ const rules = {
<img class="rotate" :src="rotateImg" alt="" />
</div>
</el-form-item>
<el-row>
<!-- <el-row>
<el-col :span="12" style="padding-right: 16px">
<el-form-item label="直播时间" prop="liveTime">
<el-date-picker
@ -70,9 +97,9 @@ const rules = {
<el-input v-model="formData.name" />
</el-form-item>
</el-col>
</el-row>
</el-row> -->
<div class="bullet-list-title">弹幕回复</div>
<div class="bullet-item">
<!-- <div class="bullet-item">
<div class="bullet-item-title">
<el-switch v-model="formData.isGlobal" />
<span>全局弹幕回复</span>
@ -98,7 +125,7 @@ const rules = {
<span>限制单用户的互动回复频率的时间间隔</span>
</div>
</div>
</div>
</div> -->
<div class="bullet-item">
<div class="bullet-item-title">
<el-switch v-model="formData.isEnter" />
@ -108,21 +135,23 @@ const rules = {
<el-col :span="12" style="padding-right: 16px">
<el-form-item
label="随机触发概率"
prop="randomTriggerProbability"
prop="reply_prob_enter_live_room"
>
<el-input v-model="formData.randomTriggerProbability">
<template #append> %</template>
</el-input>
<el-input-number
v-model="formData.reply_prob_enter_live_room"
:min="0"
:max="100"
/>
</el-form-item>
</el-col>
<div class="bullet-desc">
1%-100% 之间的数值(不可为0)若需关闭该互动功能
</div>
<div class="bullet-list-content-title">回复方式</div>
<!-- <div class="bullet-list-content-title">回复方式</div>
<el-radio-group v-model="formData.replyMethod">
<el-radio value="1" size="large">文本回复</el-radio>
<el-radio value="2" size="large">文本转语音回复</el-radio>
</el-radio-group>
</el-radio-group> -->
<div class="bullet-list-item" />
</div>
</div>
@ -133,20 +162,19 @@ const rules = {
</div>
<div class="bullet-item-content" v-if="formData.isEnter">
<el-col :span="12" style="padding-right: 16px">
<el-form-item
label="随机触发概率"
prop="randomTriggerProbability"
>
<el-input v-model="formData.randomTriggerProbability">
<template #append> %</template>
</el-input>
<el-form-item label="随机触发概率" prop="reply_prob_follow">
<el-input-number
v-model="formData.reply_prob_follow"
:min="0"
:max="100"
/>
</el-form-item>
</el-col>
<div class="bullet-list-content-title">回复方式</div>
<!-- <div class="bullet-list-content-title">回复方式</div>
<el-radio-group v-model="formData.replyMethod">
<el-radio value="1" size="large">文本回复</el-radio>
<el-radio value="2" size="large">文本转语音回复</el-radio>
</el-radio-group>
</el-radio-group> -->
<div class="bullet-list-item" />
</div>
</div>
@ -157,20 +185,19 @@ const rules = {
</div>
<div class="bullet-item-content" v-if="formData.isEnter">
<el-col :span="12" style="padding-right: 16px">
<el-form-item
label="随机触发概率"
prop="randomTriggerProbability"
>
<el-input v-model="formData.randomTriggerProbability">
<template #append> %</template>
</el-input>
<el-form-item label="随机触发概率" prop="reply_prob_gift">
<el-input-number
v-model="formData.reply_prob_gift"
:min="0"
:max="100"
/>
</el-form-item>
</el-col>
<div class="bullet-list-content-title">回复方式</div>
<!-- <div class="bullet-list-content-title">回复方式</div>
<el-radio-group v-model="formData.replyMethod">
<el-radio value="1" size="large">文本回复</el-radio>
<el-radio value="2" size="large">文本转语音回复</el-radio>
</el-radio-group>
</el-radio-group> -->
<div class="bullet-list-item" />
</div>
</div>
@ -181,22 +208,24 @@ const rules = {
</div>
<div class="bullet-item-content" v-if="formData.isEnter">
<el-col :span="12" style="padding-right: 16px">
<el-form-item
label="随机触发概率"
prop="randomTriggerProbability"
>
<el-input v-model="formData.randomTriggerProbability">
<template #append> %</template>
</el-input>
<el-form-item label="随机触发概率" prop="reply_prob_like">
<el-input-number
v-model="formData.reply_prob_like"
:min="0"
:max="100"
/>
</el-form-item>
</el-col>
<div class="bullet-list-content-title">回复方式</div>
<!-- <div class="bullet-list-content-title">回复方式</div>
<el-radio-group v-model="formData.replyMethod">
<el-radio value="1" size="large">文本回复</el-radio>
<el-radio value="2" size="large">文本转语音回复</el-radio>
</el-radio-group>
</el-radio-group> -->
<div class="bullet-list-item" />
</div>
<div class="footer-btns">
<el-button type="primary" @click="save"> </el-button>
</div>
</div>
</el-form>
</div>
@ -214,6 +243,14 @@ const rules = {
.live {
width: 453px;
border-left: 1px solid rgba(46, 128, 250, 0.1);
display: flex;
align-items: center;
justify-content: center;
.video {
width: 420px;
height: 891px;
object-fit: cover;
}
}
.main {
flex: 1;
@ -230,6 +267,7 @@ const rules = {
}
.main-content {
padding: 16px;
position: relative;
.card {
width: 480px;
height: 104px;
@ -336,6 +374,11 @@ const rules = {
margin-bottom: 16px;
}
}
.footer-btns {
position: absolute;
bottom: 0;
left: 16px;
}
}
}
}

@ -10,7 +10,7 @@ import { FormInstance } from "element-plus/es/components";
import { getSalespitch } from "@/api/chat";
import { Delete } from "@element-plus/icons-vue";
const ruleFormRef = ref<FormInstance>();
const salespitchList = ref(["2222"]);
const salespitchList = ref([]);
import { message } from "@/utils/message";
const router = useRouter();
const formData = reactive({

@ -19,6 +19,13 @@ const BasicSettingsRef = ref(undefined);
const chanageActived = (key: string) => {
activedKey.value = key;
};
const enterLiveRoom = async () => {
router.push({
path: "/Live",
query: { gender: router.currentRoute.value.query.gender }
});
// window.electronAPI.startProcess("LiveTalking", "LiveTalking.exe");
};
</script>
<template>
@ -32,8 +39,7 @@ const chanageActived = (key: string) => {
<div class="desc">{{ `最近编辑2025-06-12 12:34:00` }}</div>
</div>
<div class="top-right">
<el-button>房间号</el-button>
<el-button type="primary">进入直播间</el-button>
<el-button type="primary" @click="enterLiveRoom"></el-button>
</div>
</div>
<div class="ai-content">

@ -26,8 +26,8 @@
<span class="live_num">{{ `直播100场` }}</span>
</div>
<div class="desc-btn">
<div class="live_btn">选Ta开</div>
<div class="enter_btn" @click="enterLive"></div>
<div class="live_btn" @click="selectHuman('0')">Ta</div>
<div class="enter_btn" @click="enterLive('0')"></div>
</div>
</div>
</div>
@ -56,8 +56,8 @@
<span class="live_num">{{ `直播100场` }}</span>
</div>
<div class="desc-btn">
<div class="live_btn">选Ta开</div>
<div class="enter_btn">进入直播</div>
<div class="live_btn" @click="selectHuman('1')">Ta</div>
<div class="enter_btn" @click="enterLive('0')"></div>
</div>
</div>
</div>
@ -89,18 +89,27 @@ import { ref } from "vue";
import womenImg from "@/assets/live/girl.png";
import manImg from "@/assets/live/man.png";
import dyImg from "@/assets/live/dy.png";
import { useRouter } from "vue-router";
const showVideo1 = ref(false);
const showVideo2 = ref(false);
const dialogVisible = ref(false);
const liveRoom = ref("");
const router = useRouter();
const closed = () => {
dialogVisible.value = false;
};
//
const selectHuman = (type: string) => {
// dialogVisible.value = true;
router.push({ name: "AiLive", query: { gender: type } });
};
//
const enterLive = () => {
dialogVisible.value = true;
const enterLive = type => {
router.push({
path: "/Live",
query: { gender: type }
});
};
</script>

@ -3,10 +3,34 @@
<div class="top">
<span class="title">{{ title }}</span>
<span class="time">{{ `直播倒计时: ${time}` }}</span>
<el-button type="primary" class="back" @click="goback"></el-button>
</div>
<div class="main">
<div class="left">
<div class="tips">
<tipsImg />
<span>直播期间不能关闭页面页面关闭将自动停止直播</span>
</div>
<div class="title">直播弹幕</div>
<div v-if="isConnected" class="bullet-connect"></div>
<div v-else class="bullet-disconnect">未接管</div>
<div class="title">房间号</div>
<el-input v-model="liveRoom" style="margin-bottom: 16px" />
<el-button
type="primary"
:disabled="isStartConnectBullet || !liveRoom"
@click="connectBullet"
>连接弹幕</el-button
>
<!-- <div @click="startPlay"></div> -->
</div>
<div class="video-placeholder">
<div v-show="!isPlaying" class="video-loading">
<img :src="liveLoading" alt="" />
<span> AI主播生成中请稍后片刻...</span>
</div>
<video
v-show="isPlaying"
ref="videoRef"
autoplay
playsinline
@ -15,6 +39,10 @@
controlslist="nodownload nofullscreen noremoteplayback"
class="video-player"
/>
<div v-show="isPlaying" class="close-btn">
<el-icon><SwitchButton /></el-icon>
<span>关闭直播</span>
</div>
<img @click="enablePiP" class="btn" :src="pictureIcon" alt="" />
</div>
</div>
@ -22,32 +50,267 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import Hls from "hls.js";
import { ref, onMounted, onUnmounted } from "vue";
import tipsImg from "@/assets/svg/live/tips.svg";
import liveLoading from "@/assets/live/loading.png";
import { SwitchButton } from "@element-plus/icons-vue";
import { message } from "@/utils/message";
const title = ref("慧文的直播间");
const time = ref("00:00:00");
const videoRef = ref<HTMLVideoElement | null>(null);
// ID
let timer = null;
//
let startTime = null;
const videoRef = ref(null);
import pictureIcon from "@/assets/live/picture.png";
import { useRouter } from "vue-router";
const router = useRouter();
//
const isConnected = ref(false);
const liveRoom = ref("");
//
const isReady = ref(false);
const livetlking_enable_status = ref<any>("0");
const gptsovits_enable_status = ref<any>("0");
const chat_enable_status = ref<any>("0");
//
const isStartConnectBullet = ref(false);
//
const hlsUrl = "http://110.40.131.100:8116/live/livestream.m3u8";
// const hlsUrl =
// "http://192.168.10.209:8010/rtc/v1/whep/?app=live&stream=livestream";
let pc = null;
// ICE NAT 穿
const iceServers = [
{ urls: "stun:stun.l.google.com:19302" }, // STUN
{ urls: "stun:stun1.l.google.com:19302" }
];
// HH:MM:SS
const formatTime = seconds => {
const h = Math.floor(seconds / 3600)
.toString()
.padStart(2, "0");
const m = Math.floor((seconds % 3600) / 60)
.toString()
.padStart(2, "0");
const s = (seconds % 60).toString().padStart(2, "0");
return `${h}:${m}:${s}`;
};
//
const isPlaying = ref(false);
const statusText = ref("未播放");
const init = async () => {
window.electronAPI.startProcess("LiveTalking", "LiveTalking.exe");
window.electronAPI.startProcess("gptsovits", "go-gptsovits.bat");
// window.electronAPI.startProcess("chat", "chat.exe");
};
const goback = () => {
//
router.go(-1);
isReady.value = false;
};
onMounted(() => {
// WHEP
const startPlay = async () => {
if (isPlaying.value) return;
try {
statusText.value = "连接中...";
// 1. RTCPeerConnection
pc = new RTCPeerConnection({ iceServers });
// 2.
pc.ontrack = event => {
if (event.streams && event.streams[0]) {
// video
videoRef.value.srcObject = event.streams[0];
statusText.value = "播放中";
isPlaying.value = true;
}
};
pc.addTransceiver("video", { direction: "recvonly" });
pc.addTransceiver("audio", { direction: "recvonly" });
return pc
.createOffer()
.then(offer => {
return pc.setLocalDescription(offer);
})
.then(() => {
// wait for ICE gathering to complete
return new Promise<void>(resolve => {
if (pc.iceGatheringState === "complete") {
resolve();
} else {
const checkState = () => {
if (pc.iceGatheringState === "complete") {
pc.removeEventListener("icegatheringstatechange", checkState);
resolve();
}
};
pc.addEventListener("icegatheringstatechange", checkState);
}
});
})
.then(() => {
const offer = pc.localDescription;
return fetch("http://192.168.10.209:8010/offer", {
body: JSON.stringify({
sdp: offer.sdp,
type: offer.type
}),
headers: {
"Content-Type": "application/json"
},
method: "POST"
});
})
.then(response => {
return response.json();
})
.then(answer => {
return pc.setRemoteDescription(answer);
})
.catch(e => {});
} catch (error) {
console.error("播放失败:", error);
}
};
//
const startLiveTimer = () => {
//
if (timer) clearInterval(timer);
// 使
const currentTime = new Date().getTime();
const elapsedSeconds =
time.value === "00:00:00"
? 0
: parseInt(time.value.split(":")[0]) * 3600 +
parseInt(time.value.split(":")[1]) * 60 +
parseInt(time.value.split(":")[2]);
startTime = currentTime - elapsedSeconds * 1000;
//
timer = setInterval(() => {
const now = new Date().getTime();
const elapsed = Math.floor((now - startTime) / 1000);
time.value = formatTime(elapsed);
}, 1000);
};
//
const stopLiveTimer = () => {
if (timer) {
clearInterval(timer);
timer = null;
}
};
//
const stopPlay = () => {
//
if (videoRef.value) {
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(hlsUrl);
hls.attachMedia(videoRef.value);
} else if (videoRef.value.canPlayType("application/vnd.apple.mpegurl")) {
videoRef.value.src = hlsUrl;
}
videoRef.value.srcObject = null;
}
isPlaying.value = false;
statusText.value = "已停止";
};
const getLiveTalkingResult = async () => {
livetlking_enable_status.value = await window.electronAPI.getConfig(
"live_config",
"livetlking_enable_status"
);
gptsovits_enable_status.value = await window.electronAPI.getConfig(
"live_config",
"gptsovits_enable_status"
);
};
const getIsConnectBullet = async () => {
livetlking_enable_status.value = await window.electronAPI.getConfig(
"live_config",
"chat_enable_status"
);
};
const connectBullet = async () => {
console.log("开始弹幕");
isStartConnectBullet.value = true;
await window.electronAPI.updateConfig(
"live_config",
"live_id",
liveRoom.value
);
window.electronAPI.startProcess("chat", "chat.exe");
// 3
const checkInterval = setInterval(() => {
getIsConnectBullet();
console.log("chat_enable_status", chat_enable_status.value);
// 1
if (chat_enable_status.value === "1") {
isReady.value = true;
console.log("连接弹幕成功");
message("连接弹幕成功", { type: "success" });
isConnected.value = true;
clearInterval(checkInterval);
clearInterval(checkInterval); //
}
}, 1000);
};
const closedLive = () => {
window.electronAPI.updateConfig(
"live_config",
"livetlking_enable_status",
"0"
);
window.electronAPI.updateConfig(
"live_config",
"gptsovits_enable_status",
"0"
);
window.electronAPI.updateConfig("live_config", "chat_enable_status", "0");
window.electronAPI.stopAllProcesses();
stopLiveTimer();
isReady.value = false;
stopPlay();
clearInterval(timer);
isStartConnectBullet.value = false;
liveRoom.value = "";
};
onMounted(() => {
init();
const gender = router.currentRoute.value.query.gender;
window.electronAPI.updateConfig(
"live_config",
"human_genders",
gender === "1" ? "man" : "woman"
);
// 3
const checkInterval = setInterval(() => {
getLiveTalkingResult();
// 1
console.log(
"检查是否全部为1: ",
livetlking_enable_status.value,
gptsovits_enable_status.value
);
if (
livetlking_enable_status.value === "1" &&
gptsovits_enable_status.value === "1"
) {
isReady.value = true;
startPlay();
startLiveTimer();
clearInterval(checkInterval); //
}
}, 1000);
});
//
onUnmounted(() => {
closedLive();
});
function enablePiP() {
if (videoRef.value && document.pictureInPictureEnabled) {
videoRef.value.requestPictureInPicture();
}
// if (videoRef.value && document.pictureInPictureEnabled) {
// videoRef.value.requestPictureInPicture();
// }
}
</script>
<style lang="scss" scoped>
@ -63,7 +326,12 @@ function enablePiP() {
padding-left: 24px;
height: 64px;
background: #ffffff;
position: relative;
box-shadow: 0px 1px 0px 0px rgba(46, 128, 250, 0.2);
.back {
position: absolute;
right: 24px;
}
.title {
font-weight: bold;
font-size: 20px;
@ -80,7 +348,7 @@ function enablePiP() {
background: #f4f8ff;
flex: 1;
display: flex;
justify-content: center;
// justify-content: center;
align-items: center;
// ...existing code...
.video-player::-webkit-media-controls-fullscreen-button {
@ -89,10 +357,66 @@ function enablePiP() {
.video-player::-webkit-media-controls-enclosure {
overflow: hidden;
}
.left {
width: 650px;
margin-right: 30px;
height: 100%;
padding: 20px 0 0 24px;
.tips {
display: flex;
align-items: center;
width: 624px;
height: 32px;
background: #fff7e8;
border-radius: 4px 4px 4px 4px;
padding-left: 12px;
margin-bottom: 16px;
span {
font-size: 14px;
color: #1d2129;
margin-left: 8px;
}
}
.title {
font-weight: 500;
font-size: 14px;
color: #333333;
margin-bottom: 12px;
margin-top: 12px;
}
.bullet-disconnect {
width: 72px;
height: 24px;
background: #e5e6eb;
border-radius: 2px 2px 2px 2px;
line-height: 24px;
text-align: center;
color: #86909c;
font-size: 14px;
}
}
.video-placeholder {
position: relative;
width: 560px;
height: 968px;
height: calc(100vh - 100px);
.close-btn {
position: absolute;
left: 24px;
top: 24px;
display: flex;
height: 32px;
background: #2e80fa;
border-radius: 4px 4px 4px 4px;
align-items: center;
padding: 0 10px;
font-size: 14px;
color: #ffffff;
span {
margin-left: 4px;
}
cursor: pointer;
z-index: 9999999;
}
.video-player {
position: absolute;
top: 0;
@ -110,6 +434,24 @@ function enablePiP() {
width: 40px;
height: 40px;
}
.video-loading {
background: #272e3b;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
img {
width: 48px;
height: 48px;
margin-bottom: 16px;
}
span {
font-weight: 500;
font-size: 16px;
color: #ffffff;
}
}
}
}
}

@ -20,6 +20,7 @@ const refInput = ref();
const loading = ref(false);
const passwordType = ref("password");
const ruleFormRef = ref<FormInstance>();
const test = ref("");
const ruleForm = reactive({
username: "admin",
password: "sst123456#"
@ -44,13 +45,17 @@ const onLogin = async (formEl: FormInstance | undefined) => {
if (res.code === 200) {
loading.value = false;
setToken(res.data);
router.push("/digitalHuman");
const result = await window.electronAPI.updateConfig(
"config",
"backend_token",
res.data
);
console.log("result: ", result);
if (result.changes > 0) {
router.push("/digitalHuman");
message("登录成功", {
type: "success"
});
} else {
message("没有记录被更新,请检查 config 表是否存在", {
type: "error"
@ -66,13 +71,15 @@ const onLogin = async (formEl: FormInstance | undefined) => {
return fields;
}
});
// const result = await window.electronAPI.startProcess();
// console.log("result: ", result);
};
onMounted(async () => {
removeToken();
storageLocal().clear();
storageSession().clear();
// const sss = await window.electronAPI.getConfig("reply_prob_follow");
// console.log("sss: ", sss);
// test.value = await window.electronAPI.getConfig("reply_prob_follow");
// console.log("sss: ", test.value);
});
</script>

Loading…
Cancel
Save