浏览代码

1、调整直播营销模块

yys 1 周之前
父节点
当前提交
ab58cc4e01
共有 3 个文件被更改,包括 1005 次插入438 次删除
  1. 9 0
      src/api/live/live.js
  2. 5 0
      src/views/live/liveConfig/liveCoupon.vue
  3. 991 438
      src/views/live/liveConsole/LiveConsole.vue

+ 9 - 0
src/api/live/live.js

@@ -82,6 +82,15 @@ export function updateLiveIsAudit(data) {
   })
 }
 
+// 更新直播间购物车显示状态
+export function updateShowCart(data) {
+  return request({
+    url: '/live/live/updateShowCart',
+    method: 'post',
+    data: data
+  })
+}
+
 //上下架视频
 export function handleShelfOrUn(data) {
   return request({

+ 5 - 0
src/views/live/liveConfig/liveCoupon.vue

@@ -84,6 +84,7 @@
       >
         <template slot-scope="scope">
           <el-button
+            v-if="!isVerifyCouponType(scope.row)"
             type="text"
             size="small"
             style="color: #0fae11;"
@@ -332,6 +333,10 @@ export default {
     });
   },
   methods: {
+    isVerifyCouponType(row) {
+      const label = this.selectDictLabel(this.couponTypeOptions, row.type) || '';
+      return label.includes('核销') || label.includes('代金券');
+    },
     handleGoodsChange(row){
 
     },

+ 991 - 438
src/views/live/liveConsole/LiveConsole.vue

@@ -2,259 +2,304 @@
   <div class="console">
 
     <div class="left-panel">
-      <h2>学员列表</h2>
-      <div class="search">
-        <input type="text" placeholder="搜索用户昵称" v-model="searchKeyword">
-        <button @click="searchUsers()">搜索</button>
+      <h2 class="panel-title">学员列表</h2>
+      <div class="search-bar">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索用户昵称"
+          size="small"
+          clearable
+          @keyup.enter.native="searchUsers"
+        />
+        <el-button type="primary" size="small" @click="searchUsers">搜索</el-button>
       </div>
-      <el-tabs class="live-console-tab-right" v-model="tabLeft.activeName" @tab-click="handleUserClick" :stretch="true">
+      <el-tabs class="console-tabs" v-model="tabLeft.activeName" @tab-click="handleUserClick" :stretch="true">
         <el-tab-pane :label="alLabel" name="al">
-          <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_al" style="height: 800px; width: 100%;">
-            <el-row class='scrollbar-demo-item' v-for="u in alDisplayList" :key="u.userId">
-              <el-col :span="20">
-                <el-row type="flex" align="middle">
-                  <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
-                  <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
-                  <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
-                </el-row>
-              </el-col>
-              <el-col :span="4" >
-                <el-popover
-                  width="100"
-                  trigger="click">
-                  <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
-                  <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
-                  <i class="el-icon-more" slot="reference"></i>
-                </el-popover>
-              </el-col>
-            </el-row>
+          <el-scrollbar class="panel-scroll" ref="manageLeftRef_al">
+            <div class="user-list-item" v-for="u in alDisplayList" :key="u.userId">
+              <el-avatar :size="36" :src="u.avatar" class="user-avatar" />
+              <div class="user-meta">
+                <div class="user-name">{{ u.nickName }}</div>
+                <div class="user-id">{{ u.userId }}</div>
+              </div>
+              <el-popover width="120" trigger="click" popper-class="user-action-popover">
+                <a class="action-link" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                <a class="action-link" @click="blockUser(u)">拉黑</a>
+                <i class="el-icon-more user-more-btn" slot="reference"></i>
+              </el-popover>
+            </div>
           </el-scrollbar>
         </el-tab-pane>
         <el-tab-pane :label="onlineLabel" name="online">
-          <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_online" style="height: 800px; width: 100%;">
-            <el-row class='scrollbar-demo-item' v-for="u in onlineDisplayList" :key="u.userId">
-              <el-col :span="20">
-                <el-row type="flex" align="middle">
-                  <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
-                  <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
-                  <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
-                </el-row>
-              </el-col>
-              <el-col :span="4" >
-                <el-popover
-                  width="100"
-                  trigger="click">
-                  <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
-                  <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
-                  <i class="el-icon-more" slot="reference"></i>
-                </el-popover>
-              </el-col>
-            </el-row>
+          <el-scrollbar class="panel-scroll" ref="manageLeftRef_online">
+            <div class="user-list-item" v-for="u in onlineDisplayList" :key="u.userId">
+              <el-avatar :size="36" :src="u.avatar" class="user-avatar" />
+              <div class="user-meta">
+                <div class="user-name">{{ u.nickName }}</div>
+                <div class="user-id">{{ u.userId }}</div>
+              </div>
+              <el-popover width="120" trigger="click" popper-class="user-action-popover">
+                <a class="action-link" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                <a class="action-link" @click="blockUser(u)">拉黑</a>
+                <i class="el-icon-more user-more-btn" slot="reference"></i>
+              </el-popover>
+            </div>
           </el-scrollbar>
         </el-tab-pane>
         <el-tab-pane :label="offlineLabel" name="offline">
-          <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_offline" style="height: 800px; width: 100%;">
-            <el-row class='scrollbar-demo-item' v-for="u in offlineDisplayList" :key="u.userId">
-              <el-col :span="20">
-                <el-row type="flex" align="middle">
-                  <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
-                  <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
-                  <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
-                </el-row>
-              </el-col>
-              <el-col :span="4" >
-                <el-popover
-                  width="100"
-                  trigger="click">
-                  <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
-                  <i class="el-icon-more" slot="reference"></i>
-                  <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
-                </el-popover>
-              </el-col>
-            </el-row>
+          <el-scrollbar class="panel-scroll" ref="manageLeftRef_offline">
+            <div class="user-list-item" v-for="u in offlineDisplayList" :key="u.userId">
+              <el-avatar :size="36" :src="u.avatar" class="user-avatar" />
+              <div class="user-meta">
+                <div class="user-name">{{ u.nickName }}</div>
+                <div class="user-id">{{ u.userId }}</div>
+              </div>
+              <el-popover width="120" trigger="click" popper-class="user-action-popover">
+                <a class="action-link" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                <a class="action-link" @click="blockUser(u)">拉黑</a>
+                <i class="el-icon-more user-more-btn" slot="reference"></i>
+              </el-popover>
+            </div>
           </el-scrollbar>
         </el-tab-pane>
         <el-tab-pane :label="silencedUserLabel" name="silenced">
-          <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_silenced" style="height: 800px; width: 100%;">
-            <el-row class='scrollbar-demo-item' v-for="u in silencedDisplayList" :key="u.userId">
-              <el-col :span="20">
-                <el-row type="flex" align="middle">
-                  <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
-                  <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
-                  <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
-                </el-row>
-              </el-col>
-              <el-col :span="4" >
-                <el-popover
-                  width="100"
-                  trigger="click">
-                  <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
-                  <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
-                  <i class="el-icon-more" slot="reference"></i>
-                </el-popover>
-              </el-col>
-            </el-row>
+          <el-scrollbar class="panel-scroll" ref="manageLeftRef_silenced">
+            <div class="user-list-item" v-for="u in silencedDisplayList" :key="u.userId">
+              <el-avatar :size="36" :src="u.avatar" class="user-avatar" />
+              <div class="user-meta">
+                <div class="user-name">{{ u.nickName }}</div>
+                <div class="user-id">{{ u.userId }}</div>
+              </div>
+              <el-popover width="120" trigger="click" popper-class="user-action-popover">
+                <a class="action-link" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                <a class="action-link" @click="blockUser(u)">拉黑</a>
+                <i class="el-icon-more user-more-btn" slot="reference"></i>
+              </el-popover>
+            </div>
           </el-scrollbar>
         </el-tab-pane>
       </el-tabs>
     </div>
 
     <div class="middle-panel">
-      <h2>消息管理</h2>
-
+      <h2 class="panel-title">消息管理</h2>
 
-      <div class="discussion-messages">
-        <h3>讨论区消息</h3>
-        <div class="message-settings">
-          <label>
-            <input type="checkbox" v-model="globalVisible" @change="globalVisibleChange">
-            全局用户自见
-          </label>
+      <div class="panel-card discussion-messages">
+        <div class="section-header">
+          <h3 class="section-title">讨论区消息</h3>
+          <el-checkbox v-model="globalVisible" @change="globalVisibleChange">全局用户自见</el-checkbox>
         </div>
         <div class="message-container" @click="handleMessageBoxClick">
-          <el-scrollbar class="custom-scrollbar" style="height: 500px; width: 100%;" ref="manageRightRef">
-            <el-row v-for="m in msgList" :key="m.msgId">
-            <el-row v-if="m.userId === userId && m.msgId == null" style="padding: 8px 0" type="flex" align="top" justify="end">
-              <div style="display: flex;justify-content: flex-end">
-                <div style="display: flex;justify-content: flex-end;flex-direction: column;max-width: 200px;align-items: flex-end">
-                  <div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div>
-                  <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>
+          <el-scrollbar class="msg-scroll" ref="manageRightRef">
+            <div v-for="m in msgList" :key="m.msgId" class="msg-item">
+              <div v-if="m.userId === userId && m.msgId == null" class="msg-row msg-row--self">
+                <div class="msg-content msg-content--self">
+                  <div class="msg-nickname">{{ m.nickName }}</div>
+                  <div class="msg-bubble msg-bubble--self">{{ m.msg }}</div>
                 </div>
-                <el-avatar :src="m.avatar" style="margin-left: 10px; margin-right: 10px;"/>
+                <el-avatar :size="32" :src="m.avatar" class="msg-avatar" />
               </div>
-            </el-row>
-            <el-row v-else style="margin-top: 5px" type="flex" align="top" >
-              <el-col :span="3" style="margin-left: 10px"><el-avatar :src="m.avatar"/></el-col>
-              <el-col :span="15">
-                <el-row style="margin-left: 10px">
-                  <el-col><div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div></el-col>
-                  <el-col :span="24" style="max-width: 200px;">
-                    <div style="white-space: normal; word-wrap: break-word;background-color: #f0f2f5; padding: 8px; border-radius: 5px;font-size: 14px;width: 100%;">
-                      {{ m.msg }}
-                    </div>
-                  </el-col>
-                  <el-col>
-                    <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="changeUserState(m)">{{ m.msgStatus === 1 ? '解禁' : '禁言' }}</a>
-                    <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="blockUser(m)">拉黑</a>
-                    <a v-if="m.singleVisible === 1" style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="singleVisible(m)">解除用户自见</a>
-                    <a v-else style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="singleVisible(m)">用户自见</a>
-                    <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click.stop="deleteMsg(m)">删除</a>
-                  </el-col>
-                </el-row>
-              </el-col>
-            </el-row>
-
-          </el-row>
-          <!-- 底部留白 -->
-          <div style="height: 20px;"></div>
+              <div v-else class="msg-row">
+                <el-avatar :size="32" :src="m.avatar" class="msg-avatar" />
+                <div class="msg-content">
+                  <div class="msg-nickname">{{ m.nickName }}</div>
+                  <div class="msg-bubble">{{ m.msg }}</div>
+                  <div class="msg-actions">
+                    <a class="action-link" @click.stop="changeUserState(m)">{{ m.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                    <a class="action-link" @click.stop="blockUser(m)">拉黑</a>
+                    <a class="action-link" @click.stop="singleVisible(m)">{{ m.singleVisible === 1 ? '解除用户自见' : '用户自见' }}</a>
+                    <a class="action-link" @click.stop="deleteMsg(m)">删除</a>
+                  </div>
+                </div>
+              </div>
+            </div>
+            <div class="scroll-bottom-space"></div>
           </el-scrollbar>
-          <!-- 加载最新消息按钮 -->
           <el-button
             v-if="showLoadLatestBtn"
             class="load-latest-btn"
             type="primary"
-            size="small"
+            size="mini"
             @click.stop="loadLatestMessages"
             :disabled="isLoadingLatest"
             :loading="isLoadingLatest"
-            icon="el-icon-refresh">
-            加载最新消息
-          </el-button>
+            icon="el-icon-refresh"
+          >加载最新消息</el-button>
         </div>
-        <!--        <div class="message-list">-->
-        <!--          <div class="message-item" v-for="msg in msgList" :key="msg.id">-->
-        <!--            <div class="message-avatar">-->
-        <!--              <img :src="msg.avatar" alt="用户头像">-->
-        <!--            </div>-->
-        <!--            <div class="message-content">-->
-        <!--              <div class="message-user">{{ msg.user }}</div>-->
-        <!--              <div class="message-text">{{ msg.text }}</div>-->
-        <!--            </div>-->
-        <!--            <div class="message-actions">-->
-        <!--&lt;!&ndash;              <button @click="toggleVisible(msg)">&ndash;&gt;-->
-        <!--&lt;!&ndash;                {{ msg.isVisible ? '仅用户自见' : '全局可见' }}&ndash;&gt;-->
-        <!--&lt;!&ndash;              </button>&ndash;&gt;-->
-        <!--              <button @click="deleteMessage(msg)">删除</button>-->
-        <!--            </div>-->
-        <!--          </div>-->
-        <!--        </div>-->
       </div>
-      <div class="system-messages">
-        <h3>系统消息</h3>
-        <textarea placeholder="输入系统消息" v-model="newMsg"></textarea>
-        <div class="message-actions">
-          <button @click="sendMessage">发送消息</button>
-          <button @click="sendPopMessage">弹窗消息</button>
-          <button @click="showTopMsgDialog">顶部消息</button>
-        </div>
+
+      <div class="panel-card ops-panel">
+        <el-tabs v-model="opsTabActive" class="ops-tabs">
+          <el-tab-pane label="营销" name="marketing">
+            <div class="marketing-panel" v-loading="marketingLoading">
+              <div class="marketing-toolbar">
+                <span class="toolbar-label">显示购物车</span>
+                <el-switch v-model="showCart" @change="showCartChange" />
+                <el-alert
+                  class="marketing-tip"
+                  title="置顶商品弹窗优先展示时,其他同位置弹窗将不再展示"
+                  type="info"
+                  :closable="false"
+                  show-icon
+                />
+              </div>
+              <el-radio-group v-model="marketingCategory" size="mini" class="marketing-categories" @change="clearMarketingSelection">
+                <el-radio-button v-for="cat in marketingCategories" :key="cat.key" :label="cat.key">{{ cat.label }}</el-radio-button>
+              </el-radio-group>
+              <el-scrollbar class="marketing-scroll">
+                <!-- 商品:图片卡片 -->
+                <div class="marketing-cards" v-if="marketingCategory === 'goods' && currentMarketingList.length">
+                  <div
+                    v-for="item in currentMarketingList"
+                    :key="getMarketingItemKey(item)"
+                    class="marketing-card"
+                    :class="{ selected: isMarketingItemSelected(item) }"
+                    @click="selectMarketingItem(item)"
+                  >
+                    <div class="card-image-wrap">
+                      <img :src="item.imgUrl || item.image" class="card-image" />
+                      <div class="card-tags">
+                        <el-tag v-if="item.isShow" type="warning" size="mini" effect="dark">展示中</el-tag>
+                        <el-tag v-if="item.status == 1" type="success" size="mini" effect="dark">已上架</el-tag>
+                      </div>
+                      <div class="card-overlay">
+                        <el-button size="mini" round :disabled="item.status == 1" @click.stop="handleGoodsShelf(item, 1)">上架</el-button>
+                        <el-button size="mini" type="danger" round :disabled="item.status == 0" @click.stop="handleGoodsShelf(item, 0)">下架</el-button>
+                        <el-button size="mini" type="primary" round @click.stop="handleGoodsShowToggle(item)">{{ item.isShow ? '收起' : '展示' }}</el-button>
+                      </div>
+                    </div>
+                    <div class="card-info">
+                      <div class="card-title" :title="item.productName">{{ item.productName }}</div>
+                      <div class="card-bottom">
+                        <span class="card-price" v-if="item.price">¥{{ item.price }}</span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+                <!-- 优惠券 / 核销券 / 红包 / 抽奖:列表 -->
+                <div class="marketing-list" v-else-if="marketingCategory !== 'goods' && currentMarketingList.length">
+                  <div
+                    v-for="item in currentMarketingList"
+                    :key="getMarketingItemKey(item)"
+                    class="marketing-list-item"
+                    :class="{ selected: isMarketingItemSelected(item) }"
+                    @click="selectMarketingItem(item)"
+                  >
+                    <div class="list-main">
+                      <div class="list-title" :title="getMarketingItemTitle(item)">{{ getMarketingItemTitle(item) }}</div>
+                      <div class="list-meta">
+                        <span v-if="getMarketingItemPrice(item)" class="list-price">¥{{ getMarketingItemPrice(item) }}</span>
+                        <span v-if="getMarketingListMeta(item)">{{ getMarketingListMeta(item) }}</span>
+                      </div>
+                      <div class="list-tags">
+                        <el-tag v-if="(marketingCategory === 'coupon' || marketingCategory === 'verifyCoupon') && item.isShow" type="warning" size="mini">展示中</el-tag>
+                        <el-tag v-if="(marketingCategory === 'lottery' || marketingCategory === 'red') && isRedLotteryRunning(item)" type="success" size="mini">进行中</el-tag>
+                      </div>
+                    </div>
+                    <div class="list-actions" @click.stop>
+                      <template v-if="marketingCategory === 'lottery'">
+                        <el-button size="mini" type="primary" @click="handleLotteryStatusChange(item, '1')">开始</el-button>
+                        <el-button size="mini" @click="handleLotteryStatusChange(item, '3')">暂停</el-button>
+                        <el-button size="mini" type="danger" @click="handleLotteryStatusChange(item, '2')">结算</el-button>
+                      </template>
+                      <template v-else-if="marketingCategory === 'red'">
+                        <el-button size="mini" type="primary" @click="handleRedStatusChange(item, '1')">开始</el-button>
+                        <el-button size="mini" @click="handleRedStatusChange(item, '3')">暂停</el-button>
+                        <el-button size="mini" type="danger" @click="handleRedStatusChange(item, '2')">结算</el-button>
+                      </template>
+                      <template v-else-if="marketingCategory === 'coupon'">
+                        <el-button size="mini" @click="openCouponBindDialog(item)">绑定商品</el-button>
+                        <el-button size="mini" type="primary" @click="handleCouponShowToggle(item)">{{ item.isShow ? '收起' : '展示' }}</el-button>
+                      </template>
+                      <template v-else-if="marketingCategory === 'verifyCoupon'">
+                        <el-button size="mini" type="primary" @click="handleCouponShowToggle(item)">{{ item.isShow ? '收起' : '展示' }}</el-button>
+                      </template>
+                    </div>
+                  </div>
+                </div>
+                <el-empty v-else description="暂无数据" :image-size="60" />
+              </el-scrollbar>
+              <div class="panel-footer">
+                <el-button size="mini" @click="handleActivityRecord">活动记录</el-button>
+                <el-button size="mini" @click="handleDistributionRecord">发放记录</el-button>
+              </div>
+            </div>
+          </el-tab-pane>
+          <el-tab-pane label="系统消息" name="systemMsg">
+            <div class="system-msg-panel">
+              <el-input
+                type="textarea"
+                :rows="3"
+                placeholder="输入系统消息"
+                v-model="newMsg"
+                resize="none"
+              />
+              <div class="panel-footer">
+                <el-button type="primary" size="mini" @click="sendMessage">发送消息</el-button>
+              </div>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
       </div>
     </div>
 
     <div class="right-panel">
-      <h2>运营工具</h2>
-      <div class="live-player">
-        <h3>直播观看</h3>
+      <h2 class="panel-title">运营工具</h2>
+      <div class="panel-card live-player">
+        <h3 class="section-title">直播观看</h3>
         <LivePlayer ref="livePlayer" :videoParam="videoParam" />
       </div>
 
-      <div class="automation">
-        <h3>运营自动化</h3>
-        <div class="timeline">
-          <h4>时间轴设置</h4>
-          <div class="timeline-items">
-            <div class="timeline-item" v-for="item in timelineItems.slice(0,2)" :key="item.time">
-              <div class="time">{{ formatDate(item.absValue) }}</div>
-              <div class="action">{{ item.taskName }}</div>
-              <button class="delete" @click="removeTimelineItem(item)">删除</button>
+      <div class="panel-card automation">
+        <h3 class="section-title">运营自动化</h3>
+        <p class="section-subtitle">时间轴设置</p>
+        <div class="timeline-items">
+          <div class="timeline-item" v-for="item in timelineItems.slice(0, 2)" :key="item.id || item.absValue">
+            <div class="timeline-info">
+              <div class="timeline-time">{{ formatDate(item.absValue) }}</div>
+              <div class="timeline-action">{{ item.taskName }}</div>
             </div>
+            <el-button type="text" size="mini" class="action-link" @click="removeTimelineItem(item)">删除</el-button>
           </div>
-          <!--          <button class="add" @click="addTimelineItem">添加时间节点</button>-->
+          <el-empty v-if="!timelineItems.length" description="暂无任务" :image-size="48" />
         </div>
       </div>
 
-      <div class="watermark">
-        <h3>直播氛围自动化</h3>
-        <div class="watermark-settings">
-          <textarea :disabled="autoWatermark" v-model="watermarkTemplate" placeholder="水军弹幕内容模板,每行一条"></textarea>
-          <div class="watermark-options">
-            <label>
-              发送间隔:
-              <input type="number" :disabled="autoWatermark" v-model.number="interval" min="1">
-              秒
-            </label>
-            <label>
-              <input type="checkbox" v-model="autoWatermark" @change="changeAutoWatermark">
-              启用水军自动化
-            </label>
-          </div>
+      <div class="panel-card watermark">
+        <h3 class="section-title">直播氛围自动化</h3>
+        <el-input
+          type="textarea"
+          :rows="4"
+          :disabled="autoWatermark"
+          v-model="watermarkTemplate"
+          placeholder="水军弹幕内容模板,每行一条"
+          resize="none"
+        />
+        <div class="watermark-options">
+          <span class="toolbar-label">发送间隔</span>
+          <el-input-number v-model="interval" :min="1" :disabled="autoWatermark" size="mini" controls-position="right" />
+          <span class="toolbar-label">秒</span>
+          <el-checkbox v-model="autoWatermark" @change="changeAutoWatermark">启用水军自动化</el-checkbox>
         </div>
       </div>
     </div>
 
-    <!-- 顶部消息对话框 -->
-    <el-dialog title="发送顶部消息" :visible.sync="topMsgDialogVisible" width="500px">
-      <el-form :model="topMsgForm" label-width="100px">
-        <el-form-item label="消息内容">
-          <el-input
-            type="textarea"
-            v-model="topMsgForm.msg"
-            placeholder="请输入消息内容"
-            :rows="3"
-          ></el-input>
-        </el-form-item>
-        <el-form-item label="显示时长">
-          <el-input-number
-            v-model="topMsgForm.duration"
-            :min="1"
-            :max="60"
-            placeholder="请输入显示时长(分钟)"
-          ></el-input-number>
-          <span style="margin-left: 10px;">分钟</span>
+    <el-dialog title="绑定商品" :visible.sync="couponBindDialogVisible" width="480px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="直播商品">
+          <el-select v-model="couponBindForm.goodsId" placeholder="请选择商品" filterable style="width: 100%;">
+            <el-option
+              v-for="g in bindGoodsOptions"
+              :key="g.goodsId"
+              :label="g.productName"
+              :value="g.goodsId"
+            />
+          </el-select>
         </el-form-item>
       </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="topMsgDialogVisible = false">取 消</el-button>
-        <el-button type="primary" @click="sendTopMessage">确 定</el-button>
+      <div slot="footer">
+        <el-button @click="couponBindDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="confirmCouponBind">确 定</el-button>
       </div>
     </el-dialog>
 
@@ -265,8 +310,12 @@
 import LivePlayer from './LivePlayer.vue';
 import {blockUser, changeUserStatus, getLiveUserTotals, dashBoardWatchUserList} from '@/api/live/liveWatchUser'
 import { listLiveSingleMsg,delLiveMsg } from '@/api/live/liveMsg'
-import { getLive } from '@/api/live/live'
+import { getLive, updateShowCart } from '@/api/live/live'
 import { consoleList } from '@/api/live/task'
+import { listLiveGoods, handleShelfOrUn, handleIsShowChange as handleGoodsIsShowChange } from '@/api/live/liveGoods'
+import { listLiveCoupon, handleIsShowChange as handleCouponIsShowChange, updateLiveCouponBind } from '@/api/live/liveCoupon'
+import { listLiveLotteryConf, updateLiveLotteryConf } from '@/api/live/liveLotteryConf'
+import { listLiveRedConf, updateLiveRedConf } from '@/api/live/liveRedConf'
 import ScreenScale from './ScreenScale.vue'; // 路径根据实际位置调整
 
 
@@ -401,11 +450,6 @@ export default {
       newMsg:'',
       autoMsgTimer: null,
       checkInterval: 2000, // 检查间隔(1分钟,可根据需求调整)
-      topMsgDialogVisible: false,
-      topMsgForm: {
-        msg: '',
-        duration: 5
-      },
       // 消息滚动控制
       isAutoScrollEnabled: true, // 是否启用自动滚动
       autoScrollTimer: null, // 自动滚动定时器
@@ -416,7 +460,29 @@ export default {
         pageNum: 1,
         pageSize: 30,
         liveId: null
-      }
+      },
+      opsTabActive: 'marketing',
+      marketingCategory: 'goods',
+      marketingCategories: [
+        { key: 'goods', label: '商品' },
+        { key: 'coupon', label: '优惠券' },
+        { key: 'verifyCoupon', label: '核销券' },
+        { key: 'red', label: '红包' },
+        { key: 'lottery', label: '抽奖' }
+      ],
+      marketingLoading: false,
+      showCart: true,
+      goodsLiveList: [],
+      couponLiveList: [],
+      lotteryList: [],
+      redList: [],
+      couponTypeOptions: [],
+      lotteryStatusOptions: [],
+      redStatusOptions: [],
+      selectedMarketingItem: null,
+      couponBindDialogVisible: false,
+      couponBindForm: { couponId: null, goodsId: null },
+      bindGoodsOptions: []
     };
   },
   computed: {
@@ -437,10 +503,26 @@ export default {
     },
     silencedUserLabel() {
       return `禁言(${this.userTotal.silenced})`;
+    },
+    currentMarketingList() {
+      if (this.marketingCategory === 'goods') {
+        return this.goodsLiveList;
+      }
+      if (this.marketingCategory === 'lottery') {
+        return this.lotteryList;
+      }
+      if (this.marketingCategory === 'red') {
+        return this.redList;
+      }
+      if (this.marketingCategory === 'coupon') {
+        return this.couponLiveList.filter(item => !this.isVerifyCouponType(item));
+      }
+      return this.couponLiveList.filter(item => this.isVerifyCouponType(item));
     }
   },
   created() {
     if(!this.liveId) return
+    this.loadMarketingDicts()
     this.getList()
     this.handleUserClick({name:'al'})
     this.connectWebSocket()
@@ -457,6 +539,295 @@ export default {
     this.initScrollListeners();
   },
   methods: {
+    loadMarketingDicts() {
+      this.getDicts('store_coupon_type').then(response => {
+        this.couponTypeOptions = response.data || [];
+      });
+      this.getDicts('sys_live_lottery_status').then(response => {
+        this.lotteryStatusOptions = response.data || [];
+      });
+      this.getDicts('sys_live_red_status').then(response => {
+        this.redStatusOptions = response.data || [];
+      });
+    },
+    loadMarketingData() {
+      if (!this.liveId) return;
+      this.marketingLoading = true;
+      const query = { liveId: this.liveId, pageNum: 1, pageSize: 100 };
+      Promise.all([
+        listLiveGoods(query),
+        listLiveCoupon(query),
+        listLiveLotteryConf(query),
+        listLiveRedConf(query)
+      ]).then(([goodsRes, couponRes, lotteryRes, redRes]) => {
+        if (goodsRes.code === 200) {
+          this.goodsLiveList = (goodsRes.rows || []).map(item => ({
+            ...item,
+            isShow: item.isShow === 1 || item.isShow === true
+          }));
+        }
+        if (couponRes.code === 200) {
+          this.couponLiveList = (couponRes.rows || []).map(item => ({
+            ...item,
+            isShow: item.isShow === 1 || item.isShow === true
+          }));
+        }
+        if (lotteryRes.code === 200) {
+          this.lotteryList = lotteryRes.rows || [];
+        }
+        if (redRes.code === 200) {
+          this.redList = redRes.rows || [];
+        }
+      }).finally(() => {
+        this.marketingLoading = false;
+      });
+    },
+    clearMarketingSelection() {
+      this.selectedMarketingItem = null;
+    },
+    isVerifyCouponType(item) {
+      const label = this.selectDictLabel(this.couponTypeOptions, item.type) || '';
+      return label.includes('核销') || label.includes('代金券');
+    },
+    getMarketingItemKey(item) {
+      if (this.marketingCategory === 'goods') return `goods-${item.goodsId}`;
+      if (this.marketingCategory === 'lottery') return `lottery-${item.lotteryId}`;
+      if (this.marketingCategory === 'red') return `red-${item.redId}`;
+      return `coupon-${item.id || item.couponId}`;
+    },
+    getMarketingItemTitle(item) {
+      if (this.marketingCategory === 'lottery') {
+        return item.desc || `抽奖 #${item.lotteryId}`;
+      }
+      if (this.marketingCategory === 'red') {
+        return item.desc || `红包 #${item.redId}`;
+      }
+      return item.productName || item.title || '-';
+    },
+    getMarketingItemPrice(item) {
+      if (this.marketingCategory === 'goods') {
+        return item.price;
+      }
+      if (this.marketingCategory === 'coupon' || this.marketingCategory === 'verifyCoupon') {
+        return item.couponPrice;
+      }
+      return '';
+    },
+    getMarketingListMeta(item) {
+      if (this.marketingCategory === 'lottery') {
+        return this.selectDictLabel(this.lotteryStatusOptions, item.lotteryStatus) || '';
+      }
+      if (this.marketingCategory === 'red') {
+        return this.selectDictLabel(this.redStatusOptions, item.redStatus) || '';
+      }
+      const typeLabel = this.selectDictLabel(this.couponTypeOptions, item.type);
+      const parts = [];
+      if (item.useMinPrice) {
+        parts.push(`满${item.useMinPrice}可用`);
+      }
+      if (this.marketingCategory === 'coupon' && item.productName) {
+        parts.push(`已绑:${item.productName}`);
+      }
+      if (typeLabel) {
+        parts.push(typeLabel);
+      }
+      return parts.join(' · ');
+    },
+    isRedLotteryRunning(item) {
+      const status = this.marketingCategory === 'red' ? item.redStatus : item.lotteryStatus;
+      return status + '' === '1' || status + '' === '3';
+    },
+    isMarketingItemSelected(item) {
+      if (!this.selectedMarketingItem) return false;
+      return this.getMarketingItemKey(item) === this.getMarketingItemKey(this.selectedMarketingItem);
+    },
+    selectMarketingItem(item) {
+      this.selectedMarketingItem = item;
+    },
+    ensureSocket() {
+      if (!this.socket) {
+        this.$message.error('请从直播间开启 WebSocket 连接');
+        return false;
+      }
+      return true;
+    },
+    handleGoodsShelf(row, status) {
+      handleShelfOrUn({ goodsIds: [row.goodsId], status, liveId: this.liveId }).then(res => {
+        if (res.code === 200) {
+          row.status = status;
+          this.$message.success(status === 1 ? '上架成功' : '下架成功');
+        } else {
+          this.$message.error(res.msg);
+        }
+      });
+    },
+    handleGoodsShowToggle(row) {
+      this.toggleGoodsShow(row, !row.isShow);
+    },
+    handleCouponShowToggle(row) {
+      this.toggleCouponShow(row, !row.isShow);
+    },
+    openCouponBindDialog(row) {
+      this.couponBindForm = { couponId: row.id, goodsId: row.goodsId || null };
+      listLiveGoods({ liveId: this.liveId, pageNum: 1, pageSize: 100 }).then(res => {
+        if (res.code === 200) {
+          this.bindGoodsOptions = res.rows || [];
+          this.couponBindDialogVisible = true;
+        }
+      });
+    },
+    confirmCouponBind() {
+      if (!this.couponBindForm.goodsId) {
+        this.$message.warning('请选择商品');
+        return;
+      }
+      updateLiveCouponBind({
+        goodsId: this.couponBindForm.goodsId,
+        couponId: this.couponBindForm.couponId,
+        liveId: this.liveId
+      }).then(res => {
+        if (res.code === 200) {
+          this.$message.success('绑定成功');
+          this.couponBindDialogVisible = false;
+          this.loadMarketingData();
+        } else {
+          this.$message.error(res.msg);
+        }
+      });
+    },
+    toggleGoodsShow(row, targetStatus) {
+      if (!this.ensureSocket()) return;
+      handleGoodsIsShowChange({
+        goodsIds: [row.goodsId],
+        isShow: targetStatus,
+        liveId: this.liveId
+      }).then(res => {
+        if (res.code === 200) {
+          row.isShow = res.isShow;
+          if (res.msg === '目前仅支持单一物品展示') {
+            this.$message.error(res.msg);
+          }
+          this.socket.send(JSON.stringify({
+            cmd: 'goods',
+            data: { liveId: this.liveId, goodsId: row.goodsId, status: targetStatus ? 1 : 0 }
+          }));
+        }
+      });
+    },
+    toggleCouponShow(row, targetStatus) {
+      if (!this.ensureSocket()) return;
+      handleCouponIsShowChange({
+        couponId: row.id,
+        isShow: targetStatus,
+        liveId: this.liveId
+      }).then(res => {
+        if (res.code === 200) {
+          row.isShow = targetStatus;
+          if (res.msg === '目前仅支持单一优惠券推送') {
+            this.$message.error(res.msg);
+            return;
+          }
+          this.socket.send(JSON.stringify({
+            cmd: 'coupon',
+            data: {
+              liveId: this.liveId,
+              couponIssueId: row.id,
+              status: targetStatus ? 1 : 0,
+              goodsId: row.goodsId,
+              couponName: row.title,
+              couponPrice: row.couponPrice,
+              useMinPrice: row.useMinPrice,
+              couponTime: row.couponTime
+            }
+          }));
+        }
+      });
+    },
+    handleLotteryStatusChange(row, status) {
+      if (!this.ensureSocket()) return;
+      if (row.lotteryStatus + '' === '2') {
+        this.$message.error('已结束的抽奖不能进行操作');
+        return;
+      }
+      if (status === '1' && row.lotteryStatus + '' !== '0' && row.lotteryStatus + '' !== '3') {
+        this.$message.error('只能对未开始或暂停的抽奖进行开始操作');
+        return;
+      }
+      if (status === '2' && row.lotteryStatus + '' !== '1' && row.lotteryStatus + '' !== '3') {
+        this.$message.error('只能对进行中或暂停的抽奖进行结算');
+        return;
+      }
+      if (status === '3' && row.lotteryStatus + '' !== '1') {
+        this.$message.error('只能对进行中的抽奖执行暂停操作');
+        return;
+      }
+      const param = {
+        lotteryId: row.lotteryId,
+        lotteryStatus: status,
+        duration: row.duration,
+        liveId: this.liveId,
+        status
+      };
+      updateLiveLotteryConf(param).then(response => {
+        if (response.code === 200) {
+          this.socket.send(JSON.stringify({ cmd: 'lottery', data: param }));
+          this.$message.success('操作成功');
+          this.loadMarketingData();
+        } else {
+          this.$message.error(response.msg);
+        }
+      });
+    },
+    handleRedStatusChange(row, status) {
+      if (!this.ensureSocket()) return;
+      if (row.redStatus + '' === '2') {
+        this.$message.error('已结束的红包不能进行操作');
+        return;
+      }
+      if (status === '1' && row.redStatus + '' !== '0' && row.redStatus + '' !== '3') {
+        this.$message.error('只能对未开始或暂停的红包进行开始操作');
+        return;
+      }
+      if (status === '2' && row.redStatus + '' !== '1' && row.redStatus + '' !== '3') {
+        this.$message.error('只能对进行中或暂停的红包进行结算');
+        return;
+      }
+      if (status === '3' && row.redStatus + '' !== '1') {
+        this.$message.error('只能对进行中的红包执行暂停操作');
+        return;
+      }
+      const param = {
+        redId: row.redId,
+        redStatus: status,
+        totalLots: row.totalLots,
+        totalSend: row.totalSend,
+        redType: row.redType,
+        redNum: row.redNum,
+        liveId: this.liveId,
+        duration: row.duration,
+        status
+      };
+      updateLiveRedConf(param).then(response => {
+        if (response.code === 200) {
+          this.socket.send(JSON.stringify({ cmd: 'red', data: param }));
+          this.$message.success('操作成功');
+          this.loadMarketingData();
+        } else {
+          this.$message.error(response.msg);
+        }
+      });
+    },
+    handleActivityRecord() {
+      if (!this.timelineItems.length) {
+        this.$message.info('暂无活动记录');
+        return;
+      }
+      const names = this.timelineItems.map(item => `${this.formatDate(item.absValue)} ${item.taskName}`).join('\n');
+      this.$alert(names, '活动记录', { confirmButtonText: '确定' });
+    },
+    handleDistributionRecord() {
+      this.$message.info('发放记录请在直播配置-优惠券/红包配置中查看');
+    },
     // 点击消息框
     handleMessageBoxClick() {
       // 点击消息框时,停止自动滚动
@@ -582,6 +953,28 @@ export default {
       }
       this.socket.send(JSON.stringify(msg))
     },
+    showCartChange(val) {
+      const status = val ? 1 : 0
+      updateShowCart({ liveId: this.liveId, showCart: status }).then(res => {
+        if (res.code !== 200) {
+          this.$message.error(res.msg || '更新购物车显示状态失败')
+          this.showCart = !val
+          return
+        }
+        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
+          this.socket.send(JSON.stringify({
+            liveId: this.liveId,
+            userId: '9999',
+            userType: 0,
+            cmd: 'showCart',
+            status
+          }))
+        }
+      }).catch(() => {
+        this.showCart = !val
+        this.$message.error('更新购物车显示状态失败')
+      })
+    },
     changeAutoWatermark( val){
       this.watermarkList = this.watermarkTemplate
         .split('\n')
@@ -633,62 +1026,6 @@ export default {
       this.socket.send(JSON.stringify(msg))
       this.watermarkIndex += 1
     },
-    // 弹窗消息
-    sendPopMessage() {
-      // 发送前简单校验
-      if (this.newMsg.trim() === '') {
-        return;
-      }
-
-      let msg = {
-        msg: this.newMsg,
-        liveId: this.liveId,
-        userId: this.userId,
-        userType: 1,
-        cmd: 'sendPopMsg',
-        avatar: this.$store.state.user.user.avatar,
-        nickName: this.$store.state.user.user.nickName,
-      }
-
-      this.socket.send(JSON.stringify(msg))
-
-      this.newMsg = '';
-    },
-    // 显示顶部消息对话框
-    showTopMsgDialog() {
-      this.topMsgForm.msg = '';
-      this.topMsgForm.duration = 5;
-      this.topMsgDialogVisible = true;
-    },
-    // 发送顶部消息
-    sendTopMessage() {
-      // 发送前简单校验
-      if (this.topMsgForm.msg.trim() === '') {
-        this.$message.warning('请输入消息内容');
-        return;
-      }
-
-      if (!this.topMsgForm.duration || this.topMsgForm.duration < 1) {
-        this.$message.warning('请输入有效的显示时长');
-        return;
-      }
-
-      let msg = {
-        msg: this.topMsgForm.msg,
-        duration: this.topMsgForm.duration,
-        liveId: this.liveId,
-        userId: this.userId,
-        userType: 1,
-        cmd: 'sendTopMsg',
-        avatar: this.$store.state.user.user.avatar,
-        nickName: this.$store.state.user.user.nickName,
-      }
-
-      this.socket.send(JSON.stringify(msg))
-
-      this.topMsgDialogVisible = false;
-      this.$message.success('顶部消息发送成功');
-    },
     sendMessage() {
       // 发送前简单校验
       if (this.newMsg.trim() === '') {
@@ -826,6 +1163,7 @@ export default {
           }
           this.$nextTick(() => {
             this.globalVisible = res.data.globalVisible
+            this.showCart = res.data.showCart == null || res.data.showCart === 1
             this.$refs.livePlayer.initPlayer()
           })
           this.loading = false
@@ -1104,6 +1442,7 @@ export default {
       // this.handleClick({name:'online'})
       this.loadMsgList()
       this.loadLiveTask()
+      this.loadMarketingData()
     },
     formatDate(value) {
       // 检查值是否存在且为日期类型(或可转换为日期)
@@ -1494,296 +1833,510 @@ export default {
 </script>
 
 <style scoped>
+/* 布局 */
 .console {
   display: flex;
   height: 100vh;
+  overflow: hidden;
+  background: #f5f7fa;
 }
 
-.left-panel, .middle-panel, .right-panel {
-  padding: 20px;
+.left-panel,
+.middle-panel,
+.right-panel {
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
   box-sizing: border-box;
+  overflow: hidden;
 }
 
 .left-panel {
-  width: 30%;
-  background: #f8fafc;
-  border-right: 1px solid #e2e8f0;
+  width: 28%;
+  border-right: 1px solid #ebeef5;
 }
 
 .middle-panel {
-  width: 40%;
-  background: #f8fafc;
-  border-right: 1px solid #e2e8f0;
+  width: 42%;
+  border-right: 1px solid #ebeef5;
+  gap: 12px;
 }
 
 .right-panel {
   width: 30%;
-  background: #f8fafc;
+  overflow-y: auto;
+  gap: 12px;
 }
 
-.search {
-  margin: 10px 0;
+/* 通用标题与卡片 */
+.panel-title {
+  margin: 0 0 12px;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.4;
 }
 
-.search input {
-  width: 70%;
-  padding: 8px;
-  border: 1px solid #cbd5e1;
-  border-radius: 4px;
+.section-title {
+  margin: 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
 }
 
-.search button {
-  padding: 8px 15px;
-  background: #3b82f6;
-  color: #fff;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
+.section-subtitle {
+  margin: 0 0 8px;
+  font-size: 13px;
+  color: #909399;
 }
 
-.tabs {
+.section-header {
   display: flex;
-  margin: 10px 0;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10px;
 }
 
-.tabs button {
-  padding: 8px 15px;
-  border: 1px solid #e2e8f0;
+.panel-card {
   background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 12px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
+}
+
+.toolbar-label {
+  font-size: 13px;
+  color: #606266;
+}
+
+.action-link {
+  display: inline-block;
+  margin-right: 10px;
+  font-size: 12px;
+  color: #f56c6c;
   cursor: pointer;
 }
 
-.tabs button.active {
-  background: #3b82f6;
-  color: #fff;
-  border-color: #3b82f6;
+.action-link:hover {
+  opacity: 0.85;
 }
 
-.user-list {
-  max-height: 600px;
-  overflow-y: auto;
+.panel-footer {
+  display: flex;
+  justify-content: flex-end;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-top: 10px;
+  padding-top: 10px;
+  border-top: 1px solid #ebeef5;
 }
 
-.user-item {
+/* 学员列表 */
+.search-bar {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 10px;
+}
+
+.search-bar .el-input {
+  flex: 1;
+}
+
+.console-tabs {
+  flex: 1;
+  min-height: 0;
+}
+
+.console-tabs ::v-deep .el-tabs__content {
+  height: calc(100% - 40px);
+}
+
+.console-tabs ::v-deep .el-tab-pane {
+  height: 100%;
+}
+
+.panel-scroll {
+  height: calc(100vh - 180px);
+}
+
+.user-list-item {
   display: flex;
   align-items: center;
-  padding: 10px;
-  border-bottom: 1px solid #e2e8f0;
+  gap: 10px;
+  padding: 10px 8px;
+  border-bottom: 1px solid #f2f6fc;
 }
 
-.user-item img {
-  width: 40px;
-  height: 40px;
-  border-radius: 50%;
-  margin-right: 10px;
+.user-list-item:hover {
+  background: #f5f7fa;
 }
 
-.user-info {
+.user-meta {
   flex: 1;
+  min-width: 0;
 }
 
 .user-name {
-  font-weight: bold;
+  font-size: 13px;
+  color: #303133;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
-.user-status {
+.user-id {
+  margin-top: 2px;
   font-size: 12px;
-  color: #64748b;
+  color: #909399;
 }
 
-.online {
-  color: #10b981;
+.user-more-btn {
+  color: #909399;
+  cursor: pointer;
+  padding: 4px;
 }
 
-.offline {
-  color: #94a3b8;
+.user-more-btn:hover {
+  color: #409eff;
 }
 
-.user-actions {
+/* 讨论区消息 */
+.discussion-messages {
+  flex: 1;
+  min-height: 0;
   display: flex;
+  flex-direction: column;
 }
 
-.user-actions button {
-  padding: 5px 10px;
-  margin-left: 5px;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
+.message-container {
+  position: relative;
+  flex: 1;
+  min-height: 0;
 }
 
-.block {
-  background: #ef4444;
-  color: #fff;
+.msg-scroll {
+  height: calc(100vh - 520px);
+  min-height: 180px;
 }
 
-.unblock {
-  background: #10b981;
-  color: #fff;
+.msg-item + .msg-item {
+  margin-top: 8px;
 }
 
-.mute {
-  background: #f59e0b;
-  color: #fff;
+.msg-row {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
 }
 
-.unmute {
-  background: #3b82f6;
-  color: #fff;
+.msg-row--self {
+  justify-content: flex-end;
 }
 
-.system-messages, .discussion-messages {
-  margin: 20px 0;
-  background: #fff;
-  padding: 15px;
-  border-radius: 8px;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+.msg-content {
+  max-width: 72%;
 }
 
-.system-messages textarea {
-  width: 100%;
-  height: 100px;
-  border: 1px solid #e2e8f0;
-  border-radius: 4px;
-  padding: 8px;
-  box-sizing: border-box;
+.msg-content--self {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
 }
 
-.message-actions {
-  margin-top: 10px;
+.msg-nickname {
+  margin-bottom: 4px;
+  font-size: 12px;
+  color: #909399;
 }
 
-.message-actions button {
-  padding: 5px 10px;
-  margin-right: 5px;
-  background: #3b82f6;
-  color: #fff;
-  border: none;
+.msg-bubble {
+  padding: 8px 10px;
   border-radius: 4px;
-  cursor: pointer;
+  background: #f4f4f5;
+  font-size: 13px;
+  line-height: 1.5;
+  color: #303133;
+  word-break: break-word;
 }
 
-.message-list {
-  max-height: 300px;
-  overflow-y: auto;
-  margin-top: 10px;
+.msg-bubble--self {
+  background: #ecf5ff;
 }
 
-.message-item {
-  display: flex;
+.msg-actions {
+  margin-top: 6px;
+}
+
+.msg-avatar {
+  flex-shrink: 0;
+}
+
+.scroll-bottom-space {
+  height: 16px;
+}
+
+.load-latest-btn {
+  position: absolute;
+  right: 12px;
+  bottom: 12px;
+  z-index: 2;
+}
+
+/* 营销 / 系统消息 */
+.ops-panel {
+  flex-shrink: 0;
+}
+
+.ops-tabs ::v-deep .el-tabs__header {
   margin-bottom: 10px;
-  padding-bottom: 10px;
-  border-bottom: 1px solid #e2e8f0;
 }
 
-.message-avatar img {
-  width: 30px;
-  height: 30px;
-  border-radius: 50%;
-  margin-right: 10px;
+.ops-tabs ::v-deep .el-tabs__content {
+  overflow: visible;
 }
 
-.message-content {
-  flex: 1;
+.marketing-panel,
+.system-msg-panel {
+  display: flex;
+  flex-direction: column;
 }
 
-.message-user {
-  font-weight: bold;
+.marketing-toolbar {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 10px;
+  flex-wrap: wrap;
 }
 
-.message-text {
-  font-size: 14px;
-  color: #64748b;
+.marketing-tip {
+  flex: 1;
+  min-width: 200px;
+  padding: 4px 8px;
 }
 
-.message-actions button {
-  padding: 3px 8px;
+.marketing-tip ::v-deep .el-alert__title {
   font-size: 12px;
-  background: #3b82f6;
-  color: #fff;
-  border: none;
+}
+
+.marketing-categories {
+  margin-bottom: 10px;
+}
+
+.marketing-scroll {
+  height: 150px;
+}
+
+.marketing-cards {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 8px;
+  padding-right: 4px;
+}
+
+.marketing-card {
+  border: 1px solid #ebeef5;
   border-radius: 4px;
+  overflow: hidden;
   cursor: pointer;
+  background: #fff;
+  transition: border-color 0.2s;
 }
 
-.live-player, .automation, .watermark {
-  margin: 20px 0;
-  background: #fff;
-  padding: 15px;
-  border-radius: 8px;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+.marketing-card:hover,
+.marketing-card.selected {
+  border-color: #409eff;
 }
 
-.timeline-items {
-  margin: 10px 0;
+.card-image-wrap {
+  position: relative;
+  height: 72px;
+  overflow: hidden;
+  background: #f5f7fa;
 }
 
-.timeline-item {
+.card-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.card-tags {
+  position: absolute;
+  top: 6px;
+  left: 6px;
   display: flex;
-  justify-content: space-between;
+  gap: 4px;
+}
+
+.card-overlay {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  flex-wrap: wrap;
   align-items: center;
-  padding: 8px 0;
-  border-bottom: 1px solid #e2e8f0;
+  justify-content: center;
+  gap: 4px;
+  padding: 6px;
+  background: rgba(0, 0, 0, 0.5);
+  opacity: 0;
+  transition: opacity 0.2s;
 }
 
-.delete {
-  background: #ef4444;
-  color: #fff;
-  border: none;
-  border-radius: 4px;
-  padding: 3px 8px;
-  cursor: pointer;
+.marketing-card:hover .card-overlay {
+  opacity: 1;
+}
+
+.card-info {
+  padding: 6px 8px;
+}
+
+.card-title {
+  font-size: 12px;
+  color: #303133;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.card-bottom {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 4px;
+}
+
+.card-price {
+  font-size: 12px;
+  font-weight: 600;
+  color: #f56c6c;
 }
 
-.add {
-  background: #10b981;
-  color: #fff;
-  border: none;
+.marketing-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  padding-right: 4px;
+}
+
+.marketing-list-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  padding: 10px;
+  border: 1px solid #ebeef5;
   border-radius: 4px;
-  padding: 8px 15px;
+  background: #fff;
   cursor: pointer;
+  transition: border-color 0.2s;
 }
 
-.watermark-settings textarea {
-  width: 100%;
-  height: 100px;
-  border: 1px solid #e2e8f0;
-  border-radius: 4px;
-  padding: 8px;
-  box-sizing: border-box;
+.marketing-list-item:hover,
+.marketing-list-item.selected {
+  border-color: #409eff;
+  background: #f5f9ff;
 }
 
-.watermark-options {
-  margin-top: 10px;
+.list-main {
+  flex: 1;
+  min-width: 0;
 }
 
-.watermark-options label {
-  display: block;
-  margin-bottom: 5px;
+.list-title {
+  font-size: 13px;
+  color: #303133;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
-/* 隐藏 el-scrollbar 的横向滚动条 */
-.el-scrollbar__wrap {
-  overflow-x: hidden !important;
+
+.list-meta {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
-.custom-scrollbar .el-scrollbar__wrap {
-  overflow-x: hidden !important;
+
+.list-price {
+  margin-right: 6px;
+  color: #f56c6c;
+  font-weight: 600;
+}
+
+.list-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  margin-top: 6px;
+}
+
+.list-actions {
+  display: flex;
+  flex-shrink: 0;
+  flex-wrap: wrap;
+  gap: 4px;
+  justify-content: flex-end;
+}
+
+/* 右侧运营工具 */
+.live-player,
+.automation,
+.watermark {
+  margin: 0;
+}
+
+.timeline-items {
+  margin-top: 4px;
 }
-.scrollbar-demo-item{
+
+.timeline-item {
   display: flex;
   align-items: center;
-  justify-content: center;
-  height: 50px;
-  margin: 10px;
-  text-align: center;
-  border-radius: 4px;
+  justify-content: space-between;
+  gap: 8px;
+  padding: 8px 0;
+  border-bottom: 1px solid #f2f6fc;
 }
-.message-container {
-  position: relative;
+
+.timeline-time {
+  font-size: 12px;
+  color: #909399;
 }
-.load-latest-btn {
-  position: absolute;
-  bottom: 20px;
-  right: 20px;
-  z-index: 10;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
+.timeline-action {
+  margin-top: 2px;
+  font-size: 13px;
+  color: #303133;
+}
+
+.watermark-options {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-top: 10px;
+}
+
+/* 滚动条 */
+::v-deep .el-scrollbar__wrap {
+  overflow-x: hidden !important;
+}
+</style>
+
+<style>
+.user-action-popover .action-link {
+  display: block;
+  margin: 0 0 8px;
+  font-size: 13px;
+  color: #f56c6c;
+  cursor: pointer;
+}
+
+.user-action-popover .action-link:last-child {
+  margin-bottom: 0;
 }
 </style>