基本完成添加岗位&岗位详情&逐句播放&插入播放

This commit is contained in:
lion风
2025-12-25 15:34:00 +08:00
parent 92ec6504f9
commit 9792c88e53
4 changed files with 782 additions and 45 deletions

View File

@@ -40,6 +40,7 @@ export function setupLiveHandlers() {
hasLiveWindow: !!liveState.liveWindow, hasLiveWindow: !!liveState.liveWindow,
isVideoInserted: liveState.isVideoInserted, isVideoInserted: liveState.isVideoInserted,
cameraActive: liveState.cameraActive, cameraActive: liveState.cameraActive,
sessionId: liveState.sessionId
}, },
}; };
}); });
@@ -104,6 +105,9 @@ export function setupLiveHandlers() {
} }
liveState.sessionId = sessionResult.sessionId; liveState.sessionId = sessionResult.sessionId;
// socket
// 5. 设置直播状态 // 5. 设置直播状态
liveState.isLiveOn = true; liveState.isLiveOn = true;
@@ -200,7 +204,6 @@ export function setupLiveHandlers() {
type: "echo", type: "echo",
interrupt: true, interrupt: true,
}; };
await sendMessage(params); await sendMessage(params);
return { success: true }; return { success: true };
} catch (error: any) { } catch (error: any) {

View File

@@ -0,0 +1,38 @@
// src/types/socket.ts
/**
* Socket 配置项接口
*/
export interface SocketOptions {
/** 心跳间隔ms默认 30000 */
heartbeatInterval?: number;
/** 初始重连间隔ms默认 3000 */
reconnectInterval?: number;
/** 最大重连次数,默认 10 */
maxReconnectCount?: number;
/** 消息接收回调 */
onMessage?: (data: string | ArrayBuffer | Blob) => void;
/** 错误回调 */
onError?: (error: Event) => void;
/** 关闭回调 */
onClose?: (event: CloseEvent) => void;
/** 连接成功回调 */
onOpen?: () => void;
}
/**
* 消息记录类型
*/
export interface MessageItem {
time: string;
type: 'send' | 'receive' | 'system';
content: string | object;
}
/**
* 心跳包类型(可根据后端格式调整)
*/
export interface HeartbeatData {
type: 'ping' | 'pong';
timestamp?: number;
}

View File

@@ -0,0 +1,175 @@
// src/utils/socket.ts
import type { SocketOptions, HeartbeatData } from "@renderer/types/socket";
/**
* WebSocket 长连接工具类TS 版)
* @class SocketClient
* @param {string} url - WS/WSS 连接地址
* @param {SocketOptions} options - 配置项
*/
class SocketClient {
private url: string; // 连接地址(私有属性)
private ws: WebSocket | null; // WebSocket 实例
private isConnected: boolean; // 连接状态
private heartbeatTimer: NodeJS.Timeout | null; // 心跳定时器
private reconnectTimer: NodeJS.Timeout | null; // 重连定时器
private reconnectCount: number; // 已重连次数
private config: Required<SocketOptions>; // 完整配置(包含默认值)
constructor(url: string, options: SocketOptions = {}) {
this.url = url;
this.ws = null;
this.isConnected = false;
this.heartbeatTimer = null;
this.reconnectTimer = null;
this.reconnectCount = 0;
// 合并默认配置与用户配置(确保所有配置项有值)
this.config = {
heartbeatInterval: options.heartbeatInterval || 30000,
reconnectInterval: options.reconnectInterval || 3000,
maxReconnectCount: options.maxReconnectCount || 10,
onMessage: options.onMessage || (() => {}),
onError: options.onError || (() => {}),
onClose: options.onClose || (() => {}),
onOpen: options.onOpen || (() => {})
};
// 初始化连接
this.init();
}
/**
* 初始化 WebSocket 连接
*/
private init(): void {
// 关闭已有连接
this.close();
try {
this.ws = new WebSocket(this.url);
// 连接成功
this.ws.onopen = (): void => {
this.isConnected = true;
this.reconnectCount = 0;
this.config.onOpen();
this.startHeartbeat();
};
// 接收消息
this.ws.onmessage = (event: MessageEvent): void => {
this.config.onMessage(event.data);
};
// 错误回调
this.ws.onerror = (error: Event): void => {
this.isConnected = false;
this.config.onError(error);
};
// 关闭回调
this.ws.onclose = (event: CloseEvent): void => {
this.isConnected = false;
this.config.onClose(event);
this.stopHeartbeat();
// 自动重连(未超过最大次数)
if (this.reconnectCount < this.config.maxReconnectCount) {
this.reconnect();
}
};
} catch (error) {
this.config.onError(error as Event);
this.reconnect();
}
}
/**
* 启动心跳保活
*/
private startHeartbeat(): void {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
if (this.isConnected && this.ws) {
// 发送心跳包(强类型)
const heartbeatData: HeartbeatData = {
type: 'ping',
timestamp: Date.now()
};
this.send(heartbeatData);
}
}, this.config.heartbeatInterval);
}
/**
* 停止心跳
*/
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
/**
* 发送消息
* @param {string | object} data - 要发送的消息(字符串/对象)
* @throws {Error} 连接未建立时抛出错误
*/
public send(data: string | object): void {
if (!this.isConnected || !this.ws) {
throw new Error('WebSocket 未连接,无法发送消息');
}
// 类型判断:对象转为 JSON 字符串
const sendData: string = typeof data === 'object'
? JSON.stringify(data)
: data;
this.ws.send(sendData);
}
/**
* 断线重连(阶梯式间隔)
*/
private reconnect(): void {
this.reconnectCount++;
const currentInterval = this.config.reconnectInterval * this.reconnectCount;
this.reconnectTimer = setTimeout(() => {
console.log(`WebSocket 第 ${this.reconnectCount} 次重连...`);
this.init();
}, currentInterval);
}
/**
* 主动关闭连接
* @param {number} code - 关闭码(默认 1000正常关闭
* @param {string} reason - 关闭原因
*/
public close(code: number = 1000, reason: string = '主动关闭连接'): void {
if (this.ws) {
this.ws.close(code, reason);
this.ws = null;
}
this.isConnected = false;
this.stopHeartbeat();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
/**
* 获取当前连接状态
* @returns {boolean} 连接状态
*/
public getConnectedState(): boolean {
return this.isConnected;
}
}
export default SocketClient;

