index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. <template>
  2. <div class="video-analysis-container">
  3. <!-- 页面头部 -->
  4. <div class="header">
  5. <h1>📊 视频分析</h1>
  6. <p>为教学过程及课程内容改进提供数据支撑</p>
  7. </div>
  8. <!-- 筛选条件 -->
  9. <div class="filter-section">
  10. <h3>🔍 数据筛选</h3>
  11. <div class="filter-controls">
  12. <div class="filter-group">
  13. <label>选择课程</label>
  14. <a-select v-model:value="filters.courseId" placeholder="全部课程" style="width: 100%">
  15. <a-select-option value="">全部课程</a-select-option>
  16. <a-select-option value="course1">JavaScript基础教程</a-select-option>
  17. <a-select-option value="course2">Python数据分析</a-select-option>
  18. <a-select-option value="course3">React前端开发</a-select-option>
  19. <a-select-option value="course4">机器学习入门</a-select-option>
  20. </a-select>
  21. </div>
  22. <div class="filter-group">
  23. <label>时间范围</label>
  24. <a-select v-model:value="filters.timeRange" style="width: 100%">
  25. <a-select-option value="7">最近7天</a-select-option>
  26. <a-select-option value="30">最近30天</a-select-option>
  27. <a-select-option value="90">最近90天</a-select-option>
  28. <a-select-option value="365">最近一年</a-select-option>
  29. </a-select>
  30. </div>
  31. <div class="filter-group">
  32. <a-button type="primary" @click="updateStats">查询</a-button>
  33. </div>
  34. </div>
  35. </div>
  36. <!-- 核心统计数据 -->
  37. <div class="stats-grid">
  38. <div class="stat-card">
  39. <h3>👥 观看人数统计</h3>
  40. <div class="stat-number">{{ stats.totalViewers.toLocaleString() }}</div>
  41. <div class="stat-label">总观看人数</div>
  42. <div class="stat-number">{{ stats.completedViewers.toLocaleString() }}</div>
  43. <div class="stat-label">完成观看人数</div>
  44. <div class="completion-rate">{{ stats.completionRate }}%</div>
  45. <div class="stat-label">完成率</div>
  46. </div>
  47. <div class="stat-card">
  48. <h3>📥 讲义下载统计</h3>
  49. <div class="stat-number">{{ stats.totalDownloads.toLocaleString() }}</div>
  50. <div class="stat-label">总下载次数</div>
  51. <div class="stat-number">{{ stats.downloadRate }}%</div>
  52. <div class="stat-label">下载率</div>
  53. <div class="stat-number">{{ stats.avgDownloads }}</div>
  54. <div class="stat-label">人均下载次数</div>
  55. </div>
  56. <div class="stat-card">
  57. <h3>⏱️ 跳出时间分析</h3>
  58. <div class="stat-number">{{ stats.totalExits.toLocaleString() }}</div>
  59. <div class="stat-label">总跳出次数</div>
  60. <div class="stat-number">{{ stats.exitRate }}%</div>
  61. <div class="stat-label">跳出率</div>
  62. <div class="stat-number">{{ stats.avgExitTime }}</div>
  63. <div class="stat-label">平均跳出时间</div>
  64. </div>
  65. <div class="stat-card">
  66. <h3>📝 互动数据统计</h3>
  67. <div class="stat-number">{{ stats.totalNotes.toLocaleString() }}</div>
  68. <div class="stat-label">笔记总数</div>
  69. <div class="stat-number">{{ stats.totalDiscussions.toLocaleString() }}</div>
  70. <div class="stat-label">讨论总数</div>
  71. <div class="stat-number">{{ stats.totalReplies.toLocaleString() }}</div>
  72. <div class="stat-label">回帖总数</div>
  73. </div>
  74. </div>
  75. <!-- ECharts 图表区域 -->
  76. <div class="charts-section">
  77. <!-- 学习进度分布图 -->
  78. <div class="chart-container">
  79. <h3>📈 学习进度分布</h3>
  80. <div ref="progressChart" class="chart"></div>
  81. </div>
  82. <!-- 观看时长趋势图 -->
  83. <div class="chart-container">
  84. <h3>📊 观看时长趋势</h3>
  85. <div ref="timeChart" class="chart"></div>
  86. </div>
  87. <!-- 章节完成率对比图 -->
  88. <div class="chart-container">
  89. <h3>📚 章节完成率对比</h3>
  90. <div ref="chapterChart" class="chart"></div>
  91. </div>
  92. <!-- 互动数据统计图 -->
  93. <div class="chart-container">
  94. <h3>💬 互动数据统计</h3>
  95. <div ref="interactionChart" class="chart"></div>
  96. </div>
  97. </div>
  98. <!-- 学员详细数据表格 -->
  99. <div class="data-table">
  100. <h3>👤 学员学习行为详细数据</h3>
  101. <a-table
  102. :columns="studentColumns"
  103. :data-source="studentData"
  104. :pagination="{ pageSize: 10 }"
  105. :scroll="{ x: 1200 }"
  106. >
  107. <template #bodyCell="{ column, record }">
  108. <template v-if="column.key === 'progress'">
  109. <div class="progress-container">
  110. <a-progress :percent="record.progress" size="small" />
  111. <span :class="{ 'low-engagement': record.progress < 50 }"> {{ record.progress }}% </span>
  112. </div>
  113. </template>
  114. <template v-else-if="column.key === 'exitPoints'">
  115. <span v-for="point in record.exitPoints" :key="point" class="time-point">
  116. {{ point }}
  117. </span>
  118. </template>
  119. </template>
  120. </a-table>
  121. </div>
  122. <!-- 视频章节详细统计 -->
  123. <div class="data-table">
  124. <h3>📚 视频章节详细统计</h3>
  125. <a-table :columns="chapterColumns" :data-source="chapterData" :pagination="false">
  126. <template #bodyCell="{ column, record }">
  127. <template v-if="column.key === 'completionRate'">
  128. <span
  129. :class="{ 'completion-rate': record.completionRate >= 70, 'low-engagement': record.completionRate < 70 }"
  130. >
  131. {{ record.completionRate }}%
  132. </span>
  133. </template>
  134. <template v-else-if="column.key === 'exitRate'">
  135. <span :class="{ 'low-engagement': record.exitRate > 25 }"> {{ record.exitRate }}% </span>
  136. </template>
  137. </template>
  138. </a-table>
  139. </div>
  140. </div>
  141. </template>
  142. <script setup>
  143. import { ref, reactive, onMounted, nextTick } from 'vue'
  144. import { message } from 'ant-design-vue'
  145. import * as echarts from 'echarts'
  146. // 筛选条件
  147. const filters = reactive({
  148. courseId: '',
  149. timeRange: '30'
  150. })
  151. // 统计数据
  152. const stats = reactive({
  153. totalViewers: 1247,
  154. completedViewers: 892,
  155. completionRate: 71.6,
  156. totalDownloads: 456,
  157. downloadRate: 36.6,
  158. avgDownloads: 0.37,
  159. totalExits: 234,
  160. exitRate: 18.8,
  161. avgExitTime: '12:34',
  162. totalNotes: 1089,
  163. totalDiscussions: 567,
  164. totalReplies: 2341
  165. })
  166. // 图表引用
  167. const progressChart = ref(null)
  168. const timeChart = ref(null)
  169. const chapterChart = ref(null)
  170. const interactionChart = ref(null)
  171. // 学员数据表格列定义
  172. const studentColumns = [
  173. { title: '学员ID', dataIndex: 'id', key: 'id', width: 80 },
  174. { title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
  175. { title: '访问总时长', dataIndex: 'totalTime', key: 'totalTime', width: 120 },
  176. { title: '学习进度', dataIndex: 'progress', key: 'progress', width: 150 },
  177. { title: '观看次数', dataIndex: 'viewCount', key: 'viewCount', width: 100 },
  178. { title: '跳出时间点', dataIndex: 'exitPoints', key: 'exitPoints', width: 200 },
  179. { title: '快进快退次数', dataIndex: 'seekCount', key: 'seekCount', width: 120 },
  180. { title: '笔记数', dataIndex: 'noteCount', key: 'noteCount', width: 80 },
  181. { title: '讨论数', dataIndex: 'discussionCount', key: 'discussionCount', width: 80 },
  182. { title: '回帖数', dataIndex: 'replyCount', key: 'replyCount', width: 80 },
  183. { title: '最后访问', dataIndex: 'lastAccess', key: 'lastAccess', width: 150 }
  184. ]
  185. // 学员数据
  186. const studentData = ref([
  187. {
  188. key: '1',
  189. id: '001',
  190. name: '张三',
  191. totalTime: '2小时35分钟',
  192. progress: 85,
  193. viewCount: '3次',
  194. exitPoints: ['05:23', '18:45'],
  195. seekCount: '12次',
  196. noteCount: 5,
  197. discussionCount: 3,
  198. replyCount: 8,
  199. lastAccess: '2024-01-15 14:30'
  200. },
  201. {
  202. key: '2',
  203. id: '002',
  204. name: '李四',
  205. totalTime: '1小时48分钟',
  206. progress: 60,
  207. viewCount: '2次',
  208. exitPoints: ['08:12', '25:30', '42:15'],
  209. seekCount: '8次',
  210. noteCount: 2,
  211. discussionCount: 1,
  212. replyCount: 3,
  213. lastAccess: '2024-01-15 16:45'
  214. },
  215. {
  216. key: '3',
  217. id: '003',
  218. name: '王五',
  219. totalTime: '3小时12分钟',
  220. progress: 95,
  221. viewCount: '5次',
  222. exitPoints: ['03:45'],
  223. seekCount: '15次',
  224. noteCount: 8,
  225. discussionCount: 5,
  226. replyCount: 12,
  227. lastAccess: '2024-01-15 20:15'
  228. },
  229. {
  230. key: '4',
  231. id: '004',
  232. name: '赵六',
  233. totalTime: '45分钟',
  234. progress: 25,
  235. viewCount: '1次',
  236. exitPoints: ['12:30', '18:20', '28:45'],
  237. seekCount: '3次',
  238. noteCount: 0,
  239. discussionCount: 0,
  240. replyCount: 0,
  241. lastAccess: '2024-01-15 10:20'
  242. },
  243. {
  244. key: '5',
  245. id: '005',
  246. name: '钱七',
  247. totalTime: '2小时08分钟',
  248. progress: 78,
  249. viewCount: '4次',
  250. exitPoints: ['07:15', '22:40'],
  251. seekCount: '10次',
  252. noteCount: 4,
  253. discussionCount: 2,
  254. replyCount: 6,
  255. lastAccess: '2024-01-15 19:30'
  256. }
  257. ])
  258. // 章节数据表格列定义
  259. const chapterColumns = [
  260. { title: '章节', dataIndex: 'chapter', key: 'chapter', width: 200 },
  261. { title: '视频时长', dataIndex: 'duration', key: 'duration', width: 100 },
  262. { title: '观看人数', dataIndex: 'viewers', key: 'viewers', width: 100 },
  263. { title: '完成人数', dataIndex: 'completed', key: 'completed', width: 100 },
  264. { title: '完成率', dataIndex: 'completionRate', key: 'completionRate', width: 100 },
  265. { title: '平均观看时长', dataIndex: 'avgWatchTime', key: 'avgWatchTime', width: 120 },
  266. { title: '跳出率', dataIndex: 'exitRate', key: 'exitRate', width: 100 },
  267. { title: '下载次数', dataIndex: 'downloads', key: 'downloads', width: 100 },
  268. { title: '笔记数', dataIndex: 'notes', key: 'notes', width: 80 },
  269. { title: '讨论数', dataIndex: 'discussions', key: 'discussions', width: 80 }
  270. ]
  271. // 章节数据
  272. const chapterData = ref([
  273. {
  274. key: '1',
  275. chapter: '第1章:课程介绍',
  276. duration: '15:30',
  277. viewers: 1247,
  278. completed: 1156,
  279. completionRate: 92.7,
  280. avgWatchTime: '14:25',
  281. exitRate: 7.3,
  282. downloads: 89,
  283. notes: 45,
  284. discussions: 12
  285. },
  286. {
  287. key: '2',
  288. chapter: '第2章:基础知识',
  289. duration: '28:15',
  290. viewers: 1156,
  291. completed: 987,
  292. completionRate: 85.4,
  293. avgWatchTime: '24:30',
  294. exitRate: 14.6,
  295. downloads: 156,
  296. notes: 78,
  297. discussions: 23
  298. },
  299. {
  300. key: '3',
  301. chapter: '第3章:核心概念',
  302. duration: '35:20',
  303. viewers: 987,
  304. completed: 756,
  305. completionRate: 76.6,
  306. avgWatchTime: '28:45',
  307. exitRate: 23.4,
  308. downloads: 123,
  309. notes: 89,
  310. discussions: 34
  311. },
  312. {
  313. key: '4',
  314. chapter: '第4章:实战应用',
  315. duration: '42:10',
  316. viewers: 756,
  317. completed: 523,
  318. completionRate: 69.2,
  319. avgWatchTime: '32:15',
  320. exitRate: 30.8,
  321. downloads: 88,
  322. notes: 67,
  323. discussions: 28
  324. },
  325. {
  326. key: '5',
  327. chapter: '第5章:高级技巧',
  328. duration: '38:45',
  329. viewers: 523,
  330. completed: 345,
  331. completionRate: 66.0,
  332. avgWatchTime: '29:20',
  333. exitRate: 34.0,
  334. downloads: 67,
  335. notes: 45,
  336. discussions: 19
  337. }
  338. ])
  339. // 初始化图表
  340. const initCharts = () => {
  341. initProgressChart()
  342. initTimeChart()
  343. initChapterChart()
  344. initInteractionChart()
  345. }
  346. // 学习进度分布图
  347. const initProgressChart = () => {
  348. const chart = echarts.init(progressChart.value)
  349. const option = {
  350. title: {
  351. text: '学习进度分布',
  352. left: 'center'
  353. },
  354. tooltip: {
  355. trigger: 'item',
  356. formatter: '{a} <br/>{b}: {c} ({d}%)'
  357. },
  358. legend: {
  359. orient: 'vertical',
  360. left: 'left'
  361. },
  362. series: [
  363. {
  364. name: '学习进度',
  365. type: 'pie',
  366. radius: '50%',
  367. data: [
  368. { value: 234, name: '0-25%' },
  369. { value: 189, name: '26-50%' },
  370. { value: 345, name: '51-75%' },
  371. { value: 479, name: '76-100%' }
  372. ],
  373. emphasis: {
  374. itemStyle: {
  375. shadowBlur: 10,
  376. shadowOffsetX: 0,
  377. shadowColor: 'rgba(0, 0, 0, 0.5)'
  378. }
  379. }
  380. }
  381. ]
  382. }
  383. chart.setOption(option)
  384. }
  385. // 观看时长趋势图
  386. const initTimeChart = () => {
  387. const chart = echarts.init(timeChart.value)
  388. const option = {
  389. title: {
  390. // text: '观看时长趋势',
  391. left: 'center'
  392. },
  393. tooltip: {
  394. trigger: 'axis'
  395. },
  396. legend: {
  397. data: ['观看时长', '完成人数']
  398. },
  399. xAxis: {
  400. type: 'category',
  401. data: ['第1章', '第2章', '第3章', '第4章', '第5章']
  402. },
  403. yAxis: [
  404. {
  405. type: 'value',
  406. name: '时长(分钟)',
  407. position: 'left'
  408. },
  409. {
  410. type: 'value',
  411. name: '人数',
  412. position: 'right'
  413. }
  414. ],
  415. series: [
  416. {
  417. name: '观看时长',
  418. type: 'bar',
  419. data: [14.4, 24.5, 28.8, 32.3, 29.3],
  420. itemStyle: {
  421. color: '#3498db'
  422. }
  423. },
  424. {
  425. name: '完成人数',
  426. type: 'line',
  427. yAxisIndex: 1,
  428. data: [1156, 987, 756, 523, 345],
  429. itemStyle: {
  430. color: '#e74c3c'
  431. }
  432. }
  433. ]
  434. }
  435. chart.setOption(option)
  436. }
  437. // 章节完成率对比图
  438. const initChapterChart = () => {
  439. const chart = echarts.init(chapterChart.value)
  440. const option = {
  441. title: {
  442. text: '章节完成率对比',
  443. left: 'center'
  444. },
  445. tooltip: {
  446. trigger: 'axis',
  447. axisPointer: {
  448. type: 'shadow'
  449. }
  450. },
  451. grid: {
  452. left: '3%',
  453. right: '4%',
  454. bottom: '3%',
  455. containLabel: true
  456. },
  457. xAxis: {
  458. type: 'value',
  459. max: 100
  460. },
  461. yAxis: {
  462. type: 'category',
  463. data: ['第5章', '第4章', '第3章', '第2章', '第1章']
  464. },
  465. series: [
  466. {
  467. name: '完成率',
  468. type: 'bar',
  469. data: [66.0, 69.2, 76.6, 85.4, 92.7],
  470. itemStyle: {
  471. color: function (params) {
  472. const value = params.value
  473. if (value >= 80) return '#27ae60'
  474. if (value >= 60) return '#f39c12'
  475. return '#e74c3c'
  476. }
  477. }
  478. }
  479. ]
  480. }
  481. chart.setOption(option)
  482. }
  483. // 互动数据统计图
  484. const initInteractionChart = () => {
  485. const chart = echarts.init(interactionChart.value)
  486. const option = {
  487. title: {
  488. // text: '互动数据统计',
  489. left: 'center'
  490. },
  491. tooltip: {
  492. trigger: 'axis'
  493. },
  494. legend: {
  495. data: ['笔记数', '讨论数', '回帖数']
  496. },
  497. xAxis: {
  498. type: 'category',
  499. data: ['第1章', '第2章', '第3章', '第4章', '第5章']
  500. },
  501. yAxis: {
  502. type: 'value'
  503. },
  504. series: [
  505. {
  506. name: '笔记数',
  507. type: 'bar',
  508. data: [45, 78, 89, 67, 45],
  509. itemStyle: {
  510. color: '#3498db'
  511. }
  512. },
  513. {
  514. name: '讨论数',
  515. type: 'bar',
  516. data: [12, 23, 34, 28, 19],
  517. itemStyle: {
  518. color: '#2ecc71'
  519. }
  520. },
  521. {
  522. name: '回帖数',
  523. type: 'bar',
  524. data: [25, 45, 67, 56, 38],
  525. itemStyle: {
  526. color: '#f39c12'
  527. }
  528. }
  529. ]
  530. }
  531. chart.setOption(option)
  532. }
  533. // 更新统计数据
  534. const updateStats = () => {
  535. // 模拟数据更新
  536. stats.totalViewers = Math.floor(Math.random() * 500) + 1000
  537. stats.completedViewers = Math.floor(stats.totalViewers * 0.7)
  538. stats.completionRate = Number(((stats.completedViewers / stats.totalViewers) * 100).toFixed(1))
  539. // 重新初始化图表
  540. nextTick(() => {
  541. initCharts()
  542. })
  543. // 显示更新提示
  544. message.success('数据已更新!')
  545. }
  546. // 组件挂载后初始化图表
  547. onMounted(() => {
  548. nextTick(() => {
  549. initCharts()
  550. })
  551. // 监听窗口大小变化,重新调整图表
  552. window.addEventListener('resize', () => {
  553. const charts = [progressChart, timeChart, chapterChart, interactionChart]
  554. charts.forEach((chartRef) => {
  555. if (chartRef.value) {
  556. const chart = echarts.getInstanceByDom(chartRef.value)
  557. if (chart) {
  558. chart.resize()
  559. }
  560. }
  561. })
  562. })
  563. })
  564. </script>
  565. <style scoped>
  566. .video-analysis-container {
  567. padding: 20px;
  568. /* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); */
  569. }
  570. .header {
  571. background: rgba(255, 255, 255, 0.95);
  572. backdrop-filter: blur(10px);
  573. border-radius: 15px;
  574. padding: 30px;
  575. margin-bottom: 30px;
  576. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  577. text-align: center;
  578. }
  579. .header h1 {
  580. color: #2c3e50;
  581. font-size: 2.5em;
  582. margin-bottom: 10px;
  583. }
  584. .header p {
  585. color: #7f8c8d;
  586. font-size: 1.1em;
  587. }
  588. .filter-section {
  589. background: rgba(255, 255, 255, 0.95);
  590. backdrop-filter: blur(10px);
  591. border-radius: 15px;
  592. padding: 25px;
  593. margin-bottom: 30px;
  594. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  595. }
  596. .filter-section h3 {
  597. color: #2c3e50;
  598. margin-bottom: 20px;
  599. font-size: 1.5em;
  600. }
  601. .filter-controls {
  602. display: grid;
  603. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  604. gap: 15px;
  605. align-items: end;
  606. }
  607. .filter-group {
  608. display: flex;
  609. flex-direction: column;
  610. }
  611. .filter-group label {
  612. margin-bottom: 8px;
  613. color: #2c3e50;
  614. font-weight: 600;
  615. }
  616. .stats-grid {
  617. display: grid;
  618. grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  619. gap: 25px;
  620. margin-bottom: 30px;
  621. }
  622. .stat-card {
  623. background: rgba(255, 255, 255, 0.95);
  624. backdrop-filter: blur(10px);
  625. border-radius: 15px;
  626. padding: 25px;
  627. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  628. transition: transform 0.3s ease, box-shadow 0.3s ease;
  629. }
  630. .stat-card:hover {
  631. transform: translateY(-5px);
  632. box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
  633. }
  634. .stat-card h3 {
  635. color: #2c3e50;
  636. margin-bottom: 15px;
  637. font-size: 1.3em;
  638. border-bottom: 2px solid #3498db;
  639. padding-bottom: 10px;
  640. }
  641. .stat-number {
  642. font-size: 2.5em;
  643. font-weight: bold;
  644. color: #3498db;
  645. margin-bottom: 10px;
  646. }
  647. .stat-label {
  648. color: #7f8c8d;
  649. font-size: 0.9em;
  650. margin-bottom: 15px;
  651. }
  652. .charts-section {
  653. display: grid;
  654. grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
  655. gap: 25px;
  656. margin-bottom: 30px;
  657. }
  658. .chart-container {
  659. background: rgba(255, 255, 255, 0.95);
  660. backdrop-filter: blur(10px);
  661. border-radius: 15px;
  662. padding: 25px;
  663. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  664. }
  665. .chart-container h3 {
  666. color: #2c3e50;
  667. margin-bottom: 20px;
  668. font-size: 1.5em;
  669. }
  670. .chart {
  671. height: 400px;
  672. width: 100%;
  673. }
  674. .data-table {
  675. background: rgba(255, 255, 255, 0.95);
  676. backdrop-filter: blur(10px);
  677. border-radius: 15px;
  678. padding: 25px;
  679. margin-bottom: 30px;
  680. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  681. }
  682. .data-table h3 {
  683. color: #2c3e50;
  684. margin-bottom: 20px;
  685. font-size: 1.5em;
  686. }
  687. .progress-container {
  688. display: flex;
  689. align-items: center;
  690. gap: 10px;
  691. }
  692. .time-point {
  693. display: inline-block;
  694. background: #e74c3c;
  695. color: white;
  696. padding: 2px 8px;
  697. border-radius: 12px;
  698. font-size: 0.8em;
  699. margin: 2px;
  700. }
  701. .completion-rate {
  702. color: #27ae60;
  703. font-weight: bold;
  704. }
  705. .low-engagement {
  706. color: #e74c3c;
  707. font-weight: bold;
  708. }
  709. @media (max-width: 768px) {
  710. .stats-grid {
  711. grid-template-columns: 1fr;
  712. }
  713. .charts-section {
  714. grid-template-columns: 1fr;
  715. }
  716. .filter-controls {
  717. grid-template-columns: 1fr;
  718. }
  719. .header h1 {
  720. font-size: 2em;
  721. }
  722. }
  723. </style>