index-backup.vue 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289
  1. <template>
  2. <div>
  3. <!-- 直播中控台 start -->
  4. <el-row type="flex" justify="center" class="live-console" :gutter="10" v-loading="loading">>
  5. <!-- 聊天 start -->
  6. <el-col class="live-console-col" :span="6">
  7. <el-tabs class="live-console-tab-left" v-model="tabRight.activeName" @tab-click="handleClick" :stretch="true">
  8. <el-tab-pane label="讨论" name="talk">
  9. <el-scrollbar style="height: 500px; width: 100%;" ref="manageRightRef">
  10. <el-row v-for="m in msgList" :key="m.msgId">
  11. <el-row v-if="m.userId !== userId" style="margin-top: 5px" type="flex" align="top" >
  12. <el-col :span="3" style="margin-left: 10px"><el-avatar :src="m.avatar"/></el-col>
  13. <el-col :span="15">
  14. <el-row style="margin-left: 10px">
  15. <el-col><div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div></el-col>
  16. <el-col :span="24" style="max-width: 200px;">
  17. <div style="white-space: normal; word-wrap: break-word;background-color: #f0f2f5; padding: 8px; border-radius: 5px;font-size: 14px;width: 100%;">
  18. {{ m.msg }}
  19. </div>
  20. </el-col>
  21. <el-col>
  22. <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="changeUserState(m)">{{ m.msgStatus === 1 ? '解禁' : '禁言' }}</a>
  23. <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="blockUser(m)">拉黑</a>
  24. </el-col>
  25. </el-row>
  26. </el-col>
  27. </el-row>
  28. <el-row v-if="m.userId === userId" style="padding: 8px 0" type="flex" align="top" justify="end">
  29. <div style="display: flex;justify-content: flex-end">
  30. <div style="display: flex;justify-content: flex-end;flex-direction: column;max-width: 200px;align-items: flex-end">
  31. <div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div>
  32. <div style="white-space: normal; word-wrap: break-word;width: 100%; background-color: #e6f7ff; padding: 8px; border-radius: 5px;font-size: 14px;">{{ m.msg }}</div>
  33. </div>
  34. <el-avatar :src="m.avatar" style="margin-left: 10px; margin-right: 10px;"/>
  35. </div>
  36. </el-row>
  37. </el-row>
  38. <!-- 底部留白 -->
  39. <div style="height: 20px;"></div>
  40. </el-scrollbar>
  41. <!-- 消息输入区域 -->
  42. <div style="padding: 10px; border-top: 1px solid #ebeef5; background-color: #fff; min-height: 120px;">
  43. <el-input
  44. type="textarea"
  45. v-model="newMsg"
  46. placeholder="请输入消息..."
  47. :rows="8"
  48. @keyup.enter.native="sendMessage"
  49. clearable
  50. resize="none"
  51. style="flex: 1; margin-right: 10px;"
  52. >
  53. </el-input>
  54. <div style="display: flex; justify-content: flex-end; margin-top: 10px;">
  55. <el-button plain @click="sendMessage">发送</el-button>
  56. </div>
  57. </div>
  58. </el-tab-pane>
  59. </el-tabs>
  60. </el-col>
  61. <!-- 聊天 end -->
  62. <!-- 直播/视频 start -->
  63. <el-col class="live-console-col" :span="12">
  64. <div style="background: #000; border-radius: 5px; overflow: hidden; margin: 10px 5px;">
  65. <div style="border-radius: 5px; overflow: hidden;" v-if="!isAudit">
  66. <img :src="require('@/assets/images/videoIsAudit.png')" style="width: 100%; height: 45vh;">
  67. </div>
  68. <div style="border-radius: 5px; overflow: hidden;" v-else-if="status != 2 && status != 4">
  69. <img :src="require('@/assets/images/videoNotStart.png')" style="width: 100%; height: 45vh;">
  70. </div>
  71. <div style="border-radius: 5px; overflow: hidden;" v-else-if="liveType == 1">
  72. <video
  73. controls
  74. ref="livingPlayer"
  75. autoplay
  76. width="100%"
  77. @click.prevent
  78. @contextmenu.prevent
  79. class="custom-video"
  80. style="display: block; background: #000; height: 45vh;"
  81. ></video>
  82. <!-- 时间显示(可选) -->
  83. <div ref="liveElapsedTime" class="time-display">
  84. 已播放:<span id="liveElapsedTime">00:00:00</span>
  85. </div>
  86. </div>
  87. <div style="border-radius: 5px; overflow: hidden;" v-else-if="liveType == 2">
  88. <video
  89. controls
  90. ref="videoPlayer"
  91. loop
  92. autoplay
  93. width="100%"
  94. muted
  95. playsinline
  96. @click.prevent
  97. @contextmenu.prevent
  98. class="custom-video"
  99. style="display: block; background: #000; height: 40vh;"
  100. >
  101. <source :src="videoUrl" type="application/x-mpegURL">
  102. </video>
  103. <!-- 自定义进度条容器 -->
  104. <div ref="progressBar" class="progress-container">
  105. <div id="progressBar" class="progress-bar"></div>
  106. </div>
  107. <!-- 时间显示(可选) -->
  108. <div ref="elapsedTime" class="time-display">
  109. 已播放:<span id="elapsedTime">00:00:00</span>
  110. </div>
  111. </div>
  112. <div style="border-radius: 5px; overflow: hidden;" v-else-if="liveType == 3">
  113. <video
  114. controls
  115. ref="liveReplay"
  116. loop
  117. autoplay
  118. width="100%"
  119. playsinline
  120. style="display: block; background: #000; height: 40vh;"
  121. >
  122. <source :src="videoUrl" type="application/x-mpegURL">
  123. </video>
  124. </div>
  125. <div style="border-radius: 5px; overflow: hidden;" v-else>
  126. <img :src="require('@/assets/images/videoNotStart.png')" style="width: 100%; height: 45vh;">
  127. </div>
  128. </div>
  129. <!-- 底部导航栏 -->
  130. <div style="display: flex; justify-content: space-around; padding: 15px 0; background: #fff; border-top: 1px solid #f0f0f0;">
  131. <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickRed">
  132. <i class="el-icon-money" style="font-size: 20px;"></i>
  133. <span style="font-size: 12px; margin-top: 4px;">红包配置</span>
  134. </div>
  135. <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickLottery">
  136. <i class="el-icon-present" style="font-size: 20px;"></i>
  137. <span style="font-size: 12px; margin-top: 4px;">抽奖配置</span>
  138. </div>
  139. <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickGoods">
  140. <i class="el-icon-goods" style="font-size: 20px;"></i>
  141. <span style="font-size: 12px; margin-top: 4px;">商品</span>
  142. </div>
  143. <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickOrder">
  144. <i class="el-icon-goods" style="font-size: 20px;"></i>
  145. <span style="font-size: 12px; margin-top: 4px;">直播订单</span>
  146. </div>
  147. <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickCoupon">
  148. <i class="el-icon-goods" style="font-size: 20px;"></i>
  149. <span style="font-size: 12px; margin-top: 4px;">直播优惠券</span>
  150. </div>
  151. </div>
  152. <el-radio-group v-model="tableRadio" >
  153. <el-radio-button label="订单数">订单数</el-radio-button>
  154. </el-radio-group>
  155. <div style="position: relative;width: 100%; height: 300px;">
  156. <div ref="chartContainer" style="width: 100%; height: 100%;"></div>
  157. <div style="position: absolute; top: 10px; right: 10px; background: #fff; padding: 5px; z-index: 1;">
  158. <el-select v-model="searchQuery.timeOptions" placeholder="请选择" style="width: 150px" @change="timeChange">
  159. <el-option
  160. v-for="item in timeOptions"
  161. :key="item.value"
  162. :label="item.label"
  163. :value="item.value">
  164. </el-option>
  165. </el-select>
  166. <el-select v-model="searchQuery.timeGranularity" placeholder="请选择" style="width: 150px" @change="timeGranularityChange">
  167. <el-option
  168. v-for="item in timeGranularity"
  169. :key="item.value"
  170. :label="item.label"
  171. :value="item.value">
  172. </el-option>
  173. </el-select>
  174. <!-- <el-button type="primary" @click="applyFilter">搜索</el-button>-->
  175. </div>
  176. </div>
  177. </el-col>
  178. <!-- 直播/视频 end -->
  179. <!-- 用户列表 start -->
  180. <el-col class="live-console-col" :span="6">
  181. <el-tabs class="live-console-tab-right" v-model="tabLeft.activeName" @tab-click="handleClick" :stretch="true">
  182. <el-tab-pane :label="onlineLabel" name="online">
  183. <el-scrollbar ref="manageLeftRef_online" style="height: 800px; width: 100%;">
  184. <el-row style="margin-top: 10px" type="flex" align="middle" v-for="u in onlineDisplayList" :key="u.userId">
  185. <el-col :span="20">
  186. <el-row type="flex" align="middle">
  187. <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
  188. <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
  189. <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
  190. </el-row>
  191. </el-col>
  192. <el-col :span="4" >
  193. <el-popover
  194. width="100"
  195. trigger="click">
  196. <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
  197. <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
  198. <i class="el-icon-more" slot="reference"></i>
  199. </el-popover>
  200. </el-col>
  201. </el-row>
  202. </el-scrollbar>
  203. </el-tab-pane>
  204. <el-tab-pane :label="offlineLabel" name="offline">
  205. <el-scrollbar ref="manageLeftRef_offline" style="height: 800px; width: 100%;">
  206. <el-row style="margin-top: 10px" type="flex" align="middle" v-for="u in offlineDisplayList" :key="u.userId">
  207. <el-col :span="20">
  208. <el-row type="flex" align="middle">
  209. <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
  210. <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
  211. <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
  212. </el-row>
  213. </el-col>
  214. <el-col :span="4" >
  215. <el-popover
  216. width="100"
  217. trigger="click">
  218. <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
  219. <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
  220. <i class="el-icon-more" slot="reference"></i>
  221. </el-popover>
  222. </el-col>
  223. </el-row>
  224. </el-scrollbar>
  225. </el-tab-pane>
  226. <el-tab-pane :label="silencedUserLabel" name="silenced">
  227. <el-scrollbar ref="manageLeftRef_silenced" style="height: 800px; width: 100%;">
  228. <el-row style="margin-top: 10px" type="flex" align="middle" v-for="u in silencedDisplayList" :key="u.userId">
  229. <el-col :span="20">
  230. <el-row type="flex" align="middle">
  231. <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
  232. <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
  233. <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
  234. </el-row>
  235. </el-col>
  236. <el-col :span="4" >
  237. <el-popover
  238. width="100"
  239. trigger="click">
  240. <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
  241. <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
  242. <i class="el-icon-more" slot="reference"></i>
  243. </el-popover>
  244. </el-col>
  245. </el-row>
  246. </el-scrollbar>
  247. </el-tab-pane>
  248. </el-tabs>
  249. </el-col>
  250. <!-- 用户列表 end -->
  251. </el-row>
  252. <!-- 直播中控台 end -->
  253. </div>
  254. </template>
  255. <script>
  256. import { blockUser,changeUserStatus,getLiveUserTotals, watchUserList } from '@/api/live/liveWatchUser'
  257. import { getLiveVideoByLiveId } from '@/api/live/liveVideo'
  258. import {getLive, getLivingUrl} from '@/api/live/live'
  259. import { getLiveOrderTimeGranularity } from '@/api/live/liveOrder'
  260. import { listLiveMsg } from '@/api/live/liveMsg'
  261. import Hls from 'hls.js';
  262. import LiveLotteryConf from '@/views/live/liveConfig/liveLotteryConf.vue'
  263. import LiveRedConf from '@/views/live/liveConfig/liveRedConf.vue'
  264. import LiveGoods from '@/views/live/liveConfig/goods.vue'
  265. import LiveLiveCoupon from '@/views/live/liveConfig/liveCoupon.vue'
  266. import echarts from 'echarts'
  267. export default {
  268. name: "LiveConsole",
  269. components: { LiveLotteryConf,LiveRedConf },
  270. data() {
  271. return {
  272. loading:true,
  273. tabLeft: {
  274. activeName: "online",
  275. },
  276. tabRight: {
  277. activeName: "talk",
  278. },
  279. livingUrl:"",
  280. videoUrl: "",
  281. status: 0,
  282. loadMsgMaxPage: 2,
  283. liveVideo: {},
  284. liveWsUrl: process.env.VUE_APP_LIVE_WS_URL + '/app/webSocket',
  285. userParams:{
  286. pageNum: 1,
  287. pageSize: 10,
  288. liveId: null
  289. },
  290. msgParams: {
  291. pageNum: 1,
  292. pageSize: 10,
  293. liveId: null
  294. },
  295. userList: [],
  296. msgList: [],
  297. newMsg: '',
  298. isAudit: false,
  299. myChart: null, // 用于存储 ECharts 实例
  300. liveType: 1,
  301. tableRadio: '订单数',
  302. searchQuery: {timeOptions:'2',timeGranularity:'10',liveId: null},
  303. timeOptions: [
  304. {value:'2',label:'最近2小时',key:'2'},
  305. {value:'4',label:'最近4小时',key:'4'},
  306. {value:'all',label:'全场',key:'all'},
  307. ],
  308. timeGranularity: [
  309. {value:'10',label:'10分钟',key:'10'},
  310. {value:'30',label:'30分钟',key:'30'},
  311. {value:'60',label:'1小时',key:'60'},
  312. ],
  313. videoDuration: 0,
  314. startTime: null,
  315. processInterval: null,
  316. // ... 其他数据
  317. chatScrollTop: 0, // 保存聊天滚动位置
  318. socket: null,
  319. userTotal: {
  320. online: 0, // 在线总人数
  321. offline: 0, // 离线总人数
  322. silenced: 0 // 禁言总人数
  323. },
  324. // 各Tab的显示列表(仅存储当前需要展示的数据)
  325. onlineDisplayList: [], // 在线用户显示列表
  326. offlineDisplayList: [], // 离线用户显示列表
  327. silencedDisplayList: [], // 禁言用户显示列表
  328. // 各Tab的分页参数
  329. pageParams: {
  330. online: {
  331. currentPage: 1, // 当前页(下一页加载用)
  332. pageSize: 20, // 当前页(下一页加载用)
  333. prevPage: 0, // 上一页页码(上一页加载用)
  334. totalLoaded: 0, // 已加载总条数
  335. total: 0, // 总数据量
  336. hasMore: true, // 是否有下一页
  337. hasPrev: false // 是否有上一页
  338. },
  339. offline: {
  340. currentPage: 1,
  341. pageSize: 20,
  342. prevPage: 0,
  343. totalLoaded: 0,
  344. total: 0,
  345. hasMore: true,
  346. hasPrev: false
  347. },
  348. silenced: {
  349. currentPage: 1,
  350. pageSize: 20,
  351. prevPage: 0,
  352. totalLoaded: 0,
  353. total: 0,
  354. hasMore: true,
  355. hasPrev: false
  356. }
  357. },
  358. scrLoading: {
  359. online: { next: false, prev: false },
  360. offline: { next: false, prev: false },
  361. silenced: { next: false, prev: false }
  362. }
  363. }
  364. },
  365. created() {
  366. // this.getLiveVideo()
  367. this.getList()
  368. this.connectWebSocket()
  369. this.getLive()
  370. this.searchQuery.liveId = this.liveId
  371. },
  372. computed: {
  373. liveId() {
  374. return this.$route.params.liveId;
  375. },
  376. userId() {
  377. return this.$store.state.user.user.userId
  378. },
  379. companyId() {
  380. return this.$store.state.user.user.companyId
  381. },
  382. onlineLabel() {
  383. return `在线(${this.userTotal.online})`;
  384. },
  385. offlineLabel() {
  386. return `离线(${this.userTotal.offline})`;
  387. },
  388. silencedUserLabel() {
  389. return `禁言(${this.userTotal.silenced})`;
  390. }
  391. },
  392. mounted() {
  393. this.$nextTick(() => {
  394. this.restoreChatScrollPosition();
  395. });
  396. this.getEchartsTables();
  397. // 添加滚动事件监听器
  398. this.$nextTick(() => {
  399. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  400. this.$refs.manageRightRef.wrap.addEventListener('scroll', this.saveChatScrollPosition);
  401. }
  402. });
  403. this.initScrollListeners();
  404. },
  405. beforeDestroy() {
  406. this.saveTabScrollPositions()
  407. // 移除滚动监听(避免内存泄漏)
  408. const scrollRefs = {
  409. online: this.$refs.manageLeftRef_online,
  410. offline: this.$refs.manageLeftRef_offline,
  411. silenced: this.$refs.manageLeftRef_silenced
  412. };
  413. Object.keys(scrollRefs).forEach(tabName => {
  414. const scrollEl = scrollRefs[tabName]?.wrap;
  415. if (scrollEl) {
  416. scrollEl.removeEventListener('scroll', () =>
  417. this.handleTabScroll(tabName, scrollEl)
  418. );
  419. }
  420. })
  421. },
  422. // 使用 deactivated 和 activated 钩子替代 beforeDestroy 和 destroyed
  423. deactivated() {
  424. this.saveChatScrollPosition();
  425. },
  426. activated() {
  427. this.$nextTick(() => {
  428. this.restoreChatScrollPosition();
  429. });
  430. this.$nextTick(() => {
  431. const video = this.$refs.videoPlayer;
  432. if (video != null) {
  433. this.initVideoPlayer(this.liveInfo.startTime)
  434. }
  435. })
  436. },
  437. methods: {
  438. updateVideoPosition(video){
  439. const currentTime = new Date().getTime(); // 当前时间戳(毫秒)
  440. const elapsedTime = currentTime - this.startTime; // 已流逝时间(毫秒)
  441. if (elapsedTime < 0) {
  442. // 未开播:视频停在初始位置
  443. video.currentTime = 0;
  444. return;
  445. }
  446. // 已开播:计算视频循环后的位置(流逝时间 % 视频时长)
  447. const elapsedSeconds = elapsedTime / 1000; // 转换为秒
  448. const videoPosition = elapsedSeconds % this.videoDuration; // 视频内的播放位置(秒)
  449. // 设置视频播放位置
  450. video.currentTime = videoPosition;
  451. },
  452. updateProgress() {
  453. const progressBar = this.$refs.progressBar;
  454. const elapsedTimeEl = this.$refs.elapsedTime;
  455. if (!this.videoDuration) return; // 视频时长未加载时不更新
  456. const currentTime = new Date().getTime();
  457. const elapsedTime = currentTime - this.startTime; // 总流逝时间(毫秒)
  458. if (elapsedTime < 0) {
  459. // 未开播状态
  460. progressBar.style.width = '0%';
  461. elapsedTimeEl.textContent = '00:00:00';
  462. return;
  463. }
  464. // 计算进度百分比(基于视频循环)
  465. const elapsedSeconds = elapsedTime / 1000;
  466. const videoPosition = elapsedSeconds % this.videoDuration; // 当前在视频中的位置
  467. const progressPercent = (videoPosition / this.videoDuration) * 100; // 进度百分比
  468. // 更新进度条宽度
  469. progressBar.style.width = `${progressPercent}%`;
  470. // 格式化总流逝时间为“时:分:秒”并显示
  471. elapsedTimeEl.textContent = this.formatTime(elapsedTime);
  472. },
  473. formatTime(ms) {
  474. const totalSeconds = Math.floor(ms / 1000);
  475. const hours = Math.floor(totalSeconds / 3600);
  476. const minutes = Math.floor((totalSeconds % 3600) / 60);
  477. const seconds = totalSeconds % 60;
  478. // 补零处理(确保两位数)
  479. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  480. },
  481. initVideoPlayer: function (startTime) {
  482. const video = this.$refs.videoPlayer;
  483. this.hls = new Hls();
  484. this.hls.attachMedia(video);
  485. this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
  486. this.hls.loadSource(this.videoUrl);
  487. this.hls.on(Hls.Events.STREAM_LOADED, (event, data) => {
  488. video.play();
  489. });
  490. });
  491. this.hls.on(Hls.Events.ERROR, (event, data) => {
  492. console.error('HLS 错误:', data);
  493. });
  494. // 1. 初始化开播时间
  495. startTime = new Date(startTime).getTime();
  496. this.startTime = startTime;
  497. // 2. 监听视频元数据加载完成(获取视频时长)
  498. video.addEventListener('loadedmetadata', () => {
  499. this.videoDuration = video.duration; // 获取视频时长(秒)
  500. // 初始化视频播放位置
  501. this.updateVideoPosition(video);
  502. // 启动实时进度更新(每秒刷新一次)
  503. this.processInterval = setInterval(this.updateProgress, 1000);
  504. });
  505. },
  506. // 保存聊天滚动位置
  507. saveChatScrollPosition() {
  508. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  509. this.chatScrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight;
  510. }
  511. },
  512. // 恢复聊天滚动位置
  513. restoreChatScrollPosition() {
  514. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  515. this.$refs.manageRightRef.wrap.scrollTop = this.chatScrollTop;
  516. }
  517. },
  518. // ... 其他方法 ...
  519. timeChange(val) {
  520. this.searchQuery.timeOptions = val
  521. this.getEchartsTables(this.searchQuery)
  522. this.initChart()
  523. },
  524. timeGranularityChange(val) {
  525. this.searchQuery.timeGranularity = val
  526. this.getEchartsTables()
  527. this.initChart()
  528. },
  529. getEchartsTables() {
  530. getLiveOrderTimeGranularity(this.searchQuery).then(res => {
  531. if (res.code == 200) {
  532. this.echartsXLine = res.hourlySlots
  533. this.echartsXValue = res.hourlySlotsValue
  534. this.initChart()
  535. }
  536. })
  537. },
  538. initChart() {
  539. const chartDom = this.$refs.chartContainer;
  540. this.myChart = echarts.init(chartDom);
  541. const option = {
  542. tooltip: {trigger: 'axis'},
  543. legend: {data: ['订单数']},
  544. xAxis: {type: 'category', boundaryGap: false, data: this.echartsXLine},
  545. yAxis: {type: 'value'},
  546. series: [
  547. {name: '订单数', type: 'line', data: this.echartsXValue}
  548. ],
  549. };
  550. this.myChart.setOption(option);
  551. },
  552. handleClickGoods(){
  553. this.$router.push({
  554. name: 'LiveGoods',
  555. query: {
  556. liveId: this.liveId
  557. }
  558. })
  559. },
  560. updateLiveProgress() {
  561. const elapsedTimeEl = this.$refs.liveElapsedTime;
  562. const currentTime = new Date().getTime();
  563. const elapsedTime = currentTime - this.startTime; // 总流逝时间(毫秒)
  564. if (elapsedTime < 0) {
  565. elapsedTimeEl.textContent = '00:00:00';
  566. return;
  567. }
  568. // 格式化总流逝时间为“时:分:秒”并显示
  569. elapsedTimeEl.textContent = this.formatTime(elapsedTime);
  570. },
  571. handleClickOrder(){
  572. this.$router.push({
  573. name: 'LiveOrder',
  574. query: {
  575. liveId: this.liveId
  576. }
  577. })
  578. },
  579. handleClickCoupon(){
  580. this.$router.push({
  581. name: 'LiveLiveCoupon',
  582. query: {
  583. liveId: this.liveId
  584. }
  585. })
  586. },
  587. getLive(){
  588. getLive(this.liveId).then(res => {
  589. if (res.code == 200) {
  590. if (res.data.isAudit != 1) {
  591. this.$message.error("当前直播间未经审核");
  592. this.loading = false
  593. return
  594. }
  595. this.isAudit = true
  596. this.status = res.data.status
  597. if(res.data.status == 4){
  598. this.liveType = 3
  599. this.videoUrl = res.data.videoUrl;
  600. }else {
  601. if (res.data.status != 2) {
  602. this.$message.error("当前直播间未直播");
  603. this.loading = false
  604. return
  605. }
  606. if (res.data.liveType == 1) {
  607. this.livingUrl = res.data.flvHlsUrl
  608. this.livingUrl = this.livingUrl.replace("flv","m3u8")
  609. this.$nextTick(() => {
  610. this.initPlayer()
  611. })
  612. this.startTime = new Date(res.data.startTime).getTime()
  613. this.processInterval = setInterval(this.updateLiveProgress, 1000);
  614. } else {
  615. this.liveType = 2
  616. this.videoUrl = res.data.videoUrl;
  617. this.$nextTick(() => {
  618. this.initVideoPlayer(res.data.startTime)
  619. })
  620. }
  621. }
  622. this.loading = false
  623. } else {
  624. this.$message.error(res.msg)
  625. this.loading = false
  626. }
  627. this.liveInfo = res.data
  628. })
  629. },
  630. handleClickLottery(){
  631. this.$router.push({
  632. name: 'LiveLotteryConf',
  633. query: {
  634. liveId: this.liveId
  635. }
  636. })
  637. },
  638. handleClickRed(){
  639. this.$router.push({
  640. name: 'LiveRedConf',
  641. query: {
  642. liveId: this.liveId
  643. }
  644. })
  645. },
  646. getLiveUrl(){
  647. getLivingUrl(this.liveId).then(res=>{
  648. if(res.code === 200){
  649. this.livingUrl = res.livingUrl
  650. this.initPlayer()
  651. }
  652. })
  653. },
  654. initPlayer(){
  655. var isUrl = this.livingUrl === null || this.livingUrl.trim() === ''
  656. if (isUrl) {
  657. console.error('直播地址为空,无法初始化播放器')
  658. return
  659. }
  660. if (Hls.isSupported() && !isUrl) {
  661. const videoElement = this.$refs.livingPlayer
  662. if (!videoElement) {
  663. console.error('找不到 video 元素')
  664. return
  665. }
  666. this.hls = new Hls();
  667. this.hls.attachMedia(videoElement);
  668. this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
  669. this.hls.loadSource(this.livingUrl);
  670. this.hls.on(Hls.Events.STREAM_LOADED, (event, data) => {
  671. videoElement.play();
  672. });
  673. });
  674. this.hls.on(Hls.Events.ERROR, (event, data) => {
  675. console.error('HLS 错误:', data);
  676. });
  677. } else {
  678. console.error('浏览器不支持 HLS')
  679. }
  680. },
  681. handleClick(tab) {
  682. const tabName = tab.name;
  683. const params = this.pageParams[tabName];
  684. const displayList = this[`${tabName}DisplayList`];
  685. // 首次切换到该Tab或列表为空时初始化
  686. if (displayList.length < 20) {
  687. // 重置分页参数
  688. params.currentPage = 1;
  689. params.pageSize = 20;
  690. params.prevPage = 0;
  691. params.totalLoaded = 0;
  692. params.hasMore = true;
  693. params.hasPrev = false;
  694. // 加载第一页
  695. this.loadNextPage(tabName);
  696. } else {
  697. // 非首次切换,恢复滚动位置
  698. this.$nextTick(() => {
  699. const scrollEl = this.getScrollElement(tabName);
  700. if (scrollEl) {
  701. scrollEl.scrollTop = this.tabScrollPositions[tabName] || 0;
  702. }
  703. });
  704. }
  705. },
  706. saveTabScrollPositions() {
  707. this.tabScrollPositions = {
  708. online: this.getScrollElement('online')?.scrollTop || 0,
  709. offline: this.getScrollElement('offline')?.scrollTop || 0,
  710. silenced: this.getScrollElement('silenced')?.scrollTop || 0
  711. };
  712. },
  713. // 加载指定Tab的用户列表(核心加载逻辑)
  714. loadNextPage(tabName) {
  715. const params = this.pageParams[tabName];
  716. const displayList = this[`${tabName}DisplayList`];
  717. console.log(`加载 ${tabName} 用户列表`)
  718. console.log(!params.hasMore || this.scrLoading[tabName].next)
  719. console.log(params.currentPage)
  720. // 若没有更多数据或正在加载,直接返回
  721. if (!params.hasMore || this.scrLoading[tabName].next) {
  722. return;
  723. }
  724. this.scrLoading[tabName].next = true;
  725. const queryParams = {
  726. liveId: this.liveId,
  727. pageNum: params.currentPage,
  728. pageSize: 20,
  729. online: tabName === 'online' ? 0 : 1,
  730. msgStatus: tabName === 'silenced' ? 1 : 0
  731. };
  732. // 调用接口加载对应状态的分页数据(需后端支持按状态筛选)
  733. watchUserList(queryParams).then(response => {
  734. this.scrLoading[tabName].next = false;
  735. if (response.code !== 200) return;
  736. const { rows, total } = response;
  737. params.total = total; // 记录总数据量
  738. // 过滤重复数据(基于userId)
  739. const newRows = rows.filter(row =>
  740. !displayList.some(u => u.userId === row.userId)
  741. );
  742. displayList.push(...newRows)
  743. // 添加新数据并限制最大长度(避免内存占用过大)
  744. if (displayList.length >= 40) { // 最大保留100条
  745. this[`${tabName}DisplayList`] = displayList.slice(-40);
  746. // 记录滚动位置(用于加载后校准)
  747. const scrollEl = this.getScrollElement(tabName);
  748. // 校准滚动位置(保持视觉连续性)
  749. this.$nextTick(() => {
  750. if (scrollEl) {
  751. scrollEl.scrollTop = scrollEl.scrollHeight * 0.5;
  752. }
  753. });
  754. }
  755. // 更新分页状态
  756. params.hasMore = params.currentPage * params.pageSize < total;
  757. params.currentPage += 1;
  758. params.hasPrev = params.currentPage > 2; // 当前页>2时一定有上一页
  759. params.prevPage = params.currentPage - 2;
  760. }).catch(() => {
  761. this.scrLoading[tabName].next = false;
  762. });
  763. },
  764. // 新增:加载上一页(向上滚动时)
  765. loadPrevPage(tabName) {
  766. const params = this.pageParams[tabName];
  767. const displayList = this[`${tabName}DisplayList`];
  768. // 边界校验:无上一页/正在加载/当前页<=1
  769. console.log(`加载 ${tabName} 上一页`);
  770. console.log(!params.hasPrev || this.scrLoading[tabName].prev || params.currentPage <= 1)
  771. if (!params.hasPrev || this.scrLoading[tabName].prev || params.currentPage <= 1) {
  772. return;
  773. }
  774. this.scrLoading[tabName].prev = true;
  775. const targetPage = params.prevPage > 0 ? params.prevPage : params.currentPage - 2;
  776. const queryParams = {
  777. liveId: this.liveId,
  778. pageNum: targetPage,
  779. pageSize: 20,
  780. online: tabName === 'online' ? 0 : 1,
  781. msgStatus: tabName === 'silenced' ? 1 : 0
  782. };
  783. watchUserList(queryParams).then(response => {
  784. this.scrLoading[tabName].prev = false;
  785. if (response.code !== 200) return;
  786. const { rows } = response;
  787. if (rows.length === 0) {
  788. params.hasPrev = false;
  789. return;
  790. }
  791. // 记录滚动位置(用于加载后校准)
  792. const scrollEl = this.getScrollElement(tabName);
  793. const scrollTop = scrollEl?.scrollTop || 0;
  794. const itemHeight = 80; // 预估行高(根据实际样式调整)
  795. const newItemsHeight = rows.length * itemHeight;
  796. // 过滤重复数据并添加到列表头部
  797. const newRows = rows.filter(row => !displayList.some(u => u.userId === row.userId));
  798. this[`${tabName}DisplayList`] = [...newRows, ...displayList];
  799. params.totalLoaded += newRows.length;
  800. // 限制最大长度
  801. if (this[`${tabName}DisplayList`].length > 40) {
  802. this[`${tabName}DisplayList`] = this[`${tabName}DisplayList`].slice(0, 40);
  803. }
  804. // 更新分页状态
  805. params.prevPage = targetPage - 1;
  806. params.hasPrev = targetPage > 1; // 上一页页码>1时还有更多上一页
  807. params.currentPage = params.currentPage - 1;
  808. if(params.currentPage * 20 < params.total) params.hasMore = true;
  809. // 校准滚动位置(保持视觉连续性)
  810. this.$nextTick(() => {
  811. if (scrollEl) {
  812. scrollEl.scrollTop = scrollEl.scrollHeight * 0.5;
  813. }
  814. });
  815. }).catch(() => {
  816. this.scrLoading[tabName].prev = false;
  817. });
  818. },
  819. // 辅助:获取Tab对应的滚动容器
  820. getScrollElement(tabName) {
  821. const scrollRefs = {
  822. online: this.$refs.manageLeftRef_online,
  823. offline: this.$refs.manageLeftRef_offline,
  824. silenced: this.$refs.manageLeftRef_silenced
  825. };
  826. return scrollRefs[tabName]?.wrap;
  827. },
  828. getLiveVideo() {
  829. getLiveVideoByLiveId(this.liveId).then(res => {
  830. this.liveVideo = res.data
  831. })
  832. },
  833. getList() {
  834. this.resetParams()
  835. // this.loadUserList()
  836. this.loadUserTotals(); // 先加载总人数
  837. // this.handleClick('online')
  838. this.handleClick({name:'online'})
  839. this.loadMsgList()
  840. },
  841. loadUserTotals() {
  842. if (!this.liveId) return;
  843. // 假设后端提供一个接口返回总人数(如果没有,可通过首次加载全量数据后统计)
  844. getLiveUserTotals({ liveId: this.liveId }).then(res => {
  845. if (res.code === 200) {
  846. this.userTotal = res.data; // { online, offline, silenced }
  847. }
  848. });
  849. },
  850. resetParams() {
  851. // 重置各Tab的显示列表和分页参数
  852. this.onlineDisplayList = [];
  853. this.offlineDisplayList = [];
  854. this.silencedDisplayList = [];
  855. this.pageParams = {
  856. online: {
  857. currentPage: 1, // 当前页(下一页加载用)
  858. pageSize: 20, // 当前页(下一页加载用)
  859. prevPage: 0, // 上一页页码(上一页加载用)
  860. totalLoaded: 0, // 已加载总条数
  861. total: 0, // 总数据量
  862. hasMore: true, // 是否有下一页
  863. hasPrev: false // 是否有上一页
  864. },
  865. offline: {
  866. currentPage: 1,
  867. pageSize: 20,
  868. prevPage: 0,
  869. totalLoaded: 0,
  870. total: 0,
  871. hasMore: true,
  872. hasPrev: false
  873. },
  874. silenced: {
  875. currentPage: 1,
  876. pageSize: 20,
  877. prevPage: 0,
  878. totalLoaded: 0,
  879. total: 0,
  880. hasMore: true,
  881. hasPrev: false
  882. }
  883. };
  884. // 消息参数保留
  885. this.msgList = [];
  886. this.msgParams = {
  887. pageNum: 1,
  888. pageSize: 10,
  889. liveId: this.liveId
  890. };
  891. },
  892. loadUserList() {
  893. if(this.liveId == null) return
  894. // 直播间用户
  895. watchUserList({
  896. liveId: this.liveId,
  897. pageNum: this.userParams.pageNum,
  898. pageSize: this.userParams.pageSize
  899. }).then(response => {
  900. let {code,rows,total} = response
  901. if (code === 200) {
  902. let totalPage = (total % this.userParams.pageSize == 0) ? Math.floor(total / this.userParams.pageSize) : Math.floor(total / this.userParams.pageSize + 1);
  903. rows.forEach(row => {
  904. if (!this.userList.some(u => u.userId === row.userId)) {
  905. this.userList.push(row)
  906. }
  907. })
  908. // 没加载完继续加载
  909. if (this.userParams.pageNum < totalPage) {
  910. this.userParams.pageNum = parseInt(this.userParams.pageNum) + 1;
  911. this.loadUserList()
  912. }
  913. }
  914. })
  915. },
  916. loadMsgList() {
  917. // 直播间消息
  918. listLiveMsg({
  919. liveId:this.liveId,
  920. pageNum: this.msgParams.pageNum,
  921. pageSize: this.msgParams.pageSize
  922. }).then(response => {
  923. let {code, rows,total} = response;
  924. if (code === 200) {
  925. let totalPage = (total % this.msgParams.pageSize == 0) ? Math.floor(total / this.msgParams.pageSize) : Math.floor(total / this.msgParams.pageSize + 1);
  926. rows.forEach(row => {
  927. if (!this.msgList.some(m => m.msgId === row.msgId)) {
  928. let user = this.userList.find(u => u.userId === row.userId)
  929. if (user) {
  930. row.msgStatus = user.msgStatus
  931. } else {
  932. row.msgStatus = 0
  933. }
  934. this.msgList.push(row)
  935. // 移动到底部
  936. this.$nextTick(() => {
  937. setTimeout(() => {
  938. this.$refs.manageRightRef.wrap.scrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight
  939. }, 200)
  940. })
  941. }
  942. })
  943. // 没加载完继续加载
  944. if (this.msgParams.pageNum < totalPage) {
  945. this.msgParams.pageNum = parseInt(this.msgParams.pageNum) + 1;
  946. this.loadMsgList()
  947. }
  948. // 同步更新消息列表中相同用户的状态
  949. this.userList.forEach(u => {
  950. this.msgList.filter(m => m.userId === u.userId).forEach(m => m.msgStatus = u.msgStatus)
  951. })
  952. }
  953. })
  954. // 添加滚动监听
  955. this.$nextTick(() => {
  956. this.$refs.manageRightRef.wrap.addEventListener("scroll", this.manageRightScroll)
  957. })
  958. },
  959. manageRightScroll() {
  960. this.saveChatScrollPosition();
  961. },
  962. blockUser(u){
  963. this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
  964. confirmButtonText: "确定",
  965. cancelButtonText: "取消",
  966. type: "warning"
  967. }).then(function() {
  968. let msg = {
  969. msg: "",
  970. liveId: this.liveId,
  971. userId: u.userId,
  972. userType: 0,
  973. cmd: 'blockUser',
  974. avatar: this.$store.state.user.user.avatar,
  975. nickName: this.$store.state.user.user.nickName
  976. }
  977. this.socket.send(JSON.stringify(msg))
  978. return blockUser(u.userId);
  979. }).then(() => {
  980. this.msgSuccess("封禁成功");
  981. }).catch(() => {});
  982. },
  983. changeUserState(u) {
  984. // 修改状态
  985. changeUserStatus({liveId: u.liveId, userId: u.userId}).then(response => {
  986. let { code } = response;
  987. if (200 === code) {
  988. u.msgStatus = u.msgStatus === 0 ? 1 : 0
  989. // 同步更新消息列表中相同用户的状态
  990. this.msgList.forEach(msg => {
  991. if (msg.userId === u.userId) {
  992. msg.msgStatus = u.msgStatus;
  993. }
  994. });
  995. this.userList.forEach(user => {
  996. if (user.userId === u.userId) {
  997. user.msgStatus = u.msgStatus;
  998. }
  999. });
  1000. // 4. 关键:重新筛选所有Tab的显示列表,确保状态同步
  1001. this.refreshUserDisplayLists(u);
  1002. let msg = u.msgStatus === 0 ? "已解禁" : "已禁言"
  1003. this.msgSuccess(msg);
  1004. return
  1005. }
  1006. this.msgError("操作失败");
  1007. })
  1008. },
  1009. // 新增:重新筛选所有Tab的显示列表
  1010. refreshUserDisplayLists(user) {
  1011. const { userId, msgStatus: newStatus, online } = user;
  1012. const oldStatus = newStatus === 1 ? 0 : 1; // 操作前的状态(反向)
  1013. // 1. 禁言操作(newStatus=1):从原在线/离线列表移除,加入禁言列表
  1014. if (newStatus === 1) {
  1015. // 从在线/离线列表中移除该用户(根据当前在线状态)
  1016. if (online === 0) {
  1017. this.onlineDisplayList = this.onlineDisplayList.filter(u => u.userId !== userId);
  1018. this.userTotal.online = Math.max(0, this.userTotal.online - 1);
  1019. } else {
  1020. this.offlineDisplayList = this.offlineDisplayList.filter(u => u.userId !== userId);
  1021. this.userTotal.offline = Math.max(0, this.userTotal.offline - 1);
  1022. }
  1023. this.userTotal.silenced = this.userTotal.silenced + 1;
  1024. // 添加到禁言列表(去重+限制长度)
  1025. const silencedList = this.silencedDisplayList.filter(u => u.userId !== userId);
  1026. silencedList.push(user);
  1027. this.silencedDisplayList = silencedList.slice(-40);
  1028. }
  1029. // 2. 解禁操作(newStatus=0):从禁言列表移除,回到原在线/离线列表
  1030. else {
  1031. // 从禁言列表移除
  1032. this.silencedDisplayList = this.silencedDisplayList.filter(u => u.userId !== userId);
  1033. this.userTotal.silenced = Math.max(0, this.userTotal.silenced - 1);
  1034. // 回到对应的在线/离线列表(根据当前在线状态)
  1035. if (online === 0) {
  1036. // 加入在线列表(去重+限制长度)
  1037. const onlineList = this.onlineDisplayList.filter(u => u.userId !== userId);
  1038. onlineList.push(user);
  1039. this.onlineDisplayList = onlineList.slice(-40);
  1040. this.userTotal.online = this.userTotal.online + 1;
  1041. } else {
  1042. // 加入离线列表(去重+限制长度)
  1043. const offlineList = this.offlineDisplayList.filter(u => u.userId !== userId);
  1044. offlineList.push(user);
  1045. this.offlineDisplayList = offlineList.slice(-40);
  1046. this.userTotal.offline = this.userTotal.offline + 1;
  1047. }
  1048. }
  1049. },
  1050. connectWebSocket() {
  1051. this.$store.dispatch('initLiveWs', {
  1052. liveWsUrl: this.liveWsUrl,
  1053. liveId: this.liveId,
  1054. userId: this.userId
  1055. })
  1056. this.socket = this.$store.state.liveWs[this.liveId]
  1057. this.socket.onmessage = (event) => this.handleWsMessage(event)
  1058. },
  1059. handleWsMessage(event) {
  1060. let { code, data } = JSON.parse(event.data)
  1061. if (code === 200) {
  1062. let { cmd } = data
  1063. if (cmd === 'sendMsg') {
  1064. let message = JSON.parse(data.data)
  1065. let user = this.userList.find(u => u.userId === message.userId)
  1066. if (user) {
  1067. message.msgStatus = user.msgStatus
  1068. } else {
  1069. message.msgStatus = 0
  1070. }
  1071. delete message.params
  1072. if(this.msgList.length > 50){
  1073. this.msgList.shift()
  1074. }
  1075. this.msgList.push(message)
  1076. // 移动到底部
  1077. this.$nextTick(() => {
  1078. setTimeout(() => {
  1079. this.$refs.manageRightRef.wrap.scrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight
  1080. }, 200)
  1081. })
  1082. }
  1083. else if (cmd === 'entry' || cmd === 'out') {
  1084. const user = data;
  1085. const online = cmd === 'entry' ? 0 : 1; // 0=在线,1=离线
  1086. const info = {
  1087. online:online,
  1088. msgStatus: user.msgStatus || 0,
  1089. nickName: user.nickName || '',
  1090. userType: user.userType || 0,
  1091. userId: user.userId || '',
  1092. };
  1093. // 1. 更新总人数(在线/离线互转)
  1094. if (cmd === 'entry') {
  1095. this.userTotal.online += 1;
  1096. this.userTotal.offline = Math.max(0, this.userTotal.offline - 1); // 确保不小于0
  1097. } else {
  1098. this.userTotal.offline += 1;
  1099. this.userTotal.online = Math.max(0, this.userTotal.online - 1); // 确保不小于0
  1100. }
  1101. // 2. 强制更新相关列表(无论当前激活哪个Tab)
  1102. if (cmd === 'entry') {
  1103. // 用户进入:从离线列表删除,添加到在线列表
  1104. this.offlineDisplayList = this.offlineDisplayList.filter(u => u.userId !== user.userId);
  1105. const newOnlineList = this.onlineDisplayList.filter(u => u.userId !== user.userId);
  1106. newOnlineList.push(info);
  1107. this.onlineDisplayList = newOnlineList.slice(-40); // 限制最大50条
  1108. } else {
  1109. // 用户离开:从在线列表删除,添加到离线列表
  1110. this.onlineDisplayList = this.onlineDisplayList.filter(u => u.userId !== user.userId);
  1111. const newOfflineList = this.offlineDisplayList.filter(u => u.userId !== user.userId);
  1112. newOfflineList.push(info);
  1113. this.offlineDisplayList = newOfflineList.slice(-40); // 限制最大50条
  1114. }
  1115. // 3. 处理禁言列表(如果用户是禁言状态,无论进出都要同步)
  1116. if (info.msgStatus === 1) {
  1117. // 禁言用户:从禁言列表删除旧数据,添加新数据
  1118. const newSilencedList = this.silencedDisplayList.filter(u => u.userId !== user.userId);
  1119. newSilencedList.push(info);
  1120. this.silencedDisplayList = newSilencedList.slice(-40);
  1121. } else {
  1122. // 非禁言用户:从禁言列表删除(如果存在)
  1123. this.silencedDisplayList = this.silencedDisplayList.filter(u => u.userId !== user.userId);
  1124. }
  1125. } else if (cmd === 'live_start') {
  1126. } else if (cmd === 'live_end') {
  1127. }
  1128. }
  1129. },
  1130. sendMessage() {
  1131. // 发送前简单校验
  1132. if (this.newMsg.trim() === '') {
  1133. return;
  1134. }
  1135. let msg = {
  1136. msg: this.newMsg,
  1137. liveId: this.liveId,
  1138. userId: this.userId,
  1139. userType: 1,
  1140. cmd: 'sendMsg',
  1141. avatar: this.$store.state.user.user.avatar,
  1142. nickName: this.$store.state.user.user.nickName
  1143. }
  1144. this.socket.send(JSON.stringify(msg))
  1145. this.newMsg = '';
  1146. },
  1147. // 初始化滚动监听(在mounted中调用)
  1148. initScrollListeners() {
  1149. // 为每个Tab的滚动容器添加监听
  1150. this.$nextTick(() => {
  1151. const scrollRefs = {
  1152. online: this.$refs.manageLeftRef_online,
  1153. offline: this.$refs.manageLeftRef_offline,
  1154. silenced: this.$refs.manageLeftRef_silenced
  1155. };
  1156. Object.keys(scrollRefs).forEach(tabName => {
  1157. const scrollEl = scrollRefs[tabName]?.wrap;
  1158. if (scrollEl) {
  1159. scrollEl.addEventListener('scroll', () =>
  1160. this.handleTabScroll(tabName, scrollEl)
  1161. );
  1162. }
  1163. });
  1164. });
  1165. },
  1166. handleTabScroll(tabName, scrollEl) {
  1167. const { scrollTop, scrollHeight, clientHeight } = scrollEl;
  1168. const bottomThreshold = 50; // 距离底部100px触发下一页
  1169. const topThreshold = 50; // 距离顶部100px触发上一页
  1170. // 加载下一页(滚动到底部附近)
  1171. if (scrollHeight - scrollTop - clientHeight < bottomThreshold) {
  1172. this.loadNextPage(tabName);
  1173. }
  1174. // 加载上一页(滚动到顶部附近)
  1175. if (scrollTop < topThreshold) {
  1176. this.loadPrevPage(tabName);
  1177. }
  1178. },
  1179. },
  1180. destroyed() {
  1181. this.hls?.destroy();
  1182. clearInterval(this.processInterval)
  1183. }
  1184. }
  1185. </script>
  1186. <style scoped>
  1187. .talk-list{
  1188. display: flex;
  1189. }
  1190. .live-console {
  1191. width: 90vw;
  1192. padding: 10px 0;
  1193. }
  1194. .live-console .live-console-col {
  1195. height: 88vh;
  1196. margin-left: 5px;
  1197. padding: 0 10px;
  1198. background-color: white;
  1199. border-radius: 4px;
  1200. }
  1201. /*隐藏水平滚动条*/
  1202. ::v-deep .el-scrollbar__wrap {
  1203. overflow-x: hidden;
  1204. }
  1205. /* 消息输入区域 */
  1206. .chat-input {
  1207. display: flex;
  1208. padding: 10px;
  1209. border-top: 1px solid #ebeef5;
  1210. background-color: #fff;
  1211. min-height: 120px;
  1212. }
  1213. .chat-input .el-input {
  1214. flex: 1;
  1215. margin-right: 10px;
  1216. }
  1217. .chat-input .el-textarea__inner {
  1218. resize: none;
  1219. min-height: 100px;
  1220. }
  1221. ::v-deep .el-textarea__inner {
  1222. border: none !important;
  1223. box-shadow: none !important;
  1224. resize: none !important;
  1225. }
  1226. ::v-deep .el-textarea__inner:focus {
  1227. border: none !important;
  1228. box-shadow: none !important;
  1229. }
  1230. </style>