LiveConsole.vue 67 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011
  1. <template>
  2. <div class="console">
  3. <div class="left-panel">
  4. <h2>学员列表</h2>
  5. <div class="search">
  6. <input type="text" placeholder="搜索用户昵称" v-model="searchKeyword">
  7. <button @click="searchUsers()">搜索</button>
  8. </div>
  9. <el-tabs class="live-console-tab-right" v-model="tabLeft.activeName" @tab-click="handleUserClick" :stretch="true">
  10. <el-tab-pane :label="alLabel" name="al">
  11. <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_al" style="height: 800px; width: 100%;">
  12. <el-row class='scrollbar-demo-item' v-for="u in alDisplayList" :key="u.userId">
  13. <el-col :span="20">
  14. <el-row type="flex" align="middle">
  15. <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
  16. <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
  17. <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
  18. </el-row>
  19. </el-col>
  20. <el-col :span="4" >
  21. <el-popover
  22. width="100"
  23. trigger="click">
  24. <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
  25. <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
  26. <i class="el-icon-more" slot="reference"></i>
  27. </el-popover>
  28. </el-col>
  29. </el-row>
  30. </el-scrollbar>
  31. </el-tab-pane>
  32. <el-tab-pane :label="onlineLabel" name="online">
  33. <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_online" style="height: 800px; width: 100%;">
  34. <el-row class='scrollbar-demo-item' v-for="u in onlineDisplayList" :key="u.userId">
  35. <el-col :span="20">
  36. <el-row type="flex" align="middle">
  37. <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
  38. <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
  39. <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
  40. </el-row>
  41. </el-col>
  42. <el-col :span="4" >
  43. <el-popover
  44. width="100"
  45. trigger="click">
  46. <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
  47. <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
  48. <i class="el-icon-more" slot="reference"></i>
  49. </el-popover>
  50. </el-col>
  51. </el-row>
  52. </el-scrollbar>
  53. </el-tab-pane>
  54. <el-tab-pane :label="offlineLabel" name="offline">
  55. <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_offline" style="height: 800px; width: 100%;">
  56. <el-row class='scrollbar-demo-item' v-for="u in offlineDisplayList" :key="u.userId">
  57. <el-col :span="20">
  58. <el-row type="flex" align="middle">
  59. <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
  60. <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
  61. <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
  62. </el-row>
  63. </el-col>
  64. <el-col :span="4" >
  65. <el-popover
  66. width="100"
  67. trigger="click">
  68. <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
  69. <i class="el-icon-more" slot="reference"></i>
  70. <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
  71. </el-popover>
  72. </el-col>
  73. </el-row>
  74. </el-scrollbar>
  75. </el-tab-pane>
  76. <el-tab-pane :label="silencedUserLabel" name="silenced">
  77. <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_silenced" style="height: 800px; width: 100%;">
  78. <el-row class='scrollbar-demo-item' v-for="u in silencedDisplayList" :key="u.userId">
  79. <el-col :span="20">
  80. <el-row type="flex" align="middle">
  81. <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
  82. <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
  83. <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
  84. </el-row>
  85. </el-col>
  86. <el-col :span="4" >
  87. <el-popover
  88. width="100"
  89. trigger="click">
  90. <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
  91. <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
  92. <i class="el-icon-more" slot="reference"></i>
  93. </el-popover>
  94. </el-col>
  95. </el-row>
  96. </el-scrollbar>
  97. </el-tab-pane>
  98. </el-tabs>
  99. </div>
  100. <div class="middle-panel">
  101. <h2>消息管理</h2>
  102. <!-- 新增:用户行为记录区域 -->
  103. <div class="behavior-records">
  104. <h3>用户行为记录</h3>
  105. <div class="message-container" @click="handleBehaviorBoxClick">
  106. <el-scrollbar class="custom-scrollbar" style="height: 300px; width: 100%;" ref="behaviorScrollRef">
  107. <div v-if="behaviorList.length === 0" style="text-align: center; padding: 40px 0; color: #999;">
  108. <i class="el-icon-info" style="font-size: 48px; margin-bottom: 10px;"></i>
  109. <p>暂无行为记录</p>
  110. </div>
  111. <el-row v-else v-for="behavior in behaviorList" :key="behavior.id" style="padding: 8px 0" type="flex" align="top">
  112. <el-col :span="3" style="margin-left: 10px">
  113. <el-avatar :src="behavior.avatar" :size="40"></el-avatar>
  114. </el-col>
  115. <el-col :span="20">
  116. <el-row style="margin-left: 10px">
  117. <el-col>
  118. <div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ behavior.nickName }}</div>
  119. </el-col>
  120. <el-col :span="24">
  121. <div style="white-space: normal; word-wrap: break-word;background-color: #f0f2f5; padding: 8px; border-radius: 5px;font-size: 14px;">
  122. <i :class="getBehaviorIcon(behavior.behaviorType)" :style="{ color: getBehaviorColor(behavior.behaviorType) }"></i>
  123. <span style="margin-left: 5px; font-weight: bold;">{{ behavior.behaviorDesc }}</span>
  124. <span v-if="behavior.resourceName" style="margin-left: 8px; color: #606266;">「{{ behavior.resourceName }}」</span>
  125. </div>
  126. </el-col>
  127. <el-col style="margin-top: 5px;">
  128. <span style="font-size: 12px; color: #999;">{{ formatBehaviorTime(behavior.behaviorTime) }}</span>
  129. <span v-if="behavior.deviceInfo" style="margin-left: 10px; font-size: 12px; color: #999;">【{{ behavior.deviceInfo }}】</span>
  130. </el-col>
  131. </el-row>
  132. </el-col>
  133. </el-row>
  134. <!-- 底部留白 -->
  135. <div style="height: 20px;"></div>
  136. </el-scrollbar>
  137. <!-- 加载最新行为按钮 -->
  138. <el-button
  139. v-if="showLoadLatestBehaviorBtn"
  140. class="load-latest-btn"
  141. type="primary"
  142. size="small"
  143. @click.stop="loadLatestBehaviors"
  144. :disabled="isLoadingLatestBehavior"
  145. :loading="isLoadingLatestBehavior"
  146. icon="el-icon-refresh">
  147. 加载最新行为
  148. </el-button>
  149. </div>
  150. </div>
  151. <div class="discussion-messages">
  152. <h3>讨论区消息</h3>
  153. <div class="message-settings">
  154. <label>
  155. <input type="checkbox" v-model="globalVisible" @change="globalVisibleChange">
  156. 全局用户自见
  157. </label>
  158. </div>
  159. <div class="message-container" @click="handleMessageBoxClick">
  160. <el-scrollbar class="custom-scrollbar" style="height: 500px; width: 100%;" ref="manageRightRef">
  161. <el-row v-for="m in msgList" :key="m.msgId">
  162. <el-row v-if="m.userId === userId && m.msgId == null" style="padding: 8px 0" type="flex" align="top" justify="end">
  163. <div style="display: flex;justify-content: flex-end">
  164. <div style="display: flex;justify-content: flex-end;flex-direction: column;max-width: 200px;align-items: flex-end">
  165. <div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div>
  166. <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>
  167. </div>
  168. <el-avatar :src="m.avatar" style="margin-left: 10px; margin-right: 10px;"/>
  169. </div>
  170. </el-row>
  171. <el-row v-else style="margin-top: 5px" type="flex" align="top" >
  172. <el-col :span="3" style="margin-left: 10px"><el-avatar :src="m.avatar"/></el-col>
  173. <el-col :span="15">
  174. <el-row style="margin-left: 10px">
  175. <el-col><div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div></el-col>
  176. <el-col :span="24" style="max-width: 200px;">
  177. <div style="white-space: normal; word-wrap: break-word;background-color: #f0f2f5; padding: 8px; border-radius: 5px;font-size: 14px;width: 100%;">
  178. {{ m.msg }}
  179. </div>
  180. </el-col>
  181. <el-col>
  182. <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="changeUserState(m)">{{ m.msgStatus === 1 ? '解禁' : '禁言' }}</a>
  183. <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="blockUser(m)">拉黑</a>
  184. <a v-if="m.singleVisible === 1" style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="singleVisible(m)">解除用户自见</a>
  185. <a v-else style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="singleVisible(m)">用户自见</a>
  186. <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="deleteMsg(m)">删除</a>
  187. </el-col>
  188. </el-row>
  189. </el-col>
  190. </el-row>
  191. </el-row>
  192. <!-- 底部留白 -->
  193. <div style="height: 20px;"></div>
  194. </el-scrollbar>
  195. <!-- 加载最新消息按钮 -->
  196. <el-button
  197. v-if="showLoadLatestBtn"
  198. class="load-latest-btn"
  199. type="primary"
  200. size="small"
  201. @click.stop="loadLatestMessages"
  202. :disabled="isLoadingLatest"
  203. :loading="isLoadingLatest"
  204. icon="el-icon-refresh">
  205. 加载最新消息
  206. </el-button>
  207. </div>
  208. <!-- <div class="message-list">-->
  209. <!-- <div class="message-item" v-for="msg in msgList" :key="msg.id">-->
  210. <!-- <div class="message-avatar">-->
  211. <!-- <img :src="msg.avatar" alt="用户头像">-->
  212. <!-- </div>-->
  213. <!-- <div class="message-content">-->
  214. <!-- <div class="message-user">{{ msg.user }}</div>-->
  215. <!-- <div class="message-text">{{ msg.text }}</div>-->
  216. <!-- </div>-->
  217. <!-- <div class="message-actions">-->
  218. <!--&lt;!&ndash; <button @click="toggleVisible(msg)">&ndash;&gt;-->
  219. <!--&lt;!&ndash; {{ msg.isVisible ? '仅用户自见' : '全局可见' }}&ndash;&gt;-->
  220. <!--&lt;!&ndash; </button>&ndash;&gt;-->
  221. <!-- <button @click="deleteMessage(msg)">删除</button>-->
  222. <!-- </div>-->
  223. <!-- </div>-->
  224. <!-- </div>-->
  225. </div>
  226. <div class="system-messages">
  227. <h3>系统消息</h3>
  228. <textarea placeholder="输入系统消息" v-model="newMsg"></textarea>
  229. <div class="message-actions">
  230. <button @click="sendMessage">发送消息</button>
  231. <button @click="sendPopMessage">弹窗消息</button>
  232. <button @click="showTopMsgDialog">顶部消息</button>
  233. </div>
  234. </div>
  235. </div>
  236. <div class="right-panel">
  237. <h2>运营工具</h2>
  238. <div class="live-player">
  239. <h3>直播观看</h3>
  240. <LivePlayer ref="livePlayer" :videoParam="videoParam" />
  241. </div>
  242. <div class="automation">
  243. <h3>运营自动化</h3>
  244. <div class="timeline">
  245. <h4>时间轴设置</h4>
  246. <div class="timeline-items">
  247. <div class="timeline-item" v-for="item in timelineItems.slice(0,2)" :key="item.time">
  248. <div class="time">{{ formatDate(item.absValue) }}</div>
  249. <div class="action">{{ item.taskName }}</div>
  250. <button class="delete" @click="removeTimelineItem(item)">删除</button>
  251. </div>
  252. </div>
  253. <!-- <button class="add" @click="addTimelineItem">添加时间节点</button>-->
  254. </div>
  255. </div>
  256. <div class="watermark">
  257. <h3>直播氛围自动化</h3>
  258. <div class="watermark-settings">
  259. <textarea :disabled="autoWatermark" v-model="watermarkTemplate" placeholder="水军弹幕内容模板,每行一条"></textarea>
  260. <div class="watermark-options">
  261. <label>
  262. 发送间隔:
  263. <input type="number" :disabled="autoWatermark" v-model.number="interval" min="1">
  264. </label>
  265. <label>
  266. <input type="checkbox" v-model="autoWatermark" @change="changeAutoWatermark">
  267. 启用水军自动化
  268. </label>
  269. </div>
  270. </div>
  271. </div>
  272. </div>
  273. <!-- 顶部消息对话框 -->
  274. <el-dialog title="发送顶部消息" :visible.sync="topMsgDialogVisible" width="500px">
  275. <el-form :model="topMsgForm" label-width="100px">
  276. <el-form-item label="消息内容">
  277. <el-input
  278. type="textarea"
  279. v-model="topMsgForm.msg"
  280. placeholder="请输入消息内容"
  281. :rows="3"
  282. ></el-input>
  283. </el-form-item>
  284. <el-form-item label="显示时长">
  285. <el-input-number
  286. v-model="topMsgForm.duration"
  287. :min="1"
  288. :max="60"
  289. placeholder="请输入显示时长(分钟)"
  290. ></el-input-number>
  291. <span style="margin-left: 10px;">分钟</span>
  292. </el-form-item>
  293. </el-form>
  294. <div slot="footer" class="dialog-footer">
  295. <el-button @click="topMsgDialogVisible = false">取 消</el-button>
  296. <el-button type="primary" @click="sendTopMessage">确 定</el-button>
  297. </div>
  298. </el-dialog>
  299. </div>
  300. </template>
  301. <script>
  302. import LivePlayer from './LivePlayer.vue';
  303. import {blockUser, changeUserStatus, getLiveUserTotals, dashBoardWatchUserList} from '@/api/live/liveWatchUser'
  304. import { listLiveSingleMsg,delLiveMsg } from '@/api/live/liveMsg'
  305. import { getLive } from '@/api/live/live'
  306. import { consoleList } from '@/api/live/task'
  307. import ScreenScale from './ScreenScale.vue'; // 路径根据实际位置调整
  308. export default {
  309. components: {
  310. LivePlayer,ScreenScale
  311. },
  312. props: {
  313. liveId: {
  314. type: String,
  315. default: null
  316. }
  317. },
  318. data() {
  319. return {
  320. watermarkIndex: 0,
  321. watermarkList:[],
  322. watermarkTemplate: '',
  323. liveInfo: {},
  324. videoParam:{
  325. startTime:'',
  326. livingUrl: '',
  327. liveType: 1,
  328. videoUrl: '',
  329. },
  330. msgList:[],
  331. currentTab: 'al',
  332. searchKeyword: '',
  333. globalVisible: false,
  334. interval: 10,
  335. autoWatermark: false,
  336. streamUrl: 'rtmp://your-live-stream-url',
  337. users: [
  338. { id: 1, name: '用户1', avatar: 'https://via.placeholder.com/40', status: 'online', statusText: '在线', silenced: false, msgStatus: false },
  339. { id: 2, name: '用户2', avatar: 'https://via.placeholder.com/40', status: 'online', statusText: '在线', silenced: false, msgStatus: true },
  340. { id: 3, name: '用户3', avatar: 'https://via.placeholder.com/40', status: 'offline', statusText: '离线', silenced: true, msgStatus: false },
  341. { id: 4, name: '用户4', avatar: 'https://via.placeholder.com/40', status: 'online', statusText: '在线', silenced: false, msgStatus: false },
  342. { id: 5, name: '用户5', avatar: 'https://via.placeholder.com/40', status: 'offline', statusText: '离线', silenced: false, msgStatus: false }
  343. ],
  344. messages: [
  345. { id: 1, user: '用户1', avatar: 'https://via.placeholder.com/30', text: '这个产品怎么样?', isVisible: true },
  346. { id: 2, user: '用户2', avatar: 'https://via.placeholder.com/30', text: '看起来不错', isVisible: true },
  347. { id: 3, user: '用户3', avatar: 'https://via.placeholder.com/30', text: '有优惠吗?', isVisible: true }
  348. ],
  349. timelineItems: [
  350. { "searchValue": null,
  351. "createBy": null,
  352. "createTime": "2025-09-23 10:36:17",
  353. "updateBy": null,
  354. "updateTime": "2025-10-17 09:18:00",
  355. "remark": null,
  356. "params": {},
  357. "id": 6573,
  358. "liveId": 128,
  359. "taskName": "2",
  360. "taskType": 1,
  361. "triggerType": 2,
  362. "triggerValue": "2025-01-01T00:02:00.000+0800",
  363. "absValue": "2025-11-23T10:43:00.000+0800",
  364. "status": 1,
  365. "finishStatus": 0 },
  366. ],
  367. userTotal: {
  368. online: 0, // 在线总人数
  369. offline: 0, // 离线总人数
  370. silenced: 0, // 禁言总人数
  371. al: 0
  372. },
  373. tabLeft: {
  374. activeName: "al",
  375. },
  376. taskParams:{
  377. currentPage: 1,
  378. pageSize: 20,
  379. prevPage: 0,
  380. totalLoaded: 0,
  381. total: 0,
  382. hasMore: true,
  383. hasPrev: false
  384. },
  385. loadMsgMaxPage: 2,
  386. liveWsUrl: process.env.VUE_APP_LIVE_WS_URL + '/app/webSocket',
  387. alDisplayList: [],
  388. onlineDisplayList: [], // 在线用户显示列表
  389. offlineDisplayList: [], // 离线用户显示列表
  390. silencedDisplayList: [], // 禁言用户显示列表
  391. // 各Tab的分页参数
  392. pageParams: {
  393. al: {
  394. currentPage: 1,
  395. pageSize: 20,
  396. prevPage: 0,
  397. totalLoaded: 0,
  398. total: 0,
  399. hasMore: true,
  400. hasPrev: false
  401. },
  402. online: {
  403. currentPage: 1, // 当前页(下一页加载用)
  404. pageSize: 20, // 当前页(下一页加载用)
  405. prevPage: 0, // 上一页页码(上一页加载用)
  406. totalLoaded: 0, // 已加载总条数
  407. total: 0, // 总数据量
  408. hasMore: true, // 是否有下一页
  409. hasPrev: false // 是否有上一页
  410. },
  411. offline: {
  412. currentPage: 1,
  413. pageSize: 20,
  414. prevPage: 0,
  415. totalLoaded: 0,
  416. total: 0,
  417. hasMore: true,
  418. hasPrev: false
  419. },
  420. silenced: {
  421. currentPage: 1,
  422. pageSize: 20,
  423. prevPage: 0,
  424. totalLoaded: 0,
  425. total: 0,
  426. hasMore: true,
  427. hasPrev: false
  428. }
  429. },
  430. scrLoading: {
  431. al: { next: false, prev: false },
  432. online: { next: false, prev: false },
  433. offline: { next: false, prev: false },
  434. silenced: { next: false, prev: false }
  435. },
  436. newMsg:'',
  437. autoMsgTimer: null,
  438. checkInterval: 2000, // 检查间隔(1分钟,可根据需求调整)
  439. topMsgDialogVisible: false,
  440. topMsgForm: {
  441. msg: '',
  442. duration: 5
  443. },
  444. // 消息滚动控制
  445. isAutoScrollEnabled: true, // 是否启用自动滚动
  446. autoScrollTimer: null, // 自动滚动定时器
  447. showLoadLatestBtn: false, // 是否显示加载最新消息按钮
  448. isLoadingLatest: false, // 是否正在加载最新消息
  449. scrollDebounceTimer: null, // 滚动防抖定时器
  450. msgParams: {
  451. pageNum: 1,
  452. pageSize: 30,
  453. liveId: null
  454. },
  455. // 新增:行为记录相关
  456. behaviorList: [], // 行为列表
  457. showLoadLatestBehaviorBtn: false, // 显示加载最新行为按钮
  458. isLoadingLatestBehavior: false, // 是否正在加载最新行为
  459. isAutoBehaviorScroll: true, // 是否启用行为自动滚动
  460. // 行为类型映射(后端定义的 behaviorType)
  461. behaviorTypeMap: {
  462. 1: '进入直播间',
  463. 2: '退出直播间',
  464. 3: '点赞',
  465. 4: '发送消息',
  466. 5: '点击商品',
  467. 6: '加入购物车',
  468. 7: '购买商品',
  469. 8: '收藏商品',
  470. 9: '关注主播',
  471. 10: '领取优惠券',
  472. 11: '参与抽奖',
  473. 12: '领取红包',
  474. 13: '分享直播间',
  475. 14: '观看时长记录',
  476. 15: '完课记录'
  477. }
  478. };
  479. },
  480. computed: {
  481. userId() {
  482. return this.$store.state.user.user.userId
  483. },
  484. companyId() {
  485. return this.$store.state.user.user.companyId
  486. },
  487. onlineLabel() {
  488. return `在线(${this.userTotal.online})`;
  489. },
  490. alLabel() {
  491. return `全部(${this.userTotal.al})`;
  492. },
  493. offlineLabel() {
  494. return `离线(${this.userTotal.offline})`;
  495. },
  496. silencedUserLabel() {
  497. return `禁言(${this.userTotal.silenced})`;
  498. }
  499. },
  500. created() {
  501. if(!this.liveId) return
  502. this.getList()
  503. this.handleUserClick({name:'al'})
  504. this.connectWebSocket()
  505. this.getLive()
  506. },
  507. mounted() {
  508. this.$nextTick(() => {
  509. this.restoreChatScrollPosition();
  510. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  511. this.$refs.manageRightRef.wrap.addEventListener('scroll', this.saveChatScrollPosition);
  512. }
  513. });
  514. this.initScrollListeners();
  515. },
  516. methods: {
  517. // 点击消息框
  518. handleMessageBoxClick() {
  519. // 点击消息框时,停止自动滚动
  520. this.isAutoScrollEnabled = false;
  521. // 停止自动滚动定时器
  522. if (this.autoScrollTimer) {
  523. clearTimeout(this.autoScrollTimer);
  524. this.autoScrollTimer = null;
  525. }
  526. // 检查是否在底部,如果不在底部则显示加载最新消息按钮
  527. this.$nextTick(() => {
  528. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  529. const wrap = this.$refs.manageRightRef.wrap;
  530. const scrollHeight = wrap.scrollHeight;
  531. const clientHeight = wrap.clientHeight;
  532. const currentScrollTop = wrap.scrollTop;
  533. const maxScrollTop = scrollHeight - clientHeight;
  534. if (currentScrollTop < maxScrollTop - 50) {
  535. this.showLoadLatestBtn = true;
  536. }
  537. }
  538. });
  539. },
  540. // 滚动到底部
  541. scrollToBottom(forceScroll = false) {
  542. // 如果自动滚动被禁用且不是强制滚动,则不执行
  543. if (!this.isAutoScrollEnabled && !forceScroll) {
  544. return;
  545. }
  546. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  547. this.$nextTick(() => {
  548. const wrap = this.$refs.manageRightRef.wrap;
  549. if (!wrap) return;
  550. const scrollHeight = wrap.scrollHeight;
  551. const clientHeight = wrap.clientHeight;
  552. const currentScrollTop = wrap.scrollTop;
  553. const maxScrollTop = scrollHeight - clientHeight;
  554. // 强制滚动或启用自动滚动时,直接滚动到底部并隐藏按钮
  555. if (forceScroll || this.isAutoScrollEnabled) {
  556. this.showLoadLatestBtn = false;
  557. wrap.scrollTop = maxScrollTop;
  558. }
  559. });
  560. }
  561. },
  562. // 加载最新消息
  563. loadLatestMessages() {
  564. // 如果正在加载,直接返回
  565. if (this.isLoadingLatest) {
  566. return;
  567. }
  568. this.isLoadingLatest = true;
  569. this.showLoadLatestBtn = false;
  570. // 恢复自动滚动
  571. this.isAutoScrollEnabled = true;
  572. // 重新请求最新消息
  573. this.resetMsgParams();
  574. // loadMsgList 中会自动滚动到底部,因为 isAutoScrollEnabled 已经是 true
  575. this.loadMsgList();
  576. },
  577. singleVisible(m){
  578. // 过滤当前所有消息 找到userId的相同的消息 更改他们的自可见状态
  579. m.singleVisible= m.singleVisible === 1 ? 0 : 1
  580. this.msgList.forEach(m1 => {m1.singleVisible = m1.userId === m.userId ? m.singleVisible : !m.singleVisible})
  581. // 消息自可见
  582. let msg = {
  583. liveId: this.liveId,
  584. userId: m.userId,
  585. userType: 0,
  586. cmd: 'singleVisible',
  587. status:m.singleVisible
  588. }
  589. this.socket.send(JSON.stringify(msg))
  590. },
  591. deleteMsg(m){
  592. // 1. 弹出确认对话框
  593. this.$confirm('此操作将永久删除该消息, 是否继续?', '提示', {
  594. confirmButtonText: '确定',
  595. cancelButtonText: '取消',
  596. type: 'warning'
  597. }).then(() => {
  598. delLiveMsg(m.msgId).then(res => {
  599. if (res.code === 200) {
  600. const index = this.msgList.findIndex(item => item.msgId === m.msgId);
  601. if (index !== -1) {
  602. this.msgList.splice(index, 1);
  603. console.log(`消息 ${m.msgId} 已在本地删除。`);
  604. }
  605. let msg = {
  606. liveId: this.liveId,
  607. userId: m.userId,
  608. msg: m.msgId, // 关键:将消息ID发送给后台
  609. cmd: 'deleteMsg',
  610. };
  611. this.socket.send(JSON.stringify(msg));
  612. // 可以在这里给用户一个删除成功的提示
  613. this.$message({
  614. type: 'success',
  615. message: '消息删除成功!'
  616. });
  617. }
  618. })
  619. }).catch(() => {
  620. // 3. 用户点击“取消”或关闭对话框后的回调
  621. this.$message({
  622. type: 'info',
  623. message: '已取消删除'
  624. });
  625. });
  626. },
  627. globalVisibleChange( val){
  628. // 消息自可见
  629. let msg = {
  630. liveId: this.liveId,
  631. userId: '9999',
  632. userType: 0,
  633. cmd: 'globalVisible',
  634. status:this.globalVisible ? 1 :0
  635. }
  636. this.socket.send(JSON.stringify(msg))
  637. },
  638. changeAutoWatermark( val){
  639. this.watermarkList = this.watermarkTemplate
  640. .split('\n')
  641. .map(line => line.trim())
  642. .filter(line => line !== '');
  643. if(this.watermarkTemplate.length < 1 || this.watermarkList.length === 0) {
  644. this.$message.warning('请先填写水印模板')
  645. this.$nextTick(() => {
  646. this.autoWatermark = false
  647. });
  648. return
  649. }
  650. this.disabled = this.autoWatermark
  651. if(this.autoWatermark){
  652. this.autoMsgTimer = setInterval(() => {
  653. this.sendNormalMessage()
  654. }, 1000 * this.interval)
  655. } else {
  656. clearInterval(this.autoMsgTimer)
  657. this.watermarkIndex = 0;
  658. this.watermarkList = [];
  659. }
  660. },
  661. sendNormalMessage() {
  662. if(this.watermarkIndex >= this.watermarkList.length){
  663. clearInterval(this.autoMsgTimer)
  664. this.watermarkIndex = 0;
  665. this.watermarkList = [];
  666. this.$message.success('自动发送消息已结束');
  667. this.$nextTick(() => {
  668. this.autoWatermark = false
  669. });
  670. return;
  671. }
  672. const curMsg = this.watermarkList[this.watermarkIndex]
  673. // 发送前简单校验
  674. if (curMsg.trim() === '') {
  675. return;
  676. }
  677. let msg = {
  678. msg: curMsg,
  679. liveId: this.liveId,
  680. userId: '9999',
  681. userType: 0,
  682. cmd: 'sendNormalMsg',
  683. avatar: this.$store.state.user.user.avatar,
  684. nickName: this.$store.state.user.user.nickName,
  685. }
  686. this.socket.send(JSON.stringify(msg))
  687. this.watermarkIndex += 1
  688. },
  689. // 弹窗消息
  690. sendPopMessage() {
  691. // 发送前简单校验
  692. if (this.newMsg.trim() === '') {
  693. return;
  694. }
  695. let msg = {
  696. msg: this.newMsg,
  697. liveId: this.liveId,
  698. userId: this.userId,
  699. userType: 1,
  700. cmd: 'sendPopMsg',
  701. avatar: this.$store.state.user.user.avatar,
  702. nickName: this.$store.state.user.user.nickName,
  703. }
  704. this.socket.send(JSON.stringify(msg))
  705. this.newMsg = '';
  706. },
  707. // 显示顶部消息对话框
  708. showTopMsgDialog() {
  709. this.topMsgForm.msg = '';
  710. this.topMsgForm.duration = 5;
  711. this.topMsgDialogVisible = true;
  712. },
  713. // 发送顶部消息
  714. sendTopMessage() {
  715. // 发送前简单校验
  716. if (this.topMsgForm.msg.trim() === '') {
  717. this.$message.warning('请输入消息内容');
  718. return;
  719. }
  720. if (!this.topMsgForm.duration || this.topMsgForm.duration < 1) {
  721. this.$message.warning('请输入有效的显示时长');
  722. return;
  723. }
  724. let msg = {
  725. msg: this.topMsgForm.msg,
  726. duration: this.topMsgForm.duration,
  727. liveId: this.liveId,
  728. userId: this.userId,
  729. userType: 1,
  730. cmd: 'sendTopMsg',
  731. avatar: this.$store.state.user.user.avatar,
  732. nickName: this.$store.state.user.user.nickName,
  733. }
  734. this.socket.send(JSON.stringify(msg))
  735. this.topMsgDialogVisible = false;
  736. this.$message.success('顶部消息发送成功');
  737. },
  738. sendMessage() {
  739. // 发送前简单校验
  740. if (this.newMsg.trim() === '') {
  741. return;
  742. }
  743. let msg = {
  744. msg: this.newMsg,
  745. liveId: this.liveId,
  746. userId: this.userId,
  747. userType: 1,
  748. cmd: 'sendMsg',
  749. avatar: this.$store.state.user.user.avatar,
  750. nickName: this.$store.state.user.user.nickName,
  751. }
  752. this.socket.send(JSON.stringify(msg))
  753. this.newMsg = '';
  754. },
  755. handleWsMessage(event) {
  756. let { code, data } = JSON.parse(event.data)
  757. if (code === 200 || code === 0) { // 兼容后端返回 code: 0
  758. let { cmd } = data
  759. // 新增:处理行为记录(后端推送的 cmd: 'behaviorTrack')
  760. if (cmd === 'behaviorTrack') {
  761. this.handleBehaviorRecord(data.data); // data.data 是真正的行为数据
  762. } else if (cmd === 'sendMsg') {
  763. let message = JSON.parse(data.data)
  764. let user = this.alDisplayList.find(u => u.userId === message.userId)
  765. if (user) {
  766. message.msgStatus = user.msgStatus
  767. } else {
  768. message.msgStatus = 0
  769. }
  770. delete message.params
  771. if(this.msgList.length > 50){
  772. this.msgList.shift()
  773. }
  774. this.msgList.push(message)
  775. // 如果启用自动滚动,自动滚动到底部
  776. if (this.isAutoScrollEnabled) {
  777. this.$nextTick(() => {
  778. this.autoScrollTimer = setTimeout(() => {
  779. this.scrollToBottom();
  780. }, 200);
  781. });
  782. } else {
  783. // 自动滚动被禁用时,显示加载最新消息按钮
  784. this.showLoadLatestBtn = true;
  785. }
  786. } else if (cmd === 'entry' || cmd === 'out') {
  787. const user = data;
  788. const online = cmd === 'entry' ? 0 : 1; // 0=在线,1=离线
  789. const info = {
  790. online:online,
  791. msgStatus: user.msgStatus || 0,
  792. nickName: user.nickName || '',
  793. userType: user.userType || 0,
  794. userId: user.userId || '',
  795. };
  796. // 1. 更新总人数(在线/离线互转)
  797. if (cmd === 'entry') {
  798. this.userTotal.online += 1;
  799. this.userTotal.offline = Math.max(0, this.userTotal.offline - 1); // 确保不小于0
  800. } else {
  801. this.userTotal.offline += 1;
  802. this.userTotal.online = Math.max(0, this.userTotal.online - 1); // 确保不小于0
  803. }
  804. // 2. 强制更新相关列表(无论当前激活哪个Tab)
  805. if (cmd === 'entry') {
  806. // 用户进入:从离线列表删除,添加到在线列表
  807. this.offlineDisplayList = this.offlineDisplayList.filter(u => u.userId !== user.userId);
  808. const newOnlineList = this.onlineDisplayList.filter(u => u.userId !== user.userId);
  809. newOnlineList.push(info);
  810. this.onlineDisplayList = newOnlineList.slice(-40); // 限制最大50条
  811. } else {
  812. // 用户离开:从在线列表删除,添加到离线列表
  813. this.onlineDisplayList = this.onlineDisplayList.filter(u => u.userId !== user.userId);
  814. const newOfflineList = this.offlineDisplayList.filter(u => u.userId !== user.userId);
  815. newOfflineList.push(info);
  816. this.offlineDisplayList = newOfflineList.slice(-40); // 限制最大50条
  817. }
  818. // 3. 处理禁言列表(如果用户是禁言状态,无论进出都要同步)
  819. if (info.msgStatus === 1) {
  820. // 禁言用户:从禁言列表删除旧数据,添加新数据
  821. const newSilencedList = this.silencedDisplayList.filter(u => u.userId !== user.userId);
  822. newSilencedList.push(info);
  823. this.silencedDisplayList = newSilencedList.slice(-40);
  824. } else {
  825. // 非禁言用户:从禁言列表删除(如果存在)
  826. this.silencedDisplayList = this.silencedDisplayList.filter(u => u.userId !== user.userId);
  827. }
  828. } else if (cmd === 'live_start') {
  829. } else if (cmd === 'live_end') {
  830. }
  831. }
  832. },
  833. getLive(){
  834. getLive(this.liveId).then(res => {
  835. if (res.code == 200) {
  836. if (res.data.isAudit != 1) {
  837. this.$message.error("当前直播间未经审核");
  838. this.loading = false
  839. return
  840. }
  841. this.isAudit = true
  842. this.status = res.data.status
  843. this.videoParam.startTime = new Date(res.data.startTime).getTime()
  844. if(res.data.status == 4){
  845. this.videoParam.liveType = 3
  846. this.videoParam.videoUrl = res.data.videoUrl;
  847. }else {
  848. if (res.data.status != 2) {
  849. this.$message.error("当前直播间未直播");
  850. this.loading = false
  851. return
  852. }
  853. if (res.data.liveType == 1) {
  854. this.videoParam.livingUrl = res.data.flvHlsUrl
  855. this.videoParam.livingUrl = this.videoParam.livingUrl.replace("flv","m3u8")
  856. // this.$nextTick(() => {
  857. // this.initPlayer()
  858. // })
  859. // this.processInterval = setInterval(this.updateLiveProgress, 1000);
  860. } else {
  861. this.videoParam.liveType = 2
  862. this.videoParam.videoUrl = res.data.videoUrl;
  863. // this.$nextTick(() => {
  864. // this.initVideoPlayer(res.data.startTime)
  865. // })
  866. }
  867. }
  868. this.$nextTick(() => {
  869. this.globalVisible = res.data.globalVisible
  870. this.$refs.livePlayer.initPlayer()
  871. })
  872. this.loading = false
  873. } else {
  874. this.$message.error(res.msg)
  875. this.loading = false
  876. }
  877. this.liveInfo = res.data
  878. })
  879. },
  880. connectWebSocket() {
  881. this.$store.dispatch('initLiveWs', {
  882. liveWsUrl: this.liveWsUrl,
  883. liveId: this.liveId,
  884. userId: this.userId
  885. })
  886. this.socket = this.$store.state.liveWs[this.liveId]
  887. this.socket.onmessage = (event) => this.handleWsMessage(event)
  888. },
  889. changeUserState(u) {
  890. const displayList = this[`${this.currentTab}DisplayList`];
  891. // 修改状态
  892. changeUserStatus({liveId: u.liveId, userId: u.userId}).then(response => {
  893. let { code } = response;
  894. if (200 === code) {
  895. u.msgStatus = u.msgStatus === 0 ? 1 : 0
  896. // 同步更新消息列表中相同用户的状态
  897. this.msgList.forEach(msg => {
  898. if (msg.userId === u.userId) {
  899. msg.msgStatus = u.msgStatus;
  900. }
  901. });
  902. displayList.forEach(user => {
  903. if (user.userId === u.userId) {
  904. user.msgStatus = u.msgStatus;
  905. }
  906. });
  907. // 4. 关键:重新筛选所有Tab的显示列表,确保状态同步
  908. this.refreshUserDisplayLists(u);
  909. let msg = u.msgStatus === 0 ? "已解禁" : "已禁言"
  910. this.msgSuccess(msg);
  911. return
  912. }
  913. this.msgError("操作失败");
  914. })
  915. },
  916. refreshUserDisplayLists(user) {
  917. const { userId, msgStatus: newStatus, online } = user;
  918. const oldStatus = newStatus === 1 ? 0 : 1; // 操作前的状态(反向)
  919. // 1. 禁言操作(newStatus=1):从原在线/离线列表移除,加入禁言列表
  920. if (newStatus === 1) {
  921. // 从在线/离线列表中移除该用户(根据当前在线状态)
  922. if (online === 0) {
  923. this.onlineDisplayList = this.onlineDisplayList.filter(u => u.userId !== userId);
  924. this.userTotal.online = Math.max(0, this.userTotal.online - 1);
  925. } else {
  926. this.offlineDisplayList = this.offlineDisplayList.filter(u => u.userId !== userId);
  927. this.userTotal.offline = Math.max(0, this.userTotal.offline - 1);
  928. }
  929. this.userTotal.silenced = this.userTotal.silenced + 1;
  930. // 添加到禁言列表(去重+限制长度)
  931. const silencedList = this.silencedDisplayList.filter(u => u.userId !== userId);
  932. silencedList.push(user);
  933. this.silencedDisplayList = silencedList.slice(-40);
  934. }
  935. // 2. 解禁操作(newStatus=0):从禁言列表移除,回到原在线/离线列表
  936. else {
  937. // 从禁言列表移除
  938. this.silencedDisplayList = this.silencedDisplayList.filter(u => u.userId !== userId);
  939. this.userTotal.silenced = Math.max(0, this.userTotal.silenced - 1);
  940. // 回到对应的在线/离线列表(根据当前在线状态)
  941. if (online === 0) {
  942. // 加入在线列表(去重+限制长度)
  943. const onlineList = this.onlineDisplayList.filter(u => u.userId !== userId);
  944. onlineList.push(user);
  945. this.onlineDisplayList = onlineList.slice(-40);
  946. this.userTotal.online = this.userTotal.online + 1;
  947. } else {
  948. // 加入离线列表(去重+限制长度)
  949. const offlineList = this.offlineDisplayList.filter(u => u.userId !== userId);
  950. offlineList.push(user);
  951. this.offlineDisplayList = offlineList.slice(-40);
  952. this.userTotal.offline = this.userTotal.offline + 1;
  953. }
  954. }
  955. },
  956. blockUser(u){
  957. this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
  958. confirmButtonText: "确定",
  959. cancelButtonText: "取消",
  960. type: "warning"
  961. }).then(function() {
  962. return blockUser(u.userId);
  963. }).then(() => {
  964. if(u.msgStatus === 1){
  965. this.userTotal.silenced -= 1
  966. this.silencedDisplayList = this.silencedDisplayList.filter(user => user.userId !== u.userId)
  967. } else if( u.online === 0){
  968. this.userTotal.online -= 1
  969. this.onlineDisplayList = this.onlineDisplayList.filter(user => user.userId !== u.userId)
  970. } else {
  971. this.userTotal.offline -= 1
  972. this.offlineDisplayList = this.offlineDisplayList.filter(user => user.userId !== u.userId)
  973. }
  974. this.userTotal.al -= 1
  975. this.alDisplayList = this.alDisplayList.filter(user => user.userId !== u.userId)
  976. let msg = {
  977. msg: "",
  978. liveId: this.liveId,
  979. userId: u.userId,
  980. userType: 0,
  981. cmd: 'blockUser',
  982. avatar: this.$store.state.user.user.avatar,
  983. nickName: this.$store.state.user.user.nickName
  984. }
  985. this.socket.send(JSON.stringify(msg))
  986. this.msgSuccess("封禁成功");
  987. }).catch(() => {});
  988. },
  989. searchUsers(){
  990. this.resetUserParams()
  991. this.loadNextPage(this.currentTab)
  992. },
  993. handleUserClick(tab){
  994. const tabName = tab.name;
  995. this.currentTab = tabName;
  996. const params = this.pageParams[tabName];
  997. const displayList = this[`${tabName}DisplayList`];
  998. // 首次切换到该Tab或列表为空时初始化
  999. if (displayList.length < 20) {
  1000. // 重置分页参数
  1001. params.currentPage = 1;
  1002. params.pageSize = 20;
  1003. params.prevPage = 0;
  1004. params.totalLoaded = 0;
  1005. params.hasMore = true;
  1006. params.hasPrev = false;
  1007. // 加载第一页
  1008. this.loadNextPage(tabName);
  1009. } else {
  1010. // 非首次切换,恢复滚动位置
  1011. this.$nextTick(() => {
  1012. const scrollEl = this.getScrollElement(tabName);
  1013. if (scrollEl) {
  1014. scrollEl.scrollTop = this.tabScrollPositions[tabName] || 0;
  1015. }
  1016. });
  1017. }
  1018. },
  1019. loadNextPage(tabName) {
  1020. const params = this.pageParams[tabName];
  1021. const displayList = this[`${tabName}DisplayList`];
  1022. // 若没有更多数据或正在加载,直接返回
  1023. if (!params.hasMore || this.scrLoading[tabName].next) {
  1024. return;
  1025. }
  1026. this.scrLoading[tabName].next = true;
  1027. const queryParams = {
  1028. liveId: this.liveId,
  1029. pageNum: params.currentPage,
  1030. pageSize: 20,
  1031. online: tabName === 'online' ? 0 : 1,
  1032. msgStatus: tabName === 'silenced' ? 1 : 0,
  1033. all: tabName === 'al' ? 1 : 0,
  1034. userName: this.searchKeyword
  1035. };
  1036. // 调用接口加载对应状态的分页数据(需后端支持按状态筛选)
  1037. dashBoardWatchUserList(queryParams).then(response => {
  1038. this.scrLoading[tabName].next = false;
  1039. if (response.code !== 200) return;
  1040. const { rows, total } = response;
  1041. params.total = total; // 记录总数据量
  1042. // 过滤重复数据(基于userId)
  1043. const newRows = rows.filter(row =>
  1044. !displayList.some(u => u.userId === row.userId)
  1045. );
  1046. displayList.push(...newRows)
  1047. // 添加新数据并限制最大长度(避免内存占用过大)
  1048. if (displayList.length >= 40) { // 最大保留100条
  1049. this[`${tabName}DisplayList`] = displayList.slice(-40);
  1050. // 记录滚动位置(用于加载后校准)
  1051. const scrollEl = this.getScrollElement(tabName);
  1052. // 校准滚动位置(保持视觉连续性)
  1053. this.$nextTick(() => {
  1054. if (scrollEl) {
  1055. scrollEl.scrollTop = scrollEl.scrollHeight * 0.5;
  1056. }
  1057. });
  1058. }
  1059. // 更新分页状态
  1060. params.hasMore = params.currentPage * params.pageSize < total;
  1061. params.currentPage += 1;
  1062. params.hasPrev = params.currentPage > 2; // 当前页>2时一定有上一页
  1063. params.prevPage = params.currentPage - 2;
  1064. }).catch(() => {
  1065. this.scrLoading[tabName].next = false;
  1066. });
  1067. },
  1068. // 新增:加载上一页(向上滚动时)
  1069. loadPrevPage(tabName) {
  1070. const params = this.pageParams[tabName];
  1071. const displayList = this[`${tabName}DisplayList`];
  1072. // 边界校验:无上一页/正在加载/当前页<=1
  1073. console.log(`加载 ${tabName} 上一页`);
  1074. console.log(!params.hasPrev || this.scrLoading[tabName].prev || params.currentPage <= 1)
  1075. if (!params.hasPrev || this.scrLoading[tabName].prev || params.currentPage <= 1) {
  1076. return;
  1077. }
  1078. this.scrLoading[tabName].prev = true;
  1079. const targetPage = params.prevPage > 0 ? params.prevPage : params.currentPage - 2;
  1080. const queryParams = {
  1081. liveId: this.liveId,
  1082. pageNum: targetPage,
  1083. pageSize: 20,
  1084. online: tabName === 'online' ? 0 : 1,
  1085. msgStatus: tabName === 'silenced' ? 1 : 0,
  1086. all: tabName === 'al' ? 1 : 0,
  1087. userName: this.searchKeyword
  1088. };
  1089. dashBoardWatchUserList(queryParams).then(response => {
  1090. this.scrLoading[tabName].prev = false;
  1091. if (response.code !== 200) return;
  1092. const { rows } = response;
  1093. if (rows.length === 0) {
  1094. params.hasPrev = false;
  1095. return;
  1096. }
  1097. // 记录滚动位置(用于加载后校准)
  1098. const scrollEl = this.getScrollElement(tabName);
  1099. const scrollTop = scrollEl?.scrollTop || 0;
  1100. const itemHeight = 80; // 预估行高(根据实际样式调整)
  1101. const newItemsHeight = rows.length * itemHeight;
  1102. // 过滤重复数据并添加到列表头部
  1103. const newRows = rows.filter(row => !displayList.some(u => u.userId === row.userId));
  1104. this[`${tabName}DisplayList`] = [...newRows, ...displayList];
  1105. params.totalLoaded += newRows.length;
  1106. // 限制最大长度
  1107. if (this[`${tabName}DisplayList`].length > 40) {
  1108. this[`${tabName}DisplayList`] = this[`${tabName}DisplayList`].slice(0, 40);
  1109. }
  1110. // 更新分页状态
  1111. params.prevPage = targetPage - 1;
  1112. params.hasPrev = targetPage > 1; // 上一页页码>1时还有更多上一页
  1113. params.currentPage = params.currentPage - 1;
  1114. if(params.currentPage * 20 < params.total) params.hasMore = true;
  1115. // 校准滚动位置(保持视觉连续性)
  1116. this.$nextTick(() => {
  1117. if (scrollEl) {
  1118. scrollEl.scrollTop = scrollEl.scrollHeight * 0.5;
  1119. }
  1120. });
  1121. }).catch(() => {
  1122. this.scrLoading[tabName].prev = false;
  1123. });
  1124. },
  1125. getList() {
  1126. this.resetUserParams()
  1127. this.resetMsgParams()
  1128. // this.loadUserList()
  1129. this.loadUserTotals(); // 先加载总人数
  1130. // this.handleClick('online')
  1131. // this.handleClick({name:'online'})
  1132. this.loadMsgList()
  1133. this.loadLiveTask()
  1134. },
  1135. formatDate(value) {
  1136. // 检查值是否存在且为日期类型(或可转换为日期)
  1137. if (!value) return '';
  1138. let date;
  1139. // 处理字符串类型的日期
  1140. if (typeof value === 'string') {
  1141. date = new Date(value);
  1142. }
  1143. // 处理Date对象
  1144. else if (value instanceof Date) {
  1145. date = value;
  1146. }
  1147. // 无效类型直接返回原值
  1148. else {
  1149. return value;
  1150. }
  1151. // 检查是否为有效日期
  1152. if (isNaN(date.getTime())) {
  1153. return value;
  1154. }
  1155. // 格式化年月日时分秒(补零处理)
  1156. const year = date.getFullYear();
  1157. const month = String(date.getMonth() + 1).padStart(2, '0');
  1158. const day = String(date.getDate()).padStart(2, '0');
  1159. const hours = String(date.getHours()).padStart(2, '0');
  1160. const minutes = String(date.getMinutes()).padStart(2, '0');
  1161. const seconds = String(date.getSeconds()).padStart(2, '0');
  1162. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  1163. },
  1164. loadLiveTask(){
  1165. if(!this.taskParams.hasMore) return
  1166. const queryParams = {
  1167. liveId: this.liveId,
  1168. pageSize: 10,
  1169. pageNum: 1
  1170. }
  1171. consoleList(queryParams).then(res => {
  1172. let {code, rows,total} = res;
  1173. if (code === 200) {
  1174. this.taskParams.total = total
  1175. this.timelineItems = rows
  1176. if(rows.length < this.taskParams.pageSize){
  1177. this.taskParams.hasMore = false
  1178. }
  1179. this.startTaskTimer()
  1180. } else {
  1181. this.stopTaskTimer()
  1182. }
  1183. })
  1184. },
  1185. loadMsgList(){
  1186. // 直播间消息
  1187. listLiveSingleMsg({
  1188. liveId:this.liveId,
  1189. pageNum: this.msgParams.pageNum,
  1190. pageSize: this.msgParams.pageSize
  1191. }).then(response => {
  1192. let {code, rows,total} = response;
  1193. if (code === 200) {
  1194. let totalPage = (total % this.msgParams.pageSize == 0) ? Math.floor(total / this.msgParams.pageSize) : Math.floor(total / this.msgParams.pageSize + 1);
  1195. rows.forEach(row => {
  1196. if (!this.msgList.some(m => m.msgId === row.msgId)) {
  1197. let user = this.alDisplayList.find(u => u.userId === row.userId)
  1198. if (user) {
  1199. row.msgStatus = user.msgStatus
  1200. } else {
  1201. row.msgStatus = 0
  1202. }
  1203. this.msgList.push(row)
  1204. }
  1205. })
  1206. this.msgList.reverse()
  1207. // 同步更新消息列表中相同用户的状态
  1208. this.alDisplayList.forEach(u => {
  1209. this.msgList.filter(m => m.userId === u.userId).forEach(m => m.msgStatus = u.msgStatus)
  1210. })
  1211. // 所有消息加载完成后,根据自动滚动状态决定是否滚动
  1212. this.$nextTick(() => {
  1213. setTimeout(() => {
  1214. // 重置加载状态
  1215. this.isLoadingLatest = false;
  1216. if (this.isAutoScrollEnabled) {
  1217. // 如果启用自动滚动,强制滚动到底部并隐藏按钮
  1218. this.scrollToBottom(true);
  1219. } else {
  1220. // 如果禁用自动滚动,检查是否在底部,决定是否显示按钮
  1221. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  1222. const wrap = this.$refs.manageRightRef.wrap;
  1223. const scrollHeight = wrap.scrollHeight;
  1224. const clientHeight = wrap.clientHeight;
  1225. const currentScrollTop = wrap.scrollTop;
  1226. const maxScrollTop = scrollHeight - clientHeight;
  1227. if (currentScrollTop < maxScrollTop - 50) {
  1228. this.showLoadLatestBtn = true;
  1229. } else {
  1230. this.showLoadLatestBtn = false;
  1231. }
  1232. }
  1233. }
  1234. }, 300);
  1235. });
  1236. }
  1237. })
  1238. // 添加滚动监听
  1239. this.$nextTick(() => {
  1240. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  1241. this.$refs.manageRightRef.wrap.addEventListener("scroll", this.manageRightScroll)
  1242. }
  1243. })
  1244. },
  1245. // 消息滚动监听(带防抖)
  1246. manageRightScroll() {
  1247. // 清除之前的防抖定时器
  1248. if (this.scrollDebounceTimer) {
  1249. clearTimeout(this.scrollDebounceTimer);
  1250. }
  1251. // 设置防抖,300ms内只执行一次
  1252. this.scrollDebounceTimer = setTimeout(() => {
  1253. this.saveChatScrollPosition();
  1254. // 检查是否滚动到底部
  1255. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  1256. const wrap = this.$refs.manageRightRef.wrap;
  1257. const scrollHeight = wrap.scrollHeight;
  1258. const clientHeight = wrap.clientHeight;
  1259. const currentScrollTop = wrap.scrollTop;
  1260. const maxScrollTop = scrollHeight - clientHeight;
  1261. // 如果滚动到底部(距离底部小于50px),隐藏按钮并恢复自动滚动
  1262. if (currentScrollTop >= maxScrollTop - 50) {
  1263. this.showLoadLatestBtn = false;
  1264. this.isAutoScrollEnabled = true;
  1265. }
  1266. }
  1267. }, 300);
  1268. },
  1269. loadUserTotals() {
  1270. if (!this.liveId) return;
  1271. // 假设后端提供一个接口返回总人数(如果没有,可通过首次加载全量数据后统计)
  1272. getLiveUserTotals({ liveId: this.liveId }).then(res => {
  1273. if (res.code === 200) {
  1274. this.userTotal = res.data; // { online, offline, silenced }
  1275. }
  1276. });
  1277. },
  1278. toggleBlack(user) {
  1279. user.isBlack = !user.isBlack;
  1280. },
  1281. toggleMute(user) {
  1282. user.isMuted = !user.isMuted;
  1283. },
  1284. toggleVisible(msg) {
  1285. msg.isVisible = !msg.isVisible;
  1286. },
  1287. deleteMessage(msg) {
  1288. const index = this.messages.indexOf(msg);
  1289. if (index > -1) {
  1290. this.messages.splice(index, 1);
  1291. }
  1292. },
  1293. addTimelineItem() {
  1294. this.timelineItems.push({ time: '00:00', action: '新动作' });
  1295. },
  1296. removeTimelineItem(item) {
  1297. const index = this.timelineItems.indexOf(item);
  1298. if (index > -1) {
  1299. this.timelineItems.splice(index, 1);
  1300. }
  1301. },
  1302. resetUserParams() {
  1303. // 重置各Tab的显示列表和分页参数
  1304. this.alDisplayList = [];
  1305. this.onlineDisplayList = []; // 在线用户显示列表
  1306. this.offlineDisplayList = []; // 离线用户显示列表
  1307. this.silencedDisplayList = []; // 禁言用户显示列表
  1308. this.pageParams= {
  1309. al: {
  1310. currentPage: 1,
  1311. pageSize: 20,
  1312. prevPage: 0,
  1313. totalLoaded: 0,
  1314. total: 0,
  1315. hasMore: true,
  1316. hasPrev: false
  1317. },
  1318. online: {
  1319. currentPage: 1, // 当前页(下一页加载用)
  1320. pageSize: 20, // 当前页(下一页加载用)
  1321. prevPage: 0, // 上一页页码(上一页加载用)
  1322. totalLoaded: 0, // 已加载总条数
  1323. total: 0, // 总数据量
  1324. hasMore: true, // 是否有下一页
  1325. hasPrev: false // 是否有上一页
  1326. },
  1327. offline: {
  1328. currentPage: 1,
  1329. pageSize: 20,
  1330. prevPage: 0,
  1331. totalLoaded: 0,
  1332. total: 0,
  1333. hasMore: true,
  1334. hasPrev: false
  1335. },
  1336. silenced: {
  1337. currentPage: 1,
  1338. pageSize: 20,
  1339. prevPage: 0,
  1340. totalLoaded: 0,
  1341. total: 0,
  1342. hasMore: true,
  1343. hasPrev: false
  1344. }
  1345. },
  1346. this.scrLoading = {
  1347. al: { next: false, prev: false },
  1348. online: { next: false, prev: false },
  1349. offline: { next: false, prev: false },
  1350. silenced: { next: false, prev: false }
  1351. }
  1352. },
  1353. resetMsgParams() {
  1354. // 消息参数保留
  1355. this.msgList = [];
  1356. this.msgParams = {
  1357. pageNum: 1,
  1358. pageSize: 30,
  1359. liveId: this.liveId
  1360. };
  1361. // 重置时不改变按钮状态,由后续的滚动逻辑决定
  1362. this.taskParams = {
  1363. currentPage: 1,
  1364. pageSize: 20,
  1365. prevPage: 0,
  1366. totalLoaded: 0,
  1367. total: 0,
  1368. hasMore: true,
  1369. hasPrev: false
  1370. }
  1371. },
  1372. getScrollElement(tabName) {
  1373. const scrollRefs = {
  1374. al: this.$refs.manageLeftRef_al,
  1375. online: this.$refs.manageLeftRef_online,
  1376. offline: this.$refs.manageLeftRef_offline,
  1377. silenced: this.$refs.manageLeftRef_silenced
  1378. };
  1379. return scrollRefs[tabName]?.wrap;
  1380. },
  1381. // 初始化滚动监听(在mounted中调用)
  1382. initScrollListeners() {
  1383. // 为每个Tab的滚动容器添加监听
  1384. this.$nextTick(() => {
  1385. const scrollRefs = {
  1386. al: this.$refs.manageLeftRef_al,
  1387. online: this.$refs.manageLeftRef_online,
  1388. offline: this.$refs.manageLeftRef_offline,
  1389. silenced: this.$refs.manageLeftRef_silenced
  1390. };
  1391. Object.keys(scrollRefs).forEach(tabName => {
  1392. const scrollEl = scrollRefs[tabName]?.wrap;
  1393. if (scrollEl) {
  1394. scrollEl.addEventListener('scroll', () =>
  1395. this.handleTabScroll(tabName, scrollEl)
  1396. );
  1397. }
  1398. });
  1399. });
  1400. },
  1401. // 处理Tab滚动事件(判断是否触底)
  1402. handleTabScroll(tabName, scrollEl) {
  1403. const { scrollTop, scrollHeight, clientHeight } = scrollEl;
  1404. const bottomThreshold = 50; // 距离底部100px触发下一页
  1405. const topThreshold = 50; // 距离顶部100px触发上一页
  1406. // 加载下一页(滚动到底部附近)
  1407. if (scrollHeight - scrollTop - clientHeight < bottomThreshold) {
  1408. this.loadNextPage(tabName);
  1409. }
  1410. // 加载上一页(滚动到顶部附近)
  1411. if (scrollTop < topThreshold) {
  1412. this.loadPrevPage(tabName);
  1413. }
  1414. },
  1415. // 恢复聊天滚动位置
  1416. restoreChatScrollPosition() {
  1417. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  1418. this.$refs.manageRightRef.wrap.scrollTop = this.chatScrollTop;
  1419. }
  1420. },
  1421. // 保存聊天滚动位置
  1422. saveChatScrollPosition() {
  1423. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  1424. this.chatScrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight;
  1425. }
  1426. },
  1427. /**
  1428. * 停止任务检测定时器
  1429. */
  1430. stopTaskTimer() {
  1431. if (this.taskTimer) {
  1432. clearInterval(this.taskTimer);
  1433. this.taskTimer = null;
  1434. }
  1435. },
  1436. /**
  1437. * 启动任务检测定时器
  1438. */
  1439. startTaskTimer() {
  1440. // 先清除已有定时器,避免重复
  1441. if (this.taskTimer) {
  1442. clearInterval(this.taskTimer);
  1443. }
  1444. // 立即执行一次检查
  1445. this.checkTaskExpiration();
  1446. // 启动定时器,定期检查
  1447. this.taskTimer = setInterval(() => {
  1448. this.checkTaskExpiration();
  1449. }, this.checkInterval);
  1450. },
  1451. /**
  1452. * 检查时间轴第一条任务是否过期
  1453. */
  1454. checkTaskExpiration() {
  1455. // 如果没有任务,直接返回
  1456. if (!this.timelineItems || this.timelineItems.length === 0) {
  1457. this.stopTaskTimer()
  1458. return;
  1459. };
  1460. // 获取第一条任务的时间
  1461. const firstTask = this.timelineItems[0];
  1462. const taskTime = new Date(firstTask.absValue).getTime();
  1463. const currentTime = new Date().getTime();
  1464. // 如果任务时间已过当前时间(过期),重新加载任务列表
  1465. if (taskTime < currentTime) {
  1466. this.timelineItems.shift()
  1467. this.loadLiveTask(); // 重新加载任务列表
  1468. }
  1469. },
  1470. // 新增:处理接收到的行为记录(WebSocket推送)
  1471. handleBehaviorRecord(behaviorData) {
  1472. if (!behaviorData) return;
  1473. const behavior = {
  1474. id: behaviorData.id || Date.now(),
  1475. userId: behaviorData.userId,
  1476. nickName: this.getUserName(behaviorData.userId),
  1477. avatar: this.getUserAvatar(behaviorData.userId),
  1478. behaviorType: behaviorData.behaviorType,
  1479. behaviorDesc: behaviorData.behaviorDesc || this.getBehaviorText(behaviorData.behaviorType),
  1480. behaviorTime: behaviorData.behaviorTime || new Date().getTime(),
  1481. resourceName: this.getResourceName(behaviorData),
  1482. deviceInfo: behaviorData.deviceInfo || ''
  1483. };
  1484. // 添加到行为列表头部(最新的在上面)
  1485. if (this.behaviorList.length >= 50) {
  1486. this.behaviorList.pop();
  1487. }
  1488. this.behaviorList.unshift(behavior);
  1489. // 如果启用自动滚动,滚动到顶部
  1490. if (this.isAutoBehaviorScroll) {
  1491. this.$nextTick(() => {
  1492. if (this.$refs.behaviorScrollRef && this.$refs.behaviorScrollRef.wrap) {
  1493. this.$refs.behaviorScrollRef.wrap.scrollTop = 0;
  1494. }
  1495. });
  1496. } else {
  1497. this.showLoadLatestBehaviorBtn = true;
  1498. }
  1499. },
  1500. // 新增:获取用户头像
  1501. getUserAvatar(userId) {
  1502. const user = this.alDisplayList.find(u => u.userId === userId);
  1503. return user?.avatar || 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png';
  1504. },
  1505. // 新增:获取用户昵称
  1506. getUserName(userId) {
  1507. const user = this.alDisplayList.find(u => u.userId === userId);
  1508. return user?.nickName || `用户${userId}`;
  1509. },
  1510. // 新增:获取资源名称
  1511. getResourceName(behaviorData) {
  1512. try {
  1513. if (behaviorData.extData) {
  1514. const extData = JSON.parse(behaviorData.extData);
  1515. if ([5, 6, 7, 8].includes(behaviorData.behaviorType)) {
  1516. return extData.goodsName || '';
  1517. } else if (behaviorData.behaviorType === 10) {
  1518. return extData.couponName || '';
  1519. }
  1520. }
  1521. } catch (e) {
  1522. console.error('解析extData失败:', e);
  1523. }
  1524. return '';
  1525. },
  1526. // 新增:点击行为框
  1527. handleBehaviorBoxClick() {
  1528. this.isAutoBehaviorScroll = false;
  1529. this.$nextTick(() => {
  1530. if (this.$refs.behaviorScrollRef && this.$refs.behaviorScrollRef.wrap) {
  1531. const wrap = this.$refs.behaviorScrollRef.wrap;
  1532. const scrollHeight = wrap.scrollHeight;
  1533. const clientHeight = wrap.clientHeight;
  1534. const currentScrollTop = wrap.scrollTop;
  1535. const maxScrollTop = scrollHeight - clientHeight;
  1536. if (currentScrollTop < maxScrollTop - 50) {
  1537. this.showLoadLatestBehaviorBtn = true;
  1538. }
  1539. }
  1540. });
  1541. },
  1542. // 新增:加载最新行为
  1543. loadLatestBehaviors() {
  1544. if (this.isLoadingLatestBehavior) return;
  1545. this.isLoadingLatestBehavior = false;
  1546. this.showLoadLatestBehaviorBtn = false;
  1547. this.isAutoBehaviorScroll = true;
  1548. this.$nextTick(() => {
  1549. if (this.$refs.behaviorScrollRef && this.$refs.behaviorScrollRef.wrap) {
  1550. this.$refs.behaviorScrollRef.wrap.scrollTop = 0;
  1551. }
  1552. });
  1553. },
  1554. // 新增:格式化行为时间
  1555. formatBehaviorTime(timestamp) {
  1556. if (!timestamp) return '';
  1557. const date = new Date(timestamp);
  1558. const now = new Date();
  1559. const diff = now - date;
  1560. if (diff < 60000) return '刚刚';
  1561. if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
  1562. if (date.toDateString() === now.toDateString()) {
  1563. const hours = String(date.getHours()).padStart(2, '0');
  1564. const minutes = String(date.getMinutes()).padStart(2, '0');
  1565. return `${hours}:${minutes}`;
  1566. }
  1567. const month = String(date.getMonth() + 1).padStart(2, '0');
  1568. const day = String(date.getDate()).padStart(2, '0');
  1569. const hours = String(date.getHours()).padStart(2, '0');
  1570. const minutes = String(date.getMinutes()).padStart(2, '0');
  1571. return `${month}-${day} ${hours}:${minutes}`;
  1572. },
  1573. // 新增:获取行为文本
  1574. getBehaviorText(behaviorType) {
  1575. return this.behaviorTypeMap[behaviorType] || '未知操作';
  1576. },
  1577. // 新增:获取行为图标
  1578. getBehaviorIcon(behaviorType) {
  1579. const iconMap = {
  1580. 1: 'el-icon-user', 2: 'el-icon-back', 3: 'el-icon-star-on',
  1581. 4: 'el-icon-chat-dot-round', 5: 'el-icon-view', 6: 'el-icon-shopping-cart-1',
  1582. 7: 'el-icon-shopping-cart-2', 8: 'el-icon-star-off', 9: 'el-icon-plus',
  1583. 10: 'el-icon-discount', 11: 'el-icon-trophy', 12: 'el-icon-present',
  1584. 13: 'el-icon-share', 14: 'el-icon-video-play', 15: 'el-icon-success'
  1585. };
  1586. return iconMap[behaviorType] || 'el-icon-info';
  1587. },
  1588. // 新增:获取行为颜色
  1589. getBehaviorColor(behaviorType) {
  1590. const colorMap = {
  1591. 1: '#67C23A', 2: '#909399', 3: '#FF6B9D', 4: '#409EFF',
  1592. 5: '#43e97b', 6: '#ffa94d', 7: '#F56C6C', 8: '#ff6b6b',
  1593. 9: '#f59e0b', 10: '#ffd93d', 11: '#c084fc', 12: '#fb923c',
  1594. 13: '#38bdf8', 14: '#a78bfa', 15: '#34d399'
  1595. };
  1596. return colorMap[behaviorType] || '#909399';
  1597. }
  1598. },
  1599. beforeDestroy() {
  1600. if (this.autoMsgTimer != null) {
  1601. clearInterval(this.autoMsgTimer);
  1602. }
  1603. if (this.autoScrollTimer) {
  1604. clearTimeout(this.autoScrollTimer);
  1605. }
  1606. if (this.scrollDebounceTimer) {
  1607. clearTimeout(this.scrollDebounceTimer);
  1608. }
  1609. // 移除滚动监听器
  1610. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  1611. this.$refs.manageRightRef.wrap.removeEventListener("scroll", this.manageRightScroll);
  1612. }
  1613. },
  1614. // 使用 deactivated 和 activated 钩子替代 beforeDestroy 和 destroyed
  1615. deactivated() {
  1616. this.saveChatScrollPosition();
  1617. },
  1618. activated() {
  1619. this.$nextTick(() => {
  1620. this.restoreChatScrollPosition();
  1621. });
  1622. // todo yhq
  1623. // this.$nextTick(() => {
  1624. // const video = this.$refs.videoPlayer;
  1625. // if (video != null) {
  1626. // this.initVideoPlayer(this.liveInfo.startTime)
  1627. // }
  1628. // })
  1629. },
  1630. };
  1631. </script>
  1632. <style scoped>
  1633. .console {
  1634. display: flex;
  1635. height: 100vh;
  1636. }
  1637. .left-panel, .middle-panel, .right-panel {
  1638. padding: 20px;
  1639. box-sizing: border-box;
  1640. }
  1641. .left-panel {
  1642. width: 30%;
  1643. background: #f8fafc;
  1644. border-right: 1px solid #e2e8f0;
  1645. }
  1646. .middle-panel {
  1647. width: 40%;
  1648. background: #f8fafc;
  1649. border-right: 1px solid #e2e8f0;
  1650. }
  1651. .right-panel {
  1652. width: 30%;
  1653. background: #f8fafc;
  1654. }
  1655. .search {
  1656. margin: 10px 0;
  1657. }
  1658. .search input {
  1659. width: 70%;
  1660. padding: 8px;
  1661. border: 1px solid #cbd5e1;
  1662. border-radius: 4px;
  1663. }
  1664. .search button {
  1665. padding: 8px 15px;
  1666. background: #3b82f6;
  1667. color: #fff;
  1668. border: none;
  1669. border-radius: 4px;
  1670. cursor: pointer;
  1671. }
  1672. .tabs {
  1673. display: flex;
  1674. margin: 10px 0;
  1675. }
  1676. .tabs button {
  1677. padding: 8px 15px;
  1678. border: 1px solid #e2e8f0;
  1679. background: #fff;
  1680. cursor: pointer;
  1681. }
  1682. .tabs button.active {
  1683. background: #3b82f6;
  1684. color: #fff;
  1685. border-color: #3b82f6;
  1686. }
  1687. .user-list {
  1688. max-height: 600px;
  1689. overflow-y: auto;
  1690. }
  1691. .user-item {
  1692. display: flex;
  1693. align-items: center;
  1694. padding: 10px;
  1695. border-bottom: 1px solid #e2e8f0;
  1696. }
  1697. .user-item img {
  1698. width: 40px;
  1699. height: 40px;
  1700. border-radius: 50%;
  1701. margin-right: 10px;
  1702. }
  1703. .user-info {
  1704. flex: 1;
  1705. }
  1706. .user-name {
  1707. font-weight: bold;
  1708. }
  1709. .user-status {
  1710. font-size: 12px;
  1711. color: #64748b;
  1712. }
  1713. .online {
  1714. color: #10b981;
  1715. }
  1716. .offline {
  1717. color: #94a3b8;
  1718. }
  1719. .user-actions {
  1720. display: flex;
  1721. }
  1722. .user-actions button {
  1723. padding: 5px 10px;
  1724. margin-left: 5px;
  1725. border: none;
  1726. border-radius: 4px;
  1727. cursor: pointer;
  1728. }
  1729. .block {
  1730. background: #ef4444;
  1731. color: #fff;
  1732. }
  1733. .unblock {
  1734. background: #10b981;
  1735. color: #fff;
  1736. }
  1737. .mute {
  1738. background: #f59e0b;
  1739. color: #fff;
  1740. }
  1741. .unmute {
  1742. background: #3b82f6;
  1743. color: #fff;
  1744. }
  1745. .system-messages, .discussion-messages {
  1746. margin: 20px 0;
  1747. background: #fff;
  1748. padding: 15px;
  1749. border-radius: 8px;
  1750. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  1751. }
  1752. .system-messages textarea {
  1753. width: 100%;
  1754. height: 100px;
  1755. border: 1px solid #e2e8f0;
  1756. border-radius: 4px;
  1757. padding: 8px;
  1758. box-sizing: border-box;
  1759. }
  1760. .message-actions {
  1761. margin-top: 10px;
  1762. }
  1763. .message-actions button {
  1764. padding: 5px 10px;
  1765. margin-right: 5px;
  1766. background: #3b82f6;
  1767. color: #fff;
  1768. border: none;
  1769. border-radius: 4px;
  1770. cursor: pointer;
  1771. }
  1772. .message-list {
  1773. max-height: 300px;
  1774. overflow-y: auto;
  1775. margin-top: 10px;
  1776. }
  1777. .message-item {
  1778. display: flex;
  1779. margin-bottom: 10px;
  1780. padding-bottom: 10px;
  1781. border-bottom: 1px solid #e2e8f0;
  1782. }
  1783. .message-avatar img {
  1784. width: 30px;
  1785. height: 30px;
  1786. border-radius: 50%;
  1787. margin-right: 10px;
  1788. }
  1789. .message-content {
  1790. flex: 1;
  1791. }
  1792. .message-user {
  1793. font-weight: bold;
  1794. }
  1795. .message-text {
  1796. font-size: 14px;
  1797. color: #64748b;
  1798. }
  1799. .message-actions button {
  1800. padding: 3px 8px;
  1801. font-size: 12px;
  1802. background: #3b82f6;
  1803. color: #fff;
  1804. border: none;
  1805. border-radius: 4px;
  1806. cursor: pointer;
  1807. }
  1808. .live-player, .automation, .watermark {
  1809. margin: 20px 0;
  1810. background: #fff;
  1811. padding: 15px;
  1812. border-radius: 8px;
  1813. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  1814. }
  1815. .timeline-items {
  1816. margin: 10px 0;
  1817. }
  1818. .timeline-item {
  1819. display: flex;
  1820. justify-content: space-between;
  1821. align-items: center;
  1822. padding: 8px 0;
  1823. border-bottom: 1px solid #e2e8f0;
  1824. }
  1825. .delete {
  1826. background: #ef4444;
  1827. color: #fff;
  1828. border: none;
  1829. border-radius: 4px;
  1830. padding: 3px 8px;
  1831. cursor: pointer;
  1832. }
  1833. .add {
  1834. background: #10b981;
  1835. color: #fff;
  1836. border: none;
  1837. border-radius: 4px;
  1838. padding: 8px 15px;
  1839. cursor: pointer;
  1840. }
  1841. .watermark-settings textarea {
  1842. width: 100%;
  1843. height: 100px;
  1844. border: 1px solid #e2e8f0;
  1845. border-radius: 4px;
  1846. padding: 8px;
  1847. box-sizing: border-box;
  1848. }
  1849. .watermark-options {
  1850. margin-top: 10px;
  1851. }
  1852. .watermark-options label {
  1853. display: block;
  1854. margin-bottom: 5px;
  1855. }
  1856. /* 隐藏 el-scrollbar 的横向滚动条 */
  1857. .el-scrollbar__wrap {
  1858. overflow-x: hidden !important;
  1859. }
  1860. .custom-scrollbar .el-scrollbar__wrap {
  1861. overflow-x: hidden !important;
  1862. }
  1863. .scrollbar-demo-item{
  1864. display: flex;
  1865. align-items: center;
  1866. justify-content: center;
  1867. height: 50px;
  1868. margin: 10px;
  1869. text-align: center;
  1870. border-radius: 4px;
  1871. }
  1872. .message-container {
  1873. position: relative;
  1874. }
  1875. .load-latest-btn {
  1876. position: absolute;
  1877. bottom: 20px;
  1878. right: 20px;
  1879. z-index: 10;
  1880. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  1881. }
  1882. /* 新增:行为记录样式 */
  1883. .behavior-records {
  1884. margin-bottom: 20px;
  1885. background: #fff;
  1886. padding: 15px;
  1887. border-radius: 8px;
  1888. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  1889. }
  1890. .behavior-records h3 {
  1891. margin: 0 0 15px 0;
  1892. font-size: 16px;
  1893. font-weight: 600;
  1894. color: #303133;
  1895. }
  1896. </style>