fix(school): 统一学校端列表页搜索框为整体样式,与课程管理页一致

优化内容:
- 参考课程管理页实现,将各页 a-input-search 改为 :enter-button="false",
  使搜索图标在输入框内与边框一体,视觉为一个整体
- 教师/学生/家长/班级管理:移除自定义 #prefix(SearchOutlined),
  避免双放大镜或图标与输入框分离,并移除未使用的 SearchOutlined 导入
- 家长管理:主列表搜索 + 弹窗内学生搜索均使用一体式搜索框
- 课程管理:授权弹窗内课程搜索去掉 prefix,增加 allow-clear 与 :enter-button="false"
- 校本课程包、阅读任务、课程反馈、成长档案、任务模板:为搜索框增加 :enter-button="false"

涉及页面:
- 教师管理 TeacherListView
- 学生管理 StudentListView
- 家长管理 ParentListView
- 班级管理 ClassListView
- 课程管理 CourseListView(含授权弹窗)
- 校本课程包 SchoolCourseListView
- 阅读任务 TaskListView
- 课程反馈 FeedbackView
- 成长档案 GrowthRecordView
- 任务模板 TaskTemplateView

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-06 11:32:05 +08:00
parent c8ad14449b
commit bab12cbed3
10 changed files with 281 additions and 435 deletions

View File

