数字人直播配置修改

main
xiangcongshuai 1 week ago
parent ccef719c06
commit 8495d7c04b

@ -100,11 +100,10 @@ function getAllSystemMessages() {
console.error("数据库连接错误:", err.message);
return reject(err);
}
console.log("成功连接到数据库");
});
// 执行查询
const sql = "SELECT message FROM system_message";
const sql = "SELECT * FROM message";
db.all(sql, [], (err, rows) => {
// 关闭数据库连接
db.close(closeErr => {
@ -118,10 +117,54 @@ function getAllSystemMessages() {
console.error("查询错误:", err.message);
return reject(err);
}
resolve(rows);
});
});
}
function updateMessageById(id, data) {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(getDatabasePath(), err => {
if (err) {
return reject(err);
}
});
debugger;
// 解构data中的所有字段根据实际表结构调整字段名
const { message, status } = data;
// 构建更新字段字符串排除ID避免更新主键
const fields = [];
const params = [];
// 动态处理所有字段(确保与表结构一致)
if (message !== undefined) {
fields.push("message = ?");
params.push(message);
}
if (status !== undefined) {
fields.push("status = ?");
params.push(status);
}
// 构建SQL语句UPDATE message SET 字段1=?, 字段2=? WHERE id=?
const sql = `UPDATE message SET ${fields.join(", ")} WHERE id = ?`;
params.push(id); // 最后添加WHERE条件的ID参数
// 提取所有message字段
const messages = rows.map(row => row.message);
resolve(messages);
// 执行更新
db.run(sql, params, function (err) {
if (err) {
console.error("更新message表失败", err.message);
reject({ code: 1, msg: `更新失败:${err.message}` });
} else {
if (this.changes > 0) {
resolve({
code: 0,
msg: `成功更新ID为${id}的记录`,
affectedRows: this.changes
});
} else {
resolve({ code: 2, msg: `未找到ID为${id}的记录`, affectedRows: 0 });
}
}
});
});
}
@ -325,6 +368,23 @@ async function stopAllProcesses() {
console.error(err);
}
}
async function stopChatProcesses() {
try {
delete processes["chatProcess"];
const projectRoot = process.cwd();
const parentDir = path.dirname(projectRoot);
const exPath = path.join(parentDir, "kill-chat.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 = {
updateConfig,
getConfigValue,
@ -332,5 +392,7 @@ module.exports = {
clearSystemMessages,
startProcess,
stopAllProcesses,
getAllSystemMessages
stopChatProcesses,
getAllSystemMessages,
updateMessageById
};

@ -1,5 +1,6 @@
const { app, BrowserWindow } = require("electron");
const { ipcMain } = require("electron");
// const { initSplashScreen } = require("electron-splashscreen");
const path = require("path");
const {
updateConfig,
@ -8,8 +9,10 @@ const {
stopProcess,
startProcess,
stopAllProcesses,
stopChatProcesses,
getAllSystemMessages,
clearSystemMessages
clearSystemMessages,
updateMessageById
} = require("./dbHandler");
// 注册IPC通信处理
@ -73,6 +76,15 @@ function registerIpcHandlers() {
throw error;
}
});
ipcMain.handle("stop-chat-process", async () => {
try {
return await stopChatProcesses();
} catch (error) {
console.error("停止失败:", error);
throw error;
}
});
ipcMain.handle("get-all-system-messages", async () => {
try {
return await getAllSystemMessages();
@ -81,6 +93,14 @@ function registerIpcHandlers() {
throw error;
}
});
ipcMain.handle("update-message", async (event, id, data) => {
try {
return await updateMessageById(id, data);
} catch (error) {
console.error("更新失败:", error);
throw error;
}
});
}
function createWindow() {
const win = new BrowserWindow({
@ -92,7 +112,6 @@ function createWindow() {
contextIsolation: true
}
});
// 捕获 preload 加载错误
win.webContents.on("did-fail-load", (event, errorCode, errorDescription) => {
console.error("Preload 加载失败:", errorDescription);
@ -105,7 +124,7 @@ function createWindow() {
win.loadURL("http://localhost:8848/#/login");
win.webContents.openDevTools();
} else {
win.webContents.openDevTools();
// win.webContents.openDevTools();
win.loadFile("dist/index.html");
}
}

@ -17,6 +17,10 @@ contextBridge.exposeInMainWorld("electronAPI", {
stopProcess: (fileName, exeName) =>
ipcRenderer.invoke("stop-process", fileName, exeName),
stopAllProcesses: () => ipcRenderer.invoke("stop-all-process"),
stopChatProcesses: () => ipcRenderer.invoke("stop-chat-process"),
clearSystemMessages: () => ipcRenderer.invoke("clear-system-messages"),
getAllSystemMessages: () => ipcRenderer.invoke("get-all-system-messages")
getAllSystemMessages: () => ipcRenderer.invoke("get-all-system-messages"),
updateMessageById: async (id, data) => {
return await ipcRenderer.invoke("update-message", id, data);
}
});

@ -29,7 +29,8 @@
},
"build": {
"appId": "com.example.myapp",
"productName": "My Electron App",
"artifactName": "${productName}.exe",
"productName": "直播数字人",
"directories": {
"output": "dist-electron"
},

@ -47,7 +47,11 @@ export default defineComponent({
.page-main {
width: 100%;
height: 100%;
overflow: hidden;
overflow-x: auto;
overflow-y: hidden;
// overflow: auto;
min-width: 1200px;
min-height: 800px;
display: flex;
flex-direction: column;
.main-content {

@ -7,3 +7,8 @@ export const getSalespitch = (data?: object) => {
{ data }
);
};
export const interruptTalk = (data?: object) => {
return http.request<any>("post", `${config.services.interruptTalk}`, {
data
});
};

@ -34,6 +34,16 @@ export const getLogin = (data?: object) => {
);
};
/** 获取当前用户信息 */
export const getUserInfo = (data?: object) => {
return http.request<UserResult>(
"get",
`${config.services.liveDigital}/auth/me`,
{
params: data
}
);
};
/** 刷新token */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refreshToken", { data });

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame" clip-path="url(#clip0_838_12008)">
<path id="Vector" d="M9.99996 18.3327C14.6023 18.3327 18.3333 14.6017 18.3333 9.99935C18.3333 5.39697 14.6023 1.66602 9.99996 1.66602C5.39758 1.66602 1.66663 5.39697 1.66663 9.99935C1.66663 14.6017 5.39758 18.3327 9.99996 18.3327Z" stroke="#C9CDD4" stroke-width="1.2" stroke-linejoin="round"/>
<path id="Vector_2" d="M8.33337 10V7.11328L10.8334 8.55666L13.3334 10L10.8334 11.4434L8.33337 12.8868V10Z" stroke="#C9CDD4" stroke-width="1.2" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_838_12008">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 723 B

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame" clip-path="url(#clip0_838_11983)">
<path id="Vector" d="M17.5 10.8333V16.6667C17.5 17.1269 17.1269 17.5 16.6667 17.5H3.33333C2.8731 17.5 2.5 17.1269 2.5 16.6667V3.33333C2.5 2.8731 2.8731 2.5 3.33333 2.5H9.16667" stroke="#2E80FA" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M5.83337 11.1333V14.1667H8.88221L17.5 5.54504L14.4563 2.5L5.83337 11.1333Z" stroke="#2E80FA" stroke-width="1.2" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_838_11983">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 683 B

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame" clip-path="url(#clip0_833_7922)">
<path id="Vector" d="M9.99996 18.3327C14.6023 18.3327 18.3333 14.6017 18.3333 9.99935C18.3333 5.39697 14.6023 1.66602 9.99996 1.66602C5.39758 1.66602 1.66663 5.39697 1.66663 9.99935C1.66663 14.6017 5.39758 18.3327 9.99996 18.3327Z" stroke="#2E80FA" stroke-width="1.2" stroke-linejoin="round"/>
<path id="Vector_2" d="M8.33337 10V7.11328L10.8334 8.55666L13.3334 10L10.8334 11.4434L8.33337 12.8868V10Z" stroke="#2E80FA" stroke-width="1.2" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_833_7922">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 721 B

@ -0,0 +1,13 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame" clip-path="url(#clip0_833_8131)">
<path id="Vector" d="M16.6666 13.75L18.3333 15.4167L16.6666 17.0833" stroke="#2E80FA" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M16.6666 2.91602L18.3333 4.58268L16.6666 6.24935" stroke="#2E80FA" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M18.3333 4.58398H15.4167C12.4251 4.58398 10 7.00911 10 10.0007C10 12.9922 12.4251 15.4173 15.4167 15.4173H18.3333" stroke="#2E80FA" stroke-width="1.2" stroke-linecap="round"/>
<path id="Vector_4" d="M1.66663 15.4173H4.58329C7.57483 15.4173 9.99996 12.9922 9.99996 10.0007C9.99996 7.00911 7.57483 4.58398 4.58329 4.58398H1.66663" stroke="#2E80FA" stroke-width="1.2" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_833_8131">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 983 B

@ -0,0 +1,13 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame" clip-path="url(#clip0_835_8218)">
<path id="Vector" d="M9.99996 18.3327C5.39758 18.3327 1.66663 14.6017 1.66663 9.99935C1.66663 5.39697 5.39758 1.66602 9.99996 1.66602C14.6023 1.66602 18.3333 5.39697 18.3333 9.99935" stroke="#2E80FA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M13.75 13.75L17.0833 17.0833" stroke="#2E80FA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M17.0833 13.75L13.75 17.0833" stroke="#2E80FA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_4" d="M8.33337 10V7.11328L10.8334 8.55666L13.3334 10L10.8334 11.4434L8.33337 12.8868V10Z" stroke="#2E80FA" stroke-width="1.5" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_835_8218">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 971 B

@ -8,9 +8,12 @@ import router from "@/router";
const { logout, userAvatar, avatarsStyle } = useNav();
import { useRoute } from "vue-router";
const route = useRoute();
const userName = ref("admin");
const userName = ref<any>("admin");
onMounted(() => {});
onMounted(async () => {
const res: any = await window.electronAPI.getConfig("config", "userName");
userName.value = res || "admin";
});
</script>
<template>
<div class="NavBar">

@ -40,4 +40,5 @@ export type userType = {
roles?: Array<string>;
currentPage?: number;
loginType?: string;
userInfo?: any;
};

@ -25,7 +25,8 @@ export const useUserStore = defineStore({
roles: storageSession().getItem<DataInfo<number>>(sessionKey)?.roles ?? [],
// 判断登录页面显示哪个组件0登录默认、1手机登录、2二维码登录、3注册、4忘记密码
currentPage: 0,
loginType: "0"
loginType: "0",
userInfo: undefined
}),
actions: {
/** 存储用户名 */
@ -43,6 +44,9 @@ export const useUserStore = defineStore({
SET_LOGINTYPE(value: string) {
this.loginType = value;
},
SET_USERINFO(value: any) {
this.userInfo = value;
},
/** 登入 */
async loginByUsername(data) {
return new Promise((resolve, reject) => {

@ -6,7 +6,9 @@ interface Window {
bulkInsertSystemMessages: (messages: any) => void;
startProcess: (fileName, exeName) => void;
stopProcess: (fileName, exeName) => void;
updateMessageById: (id, data) => Promise<any>;
stopAllProcesses: () => void;
stopChatProcesses: () => void;
getAllSystemMessages: () => Promise<any>;
clearSystemMessages: () => Promise<void>;
};

@ -130,9 +130,9 @@ class PureHttp {
// router.push("/login");
// return;
// }
// if (response.data.code !== 200) {
// message(response.data.data, { type: "error" });
// }
if (response.data.code !== 200) {
message(response.data.msg, { type: "error" });
}
const $config = response.config;
// 关闭进度条动画

@ -5,6 +5,9 @@ export default {
services: {
liveDigital: isDevelopment
? "/live-digital-avatar-manage"
: "http://192.168.10.137:9909/live-digital-avatar-manage"
: "http://192.168.10.137:9909/live-digital-avatar-manage",
interruptTalk: isDevelopment
? "/set_live_room_control_mode"
: "http://127.0.0.1:8010/set_live_room_control_mode"
}
};

@ -1,18 +1,19 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import { reactive, ref, onMounted } from "vue";
import cheackImg from "@/assets/svg/live/check.svg";
import starImg from "@/assets/svg/live/star.svg";
import plusImg from "@/assets/svg/live/plus.svg";
import emptyImg from "@/assets/live/empty.png";
import picEmptyImg from "@/assets/live/goods_empty.png";
import tipsImg from "@/assets/svg/live/tips.svg";
import { FormInstance } from "element-plus/es/components";
import { FormInstance, UploadProps } from "element-plus/es/components";
import { getSalespitch } from "@/api/chat";
import { Delete } from "@element-plus/icons-vue";
import { Delete, Plus } from "@element-plus/icons-vue";
const ruleFormRef = ref<FormInstance>();
const salespitchList = ref([]);
import { message } from "@/utils/message";
const router = useRouter();
import { message } from "@/utils/message";
import { useRouter } from "vue-router";
const formData = reactive({
productName: "",
detail: "",
@ -25,6 +26,11 @@ const formData = reactive({
}
]
});
const imageUrl = ref<any>("");
const goodsList = ref([]);
const activedIndex = ref(0);
const activeText = ref("");
const addFlag = ref(false);
//
const validateSpecifications = (rule, value, callback) => {
//
@ -58,7 +64,21 @@ const rules = {
specifications: [{ required: true, validator: validateSpecifications }],
detail: [{ required: true, message: "请输入商品详情", trigger: "change" }]
};
const handleFileChange = file => {
// 5MB
if (file.size > 5 * 1024 * 1024) {
return;
}
// Base64
const reader = new FileReader();
reader.readAsDataURL(file.raw);
reader.onload = e => {
//
imageUrl.value = e.target.result;
};
};
const handlePriceInput = item => {
item.price = item.price
.replace(/[^\d.]/g, "")
@ -73,26 +93,64 @@ const addNorms = () => {
price: ""
});
};
const save = () => {
if (!ruleFormRef.value) return;
const selectIndex = index => {
activedIndex.value = index;
activeText.value = salespitchList.value[index].message;
};
//
defineExpose({
enterLiveRoom: async () => {
//
const saveSuccess = await save();
if (saveSuccess) {
router.push({
path: "/Live",
query: { gender: router.currentRoute.value.query.gender }
});
}
}
});
// savePromise
const save = (): Promise<boolean> => {
return new Promise(resolve => {
if (!ruleFormRef.value) {
resolve(false);
return;
}
//
ruleFormRef.value.validate(valid => {
if (valid) {
// electronAPIawait
window.electronAPI.updateConfig(
"live_config",
"product_name",
formData.productName
);
window.electronAPI.updateConfig(
"live_config",
"product_specification",
JSON.stringify(formData.specifications)
);
window.electronAPI.updateConfig(
"live_config",
"product_description",
formData.detail
);
message("商品信息已保存", { type: "success" });
resolve(true); //
} else {
message("请填写完整信息", { type: "warning" });
resolve(false); //
}
});
});
};
const addNext = () => {
//
ruleFormRef.value.validate(valid => {
if (valid) {
window.electronAPI.updateConfig(
"live_config",
"product_name",
formData.productName
);
window.electronAPI.updateConfig(
"live_config",
"product_specification",
JSON.stringify(formData.specifications)
);
window.electronAPI.updateConfig(
"live_config",
"product_description",
formData.detail
);
message("商品信息已保存", { type: "success" });
} else {
message("请填写完整信息", { type: "warning" });
}
@ -110,6 +168,8 @@ const generateScript = async () => {
buttonText.value = "生成AI话术";
if (res.code === 200) {
salespitchList.value = res.data;
activedIndex.value = 0;
activeText.value = salespitchList.value[0];
message("生成成功!", { type: "success", showClose: true });
await window.electronAPI.clearSystemMessages();
window.electronAPI.bulkInsertSystemMessages(res.data);
@ -146,34 +206,88 @@ onMounted(async () => {
const res = await window.electronAPI.getAllSystemMessages();
if (res && Array.isArray(res)) {
salespitchList.value = res;
activedIndex.value = 0;
activeText.value = salespitchList.value[0].message;
}
});
</script>
<template>
<div class="GoodsManage">
<!-- <div class="goods-list">
<div class="goods-title">
<span>商品图片</span>
</div>
<div v-if="goodsList.length > 0" class="goods-pic-list">
<div v-for="(item, index) in goodsList" :key="index">
<img :src="item.url" alt="" />
<el-icon
role="img"
style="
font-size: 18px;
color: rgba(234, 42, 42, 1);
cursor: pointer;
"
><Delete
/></el-icon>
</div>
</div>
<div v-else class="goods-empty">
<img :src="picEmptyImg" alt="" />
<span>暂无商品图~</span>
</div>
</div> -->
<div class="ai-script">
<div class="script-title">
<starImg />
<span class="name">AI直播讲品话术</span>
<cheackImg />
<span class="desc">生成/修改内容将自动保存</span>
<cheackImg v-if="!loading" />
<div v-if="loading" class="loading" v-loading="loading" />
<span class="desc" v-if="!loading && salespitchList.length === 0"
>生成/修改内容将自动保存</span
>
<span class="desc" v-if="!loading && salespitchList.length > 0"
>自动保存成功</span
>
<span class="desc" v-if="loading">...</span>
</div>
<div v-if="salespitchList.length === 0" class="script-empty">
<div class="loading-text" v-loading="loading" v-if="loading" />
<div v-if="salespitchList.length === 0 && !loading" class="script-empty">
<img :src="emptyImg" alt="" />
<span>暂无AI讲品话术~</span>
<el-button
style="margin-top: 16px"
:loading="loading"
type="primary"
@click="generateScript"
>
{{ buttonText }}</el-button
>
</div>
<div v-else class="script-content">
<div v-if="salespitchList.length > 0 && !loading" class="script-content">
<div class="tip">
<tipsImg />
<span>以下内容由AI自动生成</span>
</div>
<!-- <div class="script-tabs">
<div
@click="selectIndex(index)"
class="script-tabs-item"
:class="[index === activedIndex ? 'active' : '']"
v-for="(item, index) in salespitchList"
:key="item"
>
{{ index + 1 }}
</div>
</div> -->
<!-- <div class="speech-item">{{ activeText }}</div> -->
<div
v-for="(item, index) in salespitchList"
:key="index"
class="speech-item"
>
{{ item }}
<span>{{ item.message }}</span>
<span class="type">{{ item.type }}</span>
</div>
</div>
</div>
@ -203,7 +317,7 @@ onMounted(async () => {
class="norms-item"
size="large"
v-model="item.size"
maxlength="20"
maxlength="100"
show-word-limit
placeholder="请输入商品规格"
/>
@ -215,7 +329,8 @@ onMounted(async () => {
show-word-limit
placeholder="请输入商品价格"
@input="handlePriceInput(item)"
/>
><template #append>¥</template></el-input
>
<el-icon
v-show="index === formData.specifications.length - 1"
@click="formData.specifications.splice(index, 1)"
@ -233,6 +348,19 @@ onMounted(async () => {
<plusImg />
<span>添加规格</span>
</div>
<!-- <el-form-item label="商品图片" prop="productImg">
<el-upload
class="avatar-uploader"
action="#"
:show-file-list="false"
:on-change="handleFileChange"
accept="image/*"
:limit="1"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item> -->
<el-form-item label="商品详情" prop="detail">
<el-input
:rows="8"
@ -261,10 +389,14 @@ onMounted(async () => {
<el-button class="reset" @click="ruleFormRef.resetFields()"
>重置</el-button
>
<el-button :loading="loading" type="primary" @click="generateScript">
<!-- <el-button :loading="loading" type="primary" @click="generateScript">
{{ buttonText }}</el-button
> -->
<el-button type="primary" @click="save"></el-button>
<!-- <el-button v-if="addFlag" type="primary" @click="addNext"
>新建下一个</el-button
>
<el-button type="primary" @click="save"> </el-button>
<div class="add_btn" v-if="!addFlag"></div> -->
</div>
</el-form>
</div>
@ -276,14 +408,53 @@ onMounted(async () => {
flex: 1;
display: flex;
flex-direction: row-reverse;
background: #ffffff;
box-shadow: 0px 4px 8px 0px rgba(46, 128, 250, 0.1);
box-shadow: 0px 4px 8px 0px rgba(46, 128, 250, 0.1);
.goods-list {
width: 180px;
margin-left: 16px;
background: #ffffff;
border-radius: 8px 8px 8px 8px;
display: flex;
flex-direction: column;
.goods-title {
height: 54px;
border-bottom: 1px solid rgba(46, 128, 250, 0.1);
display: flex;
align-items: center;
padding-left: 16px;
}
.goods-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
font-weight: 500;
font-size: 16px;
color: #86909c;
img {
width: 132px;
height: 81px;
}
}
.goods-pic-list {
display: flex;
padding: 16px;
flex-direction: column;
img {
width: 120px;
height: 120px;
}
}
}
.ai-script {
width: 453px;
display: flex;
flex-direction: column;
border-left: 1px solid rgba(46, 128, 250, 0.1);
background: #ffffff;
.script-title {
height: 54px;
border-bottom: 1px solid rgba(46, 128, 250, 0.1);
@ -303,6 +474,21 @@ onMounted(async () => {
color: #86909c;
margin-left: 4px;
}
.loading {
width: 16px;
height: 16px;
/* 局部修改:穿透 scoped 样式 */
::v-deep .el-loading-spinner .circular {
width: 16px !important;
height: 16px !important;
top: 10px;
position: relative;
}
::v-deep .el-loading-spinner .circular path {
stroke: rgba(134, 144, 156, 1) !important;
}
}
}
.script-content {
padding: 16px;
@ -319,6 +505,24 @@ onMounted(async () => {
margin-left: 8px;
}
}
.script-tabs {
display: flex;
border-bottom: 1px solid #e7e7e7;
margin-bottom: 12px;
.script-tabs-item {
width: 32px;
height: 40px;
text-align: center;
line-height: 40px;
font-size: 14px;
color: rgba(0, 0, 0, 0.6);
cursor: pointer;
}
.active {
color: #0052d9;
border-bottom: 3px solid #0052d9;
}
}
.speech-item {
font-weight: 500;
font-size: 14px;
@ -327,6 +531,23 @@ onMounted(async () => {
border-radius: 4px 4px 4px 4px;
padding: 8px 12px;
margin-bottom: 20px;
position: relative;
padding-bottom: 36px;
.type {
position: absolute;
bottom: 8px;
right: 12px;
width: 96px;
height: 24px;
border-radius: 2px 2px 2px 2px;
border: 1px solid #86909c;
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 12px;
color: #1d2129;
line-height: 24px;
text-align: center;
}
}
}
.script-empty {
@ -343,11 +564,31 @@ onMounted(async () => {
height: 138px;
}
}
.loading-text {
height: 129px;
margin: 16px;
background: #f3f4fc !important;
border-radius: 4px 4px 4px 4px;
::v-deep(.el-loading-mask) {
background-color: unset !important;
}
::v-deep(.el-loading-spinner) {
width: 30px;
top: 15%;
.circular {
width: 19px !important;
height: 19px !important;
top: 10px;
position: relative;
}
}
}
}
.main-content {
flex: 1;
padding: 16px;
position: relative;
background: #ffffff;
.norms-list {
display: flex;
width: 100%;
@ -390,13 +631,58 @@ onMounted(async () => {
position: absolute;
bottom: 32px;
right: 16px;
display: flex;
.reset {
width: 94px;
height: 32px;
border-radius: 4px 4px 4px 4px;
border: 1px solid #2e80fa;
}
.add_btn {
width: 94px;
height: 32px;
background: #e5e6eb;
border-radius: 4px 4px 4px 4px;
border: 1px solid #e5e6eb;
font-weight: bold;
font-size: 14px;
color: #86909c;
text-align: center;
line-height: 32px;
margin-left: 16px;
cursor: no-drop;
}
}
}
}
</style>
<style scoped>
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

@ -49,6 +49,7 @@ const validateProductInfo = async () => {
}
};
const enterLiveRoom = async () => {
GoodsManageRef.value?.enterLiveRoom();
const isValid = await validateProductInfo();
if (!isValid) return;
router.push({

@ -57,7 +57,7 @@
</div>
<div class="desc-btn">
<div class="live_btn" @click="selectHuman('1')">Ta</div>
<div class="enter_btn" @click="enterLive('0')"></div>
<div class="enter_btn" @click="enterLive('1')"></div>
</div>
</div>
</div>
@ -85,7 +85,7 @@
</template>
<script setup lang="ts">
import { ref } from "vue";
import { onMounted, ref } from "vue";
import womenImg from "@/assets/live/girl.png";
import manImg from "@/assets/live/man.png";
import dyImg from "@/assets/live/dy.png";
@ -143,6 +143,18 @@ const enterLive = async type => {
query: { gender: type }
});
};
onMounted(() => {
window.electronAPI.updateConfig(
"live_config",
"livetlking_enable_status",
"0"
);
window.electronAPI.updateConfig(
"live_config",
"gptsovits_enable_status",
"0"
);
});
</script>
<style lang="scss" scoped>

@ -0,0 +1,480 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watchEffect } from "vue";
import { InfoFilled } from "@element-plus/icons-vue";
import { message } from "@/utils/message";
import { interruptTalk } from "@/api/chat";
const liveList = ref([
{
name: "livetalking",
status: "1"
},
{
name: "gptsovits",
status: "1"
},
{
name: "chat",
status: "0"
}
]);
const livetlking_enable_status = ref<any>("0");
const gptsovits_enable_status = ref<any>("0");
const chat_enable_status = ref<any>("0");
const statusMap = [
{ key: "livetalking", statusRef: livetlking_enable_status },
{ key: "gptsovits", statusRef: gptsovits_enable_status },
{ key: "chat", statusRef: chat_enable_status }
];
// liveList
watchEffect(() => {
//
statusMap.forEach(({ key, statusRef }) => {
// liveListname
const targetItem = liveList.value.find(item => item.name === key);
if (targetItem) {
// status
targetItem.status = statusRef.value;
}
});
});
const bulletSetList = ref([
{
title: "进入直播间",
key: "reply_prob_enter_live_room",
value: 100
},
{
title: "关注",
key: "reply_prob_follow",
value: 100
},
{
title: "送礼物",
key: "reply_prob_gift",
value: 100
},
{
title: "点赞",
key: "reply_prob_like",
value: 100
},
{
title: "弹幕回复",
key: "reply_prob_chat",
value: 100
}
]);
const interactiveNode = ref<any>("1");
//
const isShowBullet = ref(false);
// ai
const isAIInteractionEnabled = ref(false);
//
const isManualTakeoverEnabled = ref(false);
const getStatusText = val => {
switch (val) {
case "2":
return "启动成功";
case "1":
return "启动中";
case "3":
return "启动失败";
case "0":
return "未启动";
}
};
const start = async item => {
if (item.name === "livetalking") {
window.electronAPI.startProcess("LiveTalking", "LiveTalking.exe");
} else if (item.name === "gptsovits") {
window.electronAPI.startProcess("gptsovits", "go-gptsovits.bat");
} else if (item.name === "chat") {
const liveRoom = await window.electronAPI.getConfig(
"live_config",
"live_id"
);
if (liveRoom) {
window.electronAPI.startProcess("chat", "chat.exe");
} else {
message("请填写房间号", { type: "error" });
}
}
};
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"
);
chat_enable_status.value = await window.electronAPI.getConfig(
"live_config",
"chat_enable_status"
);
};
const resetStart = async item => {
if (item.name === "chat") {
window.electronAPI.stopChatProcesses();
setTimeout(() => {
start(item);
}, 500);
} else {
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();
setTimeout(async () => {
init();
}, 2000);
}
};
const changeNumber = (e, item) => {
window.electronAPI.updateConfig("config", item.key, e);
};
const changeType = async e => {
const livetalking_sessionid = await window.electronAPI.getConfig(
"live_config",
"livetalking_sessionid"
);
const res = await interruptTalk({
current_control_mode: e ? 0 : 1,
sessionid: Number(livetalking_sessionid)
});
if (res.code === 200) {
message("设置成功", { type: "success" });
} else {
message(res.message || "设置失败", { type: "error" });
}
};
const init = () => {
window.electronAPI.startProcess("LiveTalking", "LiveTalking.exe");
window.electronAPI.startProcess("gptsovits", "go-gptsovits.bat");
// 3
const checkInterval = setInterval(() => {
getLiveTalkingResult();
if (
livetlking_enable_status.value === "2" &&
gptsovits_enable_status.value === "2" &&
chat_enable_status.value === "2"
) {
clearInterval(checkInterval); //
}
}, 500);
};
onMounted(() => {
init();
bulletSetList.value.forEach(async item => {
item.value = Number(await window.electronAPI.getConfig("config", item.key));
});
});
</script>
<template>
<div class="live-monitor">
<div class="monitor-top">
<div class="card-title">
<span>直播服务监测</span>
</div>
<div class="live-list">
<div class="live-item" v-for="(item, index) in liveList" :key="index">
<div
class="live-item-left"
:class="{
'status-success': item.status === '2',
'status-pending': item.status === '0',
'status-loading': item.status === '1',
'status-error': item.status === '3'
}"
>
<span>{{ item.name }}</span>
<span class="point" />
<span class="status">{{ getStatusText(item.status) }}</span>
<span />
</div>
<!-- 根据不同状态显示不同按钮文本 -->
<div class="btn" @click="start(item)" v-if="item.status === '0'">
启动
</div>
<div class="loading btn" v-else-if="item.status === '1'">启动中</div>
<div
class="btn"
@click="resetStart(item)"
v-else-if="item.status === '3' || item.status === '2'"
>
重启
</div>
</div>
</div>
</div>
<div class="monitor-mid">
<div class="card-title">
<div class="title">弹幕与互动</div>
<div class="card-title-right">
<span>显示弹幕</span>
<el-switch v-model="isShowBullet" />
</div>
</div>
<div class="monitor-mid-content">
<div class="bullet-content">
<span>启动后弹幕会打印在屏幕上 未启用默认为空</span>
</div>
<div class="interaction-item">
<div class="set-title">互动设置</div>
<div class="set-content">
<el-icon><InfoFilled /></el-icon>
<span>启动AI互动</span>
<el-switch v-model="isAIInteractionEnabled" />
</div>
</div>
<div class="mid-content-title">回复方式</div>
<el-radio-group v-model="interactiveNode">
<el-radio label="1" size="large">话术类型</el-radio>
<el-radio label="2" size="large">话术段落</el-radio>
</el-radio-group>
<div class="bullet-set">
<div
class="bullet-set-item"
v-for="(item, index) in bulletSetList"
:key="index"
>
<span>{{ item.title }}</span>
<el-input-number
@change="e => changeNumber(e, item)"
v-model="item.value"
:min="0"
:max="100"
/>
</div>
</div>
</div>
</div>
<div class="monitor-bottom">
<div class="card-title">
<div class="title">人工接管</div>
</div>
<div class="monitor-bottom-main">
<span>启用人工接管</span>
<el-switch @change="changeType" v-model="isManualTakeoverEnabled" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.live-monitor {
width: 528px;
margin-left: 16px;
.monitor-top {
background: #ffffff;
height: 236px;
box-shadow: 0px 4px 8px 0px rgba(46, 128, 250, 0.1);
border-radius: 8px 8px 8px 8px;
.card-title {
border-bottom: 1px solid rgba(46, 128, 250, 0.1);
line-height: 54px;
height: 54px;
padding-left: 16px;
font-weight: bold;
font-size: 16px;
color: #1d2129;
}
.live-list {
display: flex;
flex-direction: column;
padding: 16px 16px 0 16px;
.live-item {
height: 40px;
background: rgba(46, 128, 250, 0.1);
border-radius: 2px 2px 2px 2px;
display: flex;
align-items: center;
padding: 0 16px;
justify-content: space-between;
margin-bottom: 16px;
.live-item-left {
font-weight: 500;
font-size: 14px;
color: #1d2129;
display: flex;
align-items: center;
.point {
width: 6px;
height: 6px;
// background: #c9cdd4;
margin-left: 12px;
margin-right: 4px;
border-radius: 50%;
display: block;
}
}
.btn {
width: 50px;
height: 28px;
background: #ffffff;
border-radius: 4px 4px 4px 4px;
border: 1px solid #2e80fa;
font-weight: 500;
font-size: 14px;
color: #2e80fa;
text-align: center;
line-height: 28px;
cursor: pointer;
}
.loading {
opacity: 0.6;
}
.status-success {
.point {
background: rgba(0, 180, 42, 1);
}
.status {
color: rgba(0, 180, 42, 1);
}
}
.status-pending {
.point {
background: #c9cdd4;
}
.status {
color: #c9cdd4;
}
}
.status-loading {
.point {
background: #2e80fa;
}
.status {
color: #2e80fa;
}
}
.status-error {
.point {
background: rgba(234, 42, 42, 1);
}
.status {
color: rgba(234, 42, 42, 1);
}
}
}
}
}
.monitor-mid {
margin-top: 16px;
background: #ffffff;
box-shadow: 0px 4px 8px 0px rgba(46, 128, 250, 0.1);
border-radius: 8px 8px 8px 8px;
.card-title {
border-bottom: 1px solid rgba(46, 128, 250, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
height: 54px;
padding: 0 16px;
font-weight: bold;
font-size: 16px;
color: #1d2129;
.card-title-right {
font-weight: 500;
font-size: 14px;
color: #1d2129;
span {
margin-right: 12px;
}
}
}
.monitor-mid-content {
padding: 16px;
.bullet-content {
width: 496px;
height: 200px;
background: #000000;
border-radius: 4px 4px 4px 4px;
padding: 12px;
span {
font-family: Alibaba PuHuiTi 2, Alibaba PuHuiTi 20;
font-weight: normal;
font-size: 14px;
color: #ffffff;
}
}
.interaction-item {
display: flex;
margin-top: 16px;
justify-content: space-between;
align-items: center;
.set-title {
font-weight: bold;
font-size: 14px;
color: #1d2129;
}
.set-content {
display: flex;
align-items: center;
font-weight: 500;
font-size: 14px;
color: #1d2129;
span {
margin: 0 4px;
}
}
}
.mid-content-title {
margin-top: 12px;
font-weight: 500;
font-size: 14px;
color: #1d2129;
}
.bullet-set {
display: flex;
flex-wrap: wrap;
.bullet-set-item {
display: flex;
flex-direction: column;
width: 150px;
font-weight: 500;
font-size: 14px;
color: #1e1e1e;
margin: 0 12px 12px 0;
span {
margin-bottom: 6px;
}
}
}
}
}
.monitor-bottom {
margin-top: 16px;
background: #ffffff;
box-shadow: 0px 4px 8px 0px rgba(46, 128, 250, 0.1);
border-radius: 8px 8px 8px 8px;
.card-title {
border-bottom: 1px solid rgba(46, 128, 250, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
height: 54px;
padding: 0 16px;
font-weight: bold;
font-size: 16px;
color: #1d2129;
}
.monitor-bottom-main {
padding: 16px;
span {
margin-right: 10px;
}
}
}
}
</style>

@ -0,0 +1,576 @@
<template>
<div class="LiveTalk">
<div class="main-content">
<div class="card-title">
<div class="title">直播话术</div>
<!-- <div class="card-title-right">
<randomImg />
<span>随机播放</span>
</div> -->
</div>
<div v-if="scriptList.length > 0" class="script-content">
<div class="script-type">
<span
@click="selectType = item.type"
:class="[selectType === item.type ? 'actived' : '']"
class="type-item"
v-for="(item, index) in typeList"
:key="index"
>{{ item.name }}</span
>
</div>
<div v-if="selectType === '1'" class="script-list">
<div
class="script-item"
:class="[item.actived ? 'actived' : '']"
v-for="(item, index) in liveScriptList"
:key="index"
>
<div class="script-item-top">
<div class="top-left">
<span class="title">{{ item.type }}</span>
<disablePlayImg v-if="item.actived" />
<playImg
style="cursor: pointer"
@click="playScript(item)"
v-else
/>
</div>
<editImg
v-if="!item.actived"
style="cursor: pointer"
@click="editScript(item)"
/>
</div>
<div class="script-item-text">
{{ item.message }}
</div>
</div>
</div>
<div v-if="selectType === '2'" class="script-list">
<div
class="script-item"
v-for="(item, index) in pendingScriptList"
:key="index"
>
<div class="script-item-top">
<div class="top-left">
<span class="title">{{ item.type }}</span>
</div>
</div>
<div class="script-item-text">
{{ item.message }}
</div>
</div>
</div>
</div>
<div v-else class="script-empty">
<img :src="talkLoadingImg" alt="" />
<span>AI正在努力生成直播话术</span>
</div>
</div>
<div class="live-setting">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="直播平台" name="first">
<div class="live-platform">
<div class="title">直播平台</div>
<el-select
v-model="livePlatform"
placeholder="Select"
style="margin-bottom: 16px; width: 100%"
>
<el-option
v-for="item in liveList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<div class="title">房间号</div>
<el-input type="number" @blur="changeLiveRoom" v-model="liveRoom" />
<div class="btns" v-if="!isConnected">
<!-- <button class="main" @click="connectLiveRoom">
{{ buttonText }}
</button> -->
<el-button
:loading="loading"
:disabled="loading || !liveRoom"
type="primary"
@click="connectBullet"
>
{{ buttonText }}</el-button
>
<!-- <button class="connect">连接测试</button> -->
<!-- <span class="tips">开始前建议先进行连通性测试</span> -->
</div>
</div>
</el-tab-pane>
<el-tab-pane label="直播商品 " name="second">
<div class="live-goods">
<div class="title">按商品SKU展示</div>
<div
class="goods-item"
v-for="(item, index) in goodsList"
:key="index"
>
<span>{{ item.name }}</span>
<span>{{ item.connect }}</span>
<span>{{ item.price }}</span>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<el-dialog
width="800"
title="AI直播话术"
append-to-body
v-model="dialogVisible"
:before-close="closed"
custom-class="ai-live-script"
>
<div class="ai-live-script">
<div class="title">商品名称</div>
<div class="desc">衡水老白干纯粮酿造 42</div>
<el-form
ref="ruleFormRef"
:model="formData"
:rules="rules"
label-position="top"
>
<el-form-item label="话术类型" prop="type">
<el-select
v-model="formData.type"
placeholder="Select"
style="margin-bottom: 16px; width: 100%"
>
<el-option
v-for="item in options"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="直播话术" prop="message">
<el-input
:rows="8"
type="textarea"
maxlength="500"
show-word-limit
placeholder="请输入"
v-model="formData.message"
/>
</el-form-item>
<div class="footer-btn">
<el-button type="primary" @click="save"></el-button>
<el-button class="reset" @click="ruleFormRef.resetFields()"
>重置</el-button
>
</div>
</el-form>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import randomImg from "@/assets/svg/live/random.svg";
import talkLoadingImg from "@/assets/live/talk_loading.gif";
import playImg from "@/assets/svg/live/play.svg";
import disablePlayImg from "@/assets/svg/live/disable_play.svg";
import editImg from "@/assets/svg/live/edit.svg";
import { ref, reactive, onMounted, onUnmounted } from "vue";
import { message } from "@/utils/message";
const typeList = ref([
{
name: "直播话术",
type: "1"
},
{
name: "待播话术",
type: "2"
}
]);
const selectId = ref("");
// const selectInfo = ref(null);
const liveScriptList = ref([]);
const pendingScriptList = ref([]);
const ruleFormRef = ref();
const formData = reactive({
type: "",
message: ""
});
const loading = ref(false);
//
const isConnected = ref(false);
const options = ref([
"开场欢迎",
"酒的背景与历史",
"口感细致描绘",
"饮用场景渲染",
"工艺与匠心"
]);
const liveList = ref([
{
label: "抖音",
value: "1"
}
]);
const goodsList = ref([
{
name: "衡水老白干 42度",
connect: "1号链接",
price: "99.99元"
},
{
name: "衡水老白干 42度",
connect: "1号链接",
price: "99.99元"
},
{
name: "衡水老白干 42度",
connect: "1号链接",
price: "99.99元"
}
]);
const rules = {
message: [{ required: true, message: "请输入", trigger: "change" }]
};
const dialogVisible = ref(false);
const livePlatform = ref("1");
const liveRoom = ref("");
const scriptList = ref([]);
const selectType = ref("1");
const activeName = ref("first");
const buttonText = ref("开始接管直播间");
const chat_enable_status = ref<any>("0");
let dataInfoInterval = null;
//
const editScript = item => {
dialogVisible.value = true;
selectId.value = item.id;
formData.type = item.type;
formData.message = item.message;
// selectInfo = JSON.parse(JSON.stringify(item));
};
const changeLiveRoom = () => {
window.electronAPI.updateConfig("live_config", "live_id", liveRoom.value);
};
const handleClick = () => {};
const closed = () => {
dialogVisible.value = false;
};
//
const refreshData = async () => {
scriptList.value = await window.electronAPI.getAllSystemMessages();
liveScriptList.value = scriptList.value.filter(
item => item.batch_number === 0
);
// status 1
const status1Items = liveScriptList.value.filter(item => item.status === 1);
// id
let maxIdItem = null;
if (status1Items.length > 0) {
maxIdItem = status1Items.reduce((prev, current) => {
return current.id > prev.id ? current : prev;
}, status1Items[0]);
// actived
maxIdItem.actived = true;
}
pendingScriptList.value = scriptList.value.filter(
item => item.batch_number === 1
);
};
const getData = () => {
//
dataInfoInterval = setInterval(async () => {
// AI
refreshData();
}, 1000);
};
onMounted(async () => {
refreshData();
getData();
window.electronAPI.updateConfig("live_config", "live_id", "");
});
onUnmounted(() => {
if (dataInfoInterval) {
clearInterval(dataInfoInterval);
dataInfoInterval = null;
}
});
const getIsConnectBullet = async () => {
chat_enable_status.value = await window.electronAPI.getConfig(
"live_config",
"chat_enable_status"
);
};
const connectBullet = async () => {
buttonText.value = "连接中...";
loading.value = true;
await window.electronAPI.updateConfig(
"live_config",
"live_id",
liveRoom.value
);
window.electronAPI.startProcess("chat", "chat.exe");
// 3
const checkInterval = setInterval(() => {
getIsConnectBullet();
if (chat_enable_status.value === "2") {
loading.value = false;
message("连接弹幕成功", { type: "success" });
isConnected.value = true;
clearInterval(checkInterval);
}
}, 500);
};
const save = () => {
//
ruleFormRef.value.validate(async valid => {
if (valid) {
const res = await window.electronAPI.updateMessageById(selectId.value, {
type: formData.type,
message: formData.message
});
if (res.code === 0) {
dialogVisible.value = false;
selectId.value = "";
} else {
message("保存失败", { type: "warning" });
}
} else {
message("请填写完整信息", { type: "warning" });
}
});
};
const playScript = async item => {
const res = await window.electronAPI.updateMessageById(item.id, {
status: 2
});
};
</script>
<style lang="scss" scoped>
.LiveTalk {
flex: 1;
.main-content {
background: #ffffff;
box-shadow: 0px 4px 8px 0px rgba(46, 128, 250, 0.1);
border-radius: 8px 8px 8px 8px;
.card-title {
border-bottom: 1px solid rgba(46, 128, 250, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
height: 54px;
padding: 0 16px;
font-weight: bold;
font-size: 16px;
color: #1d2129;
.card-title-right {
font-weight: 500;
font-size: 14px;
color: #1d2129;
display: flex;
cursor: pointer;
span {
margin-left: 10px;
font-weight: 500;
font-size: 14px;
color: #2e80fa;
}
}
}
}
.script-content {
padding: 16px;
.script-type {
display: flex;
.type-item {
width: 88px;
height: 32px;
background: #f2f3f5;
border-radius: 100px 100px 100px 100px;
font-weight: 500;
font-size: 14px;
color: #4e5969;
cursor: pointer;
text-align: center;
line-height: 32px;
margin-right: 16px;
}
.actived {
background: #2e80fa;
color: #ffffff;
}
}
.script-list {
display: flex;
flex-direction: column;
height: calc(100vh - 500px);
overflow-y: auto;
.script-item {
margin-top: 12px;
background: #f3f4fc;
border-radius: 4px 4px 4px 4px;
padding: 12px;
.script-item-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.top-left {
display: flex;
.title {
width: 96px;
height: 24px;
border-radius: 2px 2px 2px 2px;
border: 1px solid #86909c;
line-height: 24px;
text-align: center;
margin-right: 12px;
font-weight: 500;
font-size: 12px;
color: #1d2129;
}
}
}
.script-item-text {
font-weight: 500;
font-size: 14px;
color: #000000;
}
}
.actived {
.script-item-top {
.title {
background: #2e80fa;
font-weight: 500;
font-size: 12px;
color: #ffffff !important;
}
}
.script-item-text {
color: #2e80fa !important;
}
}
}
}
.script-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100vh - 500px);
img {
width: 190px;
height: 50px;
}
span {
font-weight: 500;
font-size: 14px;
color: #86909c;
margin-top: 12px;
}
}
.live-setting {
height: 278px;
background: #ffffff;
box-shadow: 0px 4px 8px 0px rgba(46, 128, 250, 0.1);
border-radius: 8px 8px 8px 8px;
margin-top: 16px;
padding: 16px;
.live-platform {
padding-top: 10px;
.title {
font-weight: 500;
font-size: 14px;
color: #1e1e1e;
margin-bottom: 6px;
}
.btns {
margin-top: 16px;
display: flex;
flex-direction: row-reverse;
align-items: center;
.main {
font-weight: bold;
font-size: 14px;
color: #ffffff;
width: 122px;
height: 32px;
background: #2e80fa;
border-radius: 4px 4px 4px 4px;
border: 1px solid #2e80fa;
}
.connect {
margin-right: 16px;
width: 94px;
height: 32px;
background: #ffffff;
border-radius: 4px 4px 4px 4px;
border: 1px solid #e5e6eb;
font-weight: 500;
font-size: 14px;
color: #1d2129;
}
.tips {
font-weight: 500;
font-size: 12px;
margin-right: 16px;
color: #4e5969;
}
}
}
.live-goods {
.title {
font-weight: 500;
font-size: 14px;
color: #1d2129;
}
.goods-item {
height: 32px;
background: rgba(46, 128, 250, 0.1);
border-radius: 2px 2px 2px 2px;
margin-top: 12px;
padding-left: 12px;
line-height: 32px;
font-weight: 500;
font-size: 14px;
color: #2e80fa;
span {
margin-right: 36px;
}
}
}
}
}
</style>
<style lang="scss">
.ai-live-script {
.title {
font-weight: bold;
font-size: 14px;
color: #000000;
margin-bottom: 8px;
}
.desc {
font-weight: 500;
font-size: 14px;
color: #1d2129;
margin-bottom: 12px;
}
.footer-btn {
display: flex;
flex-direction: row-reverse;
margin-top: 12px;
width: 100%;
.reset {
margin-right: 12px;
}
}
}
</style>

@ -0,0 +1,269 @@
<template>
<div class="LiveVideo">
<div class="top">
<div class="top-left">
<div class="title">主视觉预览</div>
<div v-if="!isLive" class="pending">
<span class="point" />
<span>状态: 未开始</span>
</div>
</div>
<div class="top-right">
<!-- <button class="start">开始直播</button>
<button class="end">结束</button> -->
</div>
</div>
<div class="LiveVideo-main">
<div class="video-placeholder">
<div v-show="!isLive" class="video-loading">
<img :src="liveLoading" alt="" />
<span> AI主播生成中请稍后片刻...</span>
</div>
<video
v-show="isLive"
ref="videoRef"
autoplay
playsinline
webkit-playsinline
controlslist="nodownload nofullscreen noremoteplayback"
class="video-player"
/>
<img @click="enablePiP" class="btn" :src="pictureIcon" alt="" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
import pictureIcon from "@/assets/live/picture.png";
import liveLoading from "@/assets/live/loading.gif";
//
const isReady = ref(false);
//
const isLive = ref(false);
const videoRef = ref(null);
let pc = null;
const livetlking_enable_status = ref<any>("0");
const gptsovits_enable_status = ref<any>("0");
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"
);
};
onMounted(() => {
const gender = router.currentRoute.value.query.gender;
if (gender === "1") {
window.electronAPI.updateConfig("live_config", "human_genders", "man");
window.electronAPI.updateConfig(
"livetalking_config",
"avatar_id",
"model_man"
);
window.electronAPI.updateConfig(
"livetalking_config",
"ref_file",
"input/boy.wav"
);
} else {
window.electronAPI.updateConfig("live_config", "human_genders", "woman");
window.electronAPI.updateConfig(
"livetalking_config",
"avatar_id",
"model_woman"
);
window.electronAPI.updateConfig(
"livetalking_config",
"ref_file",
"input/girl.wav"
);
}
// 3
const checkInterval = setInterval(() => {
getLiveTalkingResult();
if (
livetlking_enable_status.value === "2" &&
gptsovits_enable_status.value === "2"
) {
isReady.value = true;
startPlay();
clearInterval(checkInterval); //
}
}, 500);
});
// WHEP
const startPlay = async () => {
if (isLive.value) return;
try {
// 1. RTCPeerConnection
pc = new RTCPeerConnection({});
// 2.
pc.ontrack = event => {
if (event.streams && event.streams[0]) {
// video
videoRef.value.srcObject = event.streams[0];
isLive.value = true;
}
};
pc.addTransceiver("video", { direction: "recvonly" });
pc.addTransceiver("audio", { direction: "recvonly" });
return pc
.createOffer()
.then(offer => {
return pc.setLocalDescription(offer);
})
.then(() => {
const offer = pc.localDescription;
return fetch("http://127.0.0.1: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);
}
};
function enablePiP() {
if (videoRef.value && document.pictureInPictureEnabled) {
videoRef.value.requestPictureInPicture();
}
}
</script>
<style lang="scss" scoped>
.LiveVideo {
width: 492px;
background: #ffffff;
box-shadow: 0px 4px 8px 0px rgba(46, 128, 250, 0.1);
border-radius: 8px 8px 8px 8px;
display: flex;
flex-direction: column;
margin-left: 16px;
.top {
border-bottom: 1px solid rgba(46, 128, 250, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
height: 54px;
padding: 0 16px;
.top-left {
display: flex;
.title {
font-weight: bold;
font-size: 16px;
color: #1d2129;
margin-right: 8px;
}
.pending {
border-radius: 12px 12px 12px 12px;
border: 1px solid #c9cdd4;
font-weight: 400;
font-size: 12px;
color: #c9cdd4;
display: flex;
align-items: center;
padding: 4px 12px 4px 0;
.point {
width: 6px;
height: 6px;
background: #c9cdd4;
margin-left: 12px;
margin-right: 4px;
border-radius: 50%;
display: block;
}
}
}
.top-right {
display: flex;
.start {
background: #2e80fa;
border-radius: 4px 4px 4px 4px;
font-size: 14px;
color: #ffffff;
margin-right: 12px;
padding: 5px 8px;
}
.end {
background: rgba(234, 42, 42, 1);
border-radius: 4px 4px 4px 4px;
font-size: 14px;
color: #ffffff;
padding: 5px 8px;
}
}
}
.LiveVideo-main {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
}
.video-placeholder {
position: relative;
width: 460px;
height: calc(100vh - 200px);
.video-player {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover; /* 保持视频比例并覆盖整个屏幕 */
}
.btn {
position: absolute;
bottom: 24px;
right: 24px;
cursor: pointer;
z-index: 999999;
width: 40px;
height: 40px;
}
.video-loading {
// background-image: url(../../assets/live/loading.gif);
// background-size: 100% 100%;
background: #010102;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
img {
width: 153px;
height: 133px;
margin-bottom: 16px;
}
span {
font-weight: 500;
font-size: 16px;
color: #ffffff;
}
}
}
}
</style>

@ -2,277 +2,34 @@
<div class="Live">
<div class="top">
<span class="title">{{ title }}</span>
<span class="time">{{ `直播倒计时: ${time}` }}</span>
<!-- <el-button type="primary" class="back" @click="goback"
>保存设置</el-button
> -->
<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>{{ `livetlking_enable_status ${livetlking_enable_status}` }}</div>
<div>{{ `gptsovits_enable_status ${gptsovits_enable_status}` }}</div>
<div>{{ `chat_enable_status ${chat_enable_status}` }}</div>
<!-- <div>{{ test }}</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
:disabled="isStartConnectBullet || isConnected"
v-model="liveRoom"
style="margin-bottom: 16px"
/>
<el-button
v-if="!isConnected"
:loading="loading"
:disabled="isStartConnectBullet || !liveRoom"
type="primary"
@click="connectBullet"
>
{{ buttonText }}</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
webkit-playsinline
controlslist="nodownload nofullscreen noremoteplayback"
class="video-player"
/>
<div v-show="isPlaying" @click="goback" class="close-btn">
<el-icon><SwitchButton /></el-icon>
<span>关闭直播</span>
</div>
<img @click="enablePiP" class="btn" :src="pictureIcon" alt="" />
</div>
<LiveMonitor ref="LiveMonitorRef" />
<LiveVideo ref="LiveVideoRef" />
<LiveTalk ref="LiveTalkRef" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import tipsImg from "@/assets/svg/live/tips.svg";
import liveLoading from "@/assets/live/loading.gif";
import { SwitchButton } from "@element-plus/icons-vue";
import { message } from "@/utils/message";
import LiveTalk from "./components/LiveTalk.vue";
import LiveMonitor from "./components/LiveMonitor.vue";
import LiveVideo from "./components/LiveVideo.vue";
const title = ref("慧文的直播间");
const time = ref("00:00:00");
const loading = ref(false);
// 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 buttonText = 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 test = ref<any>("");
//
const isStartConnectBullet = ref(false);
//
// 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;
};
// WHEP
const startPlay = async () => {
if (isPlaying.value) return;
try {
statusText.value = "连接中...";
// 1. RTCPeerConnection
pc = new RTCPeerConnection({});
// 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(() => {
// test.value = "333333";
// wait for ICE gathering to complete
// return new Promise<void>(resolve => {
// test.value = "44444";
// 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://127.0.0.1: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) {
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 () => {
chat_enable_status.value = await window.electronAPI.getConfig(
"live_config",
"chat_enable_status"
);
};
const connectBullet = async () => {
console.log("开始弹幕");
buttonText.value = "连接中...";
loading.value = true;
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("连接弹幕成功");
loading.value = false;
message("连接弹幕成功", { type: "success" });
isConnected.value = true;
clearInterval(checkInterval);
clearInterval(checkInterval); //
}
}, 500);
};
const closedLive = () => {
window.electronAPI.updateConfig(
@ -287,52 +44,13 @@ const closedLive = () => {
);
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); //
}
}, 500);
});
onMounted(() => {});
//
onUnmounted(() => {
closedLive();
});
function enablePiP() {
if (videoRef.value && document.pictureInPictureEnabled) {
videoRef.value.requestPictureInPicture();
}
}
</script>
<style lang="scss" scoped>
.Live {
@ -369,122 +87,8 @@ function enablePiP() {
background: #f4f8ff;
flex: 1;
display: flex;
// justify-content: center;
align-items: center;
// ...existing code...
.video-player::-webkit-media-controls-fullscreen-button {
display: none !important;
}
.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;
font-size: 14px;
}
.bullet-connect {
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: 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;
left: 0;
width: 100%;
height: 100%;
object-fit: cover; /* 保持视频比例并覆盖整个屏幕 */
}
.btn {
position: absolute;
bottom: 24px;
right: 24px;
cursor: pointer;
z-index: 999999;
width: 40px;
height: 40px;
}
.video-loading {
// background-image: url(../../assets/live/loading.gif);
// background-size: 100% 100%;
background: #010102;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
img {
width: 153px;
height: 133px;
margin-bottom: 16px;
}
span {
font-weight: 500;
font-size: 16px;
color: #ffffff;
}
}
}
padding: 16px 24px;
flex-direction: row-reverse;
}
}
</style>

@ -0,0 +1,514 @@
<template>
<div class="Live">
<div class="top">
<span class="title">{{ title }}</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>{{ `livetlking_enable_status ${livetlking_enable_status}` }}</div>
<div>{{ `gptsovits_enable_status ${gptsovits_enable_status}` }}</div>
<div>{{ `chat_enable_status ${chat_enable_status}` }}</div> -->
<!-- <div>{{ test }}</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
type="number"
:disabled="isStartConnectBullet || isConnected"
v-model="liveRoom"
style="margin-bottom: 16px"
/>
<el-button
v-if="!isConnected"
:loading="loading"
:disabled="isStartConnectBullet || !liveRoom"
type="primary"
@click="connectBullet"
>
{{ buttonText }}</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
webkit-playsinline
controlslist="nodownload nofullscreen noremoteplayback"
class="video-player"
/>
<div v-show="isPlaying" @click="goback" class="close-btn">
<el-icon><SwitchButton /></el-icon>
<span>关闭直播</span>
</div>
<img @click="enablePiP" class="btn" :src="pictureIcon" alt="" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import tipsImg from "@/assets/svg/live/tips.svg";
import liveLoading from "@/assets/live/loading.gif";
import { SwitchButton } from "@element-plus/icons-vue";
import { message } from "@/utils/message";
const title = ref("慧文的直播间");
const time = ref("00:00:00");
const loading = ref(false);
// 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 buttonText = 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 test = ref<any>("");
//
const isStartConnectBullet = ref(false);
//
// 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;
};
// WHEP
const startPlay = async () => {
if (isPlaying.value) return;
try {
statusText.value = "连接中...";
// 1. RTCPeerConnection
pc = new RTCPeerConnection({});
// 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(() => {
// test.value = "333333";
// wait for ICE gathering to complete
// return new Promise<void>(resolve => {
// test.value = "44444";
// 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://127.0.0.1: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) {
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 () => {
chat_enable_status.value = await window.electronAPI.getConfig(
"live_config",
"chat_enable_status"
);
};
const connectBullet = async () => {
console.log("开始弹幕");
buttonText.value = "连接中...";
loading.value = true;
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("连接弹幕成功");
loading.value = false;
message("连接弹幕成功", { type: "success" });
isConnected.value = true;
clearInterval(checkInterval);
clearInterval(checkInterval); //
}
}, 500);
};
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;
if (gender === "1") {
window.electronAPI.updateConfig("live_config", "human_genders", "man");
window.electronAPI.updateConfig(
"livetalking_config",
"avatar_id",
"model_man"
);
window.electronAPI.updateConfig(
"livetalking_config",
"ref_file",
"input/boy.wav"
);
} else {
window.electronAPI.updateConfig("live_config", "human_genders", "woman");
window.electronAPI.updateConfig(
"livetalking_config",
"avatar_id",
"model_woman"
);
window.electronAPI.updateConfig(
"livetalking_config",
"ref_file",
"input/girl.wav"
);
}
// 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); //
}
}, 500);
});
//
onUnmounted(() => {
closedLive();
});
function enablePiP() {
if (videoRef.value && document.pictureInPictureEnabled) {
videoRef.value.requestPictureInPicture();
}
}
</script>
<style lang="scss" scoped>
.Live {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
.top {
display: flex;
align-items: center;
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;
color: #1d2129;
margin-right: 12px;
}
.time {
font-weight: 500;
font-size: 14px;
color: #86909c;
}
}
.main {
background: #f4f8ff;
flex: 1;
display: flex;
// justify-content: center;
align-items: center;
// ...existing code...
.video-player::-webkit-media-controls-fullscreen-button {
display: none !important;
}
.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;
font-size: 14px;
}
.bullet-connect {
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: 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;
left: 0;
width: 100%;
height: 100%;
object-fit: cover; /* 保持视频比例并覆盖整个屏幕 */
}
.btn {
position: absolute;
bottom: 24px;
right: 24px;
cursor: pointer;
z-index: 999999;
width: 40px;
height: 40px;
}
.video-loading {
// background-image: url(../../assets/live/loading.gif);
// background-size: 100% 100%;
background: #010102;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
img {
width: 153px;
height: 133px;
margin-bottom: 16px;
}
span {
font-weight: 500;
font-size: 16px;
color: #ffffff;
}
}
}
}
}
</style>

@ -1,12 +1,12 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import tipsImg from "@/assets/svg/live/tips.svg";
const modelType = ref("");
const modelType = ref("1");
const tableData = reactive([
{
name: "通义千问",
name: "苏胜天本地大模型",
apiKey: "xxxxxxxxxxxxxxxxx",
authorizationExpiry: "2022-03-28 15:14:07",
authorizationExpiry: "",
serviceStatus: "正常",
desc: "提供实时数字及npl大模型视觉模型服务能力"
}
@ -26,7 +26,7 @@ const tableData = reactive([
<div class="main-title">大模型服务授权</div>
<div class="model-content">
<div class="select-model">选择大模型接入模式</div>
<el-radio-group v-model="modelType">
<el-radio-group disabled v-model="modelType">
<el-radio label="1">私有算力模式(本地)</el-radio>
<el-radio label="2">弹性算力模式(云端)</el-radio>
</el-radio-group>
@ -43,14 +43,14 @@ const tableData = reactive([
<el-table-column prop="authorizationExpiry" label="授权有效期" />
<el-table-column prop="serviceStatus" label="服务状态" />
<el-table-column prop="desc" label="说明" />
<el-table-column prop="address" label="操作">
<!-- <el-table-column prop="address" label="操作">
<template #default="scope">
<div style="display: flex; align-items: center">
<span class="text-btn">编辑</span>
<span class="text-btn">测试连接</span>
</div>
</template>
</el-table-column>
</el-table-column> -->
</el-table>
</div>
</div>

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { FormInstance } from "element-plus";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { getLogin } from "@/api/user";
import { getLogin, getUserInfo } from "@/api/user";
import { reactive, ref, nextTick, onMounted } from "vue";
import videoImg from "@/assets/login/video.png";
import Lock from "@iconify-icons/icon-park-outline/lock";
@ -12,6 +12,7 @@ import { useRouter } from "vue-router";
import { removeToken, setToken } from "@/utils/auth";
import { storageLocal, storageSession } from "@pureadmin/utils";
import { message } from "@/utils/message";
import { useUserStoreHook } from "@/store/modules/user";
defineOptions({
name: "Login"
});
@ -20,10 +21,9 @@ 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#"
username: "",
password: ""
});
function showPass() {
if (passwordType.value === "password") {
@ -35,6 +35,19 @@ function showPass() {
refInput.value.focus();
});
}
const queryUserInfo = async () => {
const res = await getUserInfo();
if (res.code === 200) {
const userInfo = res.data;
if (userInfo) {
// Vuex
useUserStoreHook().SET_USERINFO(userInfo);
//
}
} else {
message("获取用户信息失败", { type: "error" });
}
};
const onLogin = async (formEl: FormInstance | undefined) => {
console.log("login", ruleForm);
if (!formEl) return;
@ -45,12 +58,18 @@ const onLogin = async (formEl: FormInstance | undefined) => {
if (res.code === 200) {
loading.value = false;
setToken(res.data);
queryUserInfo();
router.push("/digitalHuman");
const result = await window.electronAPI.updateConfig(
"config",
"backend_token",
res.data
);
await window.electronAPI.updateConfig(
"config",
"userName",
ruleForm.username
);
console.log("result: ", result);
if (result.changes > 0) {
message("登录成功", {
@ -78,6 +97,8 @@ onMounted(async () => {
removeToken();
storageLocal().clear();
storageSession().clear();
const username = await window.electronAPI.getConfig("config", "userName");
ruleForm.username = username || "";
// test.value = await window.electronAPI.getConfig("reply_prob_follow");
// console.log("sss: ", test.value);
});
@ -161,6 +182,7 @@ onMounted(async () => {
<el-button
class="w-full login-btn"
size="large"
:loading="loading"
type="primary"
color="#2E80FA"
@click="onLogin(ruleFormRef)"
@ -179,6 +201,8 @@ onMounted(async () => {
width: 100%;
height: 100%;
overflow: auto;
min-width: 1900px;
min-height: 800px;
display: flex;
.left {
height: 100vh; //
@ -222,6 +246,8 @@ onMounted(async () => {
display: flex;
justify-content: center;
padding-top: 160px;
background-image: url("@/assets/login/main_bg.png");
background-size: 100% 100%;
.login-box {
width: 559px;
height: 515px;

@ -46,7 +46,16 @@ export default ({ command, mode }: ConfigEnv): UserConfigExport => {
proxy: {
// 类型: Record<string, string | ProxyOp 为开发服务器配置自定义代理规则
"/live-digital-avatar-manage/": {
target: "http://192.168.10.25:9909/live-digital-avatar-manage",
target: "http://192.168.10.137:9909/live-digital-avatar-manage",
changeOrigin: true,
// 路径重写函数
rewrite: path => path.replace(/^\/live-digital-avatar-manage/, ""),
secure: false
// eslint-disable-next-line no-shadow
// rewrite: path => path.replace("/ask", "")
},
"/set_live_room_control_mode": {
target: "http://127.0.0.1:8010",
changeOrigin: true,
// 路径重写函数
rewrite: path => path.replace(/^\/live-digital-avatar-manage/, ""),

Loading…
Cancel
Save