LiveConsole.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  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 ref="manageLeftRef_al" style="height: 800px; width: 100%;">
  12. <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_online" style="height: 800px; width: 100%;">
  34. <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_offline" style="height: 800px; width: 100%;">
  56. <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_silenced" style="height: 800px; width: 100%;">
  78. <el-row style="margin-top: 10px" type="flex" align="middle" 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. <div class="system-messages">
  103. <h3>系统消息</h3>
  104. <textarea placeholder="输入系统消息"></textarea>
  105. <div class="message-actions">
  106. <button>置顶消息</button>
  107. <button>弹窗消息</button>
  108. </div>
  109. </div>
  110. <div class="discussion-messages">
  111. <h3>讨论区消息</h3>
  112. <div class="message-settings">
  113. <label>
  114. <input type="checkbox" v-model="globalVisible">
  115. 全局用户可见
  116. </label>
  117. </div>
  118. <div class="message-list">
  119. <div class="message-item" v-for="msg in messages" :key="msg.id">
  120. <div class="message-avatar">
  121. <img :src="msg.avatar" alt="用户头像">
  122. </div>
  123. <div class="message-content">
  124. <div class="message-user">{{ msg.user }}</div>
  125. <div class="message-text">{{ msg.text }}</div>
  126. </div>
  127. <div class="message-actions">
  128. <button @click="toggleVisible(msg)">
  129. {{ msg.isVisible ? '仅用户自见' : '全局可见' }}
  130. </button>
  131. <button @click="deleteMessage(msg)">删除</button>
  132. </div>
  133. </div>
  134. </div>
  135. </div>
  136. </div>
  137. <div class="right-panel">
  138. <h2>运营工具</h2>
  139. <div class="live-player">
  140. <h3>直播预览</h3>
  141. <LivePlayer :stream-url="streamUrl" />
  142. </div>
  143. <div class="automation">
  144. <h3>运营自动化</h3>
  145. <div class="timeline">
  146. <h4>时间轴设置</h4>
  147. <div class="timeline-items">
  148. <div class="timeline-item" v-for="item in timelineItems" :key="item.time">
  149. <div class="time">{{ item.time }}</div>
  150. <div class="action">{{ item.action }}</div>
  151. <button class="delete" @click="removeTimelineItem(item)">删除</button>
  152. </div>
  153. </div>
  154. <button class="add" @click="addTimelineItem">添加时间节点</button>
  155. </div>
  156. </div>
  157. <div class="watermark">
  158. <h3>直播氛围自动化</h3>
  159. <div class="watermark-settings">
  160. <textarea placeholder="水军弹幕内容模板,每行一条"></textarea>
  161. <div class="watermark-options">
  162. <label>
  163. 发送间隔:
  164. <input type="number" v-model.number="interval" min="1">
  165. </label>
  166. <label>
  167. <input type="checkbox" v-model="autoWatermark">
  168. 启用水军自动化
  169. </label>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. </div>
  175. </template>
  176. <script>
  177. import LivePlayer from './LivePlayer.vue';
  178. import {blockUser, changeUserStatus, getLiveUserTotals, dashBoardWatchUserList} from '@/api/live/liveWatchUser'
  179. export default {
  180. components: {
  181. LivePlayer
  182. },
  183. props: {
  184. liveId: {
  185. type: String,
  186. default: null
  187. }
  188. },
  189. data() {
  190. return {
  191. searchKeyword: '',
  192. globalVisible: true,
  193. interval: 10,
  194. autoWatermark: false,
  195. streamUrl: 'rtmp://your-live-stream-url',
  196. users: [
  197. { id: 1, name: '用户1', avatar: 'https://via.placeholder.com/40', status: 'online', statusText: '在线', silenced: false, msgStatus: false },
  198. { id: 2, name: '用户2', avatar: 'https://via.placeholder.com/40', status: 'online', statusText: '在线', silenced: false, msgStatus: true },
  199. { id: 3, name: '用户3', avatar: 'https://via.placeholder.com/40', status: 'offline', statusText: '离线', silenced: true, msgStatus: false },
  200. { id: 4, name: '用户4', avatar: 'https://via.placeholder.com/40', status: 'online', statusText: '在线', silenced: false, msgStatus: false },
  201. { id: 5, name: '用户5', avatar: 'https://via.placeholder.com/40', status: 'offline', statusText: '离线', silenced: false, msgStatus: false }
  202. ],
  203. messages: [
  204. { id: 1, user: '用户1', avatar: 'https://via.placeholder.com/30', text: '这个产品怎么样?', isVisible: true },
  205. { id: 2, user: '用户2', avatar: 'https://via.placeholder.com/30', text: '看起来不错', isVisible: true },
  206. { id: 3, user: '用户3', avatar: 'https://via.placeholder.com/30', text: '有优惠吗?', isVisible: true }
  207. ],
  208. timelineItems: [
  209. { time: '09:00', action: '置顶欢迎消息' },
  210. { time: '09:30', action: '上架主推商品' }
  211. ],
  212. userTotal: {
  213. online: 0, // 在线总人数
  214. offline: 0, // 离线总人数
  215. silenced: 0, // 禁言总人数
  216. al: 0
  217. },
  218. tabLeft: {
  219. activeName: "online",
  220. },
  221. loadMsgMaxPage: 2,
  222. liveWsUrl: process.env.VUE_APP_LIVE_WS_URL + '/app/webSocket',
  223. alDisplayList: [],
  224. onlineDisplayList: [], // 在线用户显示列表
  225. offlineDisplayList: [], // 离线用户显示列表
  226. silencedDisplayList: [], // 禁言用户显示列表
  227. // 各Tab的分页参数
  228. pageParams: {
  229. al: {
  230. currentPage: 1,
  231. pageSize: 20,
  232. prevPage: 0,
  233. totalLoaded: 0,
  234. total: 0,
  235. hasMore: true,
  236. hasPrev: false
  237. },
  238. online: {
  239. currentPage: 1, // 当前页(下一页加载用)
  240. pageSize: 20, // 当前页(下一页加载用)
  241. prevPage: 0, // 上一页页码(上一页加载用)
  242. totalLoaded: 0, // 已加载总条数
  243. total: 0, // 总数据量
  244. hasMore: true, // 是否有下一页
  245. hasPrev: false // 是否有上一页
  246. },
  247. offline: {
  248. currentPage: 1,
  249. pageSize: 20,
  250. prevPage: 0,
  251. totalLoaded: 0,
  252. total: 0,
  253. hasMore: true,
  254. hasPrev: false
  255. },
  256. silenced: {
  257. currentPage: 1,
  258. pageSize: 20,
  259. prevPage: 0,
  260. totalLoaded: 0,
  261. total: 0,
  262. hasMore: true,
  263. hasPrev: false
  264. }
  265. },
  266. scrLoading: {
  267. al: { next: false, prev: false },
  268. online: { next: false, prev: false },
  269. offline: { next: false, prev: false },
  270. silenced: { next: false, prev: false }
  271. }
  272. };
  273. },
  274. computed: {
  275. onlineLabel() {
  276. return `在线(${this.userTotal.online})`;
  277. },
  278. alLabel() {
  279. return `全部(${this.userTotal.al})`;
  280. },
  281. offlineLabel() {
  282. return `离线(${this.userTotal.offline})`;
  283. },
  284. silencedUserLabel() {
  285. return `禁言(${this.userTotal.silenced})`;
  286. }
  287. },
  288. created() {
  289. if(!this.liveId) return
  290. this.getList()
  291. this.handleUserClick({name:'online'})
  292. },
  293. mounted() {
  294. this.$nextTick(() => {
  295. this.restoreChatScrollPosition();
  296. });
  297. this.initScrollListeners();
  298. },
  299. methods: {
  300. changeUserState(u) {
  301. // 修改状态
  302. changeUserStatus({liveId: u.liveId, userId: u.userId}).then(response => {
  303. let { code } = response;
  304. if (200 === code) {
  305. u.msgStatus = u.msgStatus === 0 ? 1 : 0
  306. // 同步更新消息列表中相同用户的状态
  307. this.msgList.forEach(msg => {
  308. if (msg.userId === u.userId) {
  309. msg.msgStatus = u.msgStatus;
  310. }
  311. });
  312. this.userList.forEach(user => {
  313. if (user.userId === u.userId) {
  314. user.msgStatus = u.msgStatus;
  315. }
  316. });
  317. // 4. 关键:重新筛选所有Tab的显示列表,确保状态同步
  318. this.refreshUserDisplayLists(u);
  319. let msg = u.msgStatus === 0 ? "已解禁" : "已禁言"
  320. this.msgSuccess(msg);
  321. return
  322. }
  323. this.msgError("操作失败");
  324. })
  325. },
  326. blockUser(u){
  327. this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
  328. confirmButtonText: "确定",
  329. cancelButtonText: "取消",
  330. type: "warning"
  331. }).then(function() {
  332. return blockUser(u.userId);
  333. }).then(() => {
  334. let msg = {
  335. msg: "",
  336. liveId: this.liveId,
  337. userId: u.userId,
  338. userType: 0,
  339. cmd: 'blockUser',
  340. avatar: this.$store.state.user.user.avatar,
  341. nickName: this.$store.state.user.user.nickName
  342. }
  343. this.socket.send(JSON.stringify(msg))
  344. this.msgSuccess("封禁成功");
  345. }).catch(() => {});
  346. },
  347. searchUsers(){
  348. this.resetUserParams()
  349. this.loadNextPage()
  350. },
  351. handleUserClick(tab){
  352. const tabName = tab.name;
  353. const params = this.pageParams[tabName];
  354. const displayList = this[`${tabName}DisplayList`];
  355. // 首次切换到该Tab或列表为空时初始化
  356. if (displayList.length < 20) {
  357. // 重置分页参数
  358. params.currentPage = 1;
  359. params.pageSize = 20;
  360. params.prevPage = 0;
  361. params.totalLoaded = 0;
  362. params.hasMore = true;
  363. params.hasPrev = false;
  364. // 加载第一页
  365. this.loadNextPage(tabName);
  366. } else {
  367. // 非首次切换,恢复滚动位置
  368. this.$nextTick(() => {
  369. const scrollEl = this.getScrollElement(tabName);
  370. if (scrollEl) {
  371. scrollEl.scrollTop = this.tabScrollPositions[tabName] || 0;
  372. }
  373. });
  374. }
  375. },
  376. loadNextPage(tabName) {
  377. const params = this.pageParams[tabName];
  378. const displayList = this[`${tabName}DisplayList`];
  379. // 若没有更多数据或正在加载,直接返回
  380. if (!params.hasMore || this.scrLoading[tabName].next) {
  381. return;
  382. }
  383. this.scrLoading[tabName].next = true;
  384. const queryParams = {
  385. liveId: this.liveId,
  386. pageNum: params.currentPage,
  387. pageSize: 20,
  388. online: tabName === 'online' ? 0 : 1,
  389. msgStatus: tabName === 'silenced' ? 1 : 0,
  390. all: tabName === 'al' ? 1 : 0,
  391. userName: this.searchKeyword
  392. };
  393. // 调用接口加载对应状态的分页数据(需后端支持按状态筛选)
  394. dashBoardWatchUserList(queryParams).then(response => {
  395. this.scrLoading[tabName].next = false;
  396. if (response.code !== 200) return;
  397. const { rows, total } = response;
  398. params.total = total; // 记录总数据量
  399. // 过滤重复数据(基于userId)
  400. const newRows = rows.filter(row =>
  401. !displayList.some(u => u.userId === row.userId)
  402. );
  403. displayList.push(...newRows)
  404. // 添加新数据并限制最大长度(避免内存占用过大)
  405. if (displayList.length >= 40) { // 最大保留100条
  406. this[`${tabName}DisplayList`] = displayList.slice(-40);
  407. // 记录滚动位置(用于加载后校准)
  408. const scrollEl = this.getScrollElement(tabName);
  409. // 校准滚动位置(保持视觉连续性)
  410. this.$nextTick(() => {
  411. if (scrollEl) {
  412. scrollEl.scrollTop = scrollEl.scrollHeight * 0.5;
  413. }
  414. });
  415. }
  416. // 更新分页状态
  417. params.hasMore = params.currentPage * params.pageSize < total;
  418. params.currentPage += 1;
  419. params.hasPrev = params.currentPage > 2; // 当前页>2时一定有上一页
  420. params.prevPage = params.currentPage - 2;
  421. }).catch(() => {
  422. this.scrLoading[tabName].next = false;
  423. });
  424. },
  425. // 新增:加载上一页(向上滚动时)
  426. loadPrevPage(tabName) {
  427. const params = this.pageParams[tabName];
  428. const displayList = this[`${tabName}DisplayList`];
  429. // 边界校验:无上一页/正在加载/当前页<=1
  430. console.log(`加载 ${tabName} 上一页`);
  431. console.log(!params.hasPrev || this.scrLoading[tabName].prev || params.currentPage <= 1)
  432. if (!params.hasPrev || this.scrLoading[tabName].prev || params.currentPage <= 1) {
  433. return;
  434. }
  435. this.scrLoading[tabName].prev = true;
  436. const targetPage = params.prevPage > 0 ? params.prevPage : params.currentPage - 2;
  437. const queryParams = {
  438. liveId: this.liveId,
  439. pageNum: targetPage,
  440. pageSize: 20,
  441. online: tabName === 'online' ? 0 : 1,
  442. msgStatus: tabName === 'silenced' ? 1 : 0,
  443. all: tabName === 'al' ? 1 : 0,
  444. userName: this.searchKeyword
  445. };
  446. dashBoardWatchUserList(queryParams).then(response => {
  447. this.scrLoading[tabName].prev = false;
  448. if (response.code !== 200) return;
  449. const { rows } = response;
  450. if (rows.length === 0) {
  451. params.hasPrev = false;
  452. return;
  453. }
  454. // 记录滚动位置(用于加载后校准)
  455. const scrollEl = this.getScrollElement(tabName);
  456. const scrollTop = scrollEl?.scrollTop || 0;
  457. const itemHeight = 80; // 预估行高(根据实际样式调整)
  458. const newItemsHeight = rows.length * itemHeight;
  459. // 过滤重复数据并添加到列表头部
  460. const newRows = rows.filter(row => !displayList.some(u => u.userId === row.userId));
  461. this[`${tabName}DisplayList`] = [...newRows, ...displayList];
  462. params.totalLoaded += newRows.length;
  463. // 限制最大长度
  464. if (this[`${tabName}DisplayList`].length > 40) {
  465. this[`${tabName}DisplayList`] = this[`${tabName}DisplayList`].slice(0, 40);
  466. }
  467. // 更新分页状态
  468. params.prevPage = targetPage - 1;
  469. params.hasPrev = targetPage > 1; // 上一页页码>1时还有更多上一页
  470. params.currentPage = params.currentPage - 1;
  471. if(params.currentPage * 20 < params.total) params.hasMore = true;
  472. // 校准滚动位置(保持视觉连续性)
  473. this.$nextTick(() => {
  474. if (scrollEl) {
  475. scrollEl.scrollTop = scrollEl.scrollHeight * 0.5;
  476. }
  477. });
  478. }).catch(() => {
  479. this.scrLoading[tabName].prev = false;
  480. });
  481. },
  482. getList() {
  483. this.resetUserParams()
  484. this.resetMsgParams()
  485. // this.loadUserList()
  486. this.loadUserTotals(); // 先加载总人数
  487. // this.handleClick('online')
  488. // this.handleClick({name:'online'})
  489. // this.loadMsgList()
  490. },
  491. loadUserTotals() {
  492. if (!this.liveId) return;
  493. // 假设后端提供一个接口返回总人数(如果没有,可通过首次加载全量数据后统计)
  494. getLiveUserTotals({ liveId: this.liveId }).then(res => {
  495. if (res.code === 200) {
  496. this.userTotal = res.data; // { online, offline, silenced }
  497. }
  498. });
  499. },
  500. toggleBlack(user) {
  501. user.isBlack = !user.isBlack;
  502. },
  503. toggleMute(user) {
  504. user.isMuted = !user.isMuted;
  505. },
  506. toggleVisible(msg) {
  507. msg.isVisible = !msg.isVisible;
  508. },
  509. deleteMessage(msg) {
  510. const index = this.messages.indexOf(msg);
  511. if (index > -1) {
  512. this.messages.splice(index, 1);
  513. }
  514. },
  515. addTimelineItem() {
  516. this.timelineItems.push({ time: '00:00', action: '新动作' });
  517. },
  518. removeTimelineItem(item) {
  519. const index = this.timelineItems.indexOf(item);
  520. if (index > -1) {
  521. this.timelineItems.splice(index, 1);
  522. }
  523. },
  524. resetUserParams() {
  525. // 重置各Tab的显示列表和分页参数
  526. this.alDisplayList = [];
  527. this.onlineDisplayList = []; // 在线用户显示列表
  528. this.offlineDisplayList = []; // 离线用户显示列表
  529. this.silencedDisplayList = []; // 禁言用户显示列表
  530. this.pageParams= {
  531. al: {
  532. currentPage: 1,
  533. pageSize: 20,
  534. prevPage: 0,
  535. totalLoaded: 0,
  536. total: 0,
  537. hasMore: true,
  538. hasPrev: false
  539. },
  540. online: {
  541. currentPage: 1, // 当前页(下一页加载用)
  542. pageSize: 20, // 当前页(下一页加载用)
  543. prevPage: 0, // 上一页页码(上一页加载用)
  544. totalLoaded: 0, // 已加载总条数
  545. total: 0, // 总数据量
  546. hasMore: true, // 是否有下一页
  547. hasPrev: false // 是否有上一页
  548. },
  549. offline: {
  550. currentPage: 1,
  551. pageSize: 20,
  552. prevPage: 0,
  553. totalLoaded: 0,
  554. total: 0,
  555. hasMore: true,
  556. hasPrev: false
  557. },
  558. silenced: {
  559. currentPage: 1,
  560. pageSize: 20,
  561. prevPage: 0,
  562. totalLoaded: 0,
  563. total: 0,
  564. hasMore: true,
  565. hasPrev: false
  566. }
  567. },
  568. this.scrLoading = {
  569. al: { next: false, prev: false },
  570. online: { next: false, prev: false },
  571. offline: { next: false, prev: false },
  572. silenced: { next: false, prev: false }
  573. }
  574. },
  575. resetMsgParams() {
  576. // 消息参数保留
  577. this.msgList = [];
  578. this.msgParams = {
  579. pageNum: 1,
  580. pageSize: 10,
  581. liveId: this.liveId
  582. };
  583. },
  584. getScrollElement(tabName) {
  585. const scrollRefs = {
  586. al: this.$refs.manageLeftRef_al,
  587. online: this.$refs.manageLeftRef_online,
  588. offline: this.$refs.manageLeftRef_offline,
  589. silenced: this.$refs.manageLeftRef_silenced
  590. };
  591. return scrollRefs[tabName]?.wrap;
  592. },
  593. // 初始化滚动监听(在mounted中调用)
  594. initScrollListeners() {
  595. // 为每个Tab的滚动容器添加监听
  596. this.$nextTick(() => {
  597. const scrollRefs = {
  598. al: this.$refs.manageLeftRef_al,
  599. online: this.$refs.manageLeftRef_online,
  600. offline: this.$refs.manageLeftRef_offline,
  601. silenced: this.$refs.manageLeftRef_silenced
  602. };
  603. Object.keys(scrollRefs).forEach(tabName => {
  604. const scrollEl = scrollRefs[tabName]?.wrap;
  605. if (scrollEl) {
  606. scrollEl.addEventListener('scroll', () =>
  607. this.handleTabScroll(tabName, scrollEl)
  608. );
  609. }
  610. });
  611. });
  612. },
  613. // 处理Tab滚动事件(判断是否触底)
  614. handleTabScroll(tabName, scrollEl) {
  615. const { scrollTop, scrollHeight, clientHeight } = scrollEl;
  616. const bottomThreshold = 50; // 距离底部100px触发下一页
  617. const topThreshold = 50; // 距离顶部100px触发上一页
  618. // 加载下一页(滚动到底部附近)
  619. if (scrollHeight - scrollTop - clientHeight < bottomThreshold) {
  620. this.loadNextPage(tabName);
  621. }
  622. // 加载上一页(滚动到顶部附近)
  623. if (scrollTop < topThreshold) {
  624. this.loadPrevPage(tabName);
  625. }
  626. },
  627. // 恢复聊天滚动位置
  628. restoreChatScrollPosition() {
  629. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  630. this.$refs.manageRightRef.wrap.scrollTop = this.chatScrollTop;
  631. }
  632. },
  633. // 保存聊天滚动位置
  634. saveChatScrollPosition() {
  635. if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
  636. this.chatScrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight;
  637. }
  638. },
  639. },
  640. // 使用 deactivated 和 activated 钩子替代 beforeDestroy 和 destroyed
  641. deactivated() {
  642. this.saveChatScrollPosition();
  643. },
  644. activated() {
  645. this.$nextTick(() => {
  646. this.restoreChatScrollPosition();
  647. });
  648. // todo yhq
  649. // this.$nextTick(() => {
  650. // const video = this.$refs.videoPlayer;
  651. // if (video != null) {
  652. // this.initVideoPlayer(this.liveInfo.startTime)
  653. // }
  654. // })
  655. },
  656. };
  657. </script>
  658. <style scoped>
  659. .console {
  660. display: flex;
  661. height: 100vh;
  662. }
  663. .left-panel, .middle-panel, .right-panel {
  664. padding: 20px;
  665. box-sizing: border-box;
  666. }
  667. .left-panel {
  668. width: 30%;
  669. background: #f8fafc;
  670. border-right: 1px solid #e2e8f0;
  671. }
  672. .middle-panel {
  673. width: 40%;
  674. background: #f8fafc;
  675. border-right: 1px solid #e2e8f0;
  676. }
  677. .right-panel {
  678. width: 30%;
  679. background: #f8fafc;
  680. }
  681. .search {
  682. margin: 10px 0;
  683. }
  684. .search input {
  685. width: 70%;
  686. padding: 8px;
  687. border: 1px solid #cbd5e1;
  688. border-radius: 4px;
  689. }
  690. .search button {
  691. padding: 8px 15px;
  692. background: #3b82f6;
  693. color: #fff;
  694. border: none;
  695. border-radius: 4px;
  696. cursor: pointer;
  697. }
  698. .tabs {
  699. display: flex;
  700. margin: 10px 0;
  701. }
  702. .tabs button {
  703. padding: 8px 15px;
  704. border: 1px solid #e2e8f0;
  705. background: #fff;
  706. cursor: pointer;
  707. }
  708. .tabs button.active {
  709. background: #3b82f6;
  710. color: #fff;
  711. border-color: #3b82f6;
  712. }
  713. .user-list {
  714. max-height: 600px;
  715. overflow-y: auto;
  716. }
  717. .user-item {
  718. display: flex;
  719. align-items: center;
  720. padding: 10px;
  721. border-bottom: 1px solid #e2e8f0;
  722. }
  723. .user-item img {
  724. width: 40px;
  725. height: 40px;
  726. border-radius: 50%;
  727. margin-right: 10px;
  728. }
  729. .user-info {
  730. flex: 1;
  731. }
  732. .user-name {
  733. font-weight: bold;
  734. }
  735. .user-status {
  736. font-size: 12px;
  737. color: #64748b;
  738. }
  739. .online {
  740. color: #10b981;
  741. }
  742. .offline {
  743. color: #94a3b8;
  744. }
  745. .user-actions {
  746. display: flex;
  747. }
  748. .user-actions button {
  749. padding: 5px 10px;
  750. margin-left: 5px;
  751. border: none;
  752. border-radius: 4px;
  753. cursor: pointer;
  754. }
  755. .block {
  756. background: #ef4444;
  757. color: #fff;
  758. }
  759. .unblock {
  760. background: #10b981;
  761. color: #fff;
  762. }
  763. .mute {
  764. background: #f59e0b;
  765. color: #fff;
  766. }
  767. .unmute {
  768. background: #3b82f6;
  769. color: #fff;
  770. }
  771. .system-messages, .discussion-messages {
  772. margin: 20px 0;
  773. background: #fff;
  774. padding: 15px;
  775. border-radius: 8px;
  776. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  777. }
  778. .system-messages textarea {
  779. width: 100%;
  780. height: 100px;
  781. border: 1px solid #e2e8f0;
  782. border-radius: 4px;
  783. padding: 8px;
  784. box-sizing: border-box;
  785. }
  786. .message-actions {
  787. margin-top: 10px;
  788. }
  789. .message-actions button {
  790. padding: 5px 10px;
  791. margin-right: 5px;
  792. background: #3b82f6;
  793. color: #fff;
  794. border: none;
  795. border-radius: 4px;
  796. cursor: pointer;
  797. }
  798. .message-list {
  799. max-height: 300px;
  800. overflow-y: auto;
  801. margin-top: 10px;
  802. }
  803. .message-item {
  804. display: flex;
  805. margin-bottom: 10px;
  806. padding-bottom: 10px;
  807. border-bottom: 1px solid #e2e8f0;
  808. }
  809. .message-avatar img {
  810. width: 30px;
  811. height: 30px;
  812. border-radius: 50%;
  813. margin-right: 10px;
  814. }
  815. .message-content {
  816. flex: 1;
  817. }
  818. .message-user {
  819. font-weight: bold;
  820. }
  821. .message-text {
  822. font-size: 14px;
  823. color: #64748b;
  824. }
  825. .message-actions button {
  826. padding: 3px 8px;
  827. font-size: 12px;
  828. background: #3b82f6;
  829. color: #fff;
  830. border: none;
  831. border-radius: 4px;
  832. cursor: pointer;
  833. }
  834. .live-player, .automation, .watermark {
  835. margin: 20px 0;
  836. background: #fff;
  837. padding: 15px;
  838. border-radius: 8px;
  839. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  840. }
  841. .timeline-items {
  842. margin: 10px 0;
  843. }
  844. .timeline-item {
  845. display: flex;
  846. justify-content: space-between;
  847. align-items: center;
  848. padding: 8px 0;
  849. border-bottom: 1px solid #e2e8f0;
  850. }
  851. .delete {
  852. background: #ef4444;
  853. color: #fff;
  854. border: none;
  855. border-radius: 4px;
  856. padding: 3px 8px;
  857. cursor: pointer;
  858. }
  859. .add {
  860. background: #10b981;
  861. color: #fff;
  862. border: none;
  863. border-radius: 4px;
  864. padding: 8px 15px;
  865. cursor: pointer;
  866. }
  867. .watermark-settings textarea {
  868. width: 100%;
  869. height: 100px;
  870. border: 1px solid #e2e8f0;
  871. border-radius: 4px;
  872. padding: 8px;
  873. box-sizing: border-box;
  874. }
  875. .watermark-options {
  876. margin-top: 10px;
  877. }
  878. .watermark-options label {
  879. display: block;
  880. margin-bottom: 5px;
  881. }
  882. </style>