View File

@@ -20,40 +20,50 @@
<a-layout> <a-layout>
<a-layout-sider resizable width="300" :min-width="250" :max-width="500" class="sider"> <a-layout-sider resizable width="300" :min-width="250" :max-width="500" class="sider">
<div class="sider-scroll-content"> <div class="sider-top" style="overflow-y: auto;flex: 2;border-bottom: 1px solid #e8e8e8;max-height: calc(100% * 2 / 5);min-height: calc(100% * 2 / 5);">
<a-typography-title :level="5">岗位列表</a-typography-title>
<a-flex justify="space-between" align="center" style="margin-bottom: 16px">
<a-space align="left" >
<a-typography-title :level="5">
岗位列表
</a-typography-title>
</a-space>
<a-space align="right" >
<a-button type="primary" @click="handleShow" :loading="modelLoading" >
添加岗位
</a-button>
</a-space>
</a-flex>
<a-list :data-source="positions" item-layout="horizontal"> <a-list :data-source="positions" item-layout="horizontal">
<template #renderItem="{ item }"> <template #renderItem="{ item, index }" >
<a-list-item class="position-item" > <a-list-item class="position-item" >
<a-list-item-meta :title="item.title" :description="item.description"> <DeleteOutlined title="删除此项" @click.stop="deletedGw(index)"
style="color: #1677ff; margin-right: 5px;margin-bottom: 3px;" />
<a-list-item-meta :title="item.title" @click="setGwDetail(index)" >
</a-list-item-meta> </a-list-item-meta>
<RightOutlined /> <RightOutlined @click="sendMessageGwAll(index)" />
</a-list-item> </a-list-item>
</template> </template>
</a-list> </a-list>
<a-divider /> <a-divider />
<div class="position-details">
<a-typography-title :level="5">主播岗位详情</a-typography-title>
<h6 class="details-title">
<StarFilled style="color: #1677ff; margin-right: 8px" />岗位职责
</h6>
<ol class="details-list">
<li>负责日常直播内容输出</li>
<li>与观众互动提升直播间活跃度</li>
<li>配合运营完成直播活动</li>
</ol>
<h6 class="details-title">
<StarFilled style="color: #1677ff; margin-right: 8px" />岗位要求
</h6>
<ol class="details-list">
<li>形象气质佳普通话标准</li>
<li>有1年以上直播经验</li>
<li>具备良好的沟通能力</li>
</ol>
</div> </div>
<div ref="listScrollRef" class="sider-bottom" style="overflow-y: auto;flex: 3;max-height: calc(100% * 3 / 5);">
<a-typography-title :level="5">岗位详情</a-typography-title>
<a-list :data-source="dutyList" item-layout="horizontal">
<template #renderItem="{ item, index }" >
<a-list-item @click="resetSayin" :ref="($el) => insertItemList($el)" class="position-item" :class="{ 'highlight-item': index === scrollIndex }">
<h6 class="details-title" v-if="item.length === 5 " >
<StarFilled style="color: #1677ff; margin-left:-20px;margin-right:5px;" />{{ item }}
</h6>
<a-list-item-meta v-else :title="item" >
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</div> </div>
</a-layout-sider> </a-layout-sider>
@@ -226,11 +236,112 @@
</a-layout> </a-layout>
</a-layout> </a-layout>
<!-- 可输入内容的模态框 -->
<a-modal
v-model:open="showModal"
width="800px"
ok-text="提交"
cancel-text="取消"
@ok="handleOk"
@cancel="handleCancel"
:maskClosable="false"
:destroyOnClose="true"
:confirmLoading="confirmLoading"
>
<template #title>
<div class="modal-title-wrapper">
<!-- 标题栏右侧按钮组 -->
<div class="title-button-group">
<a-row>
<a-col :span="10">
<!-- 原标题文字 -->
<a-label style="font-weight: bolder;" >新增岗位信息</a-label>
<a-button
:type="btn1Type"
size="small"
@click="typeChange('btn1')"
style="margin-left:10px;"
>
文本
</a-button>
<a-button
:type="btn2Type"
size="small"
@click="typeChange('btn2')"
style="margin-left:5px;"
>
JSON
</a-button>
</a-col>
<a-col :span="14">
<a-button
type="default"
size="small"
@click="transText"
style="margin-left:13px;"
>
转换
</a-button>
<a-button
type="default"
size="small"
@click="handleCancel"
style="margin-left:5px;"
>
润色
</a-button>
</a-col>
</a-row>
</div>
</div>
</template>
<!-- 表单区域 -->
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 50 }"
layout="horizontal"
>
<a-row :gutter="16">
<a-col :span="12">
<!-- 单行输入框 -->
<a-form-item label="" name="infoin">
<a-textarea
v-model:value="formData.infoin"
placeholder="请输岗位信息"
:rows="15"
maxlength="2000"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<div style="width: 375px;height: 340px;overflow-y: scroll;">
<!-- 多行文本框 -->
<div v-for="(item, index) in formData.infoDetail"
:key="index"
>
<textarea style="overflow: hidden;border: none;line-height: 1.1;height: auto;min-height: 10px;outline: none;width: 100%;flex: none;resize: none;margin: 0px;padding: 0px 5px;word-wrap: break-word;word-break: break-all;"
v-model="formData.infoDetail[index]"
readonly="true" class="auto-wrap-textarea" wrap="soft"
/>
</div>
</div>
</a-col>
</a-row>
</a-form>
</a-modal>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'; import { ref, onMounted, onUnmounted, computed, reactive, nextTick, watch } from 'vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal, } from 'ant-design-vue';
import SocketClient from '@renderer/utils/socket';
import { import {
WifiOutlined, WifiOutlined,
SettingOutlined, SettingOutlined,
@@ -258,11 +369,366 @@ import { useUserStore } from '@renderer/stores/useUserStore';
// 从 store 获取用户信息 // 从 store 获取用户信息
const userStore = useUserStore(); const userStore = useUserStore();
// socket核心变量定义
const WS_URL = 'wss://dmdemo.hx.cn/ws?session_id='; // 你的 WSS 地址
let socketClient = null; // Socket 实例(约束类型)
// 响应式变量(强类型)
const isConnected = ref(false);
const sendMsg = ref('');
const msgList = ref([]); // 消息列表强类型
const dutyList = ref([]);
let liveIsSay = ref(false);
const listScrollRef = ref([])
const listItemRefs = ref([])
const insertItemList = (_this) => {
listItemRefs.value.push(_this)
}
let scrollIndex = ref(0);//记录现在滚动到哪了
const scrollToIndex = (index) => {
console.log("listItemRefs.value[index].$el.offsetTop",listItemRefs.value[index].$el.offsetTop)
scrollIndex.value = index;
let scrollto = listItemRefs.value[index].$el.offsetTop-listItemRefs.value[0].$el.offsetTop;
if(scrollto < 0) scrollto = 0;
nextTick(() => { // 确保 DOM 已渲染完成
listScrollRef.value.scrollTo({
top: scrollto,//每次滚动定格其实不好看,就减去一点
behavior: 'smooth'
});
});
};
const resetSayin = () => {
liveIsSay.value = false;//我说完啦
//console.info('我说完啦:重置好说话状态,现在没有在说话啦');
}
// 2. 格式化时间
const formatTime = () => {
const now = new Date();
return `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`;
};
// 初始化 WebSocket 连接
const initWebSocket = (_initWebSocket) => {
if(_initWebSocket){
socketClient = new SocketClient(WS_URL+_initWebSocket, {
heartbeatInterval: 20000,
// 连接成功回调(类型约束)
onOpen: () => {
isConnected.value = true;
msgList.value.push({
time: formatTime(),
type: 'system',
content: 'Socket 长连接建立成功'
});
console.info('Socket 长连接建立成功');
},
// 接收消息回调(类型约束)
onMessage: (data) => {
console.info('接收回调消息:', data);
let msg = data;
// 仅解析字符串类型的 JSON 消息
if (typeof data === 'string' && data.indexOf("{") == 0) {
setTimeout(resetSayin,500)
try {
msg = JSON.parse(data);
} catch (e) {
console.warn('消息非 JSON 格式:', e);
}
}
msgList.value.push({
time: formatTime(),
type: 'receive',
content: msg
});
},
// 错误回调(类型约束)
onError: (error) => {
isConnected.value = false;
msgList.value.push({
time: formatTime(),
type: 'system',
content: `Socket 错误:${error.message || '未知错误'}`
});
},
// 关闭回调(类型约束)
onClose: (event) => {
isConnected.value = false;
msgList.value.push({
time: formatTime(),
type: 'system',
content: `Socket 断开:${event.reason || '网络波动,正在重连...'}`
});
}
});
}
};
const sayMessageList = ref([]);
//将职位详情添加到虚拟的待播放队列中
const sendMessageGwAll = (_index) => {
if (dutyList.value.length == 0) {
setGwDetail(_index);
}
if(dutyList.value){
let index = 0;
for(const oneItem of dutyList.value){
const oneMessage = {};
oneMessage["type"] = "dutyDetail";
oneMessage["message"] = oneItem;
oneMessage["dutyIndex"] = index++;
sayMessageList.value.push(oneMessage);
}
console.log("待播放消息列表:",sayMessageList)
}
}
const setGwDetail = (_index) => {
const jsonData = getFromLocalStorage('gangweiList');
while (dutyList.value.length > 0) {
dutyList.value.splice(0, 1);
}
if(jsonData[_index]&&jsonData[_index]["infoDetail"]){
for (const oneItem of jsonData[_index]["infoDetail"]){
dutyList.value.push(oneItem);
}
}
}
const initGwData = () => {
while (positions.value.length > 0) {
positions.value.splice(0, 1);
}
const jsonData = getFromLocalStorage('gangweiList');
if(jsonData){
for (const oneItem of jsonData){
const oneTitle = {};
oneTitle.title = oneItem["gwName"];
positions.value.push(oneTitle);
}
}
}
const deletedGw = (_index) => {
Modal.confirm({
title: '是否确认删除岗位信息?',
content: positions.value[_index]['title'],
okText: '确认删除',
cancelText: '取消',
centered: true,
width: 400,
maskClosable: false,
onOk() {
const jsonData = getFromLocalStorage('gangweiList');
jsonData.splice(_index, 1);
const result = saveToLocalStorage(jsonData,'gangweiList');
if(result){
positions.value.splice(_index, 1);
while (dutyList.value.length > 0) {
dutyList.value.splice(0, 1);
}
message.success('删除成功')
}else{
message.error('删除失败')
}
},
onCancel() {
message.error('取消操作')
}
});
}
const doText = () => {
//遇到句号就切分
const tempStr1 = (formData.infoin || "")
.replace(/\n/g, "。")
.trim();
formData.infoDetail = tempStr1.split("。").filter(item => {
// 过滤空项、仅含空格/换行的项
return item.trim() !== "";
});
formData.gwName = formData.infoDetail[0];
}
const doJson = () => {
try {
console.log("jsonStr",formData.infoin.replace(/\n/g, "").replace(" ", "").trim())
const data = JSON.parse(formData.infoin.replace(/\n/g, "").replace(" ", "").trim());
console.log("data",data)
} catch (err) {
message.error("json转换失败");
}
}
const transText = () => {
if(textType == "text"){
doText();
}else if(textType == "json"){
doJson();
}
}
const btn1Type = ref('primary')
const btn2Type = ref('text')
let textType = "text";
const typeChange = (btnKey) => {
switch (btnKey) {
case 'btn1':
btn1Type.value = 'primary';
btn2Type.value = 'text';
textType = "text";
break
case 'btn2':
btn2Type.value = 'primary';
btn1Type.value = 'text';
textType = "json";
break
default:
break
}
}
const showModal = ref(false)
const confirmLoading = ref(false)
// 表单引用
const formRef = ref(null)
// 表单数据
const formData = reactive({
infoin: '', // 输入内容
gwName: '', // 提交时处理岗位名称
infoDetail: ref([]) // 内容解析
})
const formRules = reactive({
infoin: [
{ required: true, message: '请输入岗位信息', trigger: 'blur' },
{ min: 20, max: 2000, message: '岗位信息长度需在 20-2000 个字符之间', trigger: 'blur' }
],
gwName: [ //没有对应的form元素验证不生效
{ required: true, message: '未进行转换', trigger: 'blur' }
],
infoDetail: [ //动态列表,验证不生效
{ required: true, message: '未进行转换', trigger: 'blur' }
]
})
const saveToLocalStorage = (data, key) => {
try {
// 存储到localStoragev
localStorage.setItem(key, JSON.stringify(data));
console.log('表单数据已成功存储到localStorage');
return true;
} catch (err) {
// 捕获所有可能的报错(格式错误、大小超限等)
const errorMsg = `存储失败:${err.message}`;
console.error('localStorage存储报错', err);
return false;
}
};
const getFromLocalStorage = (key) => {
try {
// 步骤1读取存储的字符串
const jsonStr = localStorage.getItem(key);
// 步骤2判断是否存在数据
if (!jsonStr) {
console.error('localStorage中无该表单数据');
return null;
}
// 步骤3解析JSON字符串为对象/数组(核心!)
const data = JSON.parse(jsonStr);
console.log('表单数据已从localStorage加载');
return data;
} catch (err) {
const errorMsg = `读取失败:${err.message}可能是JSON格式错误`;
console.error('localStorage读取报错', err);
return null;
}
};
const refreshGwList = async () => {
const jsonData = getFromLocalStorage('gangweiList');
}
// 确认提交
const handleOk = async () => {
try {
// 1. 表单校验
await formRef.value.validate()
if(formData.gwName && formData.gwName != ''){
confirmLoading.value = true;
}else{
message.error('请先进行转换!')
return;
}
let jsonData = getFromLocalStorage('gangweiList');
if (jsonData == null) {
jsonData = []
}
jsonData.push({ ...formData });
// 2. 模拟接口请求(实际项目中替换为真实接口)
const result = saveToLocalStorage(jsonData,'gangweiList');
if(result){
const oneTitle = {};
oneTitle.title = formData.gwName;
positions.value.push(oneTitle);
// 3. 提交成功处理
message.success('提交成功!')
console.log('提交的表单数据:', { ...formData })
}else{
message.success('提交失败')
//里面已经打印了失败日志
}
// 4. 关闭模态框
showModal.value = false
} catch (error) {
// 表单校验失败
console.error('表单校验失败:', error)
message.error('请检查输入内容是否符合要求')
} finally {
// 关闭加载状态
confirmLoading.value = false
}
}
const resetForm = () => {
formData.infoin = ''; // 输入内容
formData.gwName = ''; // 提交时处理岗位名称
formData.infoDetail = []; // 内容解析
}
const handleShow = () => {
// 重置表单(清空输入值和校验状态)
resetForm()
// 关闭模态框
showModal.value = true
}
// 取消弹窗
const handleCancel = () => {
// 重置表单(清空输入值和校验状态)
formRef.value?.resetFields()
resetForm()
// 关闭模态框
showModal.value = false
}
// 左侧岗位列表数据 // 左侧岗位列表数据
const positions = ref([ const positions = ref([
{ title: '主播岗位', description: '负责直播内容输出' }, { title: '主播岗位', },
{ title: '运营岗位', description: '负责直播活动策划' }, { title: '运营岗位', },
{ title: '场控岗位', description: '负责直播现场管理' }, { title: '场控岗位', },
]); ]);
// UI 状态 - 只负责界面显示 // UI 状态 - 只负责界面显示
@@ -293,13 +759,11 @@ const currentUserId = computed(() => {
// 状态同步 - 从主进程获取直播状态 // 状态同步 - 从主进程获取直播状态
const syncLiveStatus = async () => { const syncLiveStatus = async () => {
try { try {
console.log('开始同步直播状态...'); //console.log('开始同步直播状态...');
const result = await window.electron.ipcRenderer.invoke('get-live-status'); const result = await window.electron.ipcRenderer.invoke('get-live-status');
console.log('状态同步结果:', result); //console.log('状态同步结果:', result);
if (result.success) { if (result.success) {
const newState = result.data.isLiveOn; const newState = result.data.isLiveOn;
console.log('状态变化: 从', isLiveOn.value, '到', newState);
isLiveOn.value = newState; isLiveOn.value = newState;
} else { } else {
console.error('状态同步失败:', result.error); console.error('状态同步失败:', result.error);
@@ -321,6 +785,11 @@ const startLive = async () => {
}); });
console.log('直播启动结果:', result); console.log('直播启动结果:', result);
const sessionId = result["sessionId"];
console.log('result["sessionId"]',sessionId)
if(sessionId)(
initWebSocket(sessionId) // 同步到sessionId时才进行socket
)
if (result.success) { if (result.success) {
// 等待一下确保状态已更新 // 等待一下确保状态已更新
@@ -474,25 +943,53 @@ const refreshModelStatus = async () => {
message.info('模型状态已刷新'); message.info('模型状态已刷新');
}; };
// 发送消息到数字人
const sendMessage = async () => { const sendMessageHasParam = async (syncliveSay) => {
if (!inputValue.value.trim()) {
message.warning('请输入内容'); if(syncliveSay){//正说着呢
return; //console.info('进入播放语音方法,有人正说着啦', syncliveSay);
return false;
} }
if (sayMessageList.value.length == 0) {
//console.info('进入播放语音方法,待播放内容为空',sayMessageList.value.length);
return false;
}
liveIsSay.value = true; //我准备说啦,后续需要考虑死锁问题
//console.info('进入播放语音方法,我准备开始说啦,看看对象有什么',sayMessageList.value);
try { try {
const result = await window.electron.ipcRenderer.invoke('push-explain-position', inputValue.value); const result = await window.electron.ipcRenderer.invoke('push-explain-position', sayMessageList.value[0]["message"]);
if(sayMessageList.value[0]["type"] == "dutyDetail"){
//如果是职位详情,播放是高亮,滚动
scrollToIndex(sayMessageList.value[0]["dutyIndex"]);
}
sayMessageList.value.splice(0, 1);
if (result.success) { if (result.success) {
message.success('消息已发送'); message.success('消息已发送');
inputValue.value = ''; return true;
} else { } else {
message.error('发送失败: ' + result.error); message.error('发送失败: ' + result.error);
return false;
} }
} catch (error) { } catch (error) {
liveIsSay.value = false;
console.error('发送消息出错:', error); console.error('发送消息出错:', error);
message.error('发送消息出错'); message.error('发送消息出错');
return false;
} }
}
// 发送消息到数字人
const sendMessage = () => {
//临时添加也只是先添加到 语音消息队列中
const oneMessage = {"type":"tempMessage","dutyIndex":0,message:inputValue.value};
sayMessageList.value.unshift(oneMessage);//插播内容添加到开头
inputValue.value = '';
console.log("待播放消息列表sayMessageList",sayMessageList)
}; };
// 润色文本功能 // 润色文本功能
@@ -780,8 +1277,24 @@ const handleModelDownloadConfirm = async (_event, data) => {
}); });
}; };
onUnmounted(() => {
if (socketClient) {
socketClient.close(1000, '页面卸载');
msgList.value.push({
time: formatTime(),
type: 'system',
content: '页面卸载,主动关闭 Socket 长连接'
});
}
});
// 监听直播状态变化 // 监听直播状态变化
onMounted(() => { onMounted(() => {
setInterval(() => {
sendMessageHasParam(liveIsSay.value)
}, 1000);
initGwData();
// 初始化时同步状态 // 初始化时同步状态
syncLiveStatus(); syncLiveStatus();
checkModelStatus(); // 初始化时检查模型状态 checkModelStatus(); // 初始化时检查模型状态
@@ -796,7 +1309,7 @@ onMounted(() => {
window.electron.ipcRenderer.on('model-download-confirm', handleModelDownloadConfirm); window.electron.ipcRenderer.on('model-download-confirm', handleModelDownloadConfirm);
// 定期同步状态 (可选) // 定期同步状态 (可选)
const statusInterval = setInterval(syncLiveStatus, 2000); const statusInterval = setInterval(syncLiveStatus, 1000);
// 清理定时器 // 清理定时器
onUnmounted(() => { onUnmounted(() => {
@@ -1038,4 +1551,12 @@ onMounted(() => {
.ant-card { .ant-card {
margin-bottom: 16px; margin-bottom: 16px;
} }
/* 新增index=5 时的高亮样式(可根据需求自定义) */
.highlight-item {
background-color: #e6f7ff; /* 浅蓝色背景 */
border-left: 3px solid #1677ff; /* 蓝色左侧边框强调 */
padding-left: 15px !important; /* 调整内边距 */
color: #1677ff; /* 文字变蓝 */
}
</style> </style>