@ -1,10 +1,11 @@
<template> <template>
<div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] p-0"> <div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)]"> <div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)]">
<div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4"> <div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center text-[28px] text-white bg-[linear-gradient(135deg,rgba(255,255,255,0.3)_0%,rgba(255,255,255,0.1)_100%)]"> <div
class="w-14 h-14 rounded-2xl flex items-center justify-center text-[28px] text-white bg-[linear-gradient(135deg,rgba(255,255,255,0.3)_0%,rgba(255,255,255,0.1)_100%)]">
<HomeOutlined /> <HomeOutlined />
</div> </div>
<div> <div>
@ -26,48 +27,36 @@
</div> </div>
<!-- 操作栏 --> <!-- 操作栏 -->
<div class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] search-box-wrapper"> <div
<div class="search-box"> class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] search-box-wrapper">
<a-input-search <div class="">
v-model:value="searchKeyword" <a-input-search v-model:value="searchKeyword" placeholder="搜索班级名称" class="w-[250px]" @search="handleSearch"
placeholder="搜索班级名称" allow-clear :enter-button="false" />
class="w-[250px]"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
</div> </div>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] hover:!bg-[linear-gradient(135deg,#3d9be8_0%,#00d8e4_100%)] !border-0 rounded-xl h-10 px-6 font-600 add-btn" @click="showAddModal"> <a-button type="primary"
class="!bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] hover:!bg-[linear-gradient(135deg,#3d9be8_0%,#00d8e4_100%)] !border-0 rounded-xl h-10 px-6 font-600 add-btn"
@click="showAddModal">
<PlusOutlined class="mr-2" /> <PlusOutlined class="mr-2" />
添加班级 添加班级
</a-button> </a-button>
</div> </div>
<!-- 班级卡片网格 --> <!-- 班级卡片网格 -->
<div class="grid gap-5 mb-6 class-grid" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));" v-if="!loading && classes.length > 0"> <div class="grid gap-5 mb-6 class-grid" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));"
<div v-if="!loading && classes.length > 0">
v-for="cls in classes" <div v-for="cls in classes" :key="cls.id"
:key="cls.id"
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-2 border-transparent hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] class-card" class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-2 border-transparent hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] class-card"
:class="getGradeKey(cls.grade) === 'small' ? 'border-t-4 border-t-[#43e97b]' : getGradeKey(cls.grade) === 'middle' ? 'border-t-4 border-t-[#4facfe]' : 'border-t-4 border-t-[#FF8C42]'" :class="getGradeKey(cls.grade) === 'small' ? 'border-t-4 border-t-[#43e97b]' : getGradeKey(cls.grade) === 'middle' ? 'border-t-4 border-t-[#4facfe]' : 'border-t-4 border-t-[#FF8C42]'">
>
<div class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] card-header"> <div class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] card-header">
<div <div class="w-12 h-12 rounded-xl flex items-center justify-center icon-wrapper"
class="w-12 h-12 rounded-xl flex items-center justify-center icon-wrapper" :class="getGradeKey(cls.grade) === 'small' ? 'bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)]' : getGradeKey(cls.grade) === 'middle' ? 'bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)]' : 'bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)]'">
:class="getGradeKey(cls.grade) === 'small' ? 'bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)]' : getGradeKey(cls.grade) === 'middle' ? 'bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)]' : 'bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)]'"
>
<component :is="getGradeIcon(cls.grade)" class="text-2xl text-white" /> <component :is="getGradeIcon(cls.grade)" class="text-2xl text-white" />
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class="text-base font-600 text-[#2D3436]">{{ cls.name }}</div> <div class="text-base font-600 text-[#2D3436]">{{ cls.name }}</div>
<div class="mt-1"> <div class="mt-1">
<span <span class="py-0.5 px-2.5 rounded-[10px] text-[11px] font-500 grade-badge"
class="py-0.5 px-2.5 rounded-[10px] text-[11px] font-500 grade-badge" :class="getGradeKey(cls.grade) === 'small' ? 'bg-[#E8F5E9] text-[#43A047]' : getGradeKey(cls.grade) === 'middle' ? 'bg-[#E3F2FD] text-[#1976D2]' : 'bg-[#FFF8F0] text-[#FF8C42]'">
:class="getGradeKey(cls.grade) === 'small' ? 'bg-[#E8F5E9] text-[#43A047]' : getGradeKey(cls.grade) === 'middle' ? 'bg-[#E3F2FD] text-[#1976D2]' : 'bg-[#FFF8F0] text-[#FF8C42]'"
>
{{ cls.grade }} {{ cls.grade }}
</span> </span>
</div> </div>
@ -76,13 +65,15 @@
<div class="py-4 px-5 card-body"> <div class="py-4 px-5 card-body">
<div class="flex items-center gap-2 mb-2 text-[13px] info-row"> <div class="flex items-center gap-2 mb-2 text-[13px] info-row">
<div class="w-6 h-6 rounded-md flex items-center justify-center bg-[linear-gradient(135deg,#F0F4F8_0%,#E8EDF2_100%)]"> <div
class="w-6 h-6 rounded-md flex items-center justify-center bg-[linear-gradient(135deg,#F0F4F8_0%,#E8EDF2_100%)]">
<UserOutlined class="text-xs text-[#4facfe]" /> <UserOutlined class="text-xs text-[#4facfe]" />
</div> </div>
<div class="text-[#636E72] flex flex-wrap items-center gap-0.5 teachers-value"> <div class="text-[#636E72] flex flex-wrap items-center gap-0.5 teachers-value">
<template v-if="cls.teachers && cls.teachers.length > 0"> <template v-if="cls.teachers && cls.teachers.length > 0">
<span v-for="(teacher, index) in cls.teachers.slice(0, 3)" :key="teacher.teacherId"> <span v-for="(teacher, index) in cls.teachers.slice(0, 3)" :key="teacher.teacherId">
{{ teacher.teacherName }}<span v-if="teacher.isPrimary" class="text-[10px] py-0.5 px-1 rounded bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] text-white ml-1">班主任</span> {{ teacher.teacherName }}<span v-if="teacher.isPrimary"
class="text-[10px] py-0.5 px-1 rounded bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] text-white ml-1">班主任</span>
<span v-if="index < Math.min(cls.teachers.length, 3) - 1"></span> <span v-if="index < Math.min(cls.teachers.length, 3) - 1"></span>
</span> </span>
<span v-if="cls.teachers.length > 3" class="text-[#888] text-xs ml-1">{{ cls.teachers.length }}</span> <span v-if="cls.teachers.length > 3" class="text-[#888] text-xs ml-1">{{ cls.teachers.length }}</span>
@ -91,7 +82,8 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-2 mb-2 text-[13px] info-row"> <div class="flex items-center gap-2 mb-2 text-[13px] info-row">
<div class="w-6 h-6 rounded-md flex items-center justify-center bg-[linear-gradient(135deg,#F0F4F8_0%,#E8EDF2_100%)]"> <div
class="w-6 h-6 rounded-md flex items-center justify-center bg-[linear-gradient(135deg,#F0F4F8_0%,#E8EDF2_100%)]">
<TeamOutlined class="text-xs text-[#4facfe]" /> <TeamOutlined class="text-xs text-[#4facfe]" />
</div> </div>
<span class="text-[#636E72]"> <span class="text-[#636E72]">
@ -99,7 +91,8 @@
</span> </span>
</div> </div>
<div class="flex items-center gap-2 text-[13px] info-row"> <div class="flex items-center gap-2 text-[13px] info-row">
<div class="w-6 h-6 rounded-md flex items-center justify-center bg-[linear-gradient(135deg,#F0F4F8_0%,#E8EDF2_100%)]"> <div
class="w-6 h-6 rounded-md flex items-center justify-center bg-[linear-gradient(135deg,#F0F4F8_0%,#E8EDF2_100%)]">
<BookOutlined class="text-xs text-[#4facfe]" /> <BookOutlined class="text-xs text-[#4facfe]" />
</div> </div>
<span class="text-[#636E72]"> <span class="text-[#636E72]">
@ -110,29 +103,30 @@
<div class="py-3 px-5 bg-[#FAFAFA] card-footer"> <div class="py-3 px-5 bg-[#FAFAFA] card-footer">
<div class="h-1.5 bg-[#E0E0E0] rounded overflow-hidden"> <div class="h-1.5 bg-[#E0E0E0] rounded overflow-hidden">
<div class="h-full bg-[linear-gradient(90deg,#4facfe_0%,#00f2fe_100%)] rounded transition-[width] duration-300" :style="{ width: getProgressWidth(cls.studentCount) }"></div> <div
class="h-full bg-[linear-gradient(90deg,#4facfe_0%,#00f2fe_100%)] rounded transition-[width] duration-300"
:style="{ width: getProgressWidth(cls.studentCount) }"></div>
</div> </div>
<div class="text-[11px] text-[#B2BEC3] mt-1">班级活跃度</div> <div class="text-[11px] text-[#B2BEC3] mt-1">班级活跃度</div>
</div> </div>
<div class="flex justify-end gap-2 py-3 px-5 border-t border-[#F0F0F0] card-actions"> <div class="flex justify-end gap-2 py-3 px-5 border-t border-[#F0F0F0] card-actions">
<a-button type="link" size="small" @click="handleViewStudents(cls)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1"> <a-button type="link" size="small" @click="handleViewStudents(cls)"
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<TeamOutlined /> <TeamOutlined />
学生 学生
</a-button> </a-button>
<a-button type="link" size="small" @click="handleManageTeachers(cls)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1"> <a-button type="link" size="small" @click="handleManageTeachers(cls)"
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<UsergroupAddOutlined /> <UsergroupAddOutlined />
教师 教师
</a-button> </a-button>
<a-button type="link" size="small" @click="handleEdit(cls)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1"> <a-button type="link" size="small" @click="handleEdit(cls)"
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<EditOutlined /> <EditOutlined />
编辑 编辑
</a-button> </a-button>
<a-popconfirm <a-popconfirm title="确定要删除这个班级吗?" :disabled="cls.studentCount > 0" @confirm="handleDelete(cls.id)">
title="确定要删除这个班级吗?"
:disabled="cls.studentCount > 0"
@confirm="handleDelete(cls.id)"
>
<a-tooltip v-if="cls.studentCount > 0" title="班级内有学生,无法删除"> <a-tooltip v-if="cls.studentCount > 0" title="班级内有学生,无法删除">
<a-button type="link" size="small" disabled class="!py-1 !px-2 !h-auto opacity-50"> <a-button type="link" size="small" disabled class="!py-1 !px-2 !h-auto opacity-50">
<DeleteOutlined /> <DeleteOutlined />
@ -149,12 +143,15 @@
</div> </div>
<!-- 空状态 --> <!-- 空状态 -->
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && classes.length === 0"> <div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state"
<div class="w-20 h-20 rounded-[20px] flex items-center justify-center mb-4 bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] empty-icon"> v-if="!loading && classes.length === 0">
<div
class="w-20 h-20 rounded-[20px] flex items-center justify-center mb-4 bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] empty-icon">
<BankOutlined /> <BankOutlined />
</div> </div>
<p class="text-[#636E72] text-base mb-6">暂无班级数据</p> <p class="text-[#636E72] text-base mb-6">暂无班级数据</p>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] !border-0" @click="showAddModal"> <a-button type="primary" class="!bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] !border-0"
@click="showAddModal">
创建第一个班级 创建第一个班级
</a-button> </a-button>
</div> </div>
@ -166,21 +163,9 @@
</div> </div>
<!-- 添加/编辑班级模态框 --> <!-- 添加/编辑班级模态框 -->
<a-modal <a-modal v-model:open="modalVisible" :title="isEdit ? modalEditTitle : modalAddTitle" @ok="handleModalOk"
v-model:open="modalVisible" @cancel="handleModalCancel" :confirm-loading="submitting" :width="480">
:title="isEdit ? modalEditTitle : modalAddTitle" <a-form ref="formRef" :model="formState" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
@ok="handleModalOk"
@cancel="handleModalCancel"
:confirm-loading="submitting"
:width="480"
>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="班级名称" name="name"> <a-form-item label="班级名称" name="name">
<a-input v-model:value="formState.name" placeholder="请输入班级名称"> <a-input v-model:value="formState.name" placeholder="请输入班级名称">
<template #prefix> <template #prefix>
@ -211,12 +196,7 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="班主任" name="teacherId"> <a-form-item label="班主任" name="teacherId">
<a-select <a-select v-model:value="formState.teacherId" placeholder="请选择班主任" :loading="teachersLoading" allow-clear>
v-model:value="formState.teacherId"
placeholder="请选择班主任"
:loading="teachersLoading"
allow-clear
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id"> <a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }} {{ teacher.name }}
</a-select-option> </a-select-option>
@ -226,15 +206,12 @@
</a-modal> </a-modal>
<!-- 班级学生列表模态框 --> <!-- 班级学生列表模态框 -->
<a-modal <a-modal v-model:open="studentsModalVisible" :title="studentsModalTitle" :footer="null" width="700px">
v-model:open="studentsModalVisible"
:title="studentsModalTitle"
:footer="null"
width="700px"
>
<div class="p-0"> <div class="p-0">
<div class="flex items-center gap-4 py-5 px-5 rounded-2xl mb-5 bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] class-info-header"> <div
<div class="w-16 h-16 rounded-2xl flex items-center justify-center bg-[linear-gradient(135deg,rgba(255,255,255,0.3)_0%,rgba(255,255,255,0.1)_100%)]"> class="flex items-center gap-4 py-5 px-5 rounded-2xl mb-5 bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] class-info-header">
<div
class="w-16 h-16 rounded-2xl flex items-center justify-center bg-[linear-gradient(135deg,rgba(255,255,255,0.3)_0%,rgba(255,255,255,0.1)_100%)]">
<component :is="getGradeIcon(currentClass?.grade || '')" class="text-[32px] text-white" /> <component :is="getGradeIcon(currentClass?.grade || '')" class="text-[32px] text-white" />
</div> </div>
<div class="flex-1"> <div class="flex-1">
@ -250,15 +227,10 @@
</div> </div>
<div class="grid grid-cols-2 gap-3 students-list" v-if="!studentsLoading && classStudents.length > 0"> <div class="grid grid-cols-2 gap-3 students-list" v-if="!studentsLoading && classStudents.length > 0">
<div <div v-for="student in classStudents" :key="student.id"
v-for="student in classStudents" class="flex items-center gap-3 p-3 bg-[#F8F9FA] rounded-xl student-item">
:key="student.id" <div class="w-9 h-9 rounded-full flex items-center justify-center student-avatar"
class="flex items-center gap-3 p-3 bg-[#F8F9FA] rounded-xl student-item" :class="student.gender === '男' ? 'bg-[linear-gradient(135deg,#4FC3F7_0%,#29B6F6_100%)]' : 'bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]'">
>
<div
class="w-9 h-9 rounded-full flex items-center justify-center student-avatar"
:class="student.gender === '男' ? 'bg-[linear-gradient(135deg,#4FC3F7_0%,#29B6F6_100%)]' : 'bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]'"
>
<BoyOutlined v-if="student.gender === ''" class="text-base text-white" /> <BoyOutlined v-if="student.gender === ''" class="text-base text-white" />
<GirlOutlined v-else class="text-base text-white" /> <GirlOutlined v-else class="text-base text-white" />
</div> </div>
@ -272,8 +244,10 @@
</div> </div>
</div> </div>
<div class="flex flex-col items-center py-10 empty-students" v-if="!studentsLoading && classStudents.length === 0"> <div class="flex flex-col items-center py-10 empty-students"
<div class="w-16 h-16 rounded-2xl flex items-center justify-center mb-3 bg-[linear-gradient(135deg,#E8EDF2_0%,#D4DAE0_100%)]"> v-if="!studentsLoading && classStudents.length === 0">
<div
class="w-16 h-16 rounded-2xl flex items-center justify-center mb-3 bg-[linear-gradient(135deg,#E8EDF2_0%,#D4DAE0_100%)]">
<InboxOutlined class="text-[32px] text-[#B2BEC3]" /> <InboxOutlined class="text-[32px] text-[#B2BEC3]" />
</div> </div>
<p class="text-[#636E72] mt-2">该班级暂无学生</p> <p class="text-[#636E72] mt-2">该班级暂无学生</p>
@ -284,36 +258,24 @@
<p class="text-[#636E72] mt-2">加载学生列表...</p> <p class="text-[#636E72] mt-2">加载学生列表...</p>
</div> </div>
<div class="flex justify-center mt-5 pt-4 border-t border-[#F0F0F0] pagination-wrapper" v-if="studentsPagination.total > studentsPagination.pageSize"> <div class="flex justify-center mt-5 pt-4 border-t border-[#F0F0F0] pagination-wrapper"
<a-pagination v-if="studentsPagination.total > studentsPagination.pageSize">
v-model:current="studentsPagination.current" <a-pagination v-model:current="studentsPagination.current" v-model:pageSize="studentsPagination.pageSize"
v-model:pageSize="studentsPagination.pageSize" :total="studentsPagination.total" :show-total="(total: number) => `共 ${total} 条`" size="small"
:total="studentsPagination.total" @change="handleStudentsPageChange" />
:show-total="(total: number) => `共 ${total} 条`"
size="small"
@change="handleStudentsPageChange"
/>
</div> </div>
</div> </div>
</a-modal> </a-modal>
<!-- 班级教师管理模态框 --> <!-- 班级教师管理模态框 -->
<a-modal <a-modal v-model:open="teachersModalVisible" :title="`管理 ${currentClass?.name || ''} 教师团队`" :footer="null"
v-model:open="teachersModalVisible" width="600px">
:title="`管理 ${currentClass?.name || ''} 教师团队`"
:footer="null"
width="600px"
>
<div class="p-0"> <div class="p-0">
<!-- 添加教师表单 --> <!-- 添加教师表单 -->
<div class="p-4 bg-[#F8F9FA] rounded-xl mb-4 add-teacher-form"> <div class="p-4 bg-[#F8F9FA] rounded-xl mb-4 add-teacher-form">
<div class="flex items-center gap-3 form-row"> <div class="flex items-center gap-3 form-row">
<a-select <a-select v-model:value="teacherFormState.teacherId" placeholder="选择教师" class="w-[150px]"
v-model:value="teacherFormState.teacherId" :loading="teachersLoading">
placeholder="选择教师"
class="w-[150px]"
:loading="teachersLoading"
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id"> <a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }} {{ teacher.name }}
</a-select-option> </a-select-option>
@ -332,23 +294,18 @@
<!-- 教师列表 --> <!-- 教师列表 -->
<div class="flex flex-col gap-2 teachers-list" v-if="classTeachers.length > 0"> <div class="flex flex-col gap-2 teachers-list" v-if="classTeachers.length > 0">
<div v-for="teacher in classTeachers" :key="teacher.teacherId" class="flex justify-between items-center py-3 px-4 bg-[#F8F9FA] rounded-[10px] teacher-item"> <div v-for="teacher in classTeachers" :key="teacher.teacherId"
class="flex justify-between items-center py-3 px-4 bg-[#F8F9FA] rounded-[10px] teacher-item">
<div class="flex items-center teacher-info"> <div class="flex items-center teacher-info">
<span class="font-500 text-[#2D3436]">{{ teacher.teacherName }}</span> <span class="font-500 text-[#2D3436]">{{ teacher.teacherName }}</span>
<a-select <a-select v-model:value="teacher.role" size="small" class="w-20 ml-2"
v-model:value="teacher.role" @change="handleUpdateTeacherRole(teacher)">
size="small"
class="w-20 ml-2"
@change="handleUpdateTeacherRole(teacher)"
>
<a-select-option value="MAIN">主班</a-select-option> <a-select-option value="MAIN">主班</a-select-option>
<a-select-option value="ASSIST">配班</a-select-option> <a-select-option value="ASSIST">配班</a-select-option>
<a-select-option value="CARE">保育员</a-select-option> <a-select-option value="CARE">保育员</a-select-option>
</a-select> </a-select>
<a-checkbox <a-checkbox v-model:checked="teacher.isPrimary"
v-model:checked="teacher.isPrimary" @change="handleUpdateTeacherRole(teacher)">班主任</a-checkbox>
@change="handleUpdateTeacherRole(teacher)"
>班主任</a-checkbox>
</div> </div>
<a-button type="link" danger size="small" @click="handleRemoveTeacher(teacher.teacherId)"> <a-button type="link" danger size="small" @click="handleRemoveTeacher(teacher.teacherId)">
移除 移除
@ -370,7 +327,6 @@ import {
HomeOutlined, HomeOutlined,
BankOutlined, BankOutlined,
PlusOutlined, PlusOutlined,
SearchOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
TeamOutlined, TeamOutlined,
@ -758,6 +714,7 @@ onMounted(() => {
border-radius: 12px; border-radius: 12px;
border: 2px solid #F0F0F0; border: 2px solid #F0F0F0;
} }
.search-box :deep(.ant-input-affix-wrapper:hover) { .search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #4facfe; border-color: #4facfe;
} }
@ -784,13 +741,16 @@ onMounted(() => {
.class-grid { .class-grid {
grid-template-columns: 1fr !important; grid-template-columns: 1fr !important;
} }
.students-list { .students-list {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.search-box-wrapper { .search-box-wrapper {
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
} }
.search-box :deep(.ant-input-search) { .search-box :deep(.ant-input-search) {
width: 100% !important; width: 100% !important;
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]"> <div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)]"> <div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)]">
<div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4"> <div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4">
@ -149,11 +149,14 @@
</template> </template>
<div class="flex flex-col gap-5"> <div class="flex flex-col gap-5">
<div class="auth-search"> <div class="auth-search">
<a-input-search v-model:value="searchKeyword" placeholder="输入课程名称搜索..." @search="searchCourses" size="large"> <a-input-search
<template #prefix> v-model:value="searchKeyword"
<SearchOutlined /> placeholder="输入课程名称搜索..."
</template> @search="searchCourses"
</a-input-search> size="large"
allow-clear
:enter-button="false"
/>
</div> </div>
<div class="grid grid-cols-2 gap-3 max-h-[400px] overflow-y-auto available-courses" <div class="grid grid-cols-2 gap-3 max-h-[400px] overflow-y-auto available-courses"
@ -206,7 +209,6 @@
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { import {
SearchOutlined,
BookOutlined, BookOutlined,
ReadOutlined, ReadOutlined,
StarFilled, StarFilled,

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]"> <div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]"> <div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
<div class="flex justify-between items-center gap-4 max-md:flex-col max-md:items-start"> <div class="flex justify-between items-center gap-4 max-md:flex-col max-md:items-start">
@ -55,6 +55,7 @@
class="w-full md:w-[200px]" class="w-full md:w-[200px]"
@search="handleFilter" @search="handleFilter"
allow-clear allow-clear
:enter-button="false"
/> />
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]"> <div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]"> <div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]">
<div class="flex justify-between items-center max-md:flex-col max-md:gap-4 max-md:items-start"> <div class="flex justify-between items-center max-md:flex-col max-md:gap-4 max-md:items-start">
@ -41,6 +41,7 @@
class="w-[200px]" class="w-[200px]"
@search="handleFilter" @search="handleFilter"
allow-clear allow-clear
:enter-button="false"
/> />
</div> </div>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] hover:!bg-[linear-gradient(135deg,#e080e8_0%,#e04a5d_100%)] !border-0 rounded-xl h-10 px-6 font-600" @click="showAddModal"> <a-button type="primary" class="!bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] hover:!bg-[linear-gradient(135deg,#e080e8_0%,#e04a5d_100%)] !border-0 rounded-xl h-10 px-6 font-600" @click="showAddModal">

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]"> <div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)]"> <div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)]">
<div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4"> <div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4">
@ -30,11 +30,7 @@
class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] max-md:flex-col max-md:gap-3"> class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] max-md:flex-col max-md:gap-3">
<div class="search-box"> <div class="search-box">
<a-input-search v-model:value="searchKeyword" placeholder="搜索家长姓名/手机号/账号" class="w-[280px]" <a-input-search v-model:value="searchKeyword" placeholder="搜索家长姓名/手机号/账号" class="w-[280px]"
@search="handleSearch" allow-clear> @search="handleSearch" allow-clear :enter-button="false" />
<template #prefix>
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
</div> </div>
<a-button type="primary" <a-button type="primary"
class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] hover:!bg-[linear-gradient(135deg,#FF7A2A_0%,#FFA030_100%)] !border-0 rounded-xl h-10 px-6 font-600" class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] hover:!bg-[linear-gradient(135deg,#FF7A2A_0%,#FFA030_100%)] !border-0 rounded-xl h-10 px-6 font-600"
@ -246,7 +242,7 @@
<div class="flex gap-3 mb-4 select-search-bar"> <div class="flex gap-3 mb-4 select-search-bar">
<a-input-search v-model:value="studentSearchKeyword" placeholder="搜索学生姓名" class="w-[240px]" <a-input-search v-model:value="studentSearchKeyword" placeholder="搜索学生姓名" class="w-[240px]"
@search="handleStudentSearch" allow-clear /> @search="handleStudentSearch" allow-clear :enter-button="false" />
<a-select v-model:value="studentClassFilter" placeholder="按班级筛选" class="w-[160px]" allow-clear <a-select v-model:value="studentClassFilter" placeholder="按班级筛选" class="w-[160px]" allow-clear
@change="handleStudentSearch"> @change="handleStudentSearch">
<a-select-option v-for="cls in classOptions" :key="cls.id" :value="cls.id"> <a-select-option v-for="cls in classOptions" :key="cls.id" :value="cls.id">
@ -316,7 +312,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, onMounted } from 'vue';
import { import {
SearchOutlined,
IdcardOutlined, IdcardOutlined,
PlusOutlined, PlusOutlined,
PhoneOutlined, PhoneOutlined,
@ -692,11 +687,6 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.search-box :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.search-box :deep(.ant-input-affix-wrapper:hover) { .search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #FF8C42; border-color: #FF8C42;
} }

View File

@ -3,7 +3,8 @@
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="mb-6 flex justify-between items-center gap-4 max-md:flex-col max-md:items-start"> <div class="mb-6 flex justify-between items-center gap-4 max-md:flex-col max-md:items-start">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="w-14 h-14 flex items-center justify-center bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)] rounded-[14px] shadow-[0_4px_12px_rgba(102,126,234,0.3)]"> <div
class="w-14 h-14 flex items-center justify-center bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)] rounded-[14px] shadow-[0_4px_12px_rgba(102,126,234,0.3)]">
<AppstoreOutlined class="text-[28px] text-white" /> <AppstoreOutlined class="text-[28px] text-white" />
</div> </div>
<div> <div>
@ -11,11 +12,7 @@
<p class="text-[#666] text-sm mt-1 mb-0">管理本校教师创建的校本课程包</p> <p class="text-[#666] text-sm mt-1 mb-0">管理本校教师创建的校本课程包</p>
</div> </div>
</div> </div>
<a-button <a-button type="primary" class="w-full md:w-auto" @click="handleCreate">
type="primary"
class="w-full md:w-auto"
@click="handleCreate"
>
<PlusOutlined /> 创建校本课程包 <PlusOutlined /> 创建校本课程包
</a-button> </a-button>
</div> </div>
@ -23,7 +20,8 @@
<!-- 统计概览 --> <!-- 统计概览 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]"> <div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)] text-white"> <div
class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)] text-white">
<AppstoreOutlined /> <AppstoreOutlined />
</div> </div>
<div> <div>
@ -32,7 +30,8 @@
</div> </div>
</div> </div>
<div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]"> <div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)] text-white"> <div
class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)] text-white">
<BarChartOutlined /> <BarChartOutlined />
</div> </div>
<div> <div>
@ -41,7 +40,8 @@
</div> </div>
</div> </div>
<div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]"> <div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] text-white"> <div
class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] text-white">
<CalendarOutlined /> <CalendarOutlined />
</div> </div>
<div> <div>
@ -57,25 +57,14 @@
<span>校本课程包列表</span> <span>校本课程包列表</span>
</template> </template>
<template #extra> <template #extra>
<div class="search-box"> <div class="">
<a-input-search <a-input-search v-model:value="searchKeyword" placeholder="搜索课程包名称" class="w-[220px]" @search="handleSearch"
v-model:value="searchKeyword" allow-clear :enter-button="false" />
placeholder="搜索课程包名称"
class="w-[220px]"
@search="handleSearch"
allow-clear
/>
</div> </div>
</template> </template>
<a-table <a-table :columns="columns" :data-source="filteredData" :loading="loading" row-key="id"
:columns="columns" :pagination="{ pageSize: 10 }" :scroll="{ x: true }">
:data-source="filteredData"
:loading="loading"
row-key="id"
:pagination="{ pageSize: 10 }"
:scroll="{ x: true }"
>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'"> <template v-if="column.key === 'name'">
<div class="flex flex-col"> <div class="flex flex-col">
@ -87,11 +76,8 @@
</template> </template>
<template v-else-if="column.key === 'sourceCourse'"> <template v-else-if="column.key === 'sourceCourse'">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<img <img v-if="record.sourceCourse?.coverImagePath" :src="getFileUrl(record.sourceCourse.coverImagePath)"
v-if="record.sourceCourse?.coverImagePath" class="w-10 h-10 object-cover rounded" />
:src="getFileUrl(record.sourceCourse.coverImagePath)"
class="w-10 h-10 object-cover rounded"
/>
<div v-else class="w-10 h-10 bg-[#f0f0f0] rounded flex items-center justify-center text-[#999]"> <div v-else class="w-10 h-10 bg-[#f0f0f0] rounded flex items-center justify-center text-[#999]">
<BookOutlined /> <BookOutlined />
</div> </div>
@ -149,13 +135,8 @@
</a-card> </a-card>
<!-- 预约弹窗 --> <!-- 预约弹窗 -->
<a-modal <a-modal v-model:open="reserveModalVisible" title="预约校本课程包" width="500px" @ok="handleReserve"
v-model:open="reserveModalVisible" :confirmLoading="reserveLoading">
title="预约校本课程包"
width="500px"
@ok="handleReserve"
:confirmLoading="reserveLoading"
>
<div v-if="selectedCourse"> <div v-if="selectedCourse">
<div class="bg-[#f9f9f9] rounded-lg py-3 px-4 flex gap-2"> <div class="bg-[#f9f9f9] rounded-lg py-3 px-4 flex gap-2">
<span class="text-[#666]">课程包名称</span> <span class="text-[#666]">课程包名称</span>
@ -180,35 +161,21 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="预约时间" required> <a-form-item label="预约时间" required>
<a-date-picker <a-date-picker v-model:value="reserveForm.scheduledDate" show-time format="YYYY-MM-DD HH:mm"
v-model:value="reserveForm.scheduledDate" placeholder="选择预约时间" class="w-full" />
show-time
format="YYYY-MM-DD HH:mm"
placeholder="选择预约时间"
class="w-full"
/>
</a-form-item> </a-form-item>
<a-form-item label="备注"> <a-form-item label="备注">
<a-textarea v-model:value="reserveForm.note" :rows="2" placeholder="备注信息(可选)" /> <a-textarea v-model:value="reserveForm.note" :rows="2" placeholder="备注信息(可选)" />
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-alert <a-alert v-if="conflictInfo" :type="conflictInfo.hasConflict ? 'error' : 'success'"
v-if="conflictInfo" :message="conflictInfo.message" show-icon />
:type="conflictInfo.hasConflict ? 'error' : 'success'"
:message="conflictInfo.message"
show-icon
/>
</div> </div>
</a-modal> </a-modal>
<!-- 排课弹窗 --> <!-- 排课弹窗 -->
<a-modal <a-modal v-model:open="scheduleModalVisible" title="排课管理" width="800px" :footer="null">
v-model:open="scheduleModalVisible"
title="排课管理"
width="800px"
:footer="null"
>
<div v-if="selectedCourse"> <div v-if="selectedCourse">
<div class="flex justify-between items-center mb-4 py-3 px-4 bg-[#f9f9f9] rounded-lg"> <div class="flex justify-between items-center mb-4 py-3 px-4 bg-[#f9f9f9] rounded-lg">
<span>课程包{{ selectedCourse.name }}</span> <span>课程包{{ selectedCourse.name }}</span>
@ -219,14 +186,8 @@
<a-tabs v-model:activeKey="scheduleTab"> <a-tabs v-model:activeKey="scheduleTab">
<a-tab-pane key="upcoming" tab="即将上课"> <a-tab-pane key="upcoming" tab="即将上课">
<a-table <a-table :columns="reservationColumns" :data-source="upcomingReservations" :loading="reservationLoading"
:columns="reservationColumns" row-key="id" size="small" :pagination="false">
:data-source="upcomingReservations"
:loading="reservationLoading"
row-key="id"
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag> <a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
@ -243,14 +204,8 @@
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="history" tab="历史记录"> <a-tab-pane key="history" tab="历史记录">
<a-table <a-table :columns="reservationColumns" :data-source="historyReservations" :loading="reservationLoading"
:columns="reservationColumns" row-key="id" size="small" :pagination="{ pageSize: 5 }">
:data-source="historyReservations"
:loading="reservationLoading"
row-key="id"
size="small"
:pagination="{ pageSize: 5 }"
>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag> <a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]"> <div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]"> <div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]">
<div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4"> <div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4">
@ -19,48 +19,43 @@
</div> </div>
<div class="text-center stat-item flex flex-col items-center gap-0.5"> <div class="text-center stat-item flex flex-col items-center gap-0.5">
<span class="block text-[28px] font-700 text-[#4FC3F7]">{{ boysCount }}</span> <span class="block text-[28px] font-700 text-[#4FC3F7]">{{ boysCount }}</span>
<span class="text-xs text-white/80 flex items-center gap-1"><UserOutlined class="text-sm text-[#4FC3F7]" /> 男生</span> <span class="text-xs text-white/80 flex items-center gap-1">
<UserOutlined class="text-sm text-[#4FC3F7]" /> 男生
</span>
</div> </div>
<div class="text-center stat-item flex flex-col items-center gap-0.5"> <div class="text-center stat-item flex flex-col items-center gap-0.5">
<span class="block text-[28px] font-700 text-[#FFD93D]">{{ girlsCount }}</span> <span class="block text-[28px] font-700 text-[#FFD93D]">{{ girlsCount }}</span>
<span class="text-xs text-white/80 flex items-center gap-1"><UserOutlined class="text-sm text-[#FFD93D]" /> 女生</span> <span class="text-xs text-white/80 flex items-center gap-1">
<UserOutlined class="text-sm text-[#FFD93D]" /> 女生
</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 操作栏 --> <!-- 操作栏 -->
<div class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] max-md:flex-col max-md:gap-3"> <div
class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] max-md:flex-col max-md:gap-3">
<div class="flex gap-3 max-md:w-full flex-wrap filters"> <div class="flex gap-3 max-md:w-full flex-wrap filters">
<a-select <a-select v-model:value="selectedClassId" placeholder="选择班级" class="w-[150px]" @change="handleClassChange"
v-model:value="selectedClassId" allow-clear>
placeholder="选择班级"
class="w-[150px]"
@change="handleClassChange"
allow-clear
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id"> <a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }} {{ cls.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-input-search <a-input-search v-model:value="searchKeyword" placeholder="搜索学生姓名/家长" class="w-[220px] search-input-wrap"
v-model:value="searchKeyword" @search="handleSearch" allow-clear :enter-button="false" />
placeholder="搜索学生姓名/家长"
class="w-[220px] search-input-wrap"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
</div> </div>
<div class="flex gap-3 max-md:w-full flex-wrap actions"> <div class="flex gap-3 max-md:w-full flex-wrap actions">
<a-button class="rounded-xl h-10 border-2 border-[#f093fb] text-[#f5576c] hover:bg-[#FFF0F5] hover:border-[#f5576c] hover:text-[#f5576c]" @click="showImportModal"> <a-button
class="rounded-xl h-10 border-2 border-[#f093fb] text-[#f5576c] hover:bg-[#FFF0F5] hover:border-[#f5576c] hover:text-[#f5576c]"
@click="showImportModal">
<DownloadOutlined class="mr-2" /> <DownloadOutlined class="mr-2" />
批量导入 批量导入
</a-button> </a-button>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] hover:!bg-[linear-gradient(135deg,#e080e8_0%,#e04a5d_100%)] !border-0 rounded-xl h-10 px-6 font-600" @click="showAddModal"> <a-button type="primary"
class="!bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] hover:!bg-[linear-gradient(135deg,#e080e8_0%,#e04a5d_100%)] !border-0 rounded-xl h-10 px-6 font-600"
@click="showAddModal">
<PlusOutlined class="mr-2" /> <PlusOutlined class="mr-2" />
添加学生 添加学生
</a-button> </a-button>
@ -68,17 +63,14 @@
</div> </div>
<!-- 学生卡片网格 --> <!-- 学生卡片网格 -->
<div class="grid gap-5 mb-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(280px,1fr))]" v-if="!loading && students.length > 0"> <div class="grid gap-5 mb-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(280px,1fr))]"
v-if="!loading && students.length > 0">
<div v-for="student in students" :key="student.id"
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-2 border-transparent hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] hover:border-[#f093fb]">
<div <div
v-for="student in students" class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]">
:key="student.id" <div class="w-12 h-12 rounded-full flex items-center justify-center text-2xl text-white"
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-2 border-transparent hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] hover:border-[#f093fb]" :class="normalizeGender(student.gender) === '男' ? 'bg-[linear-gradient(135deg,#4FC3F7_0%,#29B6F6_100%)]' : 'bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]'">
>
<div class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]">
<div
class="w-12 h-12 rounded-full flex items-center justify-center text-2xl text-white"
:class="normalizeGender(student.gender) === '男' ? 'bg-[linear-gradient(135deg,#4FC3F7_0%,#29B6F6_100%)]' : 'bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]'"
>
<UserOutlined /> <UserOutlined />
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
@ -90,11 +82,10 @@
<div class="py-4 px-5 card-body"> <div class="py-4 px-5 card-body">
<div class="flex items-center gap-2 mb-2 text-[13px]"> <div class="flex items-center gap-2 mb-2 text-[13px]">
<CalendarOutlined class="text-sm text-[#f5576c] w-5" /> <CalendarOutlined class="text-sm text-[#f5576c] w-5" />
<span class="text-[#636E72] flex-1">{{ calculateAge(student.birthDate) || '--' }}{{ student.birthDate ? '岁' : '' }}</span> <span class="text-[#636E72] flex-1">{{ calculateAge(student.birthDate) || '--' }}{{ student.birthDate ? '岁'
<span : '' }}</span>
class="py-0.5 px-2 rounded-[10px] text-[11px]" <span class="py-0.5 px-2 rounded-[10px] text-[11px]"
:class="normalizeGender(student.gender) === '男' ? 'bg-[#E3F2FD] text-[#1976D2]' : 'bg-[#FCE4EC] text-[#E91E63]'" :class="normalizeGender(student.gender) === '男' ? 'bg-[#E3F2FD] text-[#1976D2]' : 'bg-[#FCE4EC] text-[#E91E63]'">
>
{{ normalizeGender(student.gender) }} {{ normalizeGender(student.gender) }}
</span> </span>
</div> </div>
@ -108,21 +99,21 @@
</div> </div>
<div class="flex items-center gap-2 text-[13px]"> <div class="flex items-center gap-2 text-[13px]">
<BookOutlined class="text-sm text-[#f5576c] w-5" /> <BookOutlined class="text-sm text-[#f5576c] w-5" />
<span class="text-[#636E72]">参与课程 <strong class="text-[#f5576c]">{{ student.lessonCount || 0 }}</strong> </span> <span class="text-[#636E72]">参与课程 <strong class="text-[#f5576c]">{{ student.lessonCount || 0 }}</strong>
</span>
</div> </div>
</div> </div>
<div class="flex justify-end gap-2 py-3 px-5 border-t border-[#F0F0F0] bg-[#FAFAFA] card-actions"> <div class="flex justify-end gap-2 py-3 px-5 border-t border-[#F0F0F0] bg-[#FAFAFA] card-actions">
<a-button type="link" size="small" @click="handleEdit(student)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1"> <a-button type="link" size="small" @click="handleEdit(student)"
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<EditOutlined /> 编辑 <EditOutlined /> 编辑
</a-button> </a-button>
<a-button type="link" size="small" @click="handleTransfer(student)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1"> <a-button type="link" size="small" @click="handleTransfer(student)"
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<SwapOutlined /> 调班 <SwapOutlined /> 调班
</a-button> </a-button>
<a-popconfirm <a-popconfirm title="确定要删除这位学生吗?" @confirm="handleDelete(student.id)">
title="确定要删除这位学生吗?"
@confirm="handleDelete(student.id)"
>
<a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto"> <a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto">
<DeleteOutlined /> 删除 <DeleteOutlined /> 删除
</a-button> </a-button>
@ -132,12 +123,15 @@
</div> </div>
<!-- 空状态 --> <!-- 空状态 -->
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && students.length === 0"> <div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state"
<div class="w-20 h-20 rounded-full flex items-center justify-center mb-4 text-[40px] text-[#f5576c] bg-[linear-gradient(135deg,#FFF0F5_0%,#FCE4EC_100%)]"> v-if="!loading && students.length === 0">
<div
class="w-20 h-20 rounded-full flex items-center justify-center mb-4 text-[40px] text-[#f5576c] bg-[linear-gradient(135deg,#FFF0F5_0%,#FCE4EC_100%)]">
<InboxOutlined /> <InboxOutlined />
</div> </div>
<p class="text-[#636E72] text-base mb-6">暂无学生数据</p> <p class="text-[#636E72] text-base mb-6">暂无学生数据</p>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] !border-0 rounded-xl" @click="showAddModal"> <a-button type="primary" class="!bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] !border-0 rounded-xl"
@click="showAddModal">
添加第一位学生 添加第一位学生
</a-button> </a-button>
</div> </div>
@ -150,55 +144,41 @@
<!-- 分页 --> <!-- 分页 -->
<div class="flex justify-center py-6 bg-white rounded-2xl pagination-wrapper" v-if="students.length > 0"> <div class="flex justify-center py-6 bg-white rounded-2xl pagination-wrapper" v-if="students.length > 0">
<a-pagination <a-pagination v-model:current="pagination.current" v-model:pageSize="pagination.pageSize"
v-model:current="pagination.current" :total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`"
v-model:pageSize="pagination.pageSize" @change="handlePageChange" />
:total="pagination.total"
:show-size-changer="true"
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
/>
</div> </div>
<!-- 添加/编辑学生模态框 --> <!-- 添加/编辑学生模态框 -->
<a-modal <a-modal v-model:open="modalVisible" @ok="handleModalOk" @cancel="handleModalCancel" :confirm-loading="submitting"
v-model:open="modalVisible" :width="520">
@ok="handleModalOk"
@cancel="handleModalCancel"
:confirm-loading="submitting"
:width="520"
>
<template #title> <template #title>
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<component :is="isEdit ? EditOutlined : PlusOutlined" class="text-[#f5576c]" /> <component :is="isEdit ? EditOutlined : PlusOutlined" class="text-[#f5576c]" />
{{ isEdit ? '编辑学生' : '添加学生' }} {{ isEdit ? '编辑学生' : '添加学生' }}
</span> </span>
</template> </template>
<a-form <a-form ref="formRef" :model="formState" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="姓名" name="name"> <a-form-item label="姓名" name="name">
<a-input v-model:value="formState.name" placeholder="请输入学生姓名"> <a-input v-model:value="formState.name" placeholder="请输入学生姓名">
<template #prefix><UserOutlined class="text-[#B2BEC3]" /></template> <template #prefix>
<UserOutlined class="text-[#B2BEC3]" />
</template>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item label="性别" name="gender"> <a-form-item label="性别" name="gender">
<a-radio-group v-model:value="formState.gender"> <a-radio-group v-model:value="formState.gender">
<a-radio value="男"><UserOutlined class="mr-1 text-[#4FC3F7]" /> 男孩</a-radio> <a-radio value="男">
<a-radio value="女"><UserOutlined class="mr-1 text-[#f5576c]" /> 女孩</a-radio> <UserOutlined class="mr-1 text-[#4FC3F7]" /> 男孩
</a-radio>
<a-radio value="女">
<UserOutlined class="mr-1 text-[#f5576c]" /> 女孩
</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item label="出生日期" name="birthDate"> <a-form-item label="出生日期" name="birthDate">
<a-date-picker <a-date-picker v-model:value="formState.birthDate" class="w-full" value-format="YYYY-MM-DD"
v-model:value="formState.birthDate" placeholder="选择出生日期" />
class="w-full"
value-format="YYYY-MM-DD"
placeholder="选择出生日期"
/>
</a-form-item> </a-form-item>
<a-form-item label="所在班级" name="classId"> <a-form-item label="所在班级" name="classId">
<a-select v-model:value="formState.classId" placeholder="请选择班级" :loading="classesLoading"> <a-select v-model:value="formState.classId" placeholder="请选择班级" :loading="classesLoading">
@ -209,26 +189,24 @@
</a-form-item> </a-form-item>
<a-form-item label="家长姓名" name="parentName"> <a-form-item label="家长姓名" name="parentName">
<a-input v-model:value="formState.parentName" placeholder="请输入家长姓名"> <a-input v-model:value="formState.parentName" placeholder="请输入家长姓名">
<template #prefix><TeamOutlined class="text-[#B2BEC3]" /></template> <template #prefix>
<TeamOutlined class="text-[#B2BEC3]" />
</template>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item label="家长电话" name="parentPhone"> <a-form-item label="家长电话" name="parentPhone">
<a-input v-model:value="formState.parentPhone" placeholder="请输入家长电话"> <a-input v-model:value="formState.parentPhone" placeholder="请输入家长电话">
<template #prefix><PhoneOutlined class="text-[#B2BEC3]" /></template> <template #prefix>
<PhoneOutlined class="text-[#B2BEC3]" />
</template>
</a-input> </a-input>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
<!-- 学生调班模态框 --> <!-- 学生调班模态框 -->
<a-modal <a-modal v-model:open="transferModalVisible" title="学生调班" :confirm-loading="transferSubmitting"
v-model:open="transferModalVisible" @ok="handleTransferSubmit" @cancel="transferModalVisible = false" width="480px">
title="学生调班"
:confirm-loading="transferSubmitting"
@ok="handleTransferSubmit"
@cancel="transferModalVisible = false"
width="480px"
>
<div class="py-2 transfer-modal-content"> <div class="py-2 transfer-modal-content">
<div class="py-3 px-4 bg-[#F8F9FA] rounded-lg mb-4 current-info"> <div class="py-3 px-4 bg-[#F8F9FA] rounded-lg mb-4 current-info">
<span>当前学生</span> <span>当前学生</span>
@ -237,22 +215,14 @@
</div> </div>
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item label="目标班级" required> <a-form-item label="目标班级" required>
<a-select <a-select v-model:value="transferTargetClassId" placeholder="请选择目标班级" :loading="classesLoading">
v-model:value="transferTargetClassId"
placeholder="请选择目标班级"
:loading="classesLoading"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id"> <a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }} ({{ cls.grade }}) {{ cls.name }} ({{ cls.grade }})
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="调班原因"> <a-form-item label="调班原因">
<a-textarea <a-textarea v-model:value="transferReason" placeholder="请输入调班原因(选填)" :rows="3" />
v-model:value="transferReason"
placeholder="请输入调班原因(选填)"
:rows="3"
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -261,7 +231,8 @@
<HistoryOutlined /> 调班历史 <HistoryOutlined /> 调班历史
</div> </div>
<div class="max-h-[150px] overflow-y-auto history-list"> <div class="max-h-[150px] overflow-y-auto history-list">
<div v-for="h in transferHistory" :key="h.id" class="flex items-center gap-2 py-2 px-3 bg-[#F8F9FA] rounded-md mb-2 text-[13px] history-item"> <div v-for="h in transferHistory" :key="h.id"
class="flex items-center gap-2 py-2 px-3 bg-[#F8F9FA] rounded-md mb-2 text-[13px] history-item">
<span class="text-[#888]">{{ h.fromClass?.name || '无' }}</span> <span class="text-[#888]">{{ h.fromClass?.name || '无' }}</span>
<span class="text-[#B2BEC3]"></span> <span class="text-[#B2BEC3]"></span>
<span class="text-[#2D3436] font-500">{{ h.toClass.name }}</span> <span class="text-[#2D3436] font-500">{{ h.toClass.name }}</span>
@ -273,11 +244,7 @@
</a-modal> </a-modal>
<!-- 批量导入模态框 --> <!-- 批量导入模态框 -->
<a-modal <a-modal v-model:open="importModalVisible" :footer="null" width="560px">
v-model:open="importModalVisible"
:footer="null"
width="560px"
>
<template #title> <template #title>
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<DownloadOutlined class="text-[#f5576c]" /> <DownloadOutlined class="text-[#f5576c]" />
@ -295,17 +262,15 @@
</ol> </ol>
</div> </div>
<a-button class="rounded-xl border-2 border-dashed border-[#f093fb] text-[#f5576c] hover:border-[#f5576c] hover:text-[#f5576c] hover:bg-[#FFF0F5]" @click="downloadTemplate"> <a-button
class="rounded-xl border-2 border-dashed border-[#f093fb] text-[#f5576c] hover:border-[#f5576c] hover:text-[#f5576c] hover:bg-[#FFF0F5]"
@click="downloadTemplate">
<DownloadOutlined class="mr-2" /> <DownloadOutlined class="mr-2" />
下载导入模板 下载导入模板
</a-button> </a-button>
<a-upload-dragger <a-upload-dragger :before-upload="beforeUpload" :show-upload-list="false" accept=".xlsx,.xls,.csv"
:before-upload="beforeUpload" class="upload-area rounded-xl">
:show-upload-list="false"
accept=".xlsx,.xls,.csv"
class="upload-area rounded-xl"
>
<div class="py-6 text-center upload-content"> <div class="py-6 text-center upload-content">
<FileAddOutlined class="block text-[48px] text-[#f5576c] mb-3" /> <FileAddOutlined class="block text-[48px] text-[#f5576c] mb-3" />
<p class="text-sm text-[#636E72] mb-1">点击或拖拽文件到此区域上传</p> <p class="text-sm text-[#636E72] mb-1">点击或拖拽文件到此区域上传</p>
@ -321,26 +286,15 @@
<div v-if="importFile" class="mt-2 default-class-select"> <div v-if="importFile" class="mt-2 default-class-select">
<label class="block mb-2 text-[13px] text-[#636E72]">默认班级用于未指定班级的学生</label> <label class="block mb-2 text-[13px] text-[#636E72]">默认班级用于未指定班级的学生</label>
<a-select <a-select v-model:value="importDefaultClassId" placeholder="选择默认班级" class="w-full" allow-clear>
v-model:value="importDefaultClassId"
placeholder="选择默认班级"
class="w-full"
allow-clear
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id"> <a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }} {{ cls.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
</div> </div>
<a-button <a-button type="primary" :loading="importing" :disabled="!importFile" @click="handleImport" block
type="primary" class="h-11 rounded-xl text-[15px] font-600 !bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] hover:!bg-[linear-gradient(135deg,#e080e8_0%,#e04a5d_100%)] !border-0">
:loading="importing"
:disabled="!importFile"
@click="handleImport"
block
class="h-11 rounded-xl text-[15px] font-600 !bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] hover:!bg-[linear-gradient(135deg,#e080e8_0%,#e04a5d_100%)] !border-0"
>
<template v-if="!importing"> <template v-if="!importing">
<RocketOutlined class="mr-2" /> <RocketOutlined class="mr-2" />
开始导入 开始导入
@ -348,16 +302,21 @@
<span v-else>导入中...</span> <span v-else>导入中...</span>
</a-button> </a-button>
<div v-if="importResult" class="p-4 rounded-xl import-result" :class="importResult.failed === 0 ? 'bg-[#E8F5E9]' : 'bg-[#FFF8E1]'"> <div v-if="importResult" class="p-4 rounded-xl import-result"
:class="importResult.failed === 0 ? 'bg-[#E8F5E9]' : 'bg-[#FFF8E1]'">
<div class="flex items-center gap-2 font-600 mb-3 result-header"> <div class="flex items-center gap-2 font-600 mb-3 result-header">
<component :is="importResult.failed === 0 ? CheckCircleOutlined : WarningOutlined" class="text-xl" :class="importResult.failed === 0 ? 'text-[#43A047]' : 'text-[#FF8C42]'" /> <component :is="importResult.failed === 0 ? CheckCircleOutlined : WarningOutlined" class="text-xl"
:class="importResult.failed === 0 ? 'text-[#43A047]' : 'text-[#FF8C42]'" />
<span>导入完成</span> <span>导入完成</span>
</div> </div>
<div class="flex gap-4 mb-3 result-stats"> <div class="flex gap-4 mb-3 result-stats">
<span class="text-[13px]" :class="importResult.failed === 0 ? 'text-[#43A047]' : ''">成功 {{ importResult.success }} </span> <span class="text-[13px]" :class="importResult.failed === 0 ? 'text-[#43A047]' : ''">成功 {{
importResult.success
}} </span>
<span class="text-[13px] text-[#E53935]">失败 {{ importResult.failed }} </span> <span class="text-[13px] text-[#E53935]">失败 {{ importResult.failed }} </span>
</div> </div>
<div v-if="importResult.errors.length > 0" class="max-h-[150px] overflow-y-auto py-3 px-3 bg-white rounded-lg text-xs text-[#636E72] result-errors"> <div v-if="importResult.errors.length > 0"
class="max-h-[150px] overflow-y-auto py-3 px-3 bg-white rounded-lg text-xs text-[#636E72] result-errors">
<p v-for="(error, index) in importResult.errors" :key="index" class="my-1"> <p v-for="(error, index) in importResult.errors" :key="index" class="my-1">
{{ error.row }} {{ error.message }} {{ error.row }} {{ error.message }}
</p> </p>
@ -371,7 +330,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, onMounted } from 'vue';
import { import {
SearchOutlined,
TeamOutlined, TeamOutlined,
UserOutlined, UserOutlined,
EditOutlined, EditOutlined,
@ -792,11 +750,11 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.filters :deep(.ant-select-selector), .filters :deep(.ant-select-selector) {
.filters :deep(.ant-input-affix-wrapper) {
border-radius: 12px; border-radius: 12px;
border: 2px solid #F0F0F0; border: 2px solid #F0F0F0;
} }
.filters :deep(.ant-select-selector:hover), .filters :deep(.ant-select-selector:hover),
.filters :deep(.ant-input-affix-wrapper:hover) { .filters :deep(.ant-input-affix-wrapper:hover) {
border-color: #f093fb; border-color: #f093fb;
@ -812,8 +770,8 @@ onMounted(() => {
border: 2px dashed #E0E0E0; border: 2px dashed #E0E0E0;
background: #FAFAFA; background: #FAFAFA;
} }
.upload-area :deep(.ant-upload-drag:hover) { .upload-area :deep(.ant-upload-drag:hover) {
border-color: #f093fb; border-color: #f093fb;
} }
</style> </style>

View File

@ -62,6 +62,7 @@
class="w-full md:w-[200px]" class="w-full md:w-[200px]"
@search="loadTasks" @search="loadTasks"
allow-clear allow-clear
:enter-button="false"
/> />
</div> </div>

View File

@ -32,6 +32,8 @@
placeholder="搜索模板名称" placeholder="搜索模板名称"
class="w-[250px]" class="w-[250px]"
@search="loadTemplates" @search="loadTemplates"
allow-clear
:enter-button="false"
/> />
</div> </div>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]"> <div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]"> <div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
<div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4"> <div class="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4">
@ -26,46 +26,38 @@
</div> </div>
<!-- 操作栏 --> <!-- 操作栏 -->
<div class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] max-md:flex-col max-md:gap-3"> <div
<div class="search-box"> class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] max-md:flex-col max-md:gap-3">
<a-input-search <div class="">
v-model:value="searchKeyword" <a-input-search v-model:value="searchKeyword" placeholder="搜索教师姓名/手机号/账号" class="w-[280px]"
placeholder="搜索教师姓名/手机号/账号" @search="handleSearch" allow-clear :enter-button="false" />
class="w-[280px]"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
</div> </div>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] hover:!bg-[linear-gradient(135deg,#FF7A2A_0%,#FFA030_100%)] !border-0 rounded-xl h-10 px-6 font-600" @click="showAddModal"> <a-button type="primary"
class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] hover:!bg-[linear-gradient(135deg,#FF7A2A_0%,#FFA030_100%)] !border-0 rounded-xl h-10 px-6 font-600"
@click="showAddModal">
<PlusOutlined class="mr-2 text-sm" /> <PlusOutlined class="mr-2 text-sm" />
添加教师 添加教师
</a-button> </a-button>
</div> </div>
<!-- 教师卡片列表 --> <!-- 教师卡片列表 -->
<div class="grid gap-5 mb-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]" v-if="!loading && teachers.length > 0"> <div class="grid gap-5 mb-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]"
<div v-if="!loading && teachers.length > 0">
v-for="teacher in teachers" <div v-for="teacher in teachers" :key="teacher.id"
:key="teacher.id"
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-2 border-transparent hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] hover:border-[#FF8C42]" class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-2 border-transparent hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] hover:border-[#FF8C42]"
:class="teacher.status !== 'ACTIVE' ? 'opacity-70' : ''" :class="teacher.status !== 'ACTIVE' ? 'opacity-70' : ''">
> <div
<div class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]"> class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]"> <div
class="w-12 h-12 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
<SolutionOutlined class="text-2xl text-white" /> <SolutionOutlined class="text-2xl text-white" />
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="text-base font-600 text-[#2D3436]">{{ teacher.name }}</div> <div class="text-base font-600 text-[#2D3436]">{{ teacher.name }}</div>
<div class="text-xs text-[#636E72] mt-0.5">@{{ teacher.loginAccount }}</div> <div class="text-xs text-[#636E72] mt-0.5">@{{ teacher.loginAccount }}</div>
</div> </div>
<span <span class="py-1 px-3 rounded-[20px] text-xs font-500"
class="py-1 px-3 rounded-[20px] text-xs font-500" :class="teacher.status === 'ACTIVE' ? 'bg-[#E8F5E9] text-[#43A047]' : 'bg-[#FFEBEE] text-[#E53935]'">
:class="teacher.status === 'ACTIVE' ? 'bg-[#E8F5E9] text-[#43A047]' : 'bg-[#FFEBEE] text-[#E53935]'"
>
{{ teacher.status === 'ACTIVE' ? '在职' : '离职' }} {{ teacher.status === 'ACTIVE' ? '在职' : '离职' }}
</span> </span>
</div> </div>
@ -82,30 +74,32 @@
<div class="flex items-center gap-2 mb-2 text-[13px]"> <div class="flex items-center gap-2 mb-2 text-[13px]">
<BankOutlined class="text-sm text-[#667eea]" /> <BankOutlined class="text-sm text-[#667eea]" />
<span class="text-[#636E72] classes-tag"> <span class="text-[#636E72] classes-tag">
<span v-if="teacher.classNames && (Array.isArray(teacher.classNames) ? teacher.classNames.length > 0 : teacher.classNames)"> <span
v-if="teacher.classNames && (Array.isArray(teacher.classNames) ? teacher.classNames.length > 0 : teacher.classNames)">
{{ Array.isArray(teacher.classNames) ? teacher.classNames.slice(0, 2).join('、') : teacher.classNames }} {{ Array.isArray(teacher.classNames) ? teacher.classNames.slice(0, 2).join('、') : teacher.classNames }}
<span v-if="Array.isArray(teacher.classNames) && teacher.classNames.length > 2">{{ teacher.classNames.length }}个班级</span> <span v-if="Array.isArray(teacher.classNames) && teacher.classNames.length > 2">{{
teacher.classNames.length }}个班级</span>
</span> </span>
<span v-else class="text-[#B2BEC3] italic">未分配班级</span> <span v-else class="text-[#B2BEC3] italic">未分配班级</span>
</span> </span>
</div> </div>
<div class="flex items-center gap-2 text-[13px]"> <div class="flex items-center gap-2 text-[13px]">
<BookOutlined class="text-sm text-[#667eea]" /> <BookOutlined class="text-sm text-[#667eea]" />
<span class="text-[#636E72]">授课 <strong class="text-[#FF8C42]">{{ teacher.lessonCount || 0 }}</strong> </span> <span class="text-[#636E72]">授课 <strong class="text-[#FF8C42]">{{ teacher.lessonCount || 0 }}</strong>
</span>
</div> </div>
</div> </div>
<div class="flex justify-end gap-2 py-3 px-5 border-t border-[#F0F0F0] bg-[#FAFAFA] card-actions"> <div class="flex justify-end gap-2 py-3 px-5 border-t border-[#F0F0F0] bg-[#FAFAFA] card-actions">
<a-button type="link" size="small" @click="handleEdit(teacher)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1"> <a-button type="link" size="small" @click="handleEdit(teacher)"
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<EditOutlined /> 编辑 <EditOutlined /> 编辑
</a-button> </a-button>
<a-button type="link" size="small" @click="handleResetPassword(teacher)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1"> <a-button type="link" size="small" @click="handleResetPassword(teacher)"
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<KeyOutlined /> 重置密码 <KeyOutlined /> 重置密码
</a-button> </a-button>
<a-popconfirm <a-popconfirm title="确定要删除这位教师吗?" @confirm="handleDelete(teacher.id)">
title="确定要删除这位教师吗?"
@confirm="handleDelete(teacher.id)"
>
<a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto"> <a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto">
<DeleteOutlined /> 删除 <DeleteOutlined /> 删除
</a-button> </a-button>
@ -115,10 +109,12 @@
</div> </div>
<!-- 空状态 --> <!-- 空状态 -->
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && teachers.length === 0"> <div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state"
v-if="!loading && teachers.length === 0">
<InboxOutlined class="text-[64px] text-[#B2BEC3] mb-4" /> <InboxOutlined class="text-[64px] text-[#B2BEC3] mb-4" />
<p class="text-[#636E72] text-base mb-6">暂无教师数据</p> <p class="text-[#636E72] text-base mb-6">暂无教师数据</p>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] !border-0 rounded-xl" @click="showAddModal"> <a-button type="primary" class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] !border-0 rounded-xl"
@click="showAddModal">
添加第一位教师 添加第一位教师
</a-button> </a-button>
</div> </div>
@ -131,25 +127,14 @@
<!-- 分页 --> <!-- 分页 -->
<div class="flex justify-center py-6 bg-white rounded-2xl pagination-wrapper" v-if="teachers.length > 0"> <div class="flex justify-center py-6 bg-white rounded-2xl pagination-wrapper" v-if="teachers.length > 0">
<a-pagination <a-pagination v-model:current="pagination.current" v-model:pageSize="pagination.pageSize"
v-model:current="pagination.current" :total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`"
v-model:pageSize="pagination.pageSize" @change="handlePageChange" />
:total="pagination.total"
:show-size-changer="true"
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
/>
</div> </div>
<!-- 添加/编辑教师模态框 --> <!-- 添加/编辑教师模态框 -->
<a-modal <a-modal v-model:open="modalVisible" :title="isEdit ? '编辑教师' : '添加教师'" @ok="handleModalOk"
v-model:open="modalVisible" @cancel="handleModalCancel" :confirm-loading="submitting" :width="520">
:title="isEdit ? '编辑教师' : '添加教师'"
@ok="handleModalOk"
@cancel="handleModalCancel"
:confirm-loading="submitting"
:width="520"
>
<template #title> <template #title>
<span class="flex items-center gap-2 modal-title"> <span class="flex items-center gap-2 modal-title">
<EditOutlined v-if="isEdit" class="text-[#667eea]" /> <EditOutlined v-if="isEdit" class="text-[#667eea]" />
@ -157,49 +142,44 @@
{{ isEdit ? '编辑教师' : '添加教师' }} {{ isEdit ? '编辑教师' : '添加教师' }}
</span> </span>
</template> </template>
<a-form <a-form ref="formRef" :model="formState" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="姓名" name="name"> <a-form-item label="姓名" name="name">
<a-input v-model:value="formState.name" placeholder="请输入教师姓名"> <a-input v-model:value="formState.name" placeholder="请输入教师姓名">
<template #prefix><UserOutlined class="text-[#B2BEC3]" /></template> <template #prefix>
<UserOutlined class="text-[#B2BEC3]" />
</template>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item label="手机号" name="phone"> <a-form-item label="手机号" name="phone">
<a-input v-model:value="formState.phone" placeholder="请输入手机号"> <a-input v-model:value="formState.phone" placeholder="请输入手机号">
<template #prefix><PhoneOutlined class="text-[#B2BEC3]" /></template> <template #prefix>
<PhoneOutlined class="text-[#B2BEC3]" />
</template>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item label="邮箱" name="email"> <a-form-item label="邮箱" name="email">
<a-input v-model:value="formState.email" placeholder="请输入邮箱(可选)"> <a-input v-model:value="formState.email" placeholder="请输入邮箱(可选)">
<template #prefix><MailOutlined class="text-[#B2BEC3]" /></template> <template #prefix>
<MailOutlined class="text-[#B2BEC3]" />
</template>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item label="登录账号" name="loginAccount"> <a-form-item label="登录账号" name="loginAccount">
<a-input <a-input v-model:value="formState.loginAccount" placeholder="请输入登录账号" :disabled="isEdit">
v-model:value="formState.loginAccount" <template #prefix>
placeholder="请输入登录账号" <KeyOutlined class="text-[#B2BEC3]" />
:disabled="isEdit" </template>
>
<template #prefix><KeyOutlined class="text-[#B2BEC3]" /></template>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="!isEdit" label="密码" name="password"> <a-form-item v-if="!isEdit" label="密码" name="password">
<a-input-password v-model:value="formState.password" placeholder="请输入密码默认123456"> <a-input-password v-model:value="formState.password" placeholder="请输入密码默认123456">
<template #prefix><LockOutlined class="text-[#B2BEC3]" /></template> <template #prefix>
<LockOutlined class="text-[#B2BEC3]" />
</template>
</a-input-password> </a-input-password>
</a-form-item> </a-form-item>
<a-form-item label="负责班级" name="classIds"> <a-form-item label="负责班级" name="classIds">
<a-select <a-select v-model:value="formState.classIds" mode="multiple" placeholder="请选择负责的班级" :loading="classesLoading">
v-model:value="formState.classIds"
mode="multiple"
placeholder="请选择负责的班级"
:loading="classesLoading"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id"> <a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }} {{ cls.name }}
</a-select-option> </a-select-option>
@ -209,12 +189,7 @@
</a-modal> </a-modal>
<!-- 重置密码确认模态框 --> <!-- 重置密码确认模态框 -->
<a-modal <a-modal v-model:open="resetPasswordVisible" @ok="confirmResetPassword" :confirm-loading="resetting" :width="400">
v-model:open="resetPasswordVisible"
@ok="confirmResetPassword"
:confirm-loading="resetting"
:width="400"
>
<template #title> <template #title>
<span class="flex items-center gap-2 modal-title"> <span class="flex items-center gap-2 modal-title">
<KeyOutlined class="text-[#667eea]" /> <KeyOutlined class="text-[#667eea]" />
@ -228,7 +203,8 @@
</div> </div>
<div v-if="newPassword" class="new-password-box"> <div v-if="newPassword" class="new-password-box">
<p class="mb-2 text-[#636E72]">新密码</p> <p class="mb-2 text-[#636E72]">新密码</p>
<div class="p-4 rounded-xl font-bold text-2xl text-white bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] password-display"> <div
class="p-4 rounded-xl font-bold text-2xl text-white bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] password-display">
<a-typography-text copyable class="!text-white">{{ newPassword }}</a-typography-text> <a-typography-text copyable class="!text-white">{{ newPassword }}</a-typography-text>
</div> </div>
</div> </div>
@ -240,7 +216,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, onMounted } from 'vue';
import { import {
SearchOutlined,
SolutionOutlined, SolutionOutlined,
PlusOutlined, PlusOutlined,
PhoneOutlined, PhoneOutlined,
@ -472,6 +447,7 @@ onMounted(() => {
border-radius: 12px; border-radius: 12px;
border: 2px solid #F0F0F0; border: 2px solid #F0F0F0;
} }
.search-box :deep(.ant-input-affix-wrapper:hover) { .search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #FF8C42; border-color: #FF8C42;
} }