yuhongqi 5 часов назад
Родитель
Сommit
e97fadc0f4

+ 1 - 1
src/views/live/liveConfig/goods.vue

@@ -211,7 +211,7 @@
       :close-on-press-escape="false"
     >
       <el-form :model="stockForm" ref="stockForm" :rules="stockRules">
-        <el-form-item label="调整后库存" prop="newStock">
+        <el-form-item label="调整后库存" prop="stock">
           <el-input-number
             v-model="stockForm.stock"
             :min="0"

+ 13 - 0
src/views/live/liveConfig/liveRedConf.vue

@@ -599,6 +599,19 @@ export default {
               ? String(this.form.passwordCmd).trim()
               : this.form.passwordCmd
           }
+          const redNum = Number(data.redNum)
+          const totalLots = Number(data.totalLots)
+          const distributeType = Number(data.distributeType)
+          if (!this.isPasswordBagMode && distributeType !== 3) {
+            if (totalLots > redNum) {
+              this.msgError("奖励份数不能大于积分数")
+              return
+            }
+            if ((distributeType === 1 || !distributeType) && redNum % totalLots !== 0) {
+              this.msgError("积分数须能被奖励份数整除,保证每人积分为正整数")
+              return
+            }
+          }
           if (data.redId != null) {
             updateLiveRedConf(data).then(response => {
               this.msgSuccess("修改成功");

+ 370 - 395
src/views/live/liveConsole/LiveConsole.vue

@@ -1,165 +1,148 @@
 <template>
   <div class="console">
 
-    <div class="left-panel">
-      <h2>学员列表</h2>
-      <div class="search">
-        <input type="text" placeholder="搜索用户昵称" v-model="searchKeyword">
-        <button @click="searchUsers()">搜索</button>
+    <!-- 左侧:学员列表 -->
+    <aside class="panel left-panel">
+      <div class="panel-header">
+        <span class="panel-title">学员列表</span>
       </div>
-      <el-tabs class="live-console-tab-right" v-model="tabLeft.activeName" @tab-click="handleUserClick" :stretch="true">
+      <div class="search-bar">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索用户昵称"
+          size="small"
+          prefix-icon="el-icon-search"
+          clearable
+          @keyup.enter.native="searchUsers"
+        />
+        <el-button type="primary" size="small" @click="searchUsers">搜索</el-button>
+      </div>
+      <el-tabs class="user-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="user-scroll" ref="manageLeftRef_al">
+            <div class="user-card" v-for="u in alDisplayList" :key="u.userId">
+              <el-avatar :size="36" :src="u.avatar" />
+              <div class="user-info">
+                <div class="user-name">{{ u.nickName }}</div>
+                <div class="user-id">ID: {{ u.userId }}</div>
+              </div>
+              <el-dropdown trigger="click" @command="cmd => cmd === 'mute' ? changeUserState(u) : blockUser(u)">
+                <i class="el-icon-more action-btn"></i>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item command="mute">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</el-dropdown-item>
+                  <el-dropdown-item command="block" divided>拉黑</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
+            </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="user-scroll" ref="manageLeftRef_online">
+            <div class="user-card" v-for="u in onlineDisplayList" :key="u.userId">
+              <el-avatar :size="36" :src="u.avatar" />
+              <div class="user-info">
+                <div class="user-name">{{ u.nickName }}</div>
+                <div class="user-id">ID: {{ u.userId }}</div>
+              </div>
+              <el-dropdown trigger="click" @command="cmd => cmd === 'mute' ? changeUserState(u) : blockUser(u)">
+                <i class="el-icon-more action-btn"></i>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item command="mute">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</el-dropdown-item>
+                  <el-dropdown-item command="block" divided>拉黑</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
+            </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="user-scroll" ref="manageLeftRef_offline">
+            <div class="user-card" v-for="u in offlineDisplayList" :key="u.userId">
+              <el-avatar :size="36" :src="u.avatar" />
+              <div class="user-info">
+                <div class="user-name">{{ u.nickName }}</div>
+                <div class="user-id">ID: {{ u.userId }}</div>
+              </div>
+              <el-dropdown trigger="click" @command="cmd => cmd === 'mute' ? changeUserState(u) : blockUser(u)">
+                <i class="el-icon-more action-btn"></i>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item command="mute">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</el-dropdown-item>
+                  <el-dropdown-item command="block" divided>拉黑</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
+            </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="user-scroll" ref="manageLeftRef_silenced">
+            <div class="user-card" v-for="u in silencedDisplayList" :key="u.userId">
+              <el-avatar :size="36" :src="u.avatar" />
+              <div class="user-info">
+                <div class="user-name">{{ u.nickName }}</div>
+                <div class="user-id">ID: {{ u.userId }}</div>
+              </div>
+              <el-dropdown trigger="click" @command="cmd => cmd === 'mute' ? changeUserState(u) : blockUser(u)">
+                <i class="el-icon-more action-btn"></i>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item command="mute">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</el-dropdown-item>
+                  <el-dropdown-item command="block" divided>拉黑</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
+            </div>
           </el-scrollbar>
         </el-tab-pane>
       </el-tabs>
-    </div>
-
-    <div class="middle-panel">
-      <h2>消息管理</h2>
-
+    </aside>
+
+    <!-- 中间:消息管理 -->
+    <main class="panel middle-panel">
+      <div class="panel-header">
+        <span class="panel-title">消息管理</span>
+        <label class="global-toggle">
+          <el-switch v-model="globalVisible" @change="globalVisibleChange" active-color="#6366f1" />
+          <span>全局用户自见</span>
+        </label>
+      </div>
 
-      <div class="discussion-messages">
-        <h3>讨论区消息</h3>
-        <div class="message-settings">
-          <label>
-            <input type="checkbox" v-model="globalVisible" @change="globalVisibleChange">
-            全局用户自见
-          </label>
-        </div>
+      <div class="chat-area">
         <div class="message-container" @click="handleMessageBoxClick">
-          <el-scrollbar class="custom-scrollbar" style="height: 500px; width: 100%;" ref="manageRightRef">
-            <el-row v-for="(m, msgIndex) in msgList" :key="msgRowKey(m, msgIndex)">
-            <el-row v-if="m.userId !== userId" 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 v-if="m.replyUserName || m.replyContent" style="font-size: 12px; color: #999; margin-bottom: 4px; word-break: break-all;">
-                      回复 {{ m.replyUserName || '用户' }}:{{ m.replyContent }}
-                    </div>
-                    <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="isUserSingleVisible(m)" 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="replyUser(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 v-if="m.userId === userId" 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 v-if="m.replyUserName || m.replyContent" style="font-size: 12px; color: #999; margin-bottom: 4px; max-width: 200px; word-break: break-all; text-align: right;">
+          <el-scrollbar class="chat-scroll" ref="manageRightRef">
+            <div v-for="(m, msgIndex) in msgList" :key="msgRowKey(m, msgIndex)" class="msg-row">
+              <!-- 管理员消息(右) -->
+              <div v-if="m.userId === userId && m.msgId == null" class="msg-bubble msg-right">
+                <div class="msg-meta">{{ m.nickName }}</div>
+                <div v-if="m.replyUserName || m.replyContent" class="msg-reply">
+                  回复 {{ m.replyUserName || '用户' }}:{{ m.replyContent }}
+                </div>
+                <div class="msg-text admin-text">{{ m.msg }}</div>
+                <el-avatar :size="32" :src="m.avatar" class="msg-avatar" />
+              </div>
+              <!-- 用户消息(左) -->
+              <div v-else class="msg-bubble msg-left">
+                <el-avatar :size="32" :src="m.avatar" class="msg-avatar" />
+                <div class="msg-content">
+                  <div class="msg-meta">{{ m.nickName }}</div>
+                  <div v-if="m.replyUserName || m.replyContent" class="msg-reply">
                     回复 {{ m.replyUserName || '用户' }}:{{ m.replyContent }}
                   </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>
+                  <div class="msg-text">{{ m.msg }}</div>
+                  <div class="msg-actions">
+                    <a @click.stop="changeUserState(m)">{{ m.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                    <a @click.stop="blockUser(m)">拉黑</a>
+                    <a @click.stop="singleVisible(m)">{{ isUserSingleVisible(m) ? '解除自见' : '用户自见' }}</a>
+                    <a @click.stop="replyUser(m)">回复</a>
+                    <a @click.stop="deleteMsg(m)">删除</a>
+                  </div>
                 </div>
-                <el-avatar :src="m.avatar" style="margin-left: 10px; margin-right: 10px;"/>
               </div>
-            </el-row>
-          </el-row>
-          <!-- 底部留白 -->
-          <div style="height: 20px;"></div>
+            </div>
+            <div style="height: 16px;"></div>
           </el-scrollbar>
-          <!-- 暂停后:回到最新并恢复实时滚动 -->
           <el-button
             v-if="showLoadLatestBtn"
             class="load-latest-btn"
             type="primary"
-            size="small"
+            size="mini"
+            round
             @click.stop="loadLatestMessages"
             :disabled="isLoadingHistory"
             :loading="isLoadingHistory"
@@ -167,100 +150,79 @@
             回到最新
           </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 class="send-area">
+        <el-input
+          type="textarea"
+          v-model="newMsg"
+          placeholder="输入系统消息..."
+          :rows="2"
+          resize="none"
+        />
+        <div class="send-actions">
+          <el-button size="small" type="primary" @click="sendMessage">发送消息</el-button>
+          <el-button size="small" @click="sendPopMessage">弹窗消息</el-button>
+          <el-button size="small" @click="showTopMsgDialog">官方消息</el-button>
         </div>
       </div>
-    </div>
+    </main>
+
+    <!-- 右侧:运营工具 -->
+    <aside class="panel right-panel">
+      <div class="panel-header">
+        <span class="panel-title">运营工具</span>
+      </div>
 
-    <div class="right-panel">
-      <h2>运营工具</h2>
-      <div class="live-player">
-        <h3>直播观看</h3>
+      <div class="tool-block">
+        <div class="block-label">直播观看</div>
         <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>
+      <div class="tool-block" v-if="timelineItems.length">
+        <div class="block-label">时间轴任务</div>
+        <div class="timeline-list">
+          <div class="timeline-row" v-for="item in timelineItems.slice(0, 2)" :key="item.id || item.time">
+            <span class="timeline-time">{{ formatDate(item.absValue) }}</span>
+            <span class="timeline-name">{{ item.taskName }}</span>
+            <el-button type="text" size="mini" class="timeline-del" @click="removeTimelineItem(item)">删除</el-button>
           </div>
-          <!--          <button class="add" @click="addTimelineItem">添加时间节点</button>-->
         </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="tool-block">
+        <div class="block-label">水军自动化</div>
+        <el-input
+          type="textarea"
+          :disabled="autoWatermark"
+          v-model="watermarkTemplate"
+          placeholder="每行一条弹幕内容"
+          :rows="3"
+          resize="none"
+        />
+        <div class="watermark-opts">
+          <span>间隔</span>
+          <el-input-number :disabled="autoWatermark" v-model.number="interval" :min="1" size="mini" controls-position="right" />
+          <span>秒</span>
+          <el-switch v-model="autoWatermark" @change="changeAutoWatermark" active-color="#6366f1" inactive-text="关闭" active-text="开启" />
         </div>
       </div>
-    </div>
+    </aside>
 
     <!-- 官方消息对话框 -->
-    <el-dialog title="发送官方消息" :visible.sync="topMsgDialogVisible" width="500px">
-      <el-form :model="topMsgForm" label-width="100px">
+    <el-dialog title="发送官方消息" :visible.sync="topMsgDialogVisible" width="480px">
+      <el-form :model="topMsgForm" label-width="80px">
         <el-form-item label="消息内容">
-          <el-input
-            type="textarea"
-            v-model="topMsgForm.msg"
-            placeholder="请输入消息内容"
-            :rows="3"
-          ></el-input>
+          <el-input type="textarea" v-model="topMsgForm.msg" placeholder="请输入消息内容" :rows="3" />
         </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-input-number v-model="topMsgForm.duration" :min="1" :max="60" size="small" />
+          <span class="unit-text">分钟</span>
         </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="topMsgDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="sendTopMessage">确定</el-button>
       </div>
     </el-dialog>
 
@@ -1244,7 +1206,7 @@ export default {
         const { rows, total } = response;
         params.total = total; // 记录总数据量
         this.syncUserTotalFromList(tabName, total);
-        // 过滤重复数据(基于 userId)
+        // 过滤重复数据(基于userId)
         const newRows = (rows || []).filter(row =>
           !displayList.some(u => u.userId === row.userId)
         );
@@ -1744,293 +1706,306 @@ export default {
 .console {
   display: flex;
   height: 100vh;
+  background: #f5f8fc;
+  gap: 12px;
+  padding: 12px;
+  box-sizing: border-box;
 }
 
-.left-panel, .middle-panel, .right-panel {
-  padding: 20px;
-  box-sizing: border-box;
+.panel {
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  overflow: hidden;
+  border-radius: 12px;
+  border: 1px solid #e8eef5;
+  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
 }
 
 .left-panel {
-  width: 30%;
-  background: #f8fafc;
-  border-right: 1px solid #e2e8f0;
+  width: 28%;
+  min-width: 260px;
 }
 
 .middle-panel {
-  width: 40%;
-  background: #f8fafc;
-  border-right: 1px solid #e2e8f0;
+  width: 44%;
+  flex: 1;
 }
 
 .right-panel {
-  width: 30%;
-  background: #f8fafc;
+  width: 28%;
+  min-width: 280px;
+  overflow-y: auto;
 }
 
-.search {
-  margin: 10px 0;
+.panel-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 14px 16px 10px;
+  border-bottom: 1px solid #f1f5f9;
 }
 
-.search input {
-  width: 70%;
-  padding: 8px;
-  border: 1px solid #cbd5e1;
-  border-radius: 4px;
+.panel-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #1e293b;
 }
 
-.search button {
-  padding: 8px 15px;
-  background: #3b82f6;
-  color: #fff;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
+.search-bar {
+  display: flex;
+  gap: 8px;
+  padding: 10px 16px;
 }
 
-.tabs {
-  display: flex;
-  margin: 10px 0;
+.user-tabs {
+  flex: 1;
+  overflow: hidden;
+  padding: 0 8px;
 }
 
-.tabs button {
-  padding: 8px 15px;
-  border: 1px solid #e2e8f0;
-  background: #fff;
-  cursor: pointer;
+.user-tabs >>> .el-tabs__header {
+  margin-bottom: 8px;
 }
 
-.tabs button.active {
-  background: #3b82f6;
-  color: #fff;
-  border-color: #3b82f6;
+.user-tabs >>> .el-tabs__item {
+  font-size: 13px;
+  padding: 0 12px;
 }
 
-.user-list {
-  max-height: 600px;
-  overflow-y: auto;
+.user-scroll {
+  height: calc(100vh - 160px);
 }
 
-.user-item {
+.user-card {
   display: flex;
   align-items: center;
-  padding: 10px;
-  border-bottom: 1px solid #e2e8f0;
+  gap: 10px;
+  padding: 8px 10px;
+  margin: 4px 8px;
+  border-radius: 8px;
+  transition: background 0.15s;
 }
 
-.user-item img {
-  width: 40px;
-  height: 40px;
-  border-radius: 50%;
-  margin-right: 10px;
+.user-card:hover {
+  background: #f8fafc;
 }
 
 .user-info {
   flex: 1;
+  min-width: 0;
 }
 
 .user-name {
-  font-weight: bold;
+  font-size: 13px;
+  font-weight: 500;
+  color: #334155;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
-.user-status {
-  font-size: 12px;
-  color: #64748b;
-}
-
-.online {
-  color: #10b981;
+.user-id {
+  font-size: 11px;
+  color: #94a3b8;
+  margin-top: 2px;
 }
 
-.offline {
+.action-btn {
+  cursor: pointer;
   color: #94a3b8;
+  padding: 4px;
+  border-radius: 4px;
 }
 
-.user-actions {
-  display: flex;
+.action-btn:hover {
+  color: #6366f1;
+  background: #f1f5f9;
 }
 
-.user-actions button {
-  padding: 5px 10px;
-  margin-left: 5px;
-  border: none;
-  border-radius: 4px;
+.global-toggle {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: #64748b;
   cursor: pointer;
 }
 
-.block {
-  background: #ef4444;
-  color: #fff;
+.chat-area {
+  flex: 1;
+  padding: 0 16px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
 }
 
-.unblock {
-  background: #10b981;
-  color: #fff;
+.message-container {
+  position: relative;
+  flex: 1;
+  background: #f8fafc;
+  border-radius: 10px;
+  border: 1px solid #e8eef5;
+  overflow: hidden;
 }
 
-.mute {
-  background: #f59e0b;
-  color: #fff;
+.chat-scroll {
+  height: calc(100vh - 280px);
 }
 
-.unmute {
-  background: #3b82f6;
-  color: #fff;
+.msg-row {
+  padding: 6px 12px;
 }
 
-.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-bubble {
+  display: flex;
+  gap: 8px;
+  max-width: 85%;
 }
 
-.system-messages textarea {
-  width: 100%;
-  height: 100px;
-  border: 1px solid #e2e8f0;
-  border-radius: 4px;
-  padding: 8px;
-  box-sizing: border-box;
+.msg-right {
+  flex-direction: row-reverse;
+  margin-left: auto;
+  align-items: flex-start;
 }
 
-.message-actions {
-  margin-top: 10px;
+.msg-left {
+  align-items: flex-start;
 }
 
-.message-actions button {
-  padding: 5px 10px;
-  margin-right: 5px;
-  background: #3b82f6;
-  color: #fff;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
+.msg-content {
+  max-width: 320px;
 }
 
-.message-list {
-  max-height: 300px;
-  overflow-y: auto;
-  margin-top: 10px;
+.msg-meta {
+  font-size: 11px;
+  color: #94a3b8;
+  margin-bottom: 3px;
 }
 
-.message-item {
-  display: flex;
-  margin-bottom: 10px;
-  padding-bottom: 10px;
-  border-bottom: 1px solid #e2e8f0;
+.msg-reply {
+  font-size: 11px;
+  color: #94a3b8;
+  margin-bottom: 4px;
+  word-break: break-all;
 }
 
-.message-avatar img {
-  width: 30px;
-  height: 30px;
-  border-radius: 50%;
-  margin-right: 10px;
+.msg-text {
+  background: #fff;
+  border: 1px solid #e8ecf0;
+  padding: 8px 12px;
+  border-radius: 0 10px 10px 10px;
+  font-size: 13px;
+  color: #334155;
+  word-break: break-word;
+  line-height: 1.5;
 }
 
-.message-content {
-  flex: 1;
+.admin-text {
+  background: #eef2ff;
+  border-color: #c7d2fe;
+  border-radius: 10px 0 10px 10px;
 }
 
-.message-user {
-  font-weight: bold;
+.msg-actions {
+  margin-top: 4px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
 }
 
-.message-text {
-  font-size: 14px;
-  color: #64748b;
+.msg-actions a {
+  font-size: 11px;
+  color: #6366f1;
+  cursor: pointer;
 }
 
-.message-actions button {
-  padding: 3px 8px;
-  font-size: 12px;
-  background: #3b82f6;
-  color: #fff;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
+.msg-actions a:hover {
+  text-decoration: underline;
 }
 
-.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);
+.load-latest-btn {
+  position: absolute;
+  bottom: 12px;
+  right: 12px;
+  z-index: 10;
+  box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
 }
 
-.timeline-items {
-  margin: 10px 0;
+.send-area {
+  padding: 12px 16px 16px;
+  border-top: 1px solid #f1f5f9;
 }
 
-.timeline-item {
+.send-actions {
   display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 8px 0;
-  border-bottom: 1px solid #e2e8f0;
+  gap: 8px;
+  margin-top: 8px;
 }
 
-.delete {
-  background: #ef4444;
-  color: #fff;
-  border: none;
-  border-radius: 4px;
-  padding: 3px 8px;
-  cursor: pointer;
+.tool-block {
+  padding: 12px 16px;
+  border-bottom: 1px solid #f1f5f9;
 }
 
-.add {
-  background: #10b981;
-  color: #fff;
-  border: none;
-  border-radius: 4px;
-  padding: 8px 15px;
-  cursor: pointer;
+.block-label {
+  font-size: 12px;
+  font-weight: 600;
+  color: #64748b;
+  margin-bottom: 8px;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
 }
 
-.watermark-settings textarea {
-  width: 100%;
-  height: 100px;
-  border: 1px solid #e2e8f0;
-  border-radius: 4px;
-  padding: 8px;
-  box-sizing: border-box;
+.timeline-list {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
 }
 
-.watermark-options {
-  margin-top: 10px;
+.timeline-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 12px;
+  padding: 6px 8px;
+  background: #f8fafc;
+  border-radius: 6px;
 }
 
-.watermark-options label {
-  display: block;
-  margin-bottom: 5px;
+.timeline-time {
+  color: #6366f1;
+  white-space: nowrap;
 }
-/* 隐藏 el-scrollbar 的横向滚动条 */
-.el-scrollbar__wrap {
-  overflow-x: hidden !important;
+
+.timeline-name {
+  flex: 1;
+  color: #475569;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
-.custom-scrollbar .el-scrollbar__wrap {
-  overflow-x: hidden !important;
+
+.timeline-del {
+  color: #ef4444 !important;
 }
-.scrollbar-demo-item{
+
+.watermark-opts {
   display: flex;
   align-items: center;
-  justify-content: center;
-  height: 50px;
-  margin: 10px;
-  text-align: center;
-  border-radius: 4px;
+  gap: 8px;
+  margin-top: 8px;
+  font-size: 12px;
+  color: #64748b;
 }
-.message-container {
-  position: relative;
+
+.unit-text {
+  margin-left: 6px;
+  color: #94a3b8;
+  font-size: 13px;
 }
-.load-latest-btn {
-  position: absolute;
-  bottom: 20px;
-  right: 20px;
-  z-index: 10;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
+.el-scrollbar__wrap {
+  overflow-x: hidden !important;
 }
 </style>

+ 350 - 381
src/views/live/liveConsole/LiveDashboard.vue

@@ -1,465 +1,434 @@
 <template>
-  <div class="dashboard-container">
-    <!-- 实时大屏主体(100%占满父容器) -->
-    <div class="dashboard">
-      <!-- 顶部数据卡片区(占满宽度,高度20%) -->
-      <div class="data-cards">
-        <div class="card" v-for="item in dataCards" :key="item.title">
-          <h3>{{ item.title }}</h3>
-          <p class="value">{{ item.value }}</p>
-          <div class="sub-values" v-if="item.subValues">
-            <span class="sub-item">直播: {{ item.subValues.live }}</span>
-            <span class="sub-item">回放: {{ item.subValues.replay }}</span>
-          </div>
+  <div class="dashboard-page">
+    <!-- 顶栏 -->
+    <header class="dash-header">
+      <div class="header-left">
+        <h1 class="live-title">{{ liveName || '直播实时大屏' }}</h1>
+        <div class="meta-tags">
+          <span class="tag" v-if="startTime">
+            <i class="el-icon-time"></i> 开播 {{ formatTime(startTime) }}
+          </span>
+          <span class="tag tag-live" v-if="!liveDataFrozen">直播中</span>
+          <span class="tag tag-frozen" v-if="liveDataFrozen">直播数据已冻结</span>
+          <span class="tag tag-frozen" v-if="replayDataFrozen && liveStatus === 4">回放数据已冻结</span>
         </div>
       </div>
+      <div class="header-right">
+        <span class="refresh-tip">{{ lastRefreshText }}</span>
+        <el-button type="primary" size="small" icon="el-icon-refresh" :loading="loading" @click="refreshData">
+          刷新数据
+        </el-button>
+      </div>
+    </header>
 
-      <!-- 中间内容区(上下分两部分,各占40%高度) -->
-      <div class="middle-content">
-        <!-- 左侧用户分析区(占50%宽度) -->
-        <div class="user-analysis">
-          <div class="new-old">
-            <h3>直播新老用户占比</h3>
-            <EChartsComponent chartId="liveNewOldChart" :option="liveNewOldOption" />
-          </div>
-          <div class="region">
-            <h3>回放新老用户占比</h3>
-            <EChartsComponent chartId="replayNewOldChart" :option="replayNewOldOption" />
+    <!-- 互动指标 -->
+    <section class="section">
+      <div class="section-title">实时互动</div>
+      <div class="metric-grid metric-grid-4">
+        <div class="metric-card accent-blue" v-for="item in interactionCards" :key="item.label">
+          <div class="metric-icon">{{ item.icon }}</div>
+          <div class="metric-body">
+            <div class="metric-label">{{ item.label }}</div>
+            <div class="metric-value">{{ item.value }}</div>
+            <div class="metric-sub" v-if="item.sub">{{ item.sub }}</div>
           </div>
         </div>
+      </div>
+    </section>
 
-        <!-- 右侧来源与榜单区(占50%宽度) -->
-        <div class="source-rank">
-          <div class="source">
-            <h3>观众来源</h3>
-            <EChartsComponent chartId="sourceChart" :option="sourceOption" />
-          </div>
-          <div class="rank">
-            <h3>邀请达人榜</h3>
-            <ul class="rank-list" v-if="rankList.length > 0">
-              <li v-for="(item, index) in rankList" :key="index">
-                <span class="rank-num">{{ index + 1 }}</span>
-                <span class="rank-name">{{ item.userName }}</span>
-                <span class="rank-count">{{ item.inviteNum }}</span>
-              </li>
-            </ul>
-            <p class="no-data" style="text-align: center;" v-if="rankList.length == 0">暂无数据</p>
-          </div>
+    <!-- 订单整体数据 -->
+    <section class="section">
+      <div class="section-title">订单整体数据</div>
+      <div class="metric-grid metric-grid-4">
+        <div class="metric-card" v-for="item in orderCards" :key="item.label">
+          <div class="metric-label">{{ item.label }}</div>
+          <div class="metric-value" :class="{ money: item.isMoney }">{{ item.value }}</div>
         </div>
       </div>
+    </section>
 
-      <!-- 底部趋势图(占20%高度) -->
-      <!--      <div class="trend">-->
-      <!--        <h3>实时在线人数趋势</h3>-->
-      <!--        <EChartsComponent chartId="trendChart" :option="trendOption" />-->
-      <!--      </div>-->
-    </div>
+    <!-- 商品数据 + 邀请榜 -->
+    <section class="section section-split">
+      <div class="panel product-panel">
+        <div class="section-title">商品数据</div>
+        <el-table
+          :data="productStats"
+          size="small"
+          height="320"
+          class="dark-table"
+          empty-text="暂无商品订单数据">
+          <el-table-column prop="productName" label="商品" min-width="120" show-overflow-tooltip />
+          <el-table-column prop="totalOrderCount" label="累计下单" width="80" align="center" />
+          <el-table-column label="累计金额" width="100" align="right">
+            <template slot-scope="{ row }">{{ formatMoney(row.totalOrderAmount) }}</template>
+          </el-table-column>
+          <el-table-column prop="paidOrderCount" label="成交数" width="70" align="center" />
+          <el-table-column label="成交额" width="100" align="right">
+            <template slot-scope="{ row }">{{ formatMoney(row.paidOrderAmount) }}</template>
+          </el-table-column>
+          <el-table-column prop="unpaidOrderCount" label="待支付" width="70" align="center" />
+          <el-table-column prop="refundApplyCount" label="退款申请" width="80" align="center" />
+        </el-table>
+      </div>
+
+      <div class="panel rank-panel">
+        <div class="section-title">
+          销售邀请榜
+          <span class="invite-total">共 {{ totalInviteCount }} 人</span>
+        </div>
+        <ul class="rank-list" v-if="rankList.length">
+          <li v-for="(item, index) in rankList" :key="index" class="rank-item">
+            <span class="rank-badge" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
+            <span class="rank-name">{{ item.userName || '未知销售' }}</span>
+            <span class="rank-count">{{ item.inviteNum }} 人</span>
+          </li>
+        </ul>
+        <div class="empty-rank" v-else>暂无邀请数据</div>
+      </div>
+    </section>
   </div>
 </template>
 
 <script>
-import EChartsComponent from './EchartsComponent.vue';
-import {dashboardData} from '@/api/live/liveData'
+import { dashboardData } from '@/api/live/liveData'
 
 export default {
-  components: {EChartsComponent},
   props: {
-    liveId: {
-      type: String,
-      default: null
-    }
+    liveId: { type: String, default: null }
   },
   data() {
     return {
-      // 数据保持不变(与原代码一致)
-      dataCards: [
-        {title: '在线人数', value: 0},
-        {
-          title: '观看人次',
-          value: 0,
-          subValues: {live: 0, replay: 0}
-        },
-        {
-          title: '点赞数',
-          value: 0,
-          subValues: {live: 0, replay: 0}
-        },
-        {
-          title: '评论数',
-          value: 0,
-          subValues: {live: 0, replay: 0}
-        }
-      ],
-      liveNewOldOption: {
-        tooltip: {trigger: 'item'},
-        series: [{
-          type: 'pie', radius: '70%',
-          data: [{value: 0, name: '新用户'}, {value: 0, name: '老用户'}],
-          itemStyle: {colors: ['#36cfc9', '#722ed1']}
-        }]
-      },
-      replayNewOldOption: {
-        tooltip: {trigger: 'item'},
-        series: [{
-          type: 'pie', radius: '70%',
-          data: [{value: 0, name: '新用户'}, {value: 0, name: '老用户'}],
-          itemStyle: {colors: ['#36cfc9', '#722ed1']}
-        }]
-      },
-      regionOption: {
-        tooltip: {trigger: 'item'},
-        series: [{
-          type: 'map', map: 'china', roam: true,
-          itemStyle: {
-            normal: {areaColor: '#36cfc9', borderColor: '#fff'},
-            emphasis: {areaColor: '#722ed1'}
-          },
-          data: [{name: '北京', value: 120}, {name: '上海', value: 150}, {name: '广州', value: 90}, {
-            name: '深圳',
-            value: 80
-          }, {name: '杭州', value: 70}]
-        }]
-      },
-      sourceOption: {
-        tooltip: {trigger: 'item'},
-        series: [{
-          type: 'pie', radius: '70%',
-          data: [{value: 45, name: '分享链接'}, {value: 30, name: '直接访问'}, {value: 25, name: '其他'}],
-          itemStyle: {colors: ['#3b82f6', '#10b981', '#f59e0b']}
-        }]
-      },
-      rankList: [
-        {userName: '达人1', inviteNum: 258},
-        {userName: '达人2', inviteNum: 186},
-        {userName: '达人3', inviteNum: 152},
-        {userName: '达人4', inviteNum: 121},
-        {userName: '达人5', inviteNum: 98}
-      ],
-      trendOption: {
-        // 网格:控制图表与容器的边距,避免内容被裁剪
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          top: '10%',
-          containLabel: true // 确保坐标轴标签不被裁剪
-        },
-        // X轴配置(补充样式,适配深色背景)
-        xAxis: {
-          type: 'category',
-          data: ['09:00', '09:10', '09:20', '09:30', '09:40', '09:50'],
-          axisLine: {
-            lineStyle: {
-              color: '#475569' // 坐标轴线条颜色(深色背景可见)
-            }
-          },
-          axisLabel: {
-            color: '#cbd5e1' // 坐标轴文字颜色
-          }
-        },
-        // Y轴配置(同上)
-        yAxis: {
-          type: 'value',
-          axisLine: {
-            lineStyle: {
-              color: '#475569'
-            }
-          },
-          axisLabel: {
-            color: '#cbd5e1'
-          },
-          splitLine: {
-            lineStyle: {
-              color: 'rgba(71, 85, 105, 0.3)' // 网格线半透明
-            }
-          }
-        },
-        // 系列配置(补充线条和区域样式)
-        series: [{
-          data: [1200, 1500, 1800, 2100, 2300, 2500],
-          type: 'line',
-          smooth: true,
-          // 线条样式
-          lineStyle: {
-            color: '#36cfc9',
-            width: 3
-          },
-          // 点样式
-          itemStyle: {
-            color: '#36cfc9',
-            borderWidth: 2,
-            borderColor: '#fff' // 点边缘白色,突出显示
-          },
-          // 填充区域(可选,增强视觉效果)
-          areaStyle: {
-            color: {
-              type: 'linear',
-              x: 0,
-              y: 0,
-              x2: 0,
-              y2: 1,
-              colorStops: [{
-                offset: 0, color: 'rgba(54, 207, 201, 0.3)'
-              }, {
-                offset: 1, color: 'rgba(54, 207, 201, 0)'
-              }]
-            }
-          }
-        }]
-      },
-      socket: null,
+      loading: false,
+      liveName: '',
+      startTime: null,
+      liveStatus: null,
+      liveDataFrozen: false,
+      replayDataFrozen: false,
+      totalInviteCount: 0,
+      interactionCards: [],
+      orderCards: [],
+      productStats: [],
+      rankList: [],
+      lastRefreshAt: null,
       timer: null
-    };
+    }
+  },
+  computed: {
+    lastRefreshText() {
+      if (!this.lastRefreshAt) return '等待首次加载'
+      const diff = Math.floor((Date.now() - this.lastRefreshAt) / 1000)
+      if (diff < 60) return `${diff} 秒前更新`
+      return `${Math.floor(diff / 60)} 分钟前更新`
+    }
   },
   created() {
-    this.getInitData()
-    this.timer = setInterval(() => {
-      this.getInitData();
-    }, 30000);
+    this.refreshData()
+    this.timer = setInterval(() => this.refreshData(), 30000)
   },
   beforeDestroy() {
-    clearInterval(this.timer); // 组件销毁时清除定时器
+    if (this.timer) clearInterval(this.timer)
   },
   methods: {
-    async getInitData() {
+    refreshData() {
+      if (!this.liveId) return
+      this.loading = true
       dashboardData(this.liveId).then(res => {
-        if (res.code == 200) {
-          this.dealData(res.data)
+        if (res.code === 200 && res.data) {
+          this.applyData(res.data)
+          this.lastRefreshAt = Date.now()
         }
-      });
+      }).finally(() => {
+        this.loading = false
+      })
     },
-    dealData(data) {
-      // 计算总计
-      const totalViewNum = (data.liveViewNum || 0) + (data.replayViewNum || 0);
-      const totalLikeNum = (data.liveLikeNum || 0) + (data.replayLikeNum || 0);
-      const totalCommentNum = (data.liveCommentNum || 0) + (data.replayCommentNum || 0);
-
-      this.dataCards = [
-        {title: '在线人数', value: data.onlineNum || 0},
-        {
-          title: '观看人次',
-          value: totalViewNum,
-          subValues: {
-            live: data.liveViewNum || 0,
-            replay: data.replayViewNum || 0
-          }
-        },
-        {
-          title: '点赞数',
-          value: totalLikeNum,
-          subValues: {
-            live: data.liveLikeNum || 0,
-            replay: data.replayLikeNum || 0
-          }
-        },
-        {
-          title: '评论数',
-          value: totalCommentNum,
-          subValues: {
-            live: data.liveCommentNum || 0,
-            replay: data.replayCommentNum || 0
-          }
-        }
+    applyData(data) {
+      this.liveName = data.liveName || ''
+      this.startTime = data.startTime || null
+      this.liveStatus = data.liveStatus
+      this.liveDataFrozen = !!data.liveDataFrozen
+      this.replayDataFrozen = !!data.replayDataFrozen
+      this.totalInviteCount = data.totalInviteCount || 0
+      this.rankList = data.inviteUserList || []
+      this.productStats = data.productStats || []
+
+      const totalView = (data.liveViewNum || 0) + (data.replayViewNum || 0)
+      const totalLike = (data.liveLikeNum || 0) + (data.replayLikeNum || 0)
+      const totalComment = Number(data.liveCommentNum || 0) + Number(data.replayCommentNum || 0)
+
+      this.interactionCards = [
+        { icon: '👁', label: '实时在线', value: data.onlineNum || 0, sub: `离线 ${data.offlineNum || 0}` },
+        { icon: '📊', label: '累计观看人数', value: data.uniqueViewCount || 0, sub: `人次 ${totalView}` },
+        { icon: '👍', label: '累计点赞', value: totalLike, sub: `直播 ${data.liveLikeNum || 0} / 回放 ${data.replayLikeNum || 0}` },
+        { icon: '💬', label: '累计评论', value: totalComment, sub: `直播 ${data.liveCommentNum || 0} / 回放 ${data.replayCommentNum || 0}` }
       ]
 
-      // 直播新老用户占比
-      this.liveNewOldOption = {
-        tooltip: {trigger: 'item'},
-        series: [{
-          type: 'pie', radius: '70%',
-          data: [
-            {value: data.liveNewUserNum || 0, name: '新用户'},
-            {value: data.liveOldUserNum || 0, name: '老用户'}
-          ],
-          itemStyle: {colors: ['#36cfc9', '#722ed1']}
-        }]
-      }
-
-      // 回放新老用户占比
-      this.replayNewOldOption = {
-        tooltip: {trigger: 'item'},
-        series: [{
-          type: 'pie', radius: '70%',
-          data: [
-            {value: data.replayNewUserNum || 0, name: '新用户'},
-            {value: data.replayOldUserNum || 0, name: '老用户'}
-          ],
-          itemStyle: {colors: ['#36cfc9', '#722ed1']}
-        }]
-      }
-
-      // 观众来源
-      this.sourceOption = {
-        tooltip: {trigger: 'item'},
-        series: [{
-          type: 'pie', radius: '70%',
-          data: [
-            {value: data.shareUrlNum || 0, name: '分享链接'},
-            {value: data.directAccessNum || 0, name: '直接访问'}
-          ],
-          itemStyle: {colors: ['#3b82f6', '#10b981']}
-        }]
-      }
-
-      // 邀请用户列表
-      this.rankList = data.inviteUserList || [];
+      const os = data.orderStats || {}
+      this.orderCards = [
+        { label: '累计下单数', value: os.totalOrderCount || 0 },
+        { label: '累计下单金额', value: this.formatMoney(os.totalOrderAmount), isMoney: true },
+        { label: '现存成交订单数', value: os.paidOrderCount || 0 },
+        { label: '现存成交额', value: this.formatMoney(os.paidOrderAmount), isMoney: true },
+        { label: '申请退款订单数', value: os.refundApplyCount || 0 },
+        { label: '申请退款金额', value: this.formatMoney(os.refundApplyAmount), isMoney: true },
+        { label: '待支付订单数', value: os.unpaidOrderCount || 0 },
+        { label: '待支付金额', value: this.formatMoney(os.unpaidOrderAmount), isMoney: true }
+      ]
+    },
+    formatMoney(val) {
+      const num = Number(val || 0)
+      return '¥' + num.toFixed(2)
     },
+    formatTime(val) {
+      if (!val) return '-'
+      const d = new Date(val)
+      if (isNaN(d.getTime())) return val
+      const p = n => String(n).padStart(2, '0')
+      return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
+    }
   }
-};
+}
 </script>
 
 <style scoped>
-/* 容器占满整个屏幕 */
-.dashboard-container {
-  width: 100vw;
-  height: 92vh;
-  overflow: hidden;
-  background-color: #0f172a;
+.dashboard-page {
+  min-height: 92vh;
+  background: #f5f8fc;
+  color: #334155;
+  padding: 20px 24px;
+  box-sizing: border-box;
+  overflow-y: auto;
 }
 
-/* 大屏主体:100%宽高,内边距用百分比 */
-.dashboard {
-  width: 100%;
-  height: 100%;
-  padding: 1%; /* 用百分比内边距,适配不同屏幕 */
-  box-sizing: border-box;
-  color: #fff;
+.dash-header {
   display: flex;
-  flex-direction: column;
-  gap: 1%; /* 区域间间距 */
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 20px;
+  padding-bottom: 14px;
+  border-bottom: 1px solid #e8eef5;
+}
+
+.live-title {
+  font-size: 20px;
+  font-weight: 600;
+  margin: 0 0 8px;
+  color: #1e293b;
 }
 
-/* 顶部数据卡片区:高度20%,横向排列 */
-.data-cards {
-  width: 100%;
-  height: 20%;
+.meta-tags {
   display: flex;
-  justify-content: space-between;
-  gap: 1%; /* 卡片间间距 */
+  gap: 8px;
+  flex-wrap: wrap;
 }
 
-/* 每个卡片:平均分配宽度(4个卡片各占 ~24%) */
-.card {
-  width: 24%; /* 100% - 3个间距(1%) = 97% → 97%/4 ≈ 24% */
-  height: 100%;
-  background: #1a202c;
-  border-radius: 8px;
-  padding: 1%;
-  box-sizing: border-box;
-  text-align: center;
+.tag {
+  font-size: 12px;
+  padding: 3px 10px;
+  border-radius: 20px;
+  background: #eef2f7;
+  color: #64748b;
 }
 
-.card h3 {
-  font-size: 1.2vw; /* 字体用vw单位,随屏幕宽度缩放 */
-  margin: 0 0 5% 0;
+.tag-live {
+  background: #ecfdf5;
+  color: #059669;
 }
 
-.card .value {
-  font-size: 2vw;
-  font-weight: bold;
-  margin: 0;
+.tag-frozen {
+  background: #fffbeb;
+  color: #d97706;
 }
 
-.card .sub-values {
+.header-right {
   display: flex;
-  justify-content: space-around;
-  margin-top: 10px;
-  font-size: 0.8vw;
+  align-items: center;
+  gap: 12px;
+}
+
+.refresh-tip {
+  font-size: 12px;
   color: #94a3b8;
 }
 
-.card .sub-item {
-  padding: 3px 8px;
-  background: rgba(54, 207, 201, 0.1);
-  border-radius: 4px;
+.section {
+  margin-bottom: 18px;
 }
 
-/* 中间内容区:高度40%,左右分栏 */
-.middle-content {
-  width: 100%;
-  height: 70%;
-  display: flex;
-  gap: 1%;
+.section-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #64748b;
+  margin-bottom: 12px;
+  letter-spacing: 0.3px;
 }
 
-/* 左侧用户分析区:占50%宽度,内部上下分栏 */
-.user-analysis {
-  width: 50%;
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  gap: 1%;
+.section-split {
+  display: grid;
+  grid-template-columns: 1fr 340px;
+  gap: 16px;
+}
+
+.metric-grid {
+  display: grid;
+  gap: 12px;
 }
 
-/* 右侧来源与榜单区:占50%宽度,内部上下分栏 */
-.source-rank {
-  width: 50%;
-  height: 100%;
+.metric-grid-4 {
+  grid-template-columns: repeat(4, 1fr);
+}
+
+@media (max-width: 1200px) {
+  .metric-grid-4 { grid-template-columns: repeat(2, 1fr); }
+  .section-split { grid-template-columns: 1fr; }
+}
+
+.metric-card {
+  background: #fff;
+  border: 1px solid #e8eef5;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
+  transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.metric-card:hover {
+  border-color: #c7d7fe;
+  box-shadow: 0 4px 12px rgba(99, 102, 241, 0.08);
+}
+
+.metric-card.accent-blue {
   display: flex;
-  flex-direction: column;
-  gap: 1%;
+  align-items: center;
+  gap: 12px;
 }
 
-/* 中间区域的子模块(新老用户/地域/来源/榜单):各占50%高度 */
-.new-old, .region, .source, .rank {
-  width: 100%;
-  height: 60.5%; /* 100% - 1个间距(1%) = 99% → 99%/2 ≈ 49.5% */
-  background: #1a202c;
-  border-radius: 8px;
-  padding: 1%;
-  box-sizing: border-box;
+.metric-icon {
+  font-size: 28px;
+  line-height: 1;
+}
+
+.metric-label {
+  font-size: 12px;
+  color: #94a3b8;
+  margin-bottom: 6px;
+}
+
+.metric-value {
+  font-size: 24px;
+  font-weight: 700;
+  color: #1e293b;
+  line-height: 1.2;
+}
+
+.metric-value.money {
+  color: #ea580c;
+}
+
+.metric-sub {
+  font-size: 11px;
+  color: #94a3b8;
+  margin-top: 4px;
+}
+
+.panel {
+  background: #fff;
+  border: 1px solid #e8eef5;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
 }
 
-/* 标题样式:用vw单位适配 */
-.new-old h3, .region h3, .source h3, .rank h3, .trend h3 {
-  font-size: 1.1vw;
-  margin: 0 0 2% 0;
+.invite-total {
+  font-size: 12px;
+  font-weight: 400;
+  color: #4f46e5;
+  margin-left: 8px;
 }
 
-/* 达人榜单列表 */
 .rank-list {
   list-style: none;
   padding: 0;
-  height: calc(100% - 15%); /* 减去标题高度 */
+  margin: 0;
+  max-height: 320px;
   overflow-y: auto;
 }
 
-.rank-list li {
+.rank-item {
   display: flex;
-  justify-content: space-between;
   align-items: center;
-  padding: 2%;
-  margin-bottom: 1%;
-  border-bottom: 1px solid #2d3748;
-  font-size: 1vw;
-}
-
-.rank-num {
-  width: 3vw;
-  height: 3vw;
-  max-width: 30px;
-  max-height: 30px;
-  border-radius: 50%;
-  background: #36cfc9;
+  padding: 10px 8px;
+  border-bottom: 1px solid #f1f5f9;
+  font-size: 13px;
+}
+
+.rank-badge {
+  width: 24px;
+  height: 24px;
+  border-radius: 6px;
   display: flex;
-  justify-content: center;
   align-items: center;
-  font-size: 0.8vw;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: 700;
+  background: #f1f5f9;
+  color: #64748b;
+  flex-shrink: 0;
 }
 
-/* 底部趋势图:高度20% */
-.trend {
-  width: 100%;
-  height: 20%;
-  background: #1a202c;
-  border-radius: 8px;
-  padding: 1%;
-  box-sizing: border-box;
+.rank-1 { background: #fef3c7; color: #b45309; }
+.rank-2 { background: #e2e8f0; color: #475569; }
+.rank-3 { background: #ffedd5; color: #c2410c; }
+
+.rank-name {
+  flex: 1;
+  margin: 0 12px;
+  color: #334155;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.rank-count {
+  color: #4f46e5;
+  font-weight: 600;
+}
+
+.empty-rank {
+  text-align: center;
+  color: #94a3b8;
+  padding: 40px 0;
+  font-size: 13px;
+}
+
+.dark-table {
+  background: transparent !important;
+}
+
+.dark-table >>> .el-table {
+  background: transparent;
+  color: #334155;
+}
+
+.dark-table >>> .el-table th {
+  background: #f8fafc !important;
+  color: #64748b;
+  border-bottom: 1px solid #e8eef5;
+}
+
+.dark-table >>> .el-table td {
+  background: #fff !important;
+  border-bottom: 1px solid #f1f5f9;
+}
+
+.dark-table >>> .el-table tr {
+  background: #fff !important;
+}
+
+.dark-table >>> .el-table--enable-row-hover .el-table__body tr:hover > td {
+  background: #f8fafc !important;
 }
 
-/* ECharts图表容器:占满父元素 */
-::v-deep .chart-container {
-  width: 100% !important;
-  height: calc(100% - 15%) !important; /* 减去标题高度 */
+.dark-table >>> .el-table::before {
+  display: none;
 }
 </style>

+ 51 - 20
src/views/live/liveConsole/index.vue

@@ -1,10 +1,18 @@
 <template>
-  <div id="app">
-    <div class="nav">
-      <button @click="currentView = 'dashboard'">实时大屏</button>
-      <button @click="currentView = 'console'">中控台</button>
-    </div>
-    <component :is="currentView" :liveId="liveId"></component>
+  <div class="console-layout">
+    <nav class="top-nav">
+      <button
+        :class="['nav-btn', { active: currentView === 'dashboard' }]"
+        @click="currentView = 'dashboard'">
+        <i class="el-icon-data-analysis"></i> 实时大屏
+      </button>
+      <button
+        :class="['nav-btn', { active: currentView === 'console' }]"
+        @click="currentView = 'console'">
+        <i class="el-icon-s-operation"></i> 中控台
+      </button>
+    </nav>
+    <component :is="currentView" :liveId="liveId" />
   </div>
 </template>
 
@@ -13,7 +21,7 @@ import LiveDashboard from './LiveDashboard.vue';
 import LiveConsole from './LiveConsole.vue';
 
 export default {
-  name: 'LiveConsole',
+  name: 'LiveConsoleIndex',
   components: {
     dashboard: LiveDashboard,
     console: LiveConsole
@@ -23,9 +31,6 @@ export default {
       liveId: this.$route.params.liveId,
       currentView: 'dashboard'
     };
-  },
-  created() {
-
   }
 };
 </script>
@@ -33,25 +38,51 @@ export default {
 <style>
 body {
   margin: 0;
-  font-family: 'Arial', sans-serif;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
 }
+</style>
 
-.nav {
+<style scoped>
+.console-layout {
   display: flex;
-  background: #1e3a8a;
-  color: white;
+  flex-direction: column;
+  height: 100vh;
 }
 
-.nav button {
-  padding: 15px 20px;
+.top-nav {
+  display: flex;
+  background: #fff;
+  padding: 0 16px;
+  flex-shrink: 0;
+  border-bottom: 1px solid #e8eef5;
+}
+
+.nav-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 12px 20px;
   border: none;
   background: none;
-  color: white;
+  color: #94a3b8;
   cursor: pointer;
-  font-size: 16px;
+  font-size: 14px;
+  border-bottom: 2px solid transparent;
+  transition: all 0.2s;
+}
+
+.nav-btn:hover {
+  color: #475569;
+  background: #f8fafc;
 }
 
-.nav button:hover {
-  background: #3b82f6;
+.nav-btn.active {
+  color: #4f46e5;
+  border-bottom-color: #4f46e5;
+  background: #eef2ff;
+}
+
+.nav-btn i {
+  font-size: 16px;
 }
 </style>