Files
douyin/app/src/pages/mobile/MobileDashboard.vue
T

1283 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="stats-dashboard-mobile">
<div class="dashboard-container">
<!-- 看板 Tab 内容 -->
<div v-if="activeTab === 'dashboard'" class="tab-content">
<!-- 仅显示核心统计概览无底部详细数据 -->
<section class="stats-overview">
<!-- 合并后的总视频数和空间总计卡片保留原有移动端样式 -->
<div class="stat-card primary-card main-card">
<div class="stat-header">
<div class="header-left">
<span class="stat-meta">视频统计</span>
<!-- 拆分视频总数和总空间到左右两侧 -->
<div class="stat-value-container">
<div class="stat-value video-count">
{{ totalVideos }}
<span class="value-label"></span>
</div>
<div class="stat-value size-count">
{{ fileSizeTotal }}G
</div>
</div>
</div>
<!-- 移除SVG图标 -->
</div>
<div class="stat-subitems">
<div class="subitem" :title="`我喜欢的视频数: ${favoriteCount} (占用: ${favoriteSize}G)`">
<div class="subitem-icon like-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
</div>
<!-- 子项调整数量放在label后占用空间单独显示 -->
<span class="subitem-meta">我喜欢的 <span class="subitem-count">({{ favoriteCount }})</span></span>
<span class="subitem-size">{{ favoriteSize }}G</span>
</div>
<div class="subitem" :title="`我收藏的视频数: ${collectCount} (占用: ${collectSize}G)`">
<div class="subitem-icon collect-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
</svg>
</div>
<span class="subitem-meta">我收藏的 <span class="subitem-count">({{ collectCount }})</span></span>
<span class="subitem-size">{{ collectSize }}G</span>
</div>
<div class="subitem" :title="`我关注的视频数: ${followCount} (占用: ${followSize}G)`">
<div class="subitem-icon follow-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="20" y1="8" x2="20" y2="14"></line>
<line x1="23" y1="11" x2="17" y2="11"></line>
</svg>
</div>
<span class="subitem-meta">我关注的 <span class="subitem-count">({{ followCount }})</span></span>
<span class="subitem-size">{{ followSize }}G</span>
</div>
<div class="subitem" :title="`图文视频数: ${graphicVideoCount} (占用: ${graphicVideoSize}G)`">
<div class="subitem-icon graphic-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<span class="subitem-meta">图文视频 <span class="subitem-count">({{ graphicVideoCount }})</span></span>
<span class="subitem-size">{{ graphicVideoSize }}G</span>
</div>
</div>
</div>
<!-- 新增 Top Videos 列表使用原有移动端卡片样式体系 -->
<div class="stat-card secondary-card main-card">
<div class="stat-header">
<div class="header-left">
<span class="stat-meta">最新同步</span>
<!-- <div class="stat-value">TOP {{ topVideos.length }}</div> -->
</div>
<!-- 移除SVG图标 -->
</div>
<div class="stat-subitems video-list-container">
<div v-for="(video, index) in topVideos" :key="index" class="subitem video-item" :title="video.title">
<div class="video-info">
<!-- 点击标题播放视频 -->
<span class="subitem-meta video-title" @click="openVideoPlayer(video)">{{ video.title }}</span>
<span class="subitem-value video-time">{{ video.time }}</span>
</div>
</div>
<div v-if="topVideos.length === 0" class="empty-video-list">
暂无热门视频数据
</div>
</div>
</div>
</section>
</div>
<!-- 日志 Tab 内容完全保留原有样式 -->
<div v-if="activeTab === 'log'" class="tab-content log-tab">
<div class="log-header">
<h2 class="log-title">系统日志</h2>
<div class="log-filter">
<button class="filter-btn" :class="{ active: logFilter === 'all' }" @click="logFilter = 'all'">
全部
</button>
<button class="filter-btn" :class="{ active: logFilter === 'debug' }" @click="logFilter = 'debug'">
DEBUG
</button>
<button class="filter-btn" :class="{ active: logFilter === 'error' }" @click="logFilter = 'error'">
ERROR
</button>
</div>
</div>
<div class="log-list">
<div v-for="(log,index) in filteredLogs" :key="index" class="log-item" :class="log.type.toLowerCase()" @click="openLogDetail(log)">
<div class="log-item-header">
<span class="log-type" :class="log.type.toLowerCase()">
{{ log.type }}
</span>
<span class="log-time">{{formatShowDate(log?.date) }}</span>
</div>
<div class="log-file">{{ log.fileName }}</div>
</div>
<div v-if="filteredLogs.length === 0" class="empty-log">
暂无{{ logFilter === 'all' ? '' : logFilter.toUpperCase() }}类型日志
</div>
</div>
</div>
</div>
<!-- 底部导航菜单完全保留原有样式 -->
<div class="bottom-nav">
<div class="nav-item" :class="{ active: activeTab === 'dashboard' }" @click="activeTab = 'dashboard'">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="3 11 12 2 21 11 12 20 3 11"></polygon>
<line x1="12" y1="2" x2="12" y2="20"></line>
</svg>
<span>看板</span>
</div>
<div class="nav-item" :class="{ active: activeTab === 'log' }" @click="activeTab = 'log'">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
<span>日志</span>
</div>
</div>
<!-- 日志详情弹窗完全保留原有样式 -->
<div v-if="showLogModal" class="log-modal-mask" @click="closeLogModal">
<div class="log-modal-content" @click.stop>
<div class="log-modal-header">
<div class="header-left">
<span class="modal-type-tag" :class="currentLog?.type.toLowerCase()">
{{ currentLog?.type }}
</span>
<span class="modal-date">{{ formatShowDate(currentLog?.date) }}</span>
</div>
<button class="close-btn" @click="closeLogModal">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="log-modal-body">
<div class="file-name">{{ currentLog?.fileName }}</div>
<pre class="log-content">{{ logContent || '加载日志内容中...' }}</pre>
</div>
<div class="log-modal-footer">
<button class="copy-btn" @click="copyLogContent" :disabled="!logContent">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
复制日志
</button>
</div>
</div>
</div>
<!-- 新增视频播放弹窗移动端适配 -->
<div v-if="showVideoPlayer" class="video-modal-mask" @click="closeVideoPlayer">
<div class="video-modal-content" @click.stop>
<div class="video-modal-header">
<h3 class="video-title-text"></h3>
<button class="video-close-btn" @click="closeVideoPlayer">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="video-modal-body">
<div v-if="videoLoading" class="video-loading">
<div class="loading-spinner"></div>
<p>加载视频中...</p>
</div>
<div v-else-if="videoError" class="video-error">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#f44336" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<p>视频加载失败</p>
<button class="retry-btn" @click="loadVideo(currentVideo)">重试</button>
</div>
<video v-else class="video-player" controls autoplay playsinline :src="videoPlayUrl" @error="handleVideoError">
您的浏览器不支持HTML5视频播放
</video>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue';
import { useApiStore } from '@/store';
import { message } from 'ant-design-vue';
// 类型接口
interface Author {
name: string;
count: number;
icon: string;
}
interface Category {
name: string;
count: number;
color: string;
icon: string;
}
interface LogItem {
id: string;
type: 'DEBUG' | 'ERROR';
time: string;
message: string;
file: string;
date: string;
fileName: string;
}
// 新增 TopVideo 类型接口(增加videoId字段)
interface TopVideoItem {
title: string;
time: string;
id: string; // 视频ID
}
// 状态管理(仅保留核心数据)
const totalVideos = ref<number>(0);
const fileSizeTotal = ref<string>('0.00');
const favoriteCount = ref<number>(0);
const collectCount = ref<number>(0);
const followCount = ref<number>(0);
const graphicVideoCount = ref<number>(0);
const favoriteSize = ref<string>('0.00');
const collectSize = ref<string>('0.00');
const followSize = ref<string>('0.00');
const graphicVideoSize = ref<string>('0.00');
// 导航相关状态
const activeTab = ref<string>('dashboard');
const logFilter = ref<string>('all');
const logs = ref<LogItem[]>([]);
// 弹窗相关状态
const showLogModal = ref<boolean>(false);
const currentLog = ref<LogItem | null>(null);
const logContent = ref<string>('');
// Top Videos 相关状态
const topVideos = ref<TopVideoItem[]>([]);
// 新增:视频播放相关状态
const showVideoPlayer = ref<boolean>(false);
const currentVideo = ref<TopVideoItem | null>(null);
const videoPlayUrl = ref<string>('');
const videoLoading = ref<boolean>(false);
const videoError = ref<boolean>(false);
// 过滤后的日志列表
const filteredLogs = computed(() => {
if (logFilter.value === 'all') {
return logs.value;
}
return logs.value.filter((log) => log.type.toLowerCase() === logFilter.value);
});
// 组件名称
defineOptions({ name: 'StatsDashboardMobile' });
// 加载核心数据
onMounted(() => {
loadDashboardData();
loadLogData();
TopVideo(); // 加载热门视频数据
});
const loadDashboardData = async () => {
try {
const res = await useApiStore().VideoStatics();
totalVideos.value = res.data.videoCount;
fileSizeTotal.value = res.data.videoSizeTotal || '0.00';
favoriteCount.value = res.data.favoriteCount;
collectCount.value = res.data.collectCount;
followCount.value = res.data.followCount || 0;
graphicVideoCount.value = res.data.graphicVideoCount || 0;
favoriteSize.value = res.data.videoFavoriteSize || '0.00';
collectSize.value = res.data.videoCollectSize || '0.00';
followSize.value = res.data.videoFollowSize || '0.00';
graphicVideoSize.value = res.data.graphicVideoSize || '0.00';
} catch (err) {
console.error('加载移动端仪表盘数据失败:', err);
}
};
// 加载日志数据(模拟接口,实际项目中替换为真实接口)
const loadLogData = async () => {
try {
useApiStore()
.MobileLogs()
.then((res) => {
if (res.code == 0) {
logs.value = res.data;
}
});
} catch (err) {
console.error('加载日志数据失败:', err);
}
};
// 加载最新同步的视频5个
const TopVideo = () => {
useApiStore()
.TopVideo(5)
.then((res) => {
if (res.code == 0) {
topVideos.value = res.data;
}
});
};
// 打开日志详情弹窗
const openLogDetail = (log: LogItem) => {
currentLog.value = log;
showLogModal.value = true;
loadLogDetailContent(log);
};
// 关闭日志详情弹窗
const closeLogModal = () => {
showLogModal.value = false;
currentLog.value = null;
logContent.value = '';
};
// 加载日志详情内容
const loadLogDetailContent = (log: LogItem) => {
try {
const type = log.type.toLowerCase();
const date = log.date;
const requestParams = `type=${type}&date=${date}`;
useApiStore()
.apiGetLogs(requestParams)
.then((logContentStr: string) => {
const formattedLog = formatLogTime(logContentStr);
const lines = formattedLog.split('\n');
const reversedLines = lines.reverse();
const reversedText = reversedLines.join('\n');
logContent.value = reversedText;
})
.catch((err) => {
console.error('加载日志详情失败:', err);
logContent.value = '日志内容加载失败,请重试';
});
} catch (err) {
console.error('加载日志详情失败:', err);
logContent.value = '日志内容加载失败,请重试';
}
};
// 格式化日期显示(20251211 → 2025-12-11
const formatShowDate = (dateStr?: string) => {
if (!dateStr || dateStr.length !== 8) return dateStr || '';
return `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6, 8)}`;
};
// 处理日志时间戳,仅保留时分秒
const formatLogTime = (logContent: string) => {
const timeRegex = /\d{4}-\d{2}-\d{2} (\d{2}:\d{2}:\d{2})\.\d+ \+08:00/g;
return logContent.replace(timeRegex, '$1');
};
// 复制日志内容(移动端兼容)
const copyLogContent = () => {
if (!logContent.value) {
message.warn('暂无日志内容可复制');
return;
}
if (navigator.clipboard) {
navigator.clipboard
.writeText(logContent.value)
.then(() => message.success('日志内容已复制到剪贴板'))
.catch(() => {
fallbackCopyTextToClipboard(logContent.value);
});
} else {
fallbackCopyTextToClipboard(logContent.value);
}
};
// 复制降级方案(适配移动端)
const fallbackCopyTextToClipboard = (text: string) => {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
const msg = successful ? '日志内容已复制到剪贴板' : '复制失败,请手动复制';
message.success(msg);
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
message.error('复制失败,请手动复制');
}
document.body.removeChild(textArea);
};
// 新增:打开视频播放弹窗
const openVideoPlayer = (video: TopVideoItem) => {
if (!video.id) {
message.warn('视频ID不存在,无法播放');
return;
}
currentVideo.value = video;
showVideoPlayer.value = true;
loadVideo(video);
};
// 新增:加载视频播放地址
const loadVideo = async (video: TopVideoItem) => {
try {
videoLoading.value = true;
videoError.value = false;
const timestamp = new Date().getTime();
videoPlayUrl.value = `${import.meta.env.VITE_API_URL}api/Video/play/${video.id}?t=${timestamp}`;
} catch (err) {
console.error('加载视频失败:', err);
videoError.value = true;
message.error('视频加载失败,请稍后重试');
} finally {
videoLoading.value = false;
}
};
// 新增:关闭视频播放弹窗
const closeVideoPlayer = () => {
showVideoPlayer.value = false;
currentVideo.value = null;
videoPlayUrl.value = '';
videoLoading.value = false;
videoError.value = false;
};
// 新增:处理视频播放错误
const handleVideoError = () => {
videoError.value = true;
console.error('视频播放出错');
};
</script>
<style scoped>
/* 完全保留原有移动端样式 */
.stats-dashboard-mobile {
min-height: 100vh;
background-color: #ffffff;
color: #333333;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 15px 0;
display: flex;
flex-direction: column;
padding-bottom: 80px; /* 为底部导航留出空间 */
position: relative;
}
.dashboard-container {
width: 100%;
padding: 0 15px;
box-sizing: border-box;
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch; /* 移动端弹性滚动 */
}
.tab-content {
width: 100%;
height: 100%;
}
.stats-overview {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
margin-bottom: 20px;
}
.main-card {
padding: 20px 16px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border: 1px solid #f0f0f0;
background: #fff;
}
.main-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.08);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.header-left {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
.stat-meta {
font-size: 14px;
color: #666666;
text-transform: uppercase;
letter-spacing: 0.4px;
font-weight: 500;
}
/* 新增:视频总数和总空间容器样式 */
.stat-value-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-top: 8px;
}
.stat-value {
font-size: 26px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.1;
}
.value-label {
font-size: 14px;
color: #666;
font-weight: 500;
margin-left: 4px;
}
.video-count {
font-size: 22px;
}
.size-count {
font-size: 22px;
color: #666;
}
.unit {
font-size: 18px;
color: #444444;
font-weight: 500;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: rgba(76, 175, 80, 0.15);
color: #4caf50;
}
.secondary-card .stat-icon {
background-color: rgba(33, 150, 243, 0.15);
color: #2196f3;
}
.stat-subitems {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.subitem {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: #ffffff;
border: 1px solid #f0f0f0;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: all 0.2s ease;
cursor: default;
justify-content: space-between;
}
.subitem:hover {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
border-color: #e0e0e0;
}
.subitem-icon {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: rgba(233, 30, 99, 0.1);
color: #e91e63;
}
.collect-icon {
background-color: rgba(255, 152, 0, 0.1);
color: #ff9800;
}
.follow-icon {
background-color: rgba(156, 39, 176, 0.1);
color: #9c27b0;
}
.graphic-icon {
background-color: rgba(255, 159, 64, 0.1);
color: #d9091a;
}
/* 视频列表图标样式(沿用原有样式体系) */
.video-list-icon {
background-color: rgba(33, 150, 243, 0.15);
color: #2196f3;
}
.video-index {
font-size: 12px;
font-weight: 600;
}
.subitem-meta {
font-size: 13px;
color: #666666;
font-weight: 500;
flex: 1;
}
/* 新增:子项数量样式 */
.subitem-count {
color: #222;
font-weight: 600;
margin-left: 4px;
}
/* 新增:子项占用空间样式 */
.subitem-size {
font-size: 12px;
color: #999;
font-weight: 400;
}
/* 视频列表文字样式(适配移动端) */
.video-title {
display: -webkit-box;
-webkit-line-clamp: 2; /* 显示2行 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
max-height: 2.8em; /* 2行 * line-height */
width: 100%;
padding-right: 4px;
cursor: pointer;
transition: color 0.2s;
}
.video-title:hover {
color: #2196f3;
}
.video-time {
font-size: 11px !important;
color: #999;
margin-top: 2px;
font-weight: 300 !important;
display: flex;
align-items: center; /* 垂直居中 */
justify-content: flex-end; /* 水平右对齐 */
}
.video-info {
flex: 1;
overflow: hidden;
}
.subitem-value {
font-size: 15px;
font-weight: 600;
color: #222222;
display: flex;
align-items: baseline;
gap: 3px;
}
.subitem-value .unit {
font-size: 12px;
color: #555555;
font-weight: 500;
}
.primary-card {
border-top: 3px solid #4caf50;
}
.secondary-card {
border-top: 3px solid #2196f3;
}
/* 空视频列表样式(适配移动端) */
.empty-video-list {
text-align: center;
padding: 20px 0;
color: #999;
font-size: 14px;
}
/* 底部导航样式(完全保留) */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: space-around;
align-items: center;
z-index: 100;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
color: #666666;
transition: all 0.2s ease;
cursor: pointer;
}
.nav-item.active {
color: #4caf50;
}
.nav-item span {
font-size: 12px;
margin-top: 4px;
font-weight: 500;
}
.nav-item svg {
transition: all 0.2s ease;
}
.nav-item.active svg {
transform: scale(1.1);
}
/* 日志 Tab 样式(完全保留) */
.log-tab {
padding: 10px 0;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 0 5px;
}
.log-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
}
.log-filter {
display: flex;
gap: 8px;
}
.filter-btn {
padding: 6px 12px;
border-radius: 20px;
border: 1px solid #e0e0e0;
background-color: #ffffff;
color: #666666;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-btn.active {
background-color: #4caf50;
color: #ffffff;
border-color: #4caf50;
}
.filter-btn:hover:not(.active) {
border-color: #cccccc;
color: #333333;
}
/* 日志列表样式(完全保留) */
.log-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.log-item {
padding: 15px;
border-radius: 8px;
border: 1px solid #f0f0f0;
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
cursor: pointer;
transition: all 0.2s ease;
}
.log-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.log-item.debug {
border-left: 4px solid #2196f3;
}
.log-item.error {
border-left: 4px solid #f44336;
}
.log-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.log-type {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.log-type.debug {
background-color: rgba(33, 150, 243, 0.1);
color: #2196f3;
}
.log-type.error {
background-color: rgba(244, 67, 54, 0.1);
color: #f44336;
}
.log-time {
font-size: 11px;
color: #999999;
}
.log-file {
font-size: 12px;
color: #666666;
}
.empty-log {
text-align: center;
padding: 40px 20px;
color: #999999;
font-size: 14px;
}
/* 日志详情弹窗样式(完全保留) */
.log-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
box-sizing: border-box;
}
.log-modal-content {
width: 100%;
max-width: 500px;
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
max-height: 80vh;
display: flex;
flex-direction: column;
}
.log-modal-header {
padding: 10px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.modal-type-tag {
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.modal-type-tag.debug {
background-color: rgba(33, 150, 243, 0.1);
color: #2196f3;
}
.modal-type-tag.error {
background-color: rgba(244, 67, 54, 0.1);
color: #f44336;
}
.modal-date {
font-size: 14px;
color: #666;
font-weight: 500;
}
.close-btn {
background: transparent;
border: none;
cursor: pointer;
color: #666;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
}
.close-btn:hover {
background-color: #f5f5f5;
color: #333;
}
.log-modal-body {
padding: 10px;
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.file-name {
font-size: 14px;
color: #333;
font-weight: 600;
margin-bottom: 5px;
border-bottom: 1px solid #f0f0f0;
}
.log-content {
flex: 1;
margin: 0;
padding: 5px;
background-color: #f9f9f9;
border-radius: 8px;
overflow-y: auto;
overflow-x: auto;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
color: #333;
}
.log-modal-footer {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
}
.copy-btn {
padding: 8px 16px;
background-color: #4caf50;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s ease;
}
.copy-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.copy-btn:hover:not(:disabled) {
background-color: #43a047;
}
/* 新增:视频播放弹窗样式(移动端适配) */
.video-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
padding: 0px;
box-sizing: border-box;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
.video-modal-content {
width: 100%;
max-width: 100%;
background-color: #111;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.video-modal-header {
padding: 5px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #222;
}
.video-title-text {
font-size: 14px;
color: #fff;
font-weight: 500;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 80%;
}
.video-close-btn {
background: transparent;
border: none;
cursor: pointer;
color: #fff;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.video-close-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.video-modal-body {
padding: 0;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
position: relative;
}
.video-player {
width: 100%;
height: 100%;
min-height: 200px;
max-height: 60vh;
object-fit: contain;
}
/* 视频加载状态 */
.video-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
padding: 40px 20px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
margin-bottom: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 视频错误状态 */
.video-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
padding: 40px 20px;
text-align: center;
}
.video-error p {
margin: 16px 0 24px;
font-size: 14px;
}
.retry-btn {
padding: 8px 16px;
background-color: #2196f3;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.retry-btn:hover {
background-color: #1976d2;
}
/* 夜间模式(完全保留原有样式) */
html.dark-mode .stats-dashboard-mobile {
background-color: #1a1a2e;
color: #eaeaea;
padding-bottom: 80px;
}
html.dark-mode .main-card {
border-color: rgba(255, 255, 255, 0.1);
background-color: rgba(30, 30, 50, 0.9);
}
html.dark-mode .stat-subitems {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
html.dark-mode .subitem {
background: rgba(40, 40, 65, 0.7);
border: 1px solid rgba(255, 255, 255, 0.05);
}
html.dark-mode .subitem:hover {
background: rgba(40, 40, 65, 0.9);
border-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .stat-meta {
color: #b0b0c3;
}
html.dark-mode .stat-value {
color: #ffffff;
}
html.dark-mode .value-label {
color: #d0d0d0;
}
html.dark-mode .size-count {
color: #d0d0d0;
}
html.dark-mode .unit {
color: #d0d0d0;
}
html.dark-mode .stat-icon {
background-color: rgba(76, 175, 80, 0.25);
}
html.dark-mode .secondary-card .stat-icon {
background-color: rgba(33, 150, 243, 0.25);
}
html.dark-mode .subitem-meta {
color: #c0c0d3;
}
html.dark-mode .subitem-count {
color: #ffffff;
}
html.dark-mode .subitem-size {
color: #a0a0b3;
}
html.dark-mode .subitem-value {
color: #ffffff;
}
html.dark-mode .subitem-value .unit {
color: #b0b0c3;
}
html.dark-mode .subitem-icon {
background-color: rgba(233, 30, 99, 0.2);
}
html.dark-mode .collect-icon {
background-color: rgba(255, 152, 0, 0.2);
}
html.dark-mode .follow-icon {
background-color: rgba(156, 39, 176, 0.2);
}
html.dark-mode .graphic-icon {
background-color: rgba(255, 159, 64, 0.2);
}
html.dark-mode .video-list-icon {
background-color: rgba(33, 150, 243, 0.25);
}
html.dark-mode .video-time {
color: #a0a0b3;
}
html.dark-mode .empty-video-list {
color: #808099;
}
/* 夜间模式 - 底部导航(完全保留) */
html.dark-mode .bottom-nav {
background-color: #16213e;
border-top-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .nav-item {
color: #b0b0c3;
}
html.dark-mode .nav-item.active {
color: #4caf50;
}
/* 夜间模式 - 日志样式(完全保留) */
html.dark-mode .log-title {
color: #ffffff;
}
html.dark-mode .filter-btn {
background-color: #1f4068;
border-color: rgba(255, 255, 255, 0.1);
color: #e0e0e0;
}
html.dark-mode .filter-btn.active {
background-color: #4caf50;
color: #ffffff;
border-color: #4caf50;
}
html.dark-mode .log-item {
background-color: rgba(30, 30, 50, 0.9);
border-color: rgba(255, 255, 255, 0.05);
}
html.dark-mode .log-file {
color: #c0c0d3;
}
html.dark-mode .empty-log {
color: #808099;
}
/* 夜间模式 - 弹窗样式(完全保留) */
html.dark-mode .log-modal-content {
background-color: #1e1e3f;
border-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .log-modal-header,
html.dark-mode .log-modal-footer {
border-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .modal-date {
color: #c0c0d3;
}
html.dark-mode .file-name {
color: #ffffff;
border-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .log-content {
background-color: #2a2a4a;
color: #eaeaea;
}
html.dark-mode .close-btn {
color: #c0c0d3;
}
html.dark-mode .close-btn:hover {
background-color: #2a2a4a;
color: #ffffff;
}
.ant-message {
z-index: 10000 !important;
}
</style>