feat(移动端): 优化后台筛选与看板排版

- 看板与列表页筛选区在小屏下改为堆叠布局,减少横向溢出

- 统一处理 gutter 负 margin 导致的横向滚动问题

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-04 14:08:54 +08:00
parent 822cfc3945
commit 63fcf36ce2
5 changed files with 352 additions and 84 deletions

View File

@ -1,7 +1,7 @@
<template>
<div>
<div class="dashboard-page min-w-0">
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-5">
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 md:gap-5">
<div class="flex items-center p-5 bg-white rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] transition-all duration-300 hover:-translate-y-0.5 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)]">
<div class="w-14 h-14 rounded-[14px] flex items-center justify-center text-white mr-4 bg-[linear-gradient(135deg,#6366F1_0%,#4F46E5_100%)]">
<Building2 :size="24" :stroke-width="1.5" />
@ -41,13 +41,13 @@
</div>
<!-- 趋势图和快捷入口 -->
<a-row :gutter="24" class="mt-6">
<a-col :span="16">
<a-row :gutter="[12, 12]" class="mt-4 md:mt-6 dashboard-row">
<a-col :xs="24" :md="16">
<a-card title="使用趋势" :bordered="false" :loading="trendLoading" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div ref="trendChartRef" class="h-[300px]"></div>
<div ref="trendChartRef" class="h-[260px] sm:h-[300px] min-h-0"></div>
</a-card>
</a-col>
<a-col :span="8">
<a-col :xs="24" :md="8">
<a-card title="快捷入口" :bordered="false" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div class="flex flex-col gap-3">
<div class="flex items-center py-3 px-4 bg-[#F9FAFB] rounded-[10px] cursor-pointer transition-all duration-250 hover:bg-[#EEF2FF] hover:translate-x-1 group quick-action-item" @click="$router.push('/admin/courses/create')">
@ -74,8 +74,8 @@
</a-row>
<!-- 活跃租户和热门课程 -->
<a-row :gutter="24" class="mt-6">
<a-col :span="12">
<a-row :gutter="[12, 12]" class="mt-4 md:mt-6 dashboard-row">
<a-col :xs="24" :md="12">
<a-card title="活跃租户 TOP5" :bordered="false" :loading="tenantsLoading" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div v-if="activeTenants.length > 0">
<div v-for="(item, index) in activeTenants" :key="item.id" class="flex items-center py-3 border-b border-[#E5E7EB] last:border-b-0 cursor-pointer transition-all duration-250 hover:bg-[#F9FAFB] -mx-4 px-4 rounded-lg" @click="viewTenantDetail(item.id)">
@ -95,7 +95,7 @@
<a-empty v-else description="暂无数据" />
</a-card>
</a-col>
<a-col :span="12">
<a-col :xs="24" :md="12">
<a-card title="热门课程包 TOP5" :bordered="false" :loading="coursesLoading" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div v-if="popularCourses.length > 0">
<div v-for="(item, index) in popularCourses" :key="item.id" class="flex items-center py-3 border-b border-[#E5E7EB] last:border-b-0 cursor-pointer transition-all duration-250 hover:bg-[#F9FAFB] -mx-4 px-4 rounded-lg" @click="viewCourseDetail(item.id)">
@ -118,7 +118,7 @@
</a-row>
<!-- 最近活动 -->
<a-row :gutter="24" class="mt-6">
<a-row :gutter="[12, 12]" class="mt-4 md:mt-6">
<a-col :span="24">
<a-card title="最近活动" :bordered="false" :loading="activitiesLoading" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div v-if="recentActivities.length > 0">
@ -444,6 +444,23 @@ onUnmounted(() => {
</script>
<style scoped>
.dashboard-page {
width: 100%;
overflow-x: hidden;
}
/* 移动端 ant row gutter 负 margin 易导致横向溢出,取消 gutter 效果 */
@media (max-width: 768px) {
.dashboard-row {
margin-left: 0 !important;
margin-right: 0 !important;
}
.dashboard-row :deep(.ant-col) {
padding-left: 0 !important;
padding-right: 0 !important;
}
}
.modern-card :deep(.ant-card-head) {
border-bottom: 1px solid #E5E7EB;
}

View File

@ -197,7 +197,7 @@
</a-dropdown>
</a-layout-header>
<a-layout-content :class="['flex-1 min-h-0 bg-white rounded-2xl shadow-sm overflow-y-auto overflow-x-hidden', isMobile ? 'm-3 p-4' : 'm-5 p-6']">
<a-layout-content :class="['admin-content flex-1 min-h-0 bg-white rounded-2xl shadow-sm overflow-y-auto overflow-x-hidden', isMobile ? 'm-3 p-4' : 'm-5 p-6']">
<router-view />
</a-layout-content>
</a-layout>
@ -386,4 +386,11 @@ const handleUserMenuClick = ({ key }: { key: string | number }) => {
display: none;
}
}
/* 移动端内容区:单轴滚动、防止横向溢出、触摸滚动更顺滑 */
.admin-content {
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
overflow-y: auto;
}
</style>

View File

@ -1,17 +1,16 @@
<template>
<div class="course-list">
<div class="course-list course-list--mobile-filters">
<div class="page-header">
<a-row :gutter="16" align="middle">
<a-col :span="16">
<a-space>
<a-select v-model:value="filters.grade" placeholder="年级" style="width: 120px" allow-clear
<a-row :gutter="[12, 12]" align="middle" class="filter-row">
<a-col :xs="24" :md="16">
<div class="filter-group">
<a-select v-model:value="filters.grade" placeholder="年级" class="filter-select grade-select" allow-clear
@change="fetchCourses">
<a-select-option value="small">小班</a-select-option>
<a-select-option value="middle">中班</a-select-option>
<a-select-option value="big">大班</a-select-option>
</a-select>
<a-select v-model:value="filters.status" placeholder="状态" style="width: 120px" allow-clear
<a-select v-model:value="filters.status" placeholder="状态" class="filter-select status-select" allow-clear
@change="fetchCourses">
<a-select-option value="DRAFT">草稿</a-select-option>
<a-select-option value="PENDING">审核中</a-select-option>
@ -19,21 +18,20 @@
<a-select-option value="PUBLISHED">已发布</a-select-option>
<a-select-option value="ARCHIVED">已下架</a-select-option>
</a-select>
<a-input-search v-model:value="filters.keyword" placeholder="搜索课程包名称" style="width: 250px"
<a-input-search v-model:value="filters.keyword" placeholder="搜索课程包名称" class="filter-search"
@search="fetchCourses" @blur="fetchCourses" />
</a-space>
</div>
</a-col>
<a-col :span="8" style="text-align: right;">
<a-space>
<a-button @click="$router.push('/admin/courses/review')">
<a-col :xs="24" :md="8">
<div class="action-group">
<a-button @click="$router.push('/admin/courses/review')" class="action-btn">
<AuditOutlined /> 审核管理
<a-badge v-if="pendingCount > 0" :count="pendingCount" :offset="[10, 0]" />
</a-button>
<a-button type="primary" @click="$router.push('/admin/courses/create')">
<a-button type="primary" @click="$router.push('/admin/courses/create')" class="action-btn">
<PlusOutlined /> 新建课程包
</a-button>
</a-space>
</div>
</a-col>
</a-row>
</div>
@ -505,6 +503,9 @@ const formatDate = (dateStr: string) => {
<style scoped lang="scss">
.course-list {
overflow-x: hidden;
min-width: 0;
:deep(.ant-table-thead > tr > th) {
white-space: nowrap;
}
@ -513,6 +514,76 @@ const formatDate = (dateStr: string) => {
margin-bottom: 16px;
}
/* 筛选区:桌面端横向排列 */
.filter-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.filter-select {
width: 120px;
min-width: 0;
}
.filter-search {
width: 250px;
min-width: 0;
max-width: 100%;
}
.action-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
justify-content: flex-end;
}
.action-btn {
white-space: nowrap;
}
/* 移动端:筛选与按钮区堆叠,并避免横向溢出 */
@media (max-width: 768px) {
.filter-row {
margin-left: 0 !important;
margin-right: 0 !important;
}
.filter-row :deep(.ant-col) {
padding-left: 0 !important;
padding-right: 0 !important;
}
.filter-group {
width: 100%;
}
.filter-select {
width: 110px;
flex-shrink: 0;
}
.filter-search {
flex: 1;
min-width: 140px;
width: 100%;
max-width: 100%;
}
.action-group {
width: 100%;
flex-direction: column;
gap: 8px;
}
.action-btn {
width: 100%;
margin: 0;
}
}
.course-name {
.course-tags {
margin-top: 4px;

View File

@ -1,10 +1,10 @@
<template>
<div class="resource-list-view">
<div class="resource-list-view resource-list-view--mobile">
<a-page-header title="资源库管理" sub-title="管理平台数字资源" />
<!-- 统计卡片 -->
<a-row :gutter="16" style="margin-bottom: 16px;">
<a-col :span="6">
<!-- 统计卡片移动端 2x2桌面端 4 -->
<a-row :gutter="[12, 12]" class="stats-row !p-10px">
<a-col :xs="12" :sm="12" :md="6">
<a-card>
<a-statistic title="资源库总数" :value="stats.totalLibraries">
<template #prefix>
@ -13,7 +13,7 @@
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-col :xs="12" :sm="12" :md="6">
<a-card>
<a-statistic title="资源总数" :value="stats.totalItems">
<template #prefix>
@ -22,7 +22,7 @@
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-col :xs="12" :sm="12" :md="6">
<a-card>
<a-statistic title="绘本资源" :value="stats.itemsByLibraryType?.PICTURE_BOOK || 0">
<template #prefix>
@ -31,7 +31,7 @@
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-col :xs="12" :sm="12" :md="6">
<a-card>
<a-statistic title="教学材料" :value="stats.itemsByLibraryType?.MATERIAL || 0">
<template #prefix>
@ -42,9 +42,9 @@
</a-col>
</a-row>
<a-card :bordered="false">
<a-card :bordered="false" class="resource-card pt-10px">
<template #extra>
<a-space>
<div class="resource-card-extra">
<a-button @click="showLibraryModal">
<template #icon>
<FolderAddOutlined />
@ -57,29 +57,30 @@
</template>
上传资源
</a-button>
</a-space>
</div>
</template>
<!-- 搜索和筛选 -->
<div class="filter-bar" style="margin-bottom: 16px;">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search v-model:value="filters.keyword" placeholder="搜索资源名称" allow-clear @search="fetchItems">
<!-- 搜索和筛选移动端堆叠桌面端横向 -->
<div class="filter-bar">
<a-row :gutter="[12, 12]" class="filter-row">
<a-col :xs="24" :md="10">
<a-input-search v-model:value="filters.keyword" placeholder="搜索资源名称" allow-clear class="filter-bar__search"
@search="fetchItems">
<template #prefix>
<SearchOutlined />
</template>
</a-input-search>
</a-col>
<a-col :span="4">
<a-select v-model:value="filters.libraryId" placeholder="选择资源库" allow-clear style="width: 100%"
<a-col :xs="24" :md="7">
<a-select v-model:value="filters.libraryId" placeholder="选择资源库" allow-clear class="filter-bar__select"
@change="fetchItems">
<a-select-option v-for="lib in libraries" :key="lib.id" :value="lib.id">
{{ lib.name }}
</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select v-model:value="filters.fileType" placeholder="资源类型" allow-clear style="width: 100%"
<a-col :xs="24" :md="7">
<a-select v-model:value="filters.fileType" placeholder="资源类型" allow-clear class="filter-bar__select"
@change="fetchItems">
<a-select-option value="IMAGE">图片</a-select-option>
<a-select-option value="PDF">PDF</a-select-option>
@ -617,6 +618,8 @@ onMounted(() => {
.resource-list-view {
background: #f5f5f5;
min-height: calc(100vh - 64px);
overflow-x: hidden;
min-width: 0;
:deep(.ant-table-thead > tr > th) {
white-space: nowrap;
@ -631,6 +634,58 @@ onMounted(() => {
background: #fafafa;
padding: 16px;
border-radius: 4px;
margin-bottom: 16px;
}
.filter-bar__search,
.filter-bar__select {
width: 100%;
min-width: 0;
max-width: 100%;
}
.resource-card-extra {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
@media (max-width: 768px) {
.stats-row {
margin-left: 0 !important;
margin-right: 0 !important;
}
.stats-row :deep(.ant-col) {
padding-left: 6px !important;
padding-right: 6px !important;
}
.filter-row {
margin-left: 0 !important;
margin-right: 0 !important;
}
.filter-row :deep(.ant-col) {
padding-left: 0 !important;
padding-right: 0 !important;
}
.resource-card-extra {
width: 100%;
flex-direction: column;
align-items: stretch;
}
.resource-card-extra .ant-btn {
margin: 0;
}
:deep(.ant-card-extra) {
margin-left: 0;
width: 100%;
}
}
.resource-item {

View File

@ -1,30 +1,33 @@
<template>
<div class="bg-white p-6 rounded">
<div class="tenant-list-page bg-white p-4 md:p-6 rounded overflow-x-hidden min-w-0">
<a-page-header title="租户管理" sub-title="管理平台租户学校" />
<a-card :bordered="false" class="mt-4">
<!-- 搜索表单 -->
<a-form layout="inline" :model="searchForm" class="mb-4">
<a-form-item label="关键词">
<a-input v-model:value="searchForm.keyword" placeholder="学校名称/账号/联系人" allow-clear class="w-[200px]"
<!-- 搜索表单移动端堆叠桌面端横向 -->
<a-form layout="inline" :model="searchForm" class="tenant-search-form mb-4">
<div class="tenant-search-form__fields">
<a-form-item label="关键词" class="tenant-search-form__item tenant-search-form__item--keyword">
<a-input v-model:value="searchForm.keyword" placeholder="学校名称/账号/联系人" allow-clear class="tenant-search-form__input"
@pressEnter="handleSearch" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchForm.status" placeholder="全部状态" allow-clear class="w-[120px]">
<a-form-item label="状态" class="tenant-search-form__item">
<a-select v-model:value="searchForm.status" placeholder="全部状态" allow-clear class="tenant-search-form__select">
<a-select-option value="ACTIVE">生效中</a-select-option>
<a-select-option value="EXPIRED">已过期</a-select-option>
<a-select-option value="SUSPENDED">已暂停</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="套餐">
<a-select v-model:value="searchForm.packageType" placeholder="全部套餐" allow-clear class="w-[120px]">
<a-form-item label="套餐" class="tenant-search-form__item">
<a-select v-model:value="searchForm.packageType" placeholder="全部套餐" allow-clear class="tenant-search-form__select">
<a-select-option value="BASIC">基础版</a-select-option>
<a-select-option value="STANDARD">标准版</a-select-option>
<a-select-option value="ADVANCED">高级版</a-select-option>
<a-select-option value="CUSTOM">定制版</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
</div>
<div class="tenant-search-form__actions">
<a-form-item class="tenant-search-form__actions-inner">
<a-space class="tenant-search-form__btns">
<a-button type="primary" @click="handleSearch">
<template #icon>
<SearchOutlined />
@ -34,7 +37,7 @@
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-form-item>
<a-form-item style="float: right">
<a-form-item class="tenant-search-form__actions-inner tenant-search-form__actions-inner--add">
<a-button type="primary" @click="showAddModal">
<template #icon>
<PlusOutlined />
@ -42,6 +45,7 @@
添加租户
</a-button>
</a-form-item>
</div>
</a-form>
<!-- 数据表格 -->
@ -611,7 +615,121 @@ onMounted(() => {
</script>
<style scoped>
/* 仅保留 :deep / @keyframes / scrollbar / @media 等 */
/* 租户筛选表单:桌面端横向,移动端堆叠,避免横向溢出 */
.tenant-search-form {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 0 8px;
}
.tenant-search-form__fields {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 16px;
flex: 1;
min-width: 0;
}
.tenant-search-form__item {
margin-bottom: 16px;
margin-right: 0;
}
.tenant-search-form__item--keyword :deep(.ant-form-item-control-input) {
min-width: 180px;
}
.tenant-search-form__input {
width: 200px;
min-width: 0;
max-width: 100%;
}
.tenant-search-form__select {
width: 120px;
min-width: 0;
}
.tenant-search-form__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.tenant-search-form__actions-inner {
margin-bottom: 0 !important;
margin-right: 0 !important;
}
.tenant-search-form__actions-inner--add {
margin-left: auto;
}
.tenant-search-form__btns {
flex-wrap: wrap;
}
@media (max-width: 768px) {
.tenant-search-form {
flex-direction: column;
align-items: stretch;
}
.tenant-search-form__fields {
width: 100%;
flex-direction: column;
align-items: stretch;
}
.tenant-search-form__item {
width: 100%;
margin-bottom: 12px;
}
.tenant-search-form__item :deep(.ant-form-item-control-input-content) {
display: flex;
}
.tenant-search-form__input,
.tenant-search-form__select {
width: 100% !important;
max-width: 100%;
}
.tenant-search-form__item--keyword :deep(.ant-form-item-control-input) {
min-width: 0;
}
.tenant-search-form__actions {
width: 100%;
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.tenant-search-form__actions-inner {
margin-left: 0 !important;
}
.tenant-search-form__actions-inner--add {
margin-left: 0 !important;
}
.tenant-search-form__btns {
width: 100%;
display: flex;
gap: 8px;
}
.tenant-search-form__btns .ant-btn {
flex: 1;
min-width: 0;
}
}
:deep(.ant-table-thead > tr > th) {
white-space: nowrap;