mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-08-06 10:57:26 +08:00
Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3
This commit is contained in:
@@ -16,10 +16,12 @@
|
|||||||
"@amap/amap-jsapi-loader": "0.0.7",
|
"@amap/amap-jsapi-loader": "0.0.7",
|
||||||
"@antv/g2": "^4.1.12",
|
"@antv/g2": "^4.1.12",
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"@tencentcloud/chat": "^3.6.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"core-js": "^3.49.0",
|
"core-js": "^3.49.0",
|
||||||
"dplayer": "^1.26.0",
|
"dplayer": "^1.26.0",
|
||||||
"element-plus": "^2.14.1",
|
"element-plus": "^2.14.1",
|
||||||
|
"hls.js": "^1.6.16",
|
||||||
"js-cookie": "^2.2.1",
|
"js-cookie": "^2.2.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"price-color": "1.0.2",
|
"price-color": "1.0.2",
|
||||||
@@ -39,7 +41,6 @@
|
|||||||
"xss": "^1.0.7"
|
"xss": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"webpack": "^5.95.0",
|
|
||||||
"@vue/cli-plugin-babel": "^5.0.9",
|
"@vue/cli-plugin-babel": "^5.0.9",
|
||||||
"@vue/cli-plugin-router": "^5.0.9",
|
"@vue/cli-plugin-router": "^5.0.9",
|
||||||
"@vue/cli-plugin-vuex": "^5.0.9",
|
"@vue/cli-plugin-vuex": "^5.0.9",
|
||||||
@@ -51,7 +52,8 @@
|
|||||||
"less-loader": "^11.1.4",
|
"less-loader": "^11.1.4",
|
||||||
"style-loader": "^3.3.4",
|
"style-loader": "^3.3.4",
|
||||||
"style-resources-loader": "^1.5.0",
|
"style-resources-loader": "^1.5.0",
|
||||||
"vue-cli-plugin-style-resources-loader": "^0.1.5"
|
"vue-cli-plugin-style-resources-loader": "^0.1.5",
|
||||||
|
"webpack": "^5.95.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"minimatch": "^3.1.2",
|
"minimatch": "^3.1.2",
|
||||||
|
|||||||
22
manager/pnpm-lock.yaml
generated
22
manager/pnpm-lock.yaml
generated
@@ -21,6 +21,9 @@ importers:
|
|||||||
'@element-plus/icons-vue':
|
'@element-plus/icons-vue':
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
version: 2.3.2(vue@3.5.39)
|
version: 2.3.2(vue@3.5.39)
|
||||||
|
'@tencentcloud/chat':
|
||||||
|
specifier: ^3.6.6
|
||||||
|
version: 3.6.6
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.7.9
|
specifier: ^1.7.9
|
||||||
version: 1.18.1
|
version: 1.18.1
|
||||||
@@ -33,6 +36,9 @@ importers:
|
|||||||
element-plus:
|
element-plus:
|
||||||
specifier: ^2.14.1
|
specifier: ^2.14.1
|
||||||
version: 2.14.2(vue@3.5.39)
|
version: 2.14.2(vue@3.5.39)
|
||||||
|
hls.js:
|
||||||
|
specifier: ^1.6.16
|
||||||
|
version: 1.6.16
|
||||||
js-cookie:
|
js-cookie:
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
@@ -829,36 +835,42 @@ packages:
|
|||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@parcel/watcher-linux-arm-musl@2.5.6':
|
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||||
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||||
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||||
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||||
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@parcel/watcher-linux-x64-musl@2.5.6':
|
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||||
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@parcel/watcher-win32-arm64@2.5.6':
|
'@parcel/watcher-win32-arm64@2.5.6':
|
||||||
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
||||||
@@ -906,6 +918,9 @@ packages:
|
|||||||
'@sxzz/popperjs-es@2.11.8':
|
'@sxzz/popperjs-es@2.11.8':
|
||||||
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
|
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
|
||||||
|
|
||||||
|
'@tencentcloud/chat@3.6.6':
|
||||||
|
resolution: {integrity: sha512-PYPGUoZHw3GjDgM0N72qr0K/Wjp28HAoGaND077sbUQEwYFaMjZ1tuBQZugFG6v8OS8Ihx71yqNshR3xDHvYaA==}
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
@@ -2353,6 +2368,9 @@ packages:
|
|||||||
highlight.js@10.7.3:
|
highlight.js@10.7.3:
|
||||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||||
|
|
||||||
|
hls.js@1.6.16:
|
||||||
|
resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==}
|
||||||
|
|
||||||
hosted-git-info@2.8.9:
|
hosted-git-info@2.8.9:
|
||||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||||
|
|
||||||
@@ -5147,6 +5165,8 @@ snapshots:
|
|||||||
|
|
||||||
'@sxzz/popperjs-es@2.11.8': {}
|
'@sxzz/popperjs-es@2.11.8': {}
|
||||||
|
|
||||||
|
'@tencentcloud/chat@3.6.6': {}
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@@ -6833,6 +6853,8 @@ snapshots:
|
|||||||
|
|
||||||
highlight.js@10.7.3: {}
|
highlight.js@10.7.3: {}
|
||||||
|
|
||||||
|
hls.js@1.6.16: {}
|
||||||
|
|
||||||
hosted-git-info@2.8.9: {}
|
hosted-git-info@2.8.9: {}
|
||||||
|
|
||||||
hpack.js@2.1.6:
|
hpack.js@2.1.6:
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ export const queryLiveMessagePage = params => getRequest('/live/message/page', p
|
|||||||
export const authLiveMessage = params =>
|
export const authLiveMessage = params =>
|
||||||
putRequest('/live/message/auth', params, { 'Content-type': 'application/json' })
|
putRequest('/live/message/auth', params, { 'Content-type': 'application/json' })
|
||||||
|
|
||||||
|
export const postSendMessage = params =>
|
||||||
|
postRequest('/live/message/send', params, { 'Content-type': 'application/json' })
|
||||||
|
|
||||||
// ========== 直播用户 / 拉黑 ==========
|
// ========== 直播用户 / 拉黑 ==========
|
||||||
export const liveUserList = params => getRequest('/live/user/page', params)
|
export const liveUserList = params => getRequest('/live/user/page', params)
|
||||||
|
|
||||||
|
|||||||
251
manager/src/views/live/components/LiveChatPanel.vue
Normal file
251
manager/src/views/live/components/LiveChatPanel.vue
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<template>
|
||||||
|
<div class="live-chat-panel">
|
||||||
|
<div v-if="initError" class="chat-error">
|
||||||
|
<el-alert :title="initError" type="warning" :closable="false" show-icon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="chatListRef" class="chat-list" @scroll="onScroll">
|
||||||
|
<div v-if="isLoadingHistory" class="chat-loading">加载历史消息...</div>
|
||||||
|
<div
|
||||||
|
v-for="msg in messageList"
|
||||||
|
:key="msg.messageId"
|
||||||
|
class="chat-item"
|
||||||
|
:class="{ 'is-system': msg.isSystem }"
|
||||||
|
>
|
||||||
|
<el-avatar v-if="!msg.isSystem" :src="msg.avatar" :size="28">
|
||||||
|
{{ (msg.username || "用").charAt(0) }}
|
||||||
|
</el-avatar>
|
||||||
|
<div class="chat-bubble">
|
||||||
|
<div v-if="!msg.isSystem" class="chat-user">{{ msg.username }}</div>
|
||||||
|
<div class="chat-text">{{ msg.message }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="!messageList.length && !initError && !initializing" :image-size="64" description="暂无聊天消息" />
|
||||||
|
<div ref="bottomAnchorRef" class="chat-bottom-anchor" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="unreadCount > 0" class="chat-unread" @click="scrollToBottomAndClearUnread">
|
||||||
|
{{ unreadCount }} 条新消息
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-input-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="messageContent"
|
||||||
|
placeholder="发送消息到直播间..."
|
||||||
|
maxlength="200"
|
||||||
|
@keyup.enter="createTextMessageByInput"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" :disabled="!messageContent.trim()" @click="handleSend">
|
||||||
|
发送
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { toRef } from "vue";
|
||||||
|
import { useChat } from "../composables/useChat";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "LiveChatPanel",
|
||||||
|
props: {
|
||||||
|
liveId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
liveDetail: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
let userInfo = {};
|
||||||
|
try {
|
||||||
|
userInfo = JSON.parse(Cookies.get("userInfoManager") || "{}");
|
||||||
|
} catch {
|
||||||
|
userInfo = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return useChat(toRef(props, "liveId"), userInfo);
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
initializing: false,
|
||||||
|
chatInited: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/** 腾讯云 IM 群组:优先后端字段,否则用直播间 ID(与旧版中控台一致) */
|
||||||
|
imGroupId() {
|
||||||
|
return this.liveDetail?.imGroupId || this.liveDetail?.groupId || this.liveId || "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
imGroupId: {
|
||||||
|
immediate: true,
|
||||||
|
handler(id) {
|
||||||
|
if (!id) return;
|
||||||
|
this.setGroupID(id);
|
||||||
|
if (this.active) {
|
||||||
|
this.bootstrapChat();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
active(val) {
|
||||||
|
if (val && this.imGroupId && !this.chatInited) {
|
||||||
|
this.bootstrapChat();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
liveId() {
|
||||||
|
this.chatInited = false;
|
||||||
|
this.cleanup();
|
||||||
|
if (this.active && this.imGroupId) {
|
||||||
|
this.bootstrapChat();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.cleanup();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSend() {
|
||||||
|
this.createTextMessageByInput();
|
||||||
|
},
|
||||||
|
async bootstrapChat() {
|
||||||
|
if (!this.liveId || this.chatInited || this.initializing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupId = this.imGroupId;
|
||||||
|
if (!groupId) {
|
||||||
|
this.initError = "缺少直播间 ID,无法初始化聊天";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setGroupID(groupId);
|
||||||
|
|
||||||
|
this.initializing = true;
|
||||||
|
try {
|
||||||
|
await this.initTencentIm();
|
||||||
|
if (!this.initError) {
|
||||||
|
this.chatInited = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("初始化直播聊天失败:", error);
|
||||||
|
this.initError = "聊天初始化失败,请刷新重试";
|
||||||
|
this.chatInited = false;
|
||||||
|
} finally {
|
||||||
|
this.initializing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.live-chat-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-error {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-list {
|
||||||
|
flex: 1 1 0;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px 0;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bottom-anchor {
|
||||||
|
height: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-loading {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&.is-system {
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.chat-bubble {
|
||||||
|
background: #f4f4f5;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-text {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-user {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-unread {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 56px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: $theme_color;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-bar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
475
manager/src/views/live/composables/useChat.js
Normal file
475
manager/src/views/live/composables/useChat.js
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import { nextTick, ref, unref, watch } from "vue";
|
||||||
|
import { postSendMessage } from "@/api/live";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
|
import TencentCloudChat from "@tencentcloud/chat";
|
||||||
|
import { fetchLiveSetting } from "./useLiveSetting";
|
||||||
|
import { genTestUserSig } from "../utils/GenerateTestUserSig";
|
||||||
|
|
||||||
|
const MAX_MESSAGE_COUNT = 100;
|
||||||
|
const ADMIN_IM_USER_ID = "admin";
|
||||||
|
|
||||||
|
export function useChat(liveId, userInfo) {
|
||||||
|
const chat = ref(null);
|
||||||
|
const groupID = ref("");
|
||||||
|
const messageList = ref([]);
|
||||||
|
const messageContent = ref("");
|
||||||
|
const chatListRef = ref(null);
|
||||||
|
const bottomAnchorRef = ref(null);
|
||||||
|
const isAtBottom = ref(true);
|
||||||
|
const unreadCount = ref(0);
|
||||||
|
const isLoadingHistory = ref(false);
|
||||||
|
const hasMoreHistory = ref(true);
|
||||||
|
const nextReqMessageID = ref("");
|
||||||
|
const imReady = ref(false);
|
||||||
|
const initError = ref("");
|
||||||
|
|
||||||
|
let loginPromise = null;
|
||||||
|
let historyLoaded = false;
|
||||||
|
|
||||||
|
function resetSessionState() {
|
||||||
|
imReady.value = false;
|
||||||
|
loginPromise = null;
|
||||||
|
historyLoaded = false;
|
||||||
|
hasMoreHistory.value = true;
|
||||||
|
nextReqMessageID.value = "";
|
||||||
|
isLoadingHistory.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detachChatEvents(instance) {
|
||||||
|
if (!instance) return;
|
||||||
|
instance.off(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
|
||||||
|
instance.off(TencentCloudChat.EVENT.SDK_READY, onSdkReady);
|
||||||
|
instance.off(TencentCloudChat.EVENT.SDK_NOT_READY, onSdkNotReady);
|
||||||
|
instance.off(TencentCloudChat.EVENT.KICKED_OUT, onKickedOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachChatEvents(instance) {
|
||||||
|
instance.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
|
||||||
|
instance.on(TencentCloudChat.EVENT.SDK_READY, onSdkReady);
|
||||||
|
instance.on(TencentCloudChat.EVENT.SDK_NOT_READY, onSdkNotReady);
|
||||||
|
instance.on(TencentCloudChat.EVENT.KICKED_OUT, onKickedOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initTencentIm() {
|
||||||
|
initError.value = "";
|
||||||
|
resetSessionState();
|
||||||
|
|
||||||
|
if (!groupID.value) {
|
||||||
|
const fallbackGroupId = unref(liveId);
|
||||||
|
if (fallbackGroupId) {
|
||||||
|
groupID.value = `${fallbackGroupId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!groupID.value) {
|
||||||
|
initError.value = "直播间群组 ID 不存在";
|
||||||
|
console.warn("groupID 不存在,跳过 IM 初始化");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setting = await fetchLiveSetting();
|
||||||
|
if (!setting?.imSdkAppid) {
|
||||||
|
initError.value = "请先在系统设置中配置直播 IM SDK APPID";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!setting?.imSdkSecretKey) {
|
||||||
|
initError.value = "请先在系统设置中配置直播 IM SDK 密钥";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sdkAppId = Number(setting.imSdkAppid);
|
||||||
|
const { userSig } = genTestUserSig({
|
||||||
|
SDKAppID: sdkAppId,
|
||||||
|
secretKey: setting.imSdkSecretKey,
|
||||||
|
userID: ADMIN_IM_USER_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (chat.value) {
|
||||||
|
detachChatEvents(chat.value);
|
||||||
|
try {
|
||||||
|
await chat.value.logout?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("IM logout error:", error);
|
||||||
|
}
|
||||||
|
chat.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatInstance = TencentCloudChat.create({ SDKAppID: sdkAppId });
|
||||||
|
chat.value = chatInstance;
|
||||||
|
chatInstance.setLogLevel(1);
|
||||||
|
attachChatEvents(chatInstance);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loginChat(userSig);
|
||||||
|
await waitForSdkReady();
|
||||||
|
await joinGroupAndLoadHistory();
|
||||||
|
} catch (error) {
|
||||||
|
initError.value = error?.message || "IM 初始化失败";
|
||||||
|
console.warn("initTencentIm error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForSdkReady(timeoutMs = 15000) {
|
||||||
|
if (imReady.value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error("IM SDK 就绪超时"));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
const onReady = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
chat.value?.off(TencentCloudChat.EVENT.SDK_READY, onReady);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
chat.value?.on(TencentCloudChat.EVENT.SDK_READY, onReady);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loginChat(userSig) {
|
||||||
|
if (!chat.value) {
|
||||||
|
return Promise.reject(new Error("IM 实例不存在"));
|
||||||
|
}
|
||||||
|
if (loginPromise) {
|
||||||
|
return loginPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
loginPromise = chat.value
|
||||||
|
.login({
|
||||||
|
userID: ADMIN_IM_USER_ID,
|
||||||
|
userSig,
|
||||||
|
})
|
||||||
|
.then((imResponse) => {
|
||||||
|
if (imResponse.data.repeatLogin === true) {
|
||||||
|
imReady.value = true;
|
||||||
|
}
|
||||||
|
return imResponse;
|
||||||
|
})
|
||||||
|
.catch((imError) => {
|
||||||
|
const message = imError?.message || "IM 登录失败";
|
||||||
|
initError.value = message;
|
||||||
|
throw imError;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loginPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return loginPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSdkNotReady() {
|
||||||
|
console.log("[SDK Not Ready]");
|
||||||
|
imReady.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSdkReady() {
|
||||||
|
console.log("[SDK Ready]");
|
||||||
|
imReady.value = true;
|
||||||
|
updateUserInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKickedOut() {
|
||||||
|
console.log("[用户被踢下线]");
|
||||||
|
imReady.value = false;
|
||||||
|
initError.value = "IM 已下线,请刷新页面";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinGroupAndLoadHistory() {
|
||||||
|
if (!chat.value || !imReady.value || !groupID.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await joinGroup();
|
||||||
|
await getHistoryMessageList("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserInfo() {
|
||||||
|
if (!chat.value || !userInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chat.value.updateMyProfile({
|
||||||
|
nick: userInfo.nickName || userInfo.username || "管理员",
|
||||||
|
avatar: userInfo.face || "",
|
||||||
|
gender: TencentCloudChat.TYPES.GENDER_UNKNOWN,
|
||||||
|
allowType: TencentCloudChat.TYPES.ALLOW_TYPE_ALLOW_ANY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinGroup() {
|
||||||
|
if (!chat.value || !imReady.value || !groupID.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imResponse = await chat.value.joinGroup({
|
||||||
|
groupID: groupID.value,
|
||||||
|
type: TencentCloudChat.TYPES.GRP_AVCHATROOM,
|
||||||
|
});
|
||||||
|
console.log("[加群成功]", imResponse);
|
||||||
|
} catch (imError) {
|
||||||
|
console.warn("joinGroup error:", imError);
|
||||||
|
throw imError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHistoryMessageList(nextReqId) {
|
||||||
|
if (!chat.value || !imReady.value || !groupID.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isLoadingHistory.value || !hasMoreHistory.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!nextReqId && historyLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingHistory.value = true;
|
||||||
|
|
||||||
|
const requestParams = {
|
||||||
|
conversationID: `GROUP${groupID.value}`,
|
||||||
|
direction: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nextReqId) {
|
||||||
|
requestParams.nextReqMessageID = nextReqId;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imResponse = await chat.value.getMessageList(requestParams);
|
||||||
|
const msgList = imResponse.data.messageList;
|
||||||
|
const nextId = imResponse.data.nextReqMessageID;
|
||||||
|
const isCompleted = imResponse.data.isCompleted;
|
||||||
|
|
||||||
|
const formattedMessages = msgList.map((msg) => formatReceivedMessage(msg));
|
||||||
|
formattedMessages.forEach((msg) => {
|
||||||
|
addMessageToList(msg, "append");
|
||||||
|
});
|
||||||
|
|
||||||
|
scrollToBottom(true);
|
||||||
|
|
||||||
|
nextReqMessageID.value = nextId;
|
||||||
|
hasMoreHistory.value = !isCompleted;
|
||||||
|
historyLoaded = true;
|
||||||
|
} catch (imError) {
|
||||||
|
console.warn("getMessageList error:", imError);
|
||||||
|
throw imError;
|
||||||
|
} finally {
|
||||||
|
isLoadingHistory.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReceivedMessage(message) {
|
||||||
|
let messageText = "";
|
||||||
|
let username = "";
|
||||||
|
let isSystem = false;
|
||||||
|
|
||||||
|
if (message.from === "@TIM#SYSTEM") {
|
||||||
|
username = "系统消息";
|
||||||
|
isSystem = true;
|
||||||
|
} else {
|
||||||
|
username = message.nick || message.from || "用户";
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatar = message.avatar || null;
|
||||||
|
const textType = TencentCloudChat.TYPES.MSG_TEXT;
|
||||||
|
const customType = TencentCloudChat.TYPES.MSG_CUSTOM;
|
||||||
|
|
||||||
|
if (message.type === textType || message.type === "TIMTextElem") {
|
||||||
|
messageText = message.payload?.text || "";
|
||||||
|
} else if (message.type === customType || message.type === "TIMCustomElem") {
|
||||||
|
try {
|
||||||
|
const customData = JSON.parse(message.payload.data);
|
||||||
|
messageText = customData.content || "系统消息";
|
||||||
|
username = "系统消息";
|
||||||
|
isSystem = true;
|
||||||
|
} catch {
|
||||||
|
messageText = "系统消息";
|
||||||
|
username = "系统消息";
|
||||||
|
isSystem = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messageText = "欢迎加入直播间";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username,
|
||||||
|
message: messageText,
|
||||||
|
avatar: isSystem ? null : avatar,
|
||||||
|
messageId: message.ID,
|
||||||
|
time: message.time,
|
||||||
|
type: message.type,
|
||||||
|
isSystem,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessageReceived(event) {
|
||||||
|
const msgList = event.data;
|
||||||
|
|
||||||
|
msgList.forEach((message) => {
|
||||||
|
const formattedMessage = formatReceivedMessage(message);
|
||||||
|
if (!formattedMessage.message?.trim() && !formattedMessage.isSystem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessageToList(formattedMessage, "append");
|
||||||
|
isAtBottom.value = true;
|
||||||
|
unreadCount.value = 0;
|
||||||
|
scrollToBottom(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessageToList(message, mode = "append") {
|
||||||
|
const exists = messageList.value.some((item) => item.messageId === message.messageId);
|
||||||
|
if (exists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "append") {
|
||||||
|
messageList.value.push(message);
|
||||||
|
if (messageList.value.length > MAX_MESSAGE_COUNT) {
|
||||||
|
const removeCount = messageList.value.length - MAX_MESSAGE_COUNT;
|
||||||
|
messageList.value.splice(0, removeCount);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messageList.value.unshift(message);
|
||||||
|
if (messageList.value.length > MAX_MESSAGE_COUNT) {
|
||||||
|
const removeCount = messageList.value.length - MAX_MESSAGE_COUNT;
|
||||||
|
messageList.value.splice(-removeCount, removeCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messageList.value.sort((a, b) => a.time - b.time);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTextMessageByInput() {
|
||||||
|
const content = messageContent.value.trim();
|
||||||
|
if (!content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!imReady.value) {
|
||||||
|
Message.error("IM 未就绪,请稍后再试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await postSendMessage({
|
||||||
|
liveRoomId: unref(liveId),
|
||||||
|
message: content,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.success) {
|
||||||
|
messageContent.value = "";
|
||||||
|
isAtBottom.value = true;
|
||||||
|
unreadCount.value = 0;
|
||||||
|
scrollToBottom(true);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Message.error("发送失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom(force = false) {
|
||||||
|
if (!force && !isAtBottom.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const run = () => {
|
||||||
|
const anchor = bottomAnchorRef.value;
|
||||||
|
if (anchor?.scrollIntoView) {
|
||||||
|
anchor.scrollIntoView({ block: "end" });
|
||||||
|
}
|
||||||
|
const el = chatListRef.value;
|
||||||
|
if (el) {
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
|
requestAnimationFrame(run);
|
||||||
|
setTimeout(run, 50);
|
||||||
|
setTimeout(run, 150);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => messageList.value.length,
|
||||||
|
() => {
|
||||||
|
if (isAtBottom.value) {
|
||||||
|
scrollToBottom(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function checkIfAtBottom(scrollDetail) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollDetail;
|
||||||
|
const isNearBottom = scrollTop + clientHeight >= scrollHeight - 10;
|
||||||
|
|
||||||
|
if (isNearBottom && !isAtBottom.value) {
|
||||||
|
isAtBottom.value = true;
|
||||||
|
unreadCount.value = 0;
|
||||||
|
} else if (!isNearBottom && isAtBottom.value) {
|
||||||
|
isAtBottom.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
const el = chatListRef.value;
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfAtBottom({
|
||||||
|
scrollTop: el.scrollTop,
|
||||||
|
scrollHeight: el.scrollHeight,
|
||||||
|
clientHeight: el.clientHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottomAndClearUnread() {
|
||||||
|
unreadCount.value = 0;
|
||||||
|
isAtBottom.value = true;
|
||||||
|
scrollToBottom(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGroupID(id) {
|
||||||
|
groupID.value = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
resetSessionState();
|
||||||
|
if (chat.value) {
|
||||||
|
detachChatEvents(chat.value);
|
||||||
|
try {
|
||||||
|
chat.value.logout?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("清理腾讯云 IM 失败:", error);
|
||||||
|
}
|
||||||
|
chat.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
groupID,
|
||||||
|
messageList,
|
||||||
|
messageContent,
|
||||||
|
chatListRef,
|
||||||
|
bottomAnchorRef,
|
||||||
|
isAtBottom,
|
||||||
|
unreadCount,
|
||||||
|
isLoadingHistory,
|
||||||
|
hasMoreHistory,
|
||||||
|
imReady,
|
||||||
|
initError,
|
||||||
|
initTencentIm,
|
||||||
|
createTextMessageByInput,
|
||||||
|
scrollToBottom,
|
||||||
|
scrollToBottomAndClearUnread,
|
||||||
|
onScroll,
|
||||||
|
setGroupID,
|
||||||
|
cleanup,
|
||||||
|
};
|
||||||
|
}
|
||||||
44
manager/src/views/live/composables/useLiveSetting.js
Normal file
44
manager/src/views/live/composables/useLiveSetting.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { getSetting } from "@/api/index";
|
||||||
|
|
||||||
|
/** @type {Record<string, any> | null} */
|
||||||
|
let cachedSetting = null;
|
||||||
|
/** @type {Promise<Record<string, any> | null> | null} */
|
||||||
|
let fetchingPromise = null;
|
||||||
|
|
||||||
|
/** 获取直播配置(带缓存) */
|
||||||
|
export async function fetchLiveSetting(force = false) {
|
||||||
|
if (!force && cachedSetting) {
|
||||||
|
return cachedSetting;
|
||||||
|
}
|
||||||
|
if (!force && fetchingPromise) {
|
||||||
|
return fetchingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchingPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const res = await getSetting("LIVE_SETTING");
|
||||||
|
if (res?.success && res?.result) {
|
||||||
|
cachedSetting = res.result;
|
||||||
|
return cachedSetting;
|
||||||
|
}
|
||||||
|
console.warn("获取直播配置失败", res);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取直播配置异常:", error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
fetchingPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return fetchingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 IM SDK AppID */
|
||||||
|
export function resolveSdkAppId(setting) {
|
||||||
|
if (!setting?.imSdkAppid) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const id = Number(setting.imSdkAppid);
|
||||||
|
return Number.isNaN(id) ? 0 : id;
|
||||||
|
}
|
||||||
@@ -27,30 +27,28 @@
|
|||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<span class="panel-title">直播预览</span>
|
<span class="panel-title">直播预览</span>
|
||||||
<div class="ratio-switch">
|
<div class="ratio-switch">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="ratio-btn"
|
|
||||||
:class="{ active: aspectRatio === '16:9' }"
|
|
||||||
@click="aspectRatio = '16:9'"
|
|
||||||
>
|
|
||||||
16:9
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="ratio-btn"
|
|
||||||
:class="{ active: aspectRatio === '4:3' }"
|
|
||||||
@click="aspectRatio = '4:3'"
|
|
||||||
>
|
|
||||||
4:3
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-box" :class="aspectRatio === '16:9' ? 'ratio-169' : 'ratio-43'">
|
<div class="player-box" :class="aspectRatio === '16:9' ? 'ratio-169' : 'ratio-43'">
|
||||||
<div v-if="videoUrl" id="live-dplayer" class="player-inner" />
|
<div v-if="showPlayerCover" class="player-cover">
|
||||||
<div v-else class="player-empty">
|
<img
|
||||||
<el-icon class="is-loading" :size="32"><Loading /></el-icon>
|
v-if="liveDetail.coverImg"
|
||||||
<span>暂无拉流地址</span>
|
class="cover-img"
|
||||||
|
:src="liveDetail.coverImg"
|
||||||
|
alt="直播封面"
|
||||||
|
/>
|
||||||
|
<div class="cover-mask">
|
||||||
|
<el-tag :type="liveStatusTagType(liveStatus)" size="small">{{ liveStatusText(liveStatus) }}</el-tag>
|
||||||
|
<span v-if="liveStatus === 'NEW' && liveDetail.startTime" class="cover-tip">
|
||||||
|
开播:{{ formatTime(liveDetail.startTime) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="liveStatus === 'PAUSED'" class="cover-tip">主播暂时离开,请稍候</span>
|
||||||
|
<span v-else-if="liveStatus === 'ENDED'" class="cover-tip">直播已结束</span>
|
||||||
|
<span v-else-if="liveStatus === 'LIVING' && !videoUrl" class="cover-tip">等待拉流地址...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="shouldPlayStream" ref="hlsContainer" class="player-inner" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -112,8 +110,12 @@
|
|||||||
<div class="area-chat panel-box">
|
<div class="area-chat panel-box">
|
||||||
<el-tabs v-model="chatTab" class="panel-tabs fill-tabs">
|
<el-tabs v-model="chatTab" class="panel-tabs fill-tabs">
|
||||||
<el-tab-pane label="互动聊天" name="chat">
|
<el-tab-pane label="互动聊天" name="chat">
|
||||||
<div class="tab-body chat-placeholder">
|
<div class="tab-body chat-tab-body">
|
||||||
<el-empty :image-size="64" description="暂无聊天消息" />
|
<LiveChatPanel
|
||||||
|
:live-id="liveId"
|
||||||
|
:live-detail="liveDetail"
|
||||||
|
:active="chatTab === 'chat'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="在线用户" name="online">
|
<el-tab-pane label="在线用户" name="online">
|
||||||
@@ -237,8 +239,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import DPlayer from "dplayer";
|
import { getSetting } from "@/api/index";
|
||||||
import { DocumentCopy, Loading, Refresh } from "@element-plus/icons-vue";
|
|
||||||
import {
|
import {
|
||||||
endLive,
|
endLive,
|
||||||
getLiveInfo,
|
getLiveInfo,
|
||||||
@@ -246,16 +247,17 @@ import {
|
|||||||
liveUserList,
|
liveUserList,
|
||||||
startLive,
|
startLive,
|
||||||
} from "@/api/live";
|
} from "@/api/live";
|
||||||
import { getSetting } from "@/api/index";
|
|
||||||
import config from "@/config";
|
import config from "@/config";
|
||||||
|
import { DocumentCopy, Refresh } from "@element-plus/icons-vue";
|
||||||
import CommentReview from "./components/CommentReview.vue";
|
import CommentReview from "./components/CommentReview.vue";
|
||||||
import Coupon from "./components/Coupon.vue";
|
import Coupon from "./components/Coupon.vue";
|
||||||
|
import LiveChatPanel from "./components/LiveChatPanel.vue";
|
||||||
import LiveGoods from "./components/LiveGoods.vue";
|
import LiveGoods from "./components/LiveGoods.vue";
|
||||||
import LivePurchaseRecords from "./components/LivePurchaseRecords.vue";
|
import LivePurchaseRecords from "./components/LivePurchaseRecords.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "live-control-panel",
|
name: "live-control-panel",
|
||||||
components: { CommentReview, Coupon, LiveGoods, LivePurchaseRecords, DocumentCopy, Loading, Refresh },
|
components: { CommentReview, Coupon, LiveChatPanel, LiveGoods, LivePurchaseRecords, DocumentCopy, Refresh },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -275,15 +277,31 @@ export default {
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
},
|
},
|
||||||
player: null,
|
|
||||||
nowTick: Date.now(),
|
nowTick: Date.now(),
|
||||||
durationTimer: null,
|
durationTimer: null,
|
||||||
|
pollTimer: null,
|
||||||
|
hlsHttpFallbackUsed: false,
|
||||||
|
lastHlsPullUrl: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.hlsInstance = null;
|
||||||
|
this.hlsVideoElement = null;
|
||||||
|
this._hlsVideoClickHandler = null;
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
liveStatus() {
|
liveStatus() {
|
||||||
return this.liveDetail.liveStatus || this.liveDetail.status || "";
|
return this.liveDetail.liveStatus || this.liveDetail.status || "";
|
||||||
},
|
},
|
||||||
|
isLiving() {
|
||||||
|
return this.liveStatus === "LIVING";
|
||||||
|
},
|
||||||
|
shouldPlayStream() {
|
||||||
|
return this.isLiving && !!this.videoUrl;
|
||||||
|
},
|
||||||
|
showPlayerCover() {
|
||||||
|
return !this.shouldPlayStream;
|
||||||
|
},
|
||||||
cumulativeViewers() {
|
cumulativeViewers() {
|
||||||
return Number(this.liveDetail.actualViewNumber ?? 0);
|
return Number(this.liveDetail.actualViewNumber ?? 0);
|
||||||
},
|
},
|
||||||
@@ -318,10 +336,19 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
videoUrl() {
|
videoUrl() {
|
||||||
this.initPlayer();
|
this.syncH5Player();
|
||||||
|
},
|
||||||
|
isLiving() {
|
||||||
|
this.syncH5Player();
|
||||||
|
if (this.isLiving) {
|
||||||
|
this.startPolling();
|
||||||
|
} else {
|
||||||
|
this.stopPolling();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
aspectRatio() {
|
aspectRatio() {
|
||||||
this.$nextTick(() => this.initPlayer());
|
this.lastHlsPullUrl = "";
|
||||||
|
this.$nextTick(() => this.syncH5Player());
|
||||||
},
|
},
|
||||||
chatTab(tab) {
|
chatTab(tab) {
|
||||||
if (tab === "online") this.loadOnlineUsers();
|
if (tab === "online") this.loadOnlineUsers();
|
||||||
@@ -341,7 +368,8 @@ export default {
|
|||||||
this.startDurationTimer();
|
this.startDurationTimer();
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.destroyPlayer();
|
this.destroyH5HlsPlayer();
|
||||||
|
this.stopPolling();
|
||||||
this.stopDurationTimer();
|
this.stopDurationTimer();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -385,55 +413,208 @@ export default {
|
|||||||
return Number(val || 0).toFixed(2);
|
return Number(val || 0).toFixed(2);
|
||||||
},
|
},
|
||||||
liveStatusText(status) {
|
liveStatusText(status) {
|
||||||
const map = { NEW: "未开始", LIVING: "直播中", ENDED: "已结束" };
|
const map = { NEW: "未开始", LIVING: "直播中", PAUSED: "已暂停", ENDED: "已结束" };
|
||||||
return map[status] || "未知";
|
return map[status] || "未知";
|
||||||
},
|
},
|
||||||
liveStatusTagType(status) {
|
liveStatusTagType(status) {
|
||||||
const map = { NEW: "info", LIVING: "success", ENDED: "warning" };
|
const map = { NEW: "info", LIVING: "success", PAUSED: "warning", ENDED: "warning" };
|
||||||
return map[status] || "info";
|
return map[status] || "info";
|
||||||
},
|
},
|
||||||
|
startPolling() {
|
||||||
|
this.stopPolling();
|
||||||
|
if (!this.isLiving) return;
|
||||||
|
this.pollTimer = setInterval(() => {
|
||||||
|
this.loadDetail(false);
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
stopPolling() {
|
||||||
|
if (this.pollTimer) {
|
||||||
|
clearInterval(this.pollTimer);
|
||||||
|
this.pollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
refreshAll() {
|
refreshAll() {
|
||||||
this.loadDetail();
|
this.loadDetail();
|
||||||
this.loadOrderStats();
|
this.loadOrderStats();
|
||||||
this.$refs.purchaseRecords?.refresh();
|
this.$refs.purchaseRecords?.refresh();
|
||||||
if (this.chatTab === "online") this.loadOnlineUsers();
|
if (this.chatTab === "online") this.loadOnlineUsers();
|
||||||
},
|
},
|
||||||
destroyPlayer() {
|
buildHlsPlayUrl(url, useHttpFallback = false) {
|
||||||
if (this.player) {
|
if (!url) return "";
|
||||||
this.player.destroy();
|
const httpUrl =
|
||||||
this.player = null;
|
useHttpFallback && url.startsWith("https://")
|
||||||
}
|
? url.replace("https://", "http://")
|
||||||
|
: url;
|
||||||
|
return httpUrl.replace(".flv", ".m3u8");
|
||||||
},
|
},
|
||||||
initPlayer() {
|
syncH5Player(retry = 0) {
|
||||||
this.destroyPlayer();
|
const shouldPlay = this.shouldPlayStream;
|
||||||
if (!this.videoUrl) return;
|
if (!shouldPlay) {
|
||||||
|
this.destroyH5HlsPlayer();
|
||||||
|
this.lastHlsPullUrl = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.videoUrl === this.lastHlsPullUrl &&
|
||||||
|
this.hlsInstance &&
|
||||||
|
this.hlsVideoElement &&
|
||||||
|
!this.hlsVideoElement.paused
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.videoUrl === this.lastHlsPullUrl && this.hlsInstance) {
|
||||||
|
this.ensureH5VideoPlaying(this.hlsVideoElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastHlsPullUrl = this.videoUrl;
|
||||||
|
this.hlsHttpFallbackUsed = false;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const container = document.getElementById("live-dplayer");
|
const container = this.$refs.hlsContainer;
|
||||||
if (!container) return;
|
if (!container) {
|
||||||
this.player = new DPlayer({
|
if (retry < 8) {
|
||||||
container,
|
setTimeout(() => this.syncH5Player(retry + 1), 80);
|
||||||
live: this.liveStatus === "LIVING",
|
}
|
||||||
autoplay: true,
|
return;
|
||||||
video: {
|
}
|
||||||
url: this.videoUrl,
|
this.initH5HlsPlayer();
|
||||||
type: "auto",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadDetail() {
|
ensureH5VideoPlaying(video) {
|
||||||
this.loading = true;
|
if (!video) return Promise.resolve();
|
||||||
|
const tryPlay = (muted) => {
|
||||||
|
video.muted = muted;
|
||||||
|
return video.play().catch(() => Promise.reject());
|
||||||
|
};
|
||||||
|
return tryPlay(false)
|
||||||
|
.catch(() => tryPlay(true))
|
||||||
|
.catch(() => {
|
||||||
|
console.warn("[HLS] 自动播放被阻止,请点击画面播放");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async initH5HlsPlayer(useHttpFallback = false) {
|
||||||
|
const originalUrl = this.videoUrl;
|
||||||
|
const container = this.$refs.hlsContainer;
|
||||||
|
if (!originalUrl || !container) return;
|
||||||
|
|
||||||
|
const url = this.buildHlsPlayUrl(originalUrl, useHttpFallback);
|
||||||
|
this.destroyH5HlsPlayer();
|
||||||
|
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.style.cssText = "width:100%;height:100%;object-fit:cover;background:#000;";
|
||||||
|
video.autoplay = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.defaultMuted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.setAttribute("autoplay", "");
|
||||||
|
video.setAttribute("muted", "");
|
||||||
|
video.setAttribute("playsinline", "");
|
||||||
|
video.setAttribute("webkit-playsinline", "");
|
||||||
|
video.controls = false;
|
||||||
|
|
||||||
|
const onVideoClick = () => {
|
||||||
|
if (video.muted) {
|
||||||
|
video.muted = false;
|
||||||
|
}
|
||||||
|
video.play().catch(() => {});
|
||||||
|
};
|
||||||
|
video.addEventListener("click", onVideoClick);
|
||||||
|
container.appendChild(video);
|
||||||
|
this.hlsVideoElement = video;
|
||||||
|
this._hlsVideoClickHandler = onVideoClick;
|
||||||
|
|
||||||
|
const onReadyPlay = () => {
|
||||||
|
this.ensureH5VideoPlaying(video);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Hls = null;
|
||||||
|
try {
|
||||||
|
const hlsModule = await import("hls.js");
|
||||||
|
Hls = hlsModule.default;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[HLS] 加载播放器失败:", error);
|
||||||
|
this.$Message.error("直播播放器加载失败,请刷新页面重试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
const hls = new Hls({
|
||||||
|
enableWorker: true,
|
||||||
|
lowLatencyMode: true,
|
||||||
|
liveSyncDurationCount: 3,
|
||||||
|
liveMaxLatencyDurationCount: 6,
|
||||||
|
maxBufferLength: 10,
|
||||||
|
maxMaxBufferLength: 30,
|
||||||
|
});
|
||||||
|
this.hlsInstance = hls;
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, onReadyPlay);
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, onReadyPlay);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.loadSource(url);
|
||||||
|
hls.on(Hls.Events.ERROR, (_event, data) => {
|
||||||
|
if (!data.fatal) return;
|
||||||
|
switch (data.type) {
|
||||||
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
if (!this.hlsHttpFallbackUsed && originalUrl.startsWith("https://")) {
|
||||||
|
console.warn("[HLS] HTTPS 加载失败,尝试 HTTP 降级...");
|
||||||
|
this.hlsHttpFallbackUsed = true;
|
||||||
|
this.destroyH5HlsPlayer();
|
||||||
|
this.$nextTick(() => this.initH5HlsPlayer(true));
|
||||||
|
} else {
|
||||||
|
console.error("[HLS] 网络错误,尝试重连...");
|
||||||
|
hls.startLoad();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
console.error("[HLS] 媒体错误,尝试恢复...");
|
||||||
|
hls.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("[HLS] 致命错误:", data);
|
||||||
|
this.destroyH5HlsPlayer();
|
||||||
|
this.$Message.error("直播加载失败");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
||||||
|
video.src = url;
|
||||||
|
video.addEventListener("loadedmetadata", onReadyPlay);
|
||||||
|
video.addEventListener("canplay", onReadyPlay);
|
||||||
|
} else {
|
||||||
|
this.$Message.error("当前浏览器不支持 HLS 播放");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyH5HlsPlayer() {
|
||||||
|
if (this.hlsInstance) {
|
||||||
|
this.hlsInstance.destroy();
|
||||||
|
this.hlsInstance = null;
|
||||||
|
}
|
||||||
|
if (this.hlsVideoElement) {
|
||||||
|
if (this._hlsVideoClickHandler) {
|
||||||
|
this.hlsVideoElement.removeEventListener("click", this._hlsVideoClickHandler);
|
||||||
|
this._hlsVideoClickHandler = null;
|
||||||
|
}
|
||||||
|
this.hlsVideoElement.remove();
|
||||||
|
this.hlsVideoElement = null;
|
||||||
|
}
|
||||||
|
const container = this.$refs.hlsContainer;
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadDetail(showLoading = true) {
|
||||||
|
if (showLoading) this.loading = true;
|
||||||
getLiveInfo(this.liveId)
|
getLiveInfo(this.liveId)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.loading = false;
|
if (showLoading) this.loading = false;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
const data = res.result || {};
|
const data = res.result || {};
|
||||||
this.liveDetail = data;
|
this.liveDetail = data;
|
||||||
this.videoUrl = data.pullStreamUrl || data.streamUrl || "";
|
this.videoUrl = data.pullStreamUrl || data.streamUrl || "";
|
||||||
|
this.syncH5Player();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.loading = false;
|
if (showLoading) this.loading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadOrderStats() {
|
loadOrderStats() {
|
||||||
@@ -503,7 +684,8 @@ export default {
|
|||||||
return endLive(this.liveId).then((res) => {
|
return endLive(this.liveId).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
this.$Message.success("直播已结束");
|
this.$Message.success("直播已结束");
|
||||||
this.destroyPlayer();
|
this.destroyH5HlsPlayer();
|
||||||
|
this.stopPolling();
|
||||||
this.refreshAll();
|
this.refreshAll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -611,6 +793,13 @@ export default {
|
|||||||
.area-chat,
|
.area-chat,
|
||||||
.area-stats {
|
.area-stats {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-chat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.area-manage {
|
.area-manage {
|
||||||
@@ -687,17 +876,37 @@ export default {
|
|||||||
.player-inner {
|
.player-inner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-empty {
|
.player-cover {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: #000;
|
||||||
|
|
||||||
|
.cover-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-mask {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
color: #909399;
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-tip {
|
||||||
|
color: #fff;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,12 +1083,14 @@ export default {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-placeholder {
|
.chat-tab-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
align-items: center;
|
padding: 12px 16px 12px;
|
||||||
justify-content: center;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
|
|||||||
38
manager/src/views/live/utils/GenerateTestUserSig.js
Normal file
38
manager/src/views/live/utils/GenerateTestUserSig.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import LibGenerateTestUserSig from './lib-generate-test-usersig-es.min.js';
|
||||||
|
/**
|
||||||
|
* Signature expiration time, which should not be too short
|
||||||
|
* Time unit: second
|
||||||
|
* Default time: 7 * 24 * 60 * 60 = 604800 = 7days
|
||||||
|
*/
|
||||||
|
const EXPIRETIME = 604800;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module: GenerateTestUserSig
|
||||||
|
*
|
||||||
|
* Description: Generates UserSig for testing. UserSig is a security signature designed by Tencent Cloud for its cloud services.
|
||||||
|
* It is calculated based on `SDKAppID`, `UserID`, and `EXPIRETIME` using the HMAC-SHA256 encryption algorithm.
|
||||||
|
*
|
||||||
|
* Attention: For the following reasons, do not use the code below in your commercial application.
|
||||||
|
*
|
||||||
|
* The code may be able to calculate UserSig correctly, but it is only for quick testing of the SDK’s basic features, not for commercial applications.
|
||||||
|
* `SECRETKEY` in client code can be easily decompiled and reversed, especially on web.
|
||||||
|
* Once your key is disclosed, attackers will be able to steal your Tencent Cloud traffic.
|
||||||
|
*
|
||||||
|
* The correct method is to deploy the `UserSig` calculation code and encryption key on your project server so that your application can request from your server a `UserSig` that is calculated whenever one is needed.
|
||||||
|
* Given that it is more difficult to hack a server than a client application, server-end calculation can better protect your key.
|
||||||
|
*
|
||||||
|
* Reference: https://cloud.tencent.com/document/product/647/17275#Server
|
||||||
|
*/
|
||||||
|
|
||||||
|
function genTestUserSig(options) {
|
||||||
|
const { SDKAppID, secretKey, userID } = options;
|
||||||
|
const sdkAppId = Number(SDKAppID);
|
||||||
|
const generator = new LibGenerateTestUserSig(sdkAppId, secretKey, EXPIRETIME);
|
||||||
|
const userSig = generator.genTestUserSig(userID);
|
||||||
|
return {
|
||||||
|
SDKAppID: sdkAppId,
|
||||||
|
userSig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { genTestUserSig, EXPIRETIME };
|
||||||
2
manager/src/views/live/utils/lib-generate-test-usersig-es.min.js
vendored
Normal file
2
manager/src/views/live/utils/lib-generate-test-usersig-es.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user