基本完成添加岗位&岗位详情&逐句播放&插入播放
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
38
src/renderer/src/types/socket.ts
Normal file
38
src/renderer/src/types/socket.ts
Normal 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;
|
||||||
|
}
|
||||||
175
src/renderer/src/utils/socket.ts
Normal file
175
src/renderer/src/utils/socket.ts
Normal 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;
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user