移动端适配
This commit is contained in:
@@ -8,7 +8,7 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
import type { SysMenu } from '@/types';
|
||||
import { MenuType } from '@/types/enums';
|
||||
import { routes } from '@/router';
|
||||
import { getResponsiveLayout } from './routeAdapter';
|
||||
import { getResponsiveLayout, createResponsiveRoute, type RouteAdapter } from './routeAdapter';
|
||||
|
||||
// 预注册所有视图组件,构建时由 Vite 解析并生成按需加载的 chunk
|
||||
const VIEW_MODULES = import.meta.glob('../views/**/*.vue');
|
||||
@@ -280,7 +280,7 @@ function findFirstMenuWithUrl(menus: SysMenu[]): SysMenu | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据组件名称获取组件
|
||||
* 根据组件名称获取组件(支持响应式组件)
|
||||
* @param componentName 组件名称/路径
|
||||
* @returns 组件异步加载函数
|
||||
*/
|
||||
@@ -302,17 +302,23 @@ function getComponent(componentName: string) {
|
||||
}
|
||||
|
||||
// 将别名 @/ 转为相对于当前文件的路径,必须与 import.meta.glob 中的模式一致
|
||||
componentPath = componentPath.replace(/^@\//, '../'); // => '../views/user/home/HomeView'
|
||||
const originalPath = componentPath.replace(/^@\//, '../'); // => '../views/user/home/HomeView'
|
||||
|
||||
// 补全 .vue 后缀
|
||||
if (!componentPath.endsWith('.vue')) {
|
||||
componentPath += '.vue';
|
||||
if (!originalPath.endsWith('.vue')) {
|
||||
componentPath = originalPath + '.vue';
|
||||
} else {
|
||||
componentPath = originalPath;
|
||||
}
|
||||
|
||||
// 3. 从 VIEW_MODULES 中查找对应的 loader
|
||||
const loader = VIEW_MODULES[componentPath];
|
||||
// 3. 检查是否有移动端版本
|
||||
const mobileComponentPath = componentPath.replace('.vue', '.mobile.vue');
|
||||
|
||||
if (!loader) {
|
||||
// 从 VIEW_MODULES 中查找对应的 loader
|
||||
const originalLoader = VIEW_MODULES[componentPath];
|
||||
const mobileLoader = VIEW_MODULES[mobileComponentPath];
|
||||
|
||||
if (!originalLoader) {
|
||||
console.error('[路由生成] 未找到组件模块', {
|
||||
原始组件名: componentName,
|
||||
期望路径: componentPath,
|
||||
@@ -322,7 +328,17 @@ function getComponent(componentName: string) {
|
||||
return () => import('@/views/public/error/404.vue');
|
||||
}
|
||||
|
||||
return loader as () => Promise<any>;
|
||||
// 4. 如果有移动端版本,创建响应式路由适配器
|
||||
if (mobileLoader) {
|
||||
const adapter: RouteAdapter = {
|
||||
original: originalLoader as () => Promise<any>,
|
||||
mobile: mobileLoader as () => Promise<any>
|
||||
};
|
||||
return createResponsiveRoute(adapter);
|
||||
}
|
||||
|
||||
// 5. 没有移动端版本,直接返回原始组件
|
||||
return originalLoader as () => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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-info-card {
|
||||
.task-info-section {
|
||||
.task-title {
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
.meta-row {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
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-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div class="resource-center-view">
|
||||
<CenterHead
|
||||
title="资源中心"
|
||||
:category-name="currentCategoryName"
|
||||
:show-article-mode="showArticle"
|
||||
/>
|
||||
<!-- <div class="search-wrapper">
|
||||
<Search @search="handleSearch" />
|
||||
</div> -->
|
||||
<div class="content-wrapper">
|
||||
<div class="content-container">
|
||||
<ResourceSideBar
|
||||
:activeTagID="currentCategoryId"
|
||||
@category-change="handleCategoryChange"
|
||||
/>
|
||||
<ResourceList
|
||||
v-show="!showArticle"
|
||||
ref="resourceListRef"
|
||||
:tagID="currentCategoryId"
|
||||
:search-keyword="searchKeyword"
|
||||
@resource-click="handleResourceClick"
|
||||
@list-updated="handleListUpdated"
|
||||
/>
|
||||
<ResourceArticle
|
||||
v-if="showArticle"
|
||||
:resource-id="currentResourceId"
|
||||
:tagID="currentCategoryId"
|
||||
:resource-list="resourceList"
|
||||
@resource-change="handleResourceChange"
|
||||
@navigate="handleArticleNavigate"
|
||||
@back-to-list="backToList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ResourceSideBar, ResourceList, ResourceArticle } from './components';
|
||||
import { Search, CenterHead } from '@/components/base';
|
||||
import type { Resource, Tag } from '@/types/resource';
|
||||
import { resourceApi, resourceTagApi } from '@/apis/resource';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const showArticle = ref(false);
|
||||
const currentCategoryId = ref('tag_article_001');
|
||||
const currentCategoryName = ref('党史学习');
|
||||
const currentResourceId = ref('');
|
||||
const searchKeyword = ref('');
|
||||
const resourceListRef = ref();
|
||||
const resourceList = ref<Resource[]>([]);
|
||||
|
||||
// 组件加载时检查 URL 参数
|
||||
onMounted(async () => {
|
||||
const tagID = route.query.tagID as string;
|
||||
if (tagID) {
|
||||
await loadTagInfo(tagID);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听路由参数变化
|
||||
watch(() => route.query.tagID, async (newTagID) => {
|
||||
if (newTagID && typeof newTagID === 'string') {
|
||||
await loadTagInfo(newTagID);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载标签信息
|
||||
async function loadTagInfo(tagID: string) {
|
||||
try {
|
||||
const res = await resourceTagApi.getTagsByType(1); // 1 = 文章分类标签
|
||||
if (res.success && res.dataList) {
|
||||
const tag = res.dataList.find((t: Tag) => t.tagID === tagID);
|
||||
if (tag) {
|
||||
currentCategoryId.value = tag.tagID || '';
|
||||
currentCategoryName.value = tag.name || '';
|
||||
searchKeyword.value = '';
|
||||
showArticle.value = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载标签信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCategoryChange(category: Tag) {
|
||||
currentCategoryId.value = category.tagID || category.id || '';
|
||||
currentCategoryName.value = category.name || '';
|
||||
searchKeyword.value = '';
|
||||
showArticle.value = false;
|
||||
|
||||
// 清除 URL 中的 tagID 参数,确保 URL 与实际显示的分类一致
|
||||
if (route.query.tagID) {
|
||||
router.replace({ path: route.path, query: {} });
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch(keyword: string) {
|
||||
searchKeyword.value = keyword;
|
||||
showArticle.value = false;
|
||||
|
||||
// 清除 URL 中的 tagID 参数
|
||||
if (route.query.tagID) {
|
||||
router.replace({ path: route.path, query: {} });
|
||||
}
|
||||
}
|
||||
|
||||
function handleListUpdated(list: Resource[]) {
|
||||
resourceList.value = list;
|
||||
}
|
||||
|
||||
function handleResourceClick(resource: Resource) {
|
||||
// 增加浏览次数
|
||||
resourceApi.incrementViewCount(resource.resourceID || '');
|
||||
currentResourceId.value = resource.resourceID || '';
|
||||
showArticle.value = true;
|
||||
}
|
||||
|
||||
function handleResourceChange(resourceId: string) {
|
||||
currentResourceId.value = resourceId;
|
||||
// ArticleShowView 会自动重新加载
|
||||
}
|
||||
|
||||
function backToList() {
|
||||
showArticle.value = false;
|
||||
currentResourceId.value = '';
|
||||
}
|
||||
|
||||
// 文章内前后切换时,靠近列表头部或尾部触发列表翻页
|
||||
async function handleArticleNavigate(direction: 'prev' | 'next', resourceId: string) {
|
||||
const list = resourceListRef.value?.getResources?.() || [];
|
||||
const index = list.findIndex((r: any) => r.resourceID === resourceId);
|
||||
if (index === -1) return;
|
||||
const nearHead = index <= 2;
|
||||
const nearTail = index >= list.length - 3;
|
||||
if (nearHead && direction === 'prev') {
|
||||
await resourceListRef.value?.loadPrevPage?.();
|
||||
} else if (nearTail && direction === 'next') {
|
||||
await resourceListRef.value?.loadNextPage?.();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.resource-center-view {
|
||||
background: #F9F9F9;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
|
||||
:deep(.resource-search) {
|
||||
// width: 1200px;
|
||||
width: 90%;
|
||||
height: 60px;
|
||||
padding: 0;
|
||||
|
||||
.search-box {
|
||||
height: 60px;
|
||||
|
||||
input {
|
||||
font-size: 20px;
|
||||
padding: 0 100px 0 40px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
width: 72px;
|
||||
height: 60px;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
:deep(.resource-sidebar) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.resource-list) {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(.resource-article) {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
>
|
||||
<div class="resource-cover">
|
||||
<img
|
||||
:src="resource.coverImage ? (FILE_DOWNLOAD_URL + resource.coverImage) : defaultArticleImg"
|
||||
:src="resource.coverImage ? (FILE_DOWNLOAD_URL + resource.coverImage) : staticAssets.defaultArticleImg"
|
||||
alt="cover"
|
||||
/>
|
||||
</div>
|
||||
@@ -27,13 +27,19 @@
|
||||
<div v-if="resources.length === 0 && !loading" class="empty">暂无数据</div>
|
||||
</div>
|
||||
|
||||
<div v-if="total > 0" class="pagination-container">
|
||||
<!-- 移动端显示加载更多提示,桌面端显示分页 -->
|
||||
<div v-if="isMobileDevice">
|
||||
<div v-if="isLoadingMore" class="loading-more">正在加载更多...</div>
|
||||
<div v-else-if="!hasMoreData && resources.length > 0 && hasTriggeredLoadMore" class="no-more-data">已加载全部数据</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isMobileDevice && total > 0" class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-sizes="[10, 15, 20, 50]"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
@@ -42,12 +48,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, nextTick } from 'vue';
|
||||
import { ref, reactive, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { resourceApi } from '@/apis/resource';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import type { PageParam } from '@/types';
|
||||
import defaultArticleImg from '@/assets/imgs/article-default.png';
|
||||
import defaultArticleImgUrl from '@/assets/imgs/article-default.png';
|
||||
import { useDevice } from '@/utils/deviceUtils';
|
||||
|
||||
// 创建响应式数据对象,包含静态资源
|
||||
const staticAssets = reactive({
|
||||
defaultArticleImg: defaultArticleImgUrl
|
||||
});
|
||||
|
||||
interface Props {
|
||||
tagID?: string;
|
||||
@@ -67,20 +79,61 @@ const total = ref(0);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const listContainerRef = ref<HTMLElement>();
|
||||
const hasMoreData = ref(true);
|
||||
const isLoadingMore = ref(false);
|
||||
const hasTriggeredLoadMore = ref(false); // 标记是否触发过加载更多
|
||||
|
||||
// 设备检测
|
||||
const { isMobileDevice } = useDevice();
|
||||
|
||||
onMounted(() => {
|
||||
loadResources();
|
||||
// 在移动端添加滚动监听
|
||||
nextTick(() => {
|
||||
if (isMobileDevice.value && listContainerRef.value) {
|
||||
setupInfiniteScroll();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理滚动监听
|
||||
if (isMobileDevice.value && listContainerRef.value) {
|
||||
listContainerRef.value.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => [props.tagID, props.searchKeyword], () => {
|
||||
currentPage.value = 1;
|
||||
resources.value = []; // 清空现有数据
|
||||
hasMoreData.value = true;
|
||||
hasTriggeredLoadMore.value = false; // 重置加载更多标记
|
||||
loadResources();
|
||||
}, { deep: true });
|
||||
|
||||
async function loadResources() {
|
||||
if (loading.value) return;
|
||||
// 监听设备变化
|
||||
watch(isMobileDevice, (isMobile) => {
|
||||
if (isMobile) {
|
||||
nextTick(() => {
|
||||
if (listContainerRef.value) {
|
||||
setupInfiniteScroll();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 切换到桌面端时移除滚动监听
|
||||
if (listContainerRef.value) {
|
||||
listContainerRef.value.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function loadResources(isAppend = false) {
|
||||
if (loading.value || (isAppend && !hasMoreData.value)) return;
|
||||
|
||||
loading.value = true;
|
||||
if (isAppend) {
|
||||
isLoadingMore.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const filter: Resource = {
|
||||
@@ -97,22 +150,35 @@ async function loadResources() {
|
||||
const res = await resourceApi.getResourcePage(pageParam, filter);
|
||||
|
||||
if (res.success && res.dataList) {
|
||||
resources.value = res.dataList;
|
||||
total.value = res.pageDomain?.pageParam.totalElements || 0;
|
||||
|
||||
// 通知父组件列表已更新
|
||||
emit('list-updated', res.dataList);
|
||||
const newData = res.dataList;
|
||||
|
||||
if (isAppend) {
|
||||
// 追加模式:将新数据添加到现有列表
|
||||
resources.value = [...resources.value, ...newData];
|
||||
} else {
|
||||
// 替换模式:重新加载数据
|
||||
resources.value = newData;
|
||||
// 重置滚动位置到顶部
|
||||
await nextTick();
|
||||
if (listContainerRef.value) {
|
||||
listContainerRef.value.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
total.value = res.pageDomain?.pageParam.totalElements || 0;
|
||||
|
||||
// 检查是否还有更多数据
|
||||
const totalPages = Math.ceil(total.value / pageSize.value);
|
||||
hasMoreData.value = currentPage.value < totalPages;
|
||||
|
||||
// 通知父组件列表已更新
|
||||
emit('list-updated', resources.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载资源列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
isLoadingMore.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +220,40 @@ function handleResourceClick(resource: Resource) {
|
||||
emit('resource-click', resource);
|
||||
}
|
||||
|
||||
// 无限滚动相关函数
|
||||
function setupInfiniteScroll() {
|
||||
if (!listContainerRef.value) return;
|
||||
|
||||
listContainerRef.value.addEventListener('scroll', handleScroll, { passive: true });
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!listContainerRef.value || !isMobileDevice.value || isLoadingMore.value || !hasMoreData.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = listContainerRef.value;
|
||||
const scrollTop = container.scrollTop;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const clientHeight = container.clientHeight;
|
||||
|
||||
// 计算距离底部的像素距离
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
// 当距离底部小于等于2像素时,加载更多数据
|
||||
if (distanceFromBottom <= 2) {
|
||||
loadMoreData();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreData() {
|
||||
if (!hasMoreData.value || isLoadingMore.value) return;
|
||||
|
||||
hasTriggeredLoadMore.value = true; // 标记用户已触发加载更多
|
||||
currentPage.value++;
|
||||
await loadResources(true); // true 表示追加模式
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
loadResources,
|
||||
getResources,
|
||||
@@ -181,6 +281,7 @@ defineExpose({
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
min-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.resource-item {
|
||||
@@ -275,7 +376,8 @@ defineExpose({
|
||||
}
|
||||
|
||||
.loading-more,
|
||||
.empty {
|
||||
.empty,
|
||||
.no-more-data {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-family: "Source Han Sans SC";
|
||||
@@ -283,5 +385,70 @@ defineExpose({
|
||||
color: #979797;
|
||||
}
|
||||
|
||||
.no-more-data {
|
||||
color: #999999;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid #EEEEEE;
|
||||
|
||||
:deep(.el-pagination) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.resource-list {
|
||||
border-radius: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
max-height: calc(100vh - 200px); // 确保有足够高度可以滚动
|
||||
}
|
||||
|
||||
.resource-item {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.resource-cover {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.resource-title {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.resource-summary {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
padding: 12px 16px;
|
||||
|
||||
:deep(.el-pagination) {
|
||||
.el-pagination__sizes,
|
||||
.el-pagination__total,
|
||||
.el-pagination__jump {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -101,7 +101,6 @@ function handleCategoryClick(category: Tag) {
|
||||
background: #C62828;
|
||||
border-radius: 8px;
|
||||
z-index: 1;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,5 +113,60 @@ function handleCategoryClick(category: Tag) {
|
||||
color: #334155;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
</style>
|
||||
|
||||
// 移动端样式
|
||||
@media (max-width: 768px) {
|
||||
.resource-sidebar {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #CCCCCC;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
flex-shrink: 0;
|
||||
height: auto;
|
||||
padding: 8px 16px;
|
||||
background: #F5F5F5;
|
||||
border-radius: 20px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: #E0E0E0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #C62828;
|
||||
|
||||
.active-overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}</style>
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
<el-empty description="暂无课程" />
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="total > 0" class="pagination-container">
|
||||
<!-- 桌面端分页 -->
|
||||
<div v-if="total > 0 && !isMobile" class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
@@ -88,20 +88,28 @@
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 移动端加载更多 -->
|
||||
<div v-if="isMobile && hasMore && courseList.length > 0" class="mobile-loading">
|
||||
<div v-if="loadingMore" class="loading-more">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StudyPlanLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Search, VideoPlay } from '@element-plus/icons-vue';
|
||||
import { Search, VideoPlay, Loading } from '@element-plus/icons-vue';
|
||||
import { courseApi } from '@/apis/study';
|
||||
import type { Course, PageParam } from '@/types';
|
||||
import { StudyPlanLayout } from '@/views/user/study-plan';
|
||||
import defaultCover from '@/assets/imgs/default-course-bg.png'
|
||||
import defaultCoverImg from '@/assets/imgs/default-course-bg.png'
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
|
||||
defineOptions({
|
||||
@@ -110,9 +118,15 @@ defineOptions({
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
const courseList = ref<Course[]>([]);
|
||||
const total = ref(0);
|
||||
const isMobile = ref(false);
|
||||
const hasMore = ref(true);
|
||||
|
||||
// 默认封面图片
|
||||
const defaultCover = defaultCoverImg;
|
||||
|
||||
// 分页参数
|
||||
const pageParam = ref<PageParam>({
|
||||
@@ -121,11 +135,91 @@ const pageParam = ref<PageParam>({
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
checkMobile();
|
||||
loadCourseList();
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 检查是否为移动端
|
||||
function checkMobile() {
|
||||
isMobile.value = window.innerWidth < 768;
|
||||
// 移动端使用更大的页面大小以减少请求次数
|
||||
if (isMobile.value) {
|
||||
pageParam.value.pageSize = 12;
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口大小变化处理
|
||||
function handleResize() {
|
||||
checkMobile();
|
||||
}
|
||||
|
||||
// 滚动事件处理
|
||||
function handleScroll() {
|
||||
if (!isMobile.value || loadingMore.value || !hasMore.value) return;
|
||||
|
||||
const scrollHeight = document.documentElement.scrollHeight;
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const clientHeight = document.documentElement.clientHeight;
|
||||
|
||||
// 滚动到底部-2px时触发加载
|
||||
if (scrollHeight - scrollTop - clientHeight <= 2) {
|
||||
loadMoreCourses();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多课程(移动端)
|
||||
async function loadMoreCourses() {
|
||||
if (loadingMore.value || !hasMore.value) return;
|
||||
|
||||
loadingMore.value = true;
|
||||
const nextPage = pageParam.value.pageNumber + 1;
|
||||
|
||||
try {
|
||||
const filter: Course = {
|
||||
status: 1
|
||||
};
|
||||
|
||||
if (searchKeyword.value) {
|
||||
filter.name = searchKeyword.value;
|
||||
}
|
||||
|
||||
const res = await courseApi.getCoursePage(
|
||||
{ ...pageParam.value, pageNumber: nextPage },
|
||||
filter
|
||||
);
|
||||
|
||||
if (res.success && res.dataList) {
|
||||
courseList.value = [...courseList.value, ...res.dataList];
|
||||
pageParam.value.pageNumber = nextPage;
|
||||
total.value = res.pageParam?.totalElements || 0;
|
||||
|
||||
// 检查是否还有更多数据
|
||||
hasMore.value = courseList.value.length < total.value;
|
||||
} else {
|
||||
ElMessage.error('加载更多课程失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载更多课程失败:', error);
|
||||
ElMessage.error('加载更多课程失败');
|
||||
} finally {
|
||||
loadingMore.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载课程列表
|
||||
async function loadCourseList() {
|
||||
async function loadCourseList(isRefresh = false) {
|
||||
if (isRefresh) {
|
||||
pageParam.value.pageNumber = 1;
|
||||
hasMore.value = true;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const filter: Course = {
|
||||
@@ -141,6 +235,11 @@ async function loadCourseList() {
|
||||
if (res.success) {
|
||||
courseList.value = res.dataList || [];
|
||||
total.value = res.pageParam?.totalElements || 0;
|
||||
|
||||
// 移动端下检查是否还有更多数据
|
||||
if (isMobile.value) {
|
||||
hasMore.value = courseList.value.length < total.value;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('加载课程列表失败');
|
||||
}
|
||||
@@ -155,7 +254,8 @@ async function loadCourseList() {
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pageParam.value.pageNumber = 1;
|
||||
loadCourseList();
|
||||
hasMore.value = true;
|
||||
loadCourseList(true);
|
||||
}
|
||||
|
||||
// 分页大小改变
|
||||
@@ -323,14 +423,13 @@ function getCategoryName(): string {
|
||||
|
||||
.course-info {
|
||||
padding: 22px;
|
||||
position: relative;
|
||||
|
||||
.view-count {
|
||||
position: absolute;
|
||||
top: 232px;
|
||||
right: 22px;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
@@ -370,4 +469,22 @@ function getCategoryName(): string {
|
||||
justify-content: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.mobile-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0 40px;
|
||||
|
||||
.loading-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -682,4 +682,175 @@ function getDeadlineStatus(task: TaskItemVO): { show: boolean; text: string; typ
|
||||
justify-content: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
.progress-info {
|
||||
.progress-header {
|
||||
gap: 16px;
|
||||
|
||||
.progress-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
.stat-card {
|
||||
width: 100%;
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
|
||||
.stat-title {
|
||||
width: auto !important;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
color: #666666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
width: auto !important;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #141F38;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 28px !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pending {
|
||||
.stat-content .stat-number {
|
||||
color: #F53F3F;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
:deep(.el-icon) {
|
||||
img {
|
||||
transform: scale(0.9); // 红色图标稍微缩小
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
.stat-content .stat-number {
|
||||
color: #00B42A;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
:deep(.el-icon) {
|
||||
img {
|
||||
transform: scale(1.2); // 蓝色图标放大以补偿白边
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-card {
|
||||
padding: 20px 16px;
|
||||
height: auto;
|
||||
|
||||
.task-content {
|
||||
gap: 12px;
|
||||
|
||||
.task-header {
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.task-desc {
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 24px;
|
||||
|
||||
:deep(.el-pagination) {
|
||||
.el-pagination__sizes,
|
||||
.el-pagination__total,
|
||||
.el-pagination__jump {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-pager li {
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-prev,
|
||||
.btn-next {
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user