移动端适配

This commit is contained in:
2025-12-09 14:59:41 +08:00
parent 490e8e70f1
commit 242a263daa
9 changed files with 1063 additions and 58 deletions

View File

@@ -15,7 +15,7 @@
<!-- 课程信息看板 -->
<div class="course-info-panel">
<div class="panel-container">
<!-- 左侧课程封面 -->
<!-- 课程封面 -->
<div class="course-cover">
<img
:src="courseItemVO.coverImage ? FILE_DOWNLOAD_URL + courseItemVO.coverImage : defaultCover"
@@ -24,7 +24,7 @@
/>
</div>
<!-- 右侧课程信息 -->
<!-- 课程信息 -->
<div class="course-info">
<div class="info-content">
<!-- 课程标题 -->
@@ -65,14 +65,6 @@
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button
type="primary"
size="large"
@click="handleStartLearning"
:loading="enrolling"
>
{{ isEnrolled ? '继续学习' : '开始学习' }}
</el-button>
<el-button
size="large"
:plain="!isCollected"
@@ -81,6 +73,15 @@
<el-icon><Star /></el-icon>
收藏课程
</el-button>
<el-button
type="primary"
size="large"
@click="handleStartLearning"
:loading="enrolling"
>
{{ isEnrolled ? '继续学习' : '开始学习' }}
</el-button>
</div>
</div>
</div>
@@ -500,6 +501,13 @@ function formatDuration(minutes?: number): string {
background: #FFFFFF;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
// 移动端垂直布局
@media (max-width: 768px) {
flex-direction: column;
gap: 20px;
padding: 20px 16px;
}
}
}
@@ -515,6 +523,12 @@ function formatDuration(minutes?: number): string {
height: 100%;
object-fit: cover;
}
// 移动端全宽
@media (max-width: 768px) {
width: 100%;
height: 200px;
}
}
.course-info {
@@ -529,6 +543,11 @@ function formatDuration(minutes?: number): string {
gap: 10px;
}
// 移动端样式调整
@media (max-width: 768px) {
gap: 16px;
}
.course-title {
font-family: "Source Han Sans SC";
font-weight: 600;
@@ -634,6 +653,12 @@ function formatDuration(minutes?: number): string {
align-items: center;
gap: 27px;
// 移动端按钮布局 - 保持水平排列
@media (max-width: 768px) {
gap: 12px;
justify-content: space-between;
}
:deep(.el-button) {
height: 42px;
border-radius: 8px;
@@ -651,6 +676,12 @@ function formatDuration(minutes?: number): string {
background: #d32f2f;
border-color: #d32f2f;
}
// 移动端等宽
@media (max-width: 768px) {
flex: 1;
width: auto;
}
}
&.el-button--default {
@@ -658,6 +689,12 @@ function formatDuration(minutes?: number): string {
border-color: #86909C;
color: #86909C;
// 移动端等宽
@media (max-width: 768px) {
flex: 1;
width: auto;
}
img {
width: 20px;
height: 20px;

View File

@@ -22,6 +22,9 @@
<el-button circle :icon="Expand" />
</div>
<!-- 移动端遮罩层侧边栏展开时显示 -->
<div v-if="isMobile && !sidebarCollapsed" class="mobile-overlay" @click="toggleSidebar"></div>
<!-- 左侧章节目录 -->
<div class="chapter-sidebar" :class="{ collapsed: sidebarCollapsed }">
<div class="sidebar-header">
@@ -179,7 +182,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { ref, computed, watch, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import {
@@ -230,6 +233,7 @@ const currentChapterIndex = ref(0);
const currentNodeIndex = ref(0);
const sidebarCollapsed = ref(false);
const activeChapters = ref<number[]>([0]);
const isMobile = ref(false);
const articleData = ref<any>(null);
const contentAreaRef = ref<HTMLElement | null>(null);
@@ -348,8 +352,31 @@ watch(currentNode, async () => {
onMounted(() => {
// 不在这里启动定时器,等待学习记录加载完成后再启动
// startLearningTimer(); 移到loadLearningRecord和createLearningRecord成功后
// 检查移动端并设置侧边栏默认状态
checkMobile();
window.addEventListener('resize', checkMobile);
});
onUnmounted(() => {
window.removeEventListener('resize', checkMobile);
});
// 检查是否为移动端
function checkMobile() {
const wasMobile = isMobile.value;
isMobile.value = window.innerWidth < 768;
// 如果是初始化或从桌面端切换到移动端,默认收起侧边栏
if (isMobile.value && (!wasMobile || wasMobile === undefined)) {
sidebarCollapsed.value = true;
}
// 如果从移动端切换到桌面端,默认展开侧边栏
else if (!isMobile.value && wasMobile) {
sidebarCollapsed.value = false;
}
}
onBeforeUnmount(() => {
stopLearningTimer();
saveLearningProgress();
@@ -1074,6 +1101,21 @@ function handleBack() {
}
}
.mobile-overlay {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
@media (max-width: 768px) {
display: block;
}
}
.chapter-sidebar {
width: 320px;
background: #fff;
@@ -1086,6 +1128,22 @@ function handleBack() {
overflow: hidden;
}
// 移动端适配
@media (max-width: 768px) {
position: absolute;
left: 0;
top: 0;
bottom: 0;
z-index: 1000;
width: 280px; // 移动端稍窄一些
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
&.collapsed {
left: -280px; // 滑出屏幕外
width: 280px; // 保持宽度用于动画
}
}
.sidebar-header {
display: flex;
justify-content: space-between;

View File

@@ -898,26 +898,163 @@ function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
padding: 40px;
}
// 响应式设计
// 移动端响应式设计
@media (max-width: 768px) {
.task-detail {
.back-header {
padding: 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
}
.task-content {
padding: 16px;
}
.task-info {
.task-title {
font-size: 22px;
}
.task-info-card {
.task-info-section {
.task-title {
font-size: 20px;
line-height: 1.4;
margin-bottom: 12px;
}
.task-meta {
.meta-row {
flex-direction: column;
.task-description {
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
}
.task-meta {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px 16px; // 垂直间距8px水平间距16px
align-items: center;
.meta-item {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap; // 防止单个meta-item内部换行
span {
font-size: 13px;
color: #6B7280;
}
.creator-avatar,
.meta-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
}
.meta-divider {
display: none; // 移动端隐藏分隔线
}
}
// 关键修改统计卡片改为2x2网格布局
.task-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 20px;
.stat-card {
width: 100%;
height: 88px;
padding: 16px 12px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 12px;
background: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
.stat-content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 4px;
flex: 1;
.stat-label {
font-size: 13px;
color: #6B7280;
font-weight: 500;
line-height: 1.3;
}
.stat-value {
font-size: 22px;
font-weight: 700;
color: #1F2937;
line-height: 1.1;
}
}
.meta-icon {
width: 28px;
height: 28px;
flex-shrink: 0;
object-fit: contain;
opacity: 0.8;
}
}
}
.progress-section {
padding: 16px 20px;
height: auto;
margin-top: 16px;
.progress-header {
.progress-label,
.progress-value {
font-size: 14px;
}
}
.progress-bar-container .progress-bar {
height: 6px;
}
}
}
}
.task-stats {
grid-template-columns: repeat(2, 1fr);
.task-content-section {
margin-top: 16px;
.course-card,
.resource-card {
margin-top: 16px;
:deep(.el-card__header) {
padding: 16px;
.card-header {
.header-title {
font-size: 16px;
}
.item-count {
font-size: 12px;
}
}
}
:deep(.el-card__body) {
padding: 16px;
}
}
}
@@ -925,6 +1062,37 @@ function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
.resource-item {
flex-direction: column;
align-items: flex-start;
padding: 16px;
gap: 12px;
.course-index,
.resource-index {
width: 32px;
height: 32px;
font-size: 14px;
}
.course-info,
.resource-info {
width: 100%;
.course-name-row,
.resource-name-row {
.course-name,
.resource-name {
font-size: 16px;
}
}
.course-meta,
.resource-meta {
margin-top: 8px;
.progress-text {
font-size: 12px;
}
}
}
.course-action,
.resource-action {
@@ -932,6 +1100,8 @@ function getItemStatusType(status?: number): 'info' | 'warning' | 'success' {
.el-button {
width: 100%;
height: 36px;
font-size: 14px;
}
}
}