Compare commits

...

3 Commits

Author SHA1 Message Date
zhonghua
a986709776 课程包详情优化 2026-03-09 17:24:43 +08:00
zhonghua
bab12cbed3 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
2026-03-06 11:32:05 +08:00
zhonghua
c8ad14449b test 2026-03-04 16:10:34 +08:00
16 changed files with 321 additions and 493 deletions

View File

@ -11,35 +11,22 @@ declare module 'vue' {
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
AList: typeof import('ant-design-vue/es')['List']
AListItem: typeof import('ant-design-vue/es')['ListItem']
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
@ -48,34 +35,20 @@ declare module 'vue' {
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ARate: typeof import('ant-design-vue/es')['Rate']
AResult: typeof import('ant-design-vue/es')['Result']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
AStep: typeof import('ant-design-vue/es')['Step']
ASteps: typeof import('ant-design-vue/es')['Steps']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
AUpload: typeof import('ant-design-vue/es')['Upload']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
FileUploader: typeof import('./components/course/FileUploader.vue')['default']
LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default']

View File

@ -1,8 +1,8 @@
<template>
<div class="course-detail-view">
<!-- 顶部导航 -->
<div class="detail-header">
<div class="header-left">
<div class="detail-header flex flex-wrap items-center">
<div class="header-left flex-shrink-0">
<a-button type="text" @click="router.back()">
<ArrowLeftOutlined />
</a-button>
@ -788,7 +788,9 @@ const fetchCourseDetail = async () => {
background: white;
border-bottom: 1px solid #f0f0f0;
position: sticky;
top: 0;
padding-top: 1.5rem;
top: -1.5rem;
margin-top: -1.5rem;
z-index: 100;
.header-left {

View File

@ -1,10 +1,11 @@
<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="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="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 />
</div>
<div>
@ -26,48 +27,36 @@
</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 class="search-box">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索班级名称"
class="w-[250px]"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
<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 class="">
<a-input-search v-model:value="searchKeyword" placeholder="搜索班级名称" class="w-[250px]" @search="handleSearch"
allow-clear :enter-button="false" />
</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" />
添加班级
</a-button>
</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
v-for="cls in classes"
:key="cls.id"
<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 v-for="cls in classes" :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="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="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%)]'"
>
<div 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%)]'">
<component :is="getGradeIcon(cls.grade)" class="text-2xl text-white" />
</div>
<div class="flex-1">
<div class="text-base font-600 text-[#2D3436]">{{ cls.name }}</div>
<div class="mt-1">
<span
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]'"
>
<span 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]'">
{{ cls.grade }}
</span>
</div>
@ -76,13 +65,15 @@
<div class="py-4 px-5 card-body">
<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]" />
</div>
<div class="text-[#636E72] flex flex-wrap items-center gap-0.5 teachers-value">
<template v-if="cls.teachers && cls.teachers.length > 0">
<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>
<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 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]" />
</div>
<span class="text-[#636E72]">
@ -99,7 +91,8 @@
</span>
</div>
<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]" />
</div>
<span class="text-[#636E72]">
@ -110,29 +103,30 @@
<div class="py-3 px-5 bg-[#FAFAFA] card-footer">
<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 class="text-[11px] text-[#B2BEC3] mt-1">班级活跃度</div>
</div>
<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 />
学生
</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 />
教师
</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 />
编辑
</a-button>
<a-popconfirm
title="确定要删除这个班级吗?"
:disabled="cls.studentCount > 0"
@confirm="handleDelete(cls.id)"
>
<a-popconfirm title="确定要删除这个班级吗?" :disabled="cls.studentCount > 0" @confirm="handleDelete(cls.id)">
<a-tooltip v-if="cls.studentCount > 0" title="班级内有学生,无法删除">
<a-button type="link" size="small" disabled class="!py-1 !px-2 !h-auto opacity-50">
<DeleteOutlined />
@ -149,12 +143,15 @@
</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="w-20 h-20 rounded-[20px] flex items-center justify-center mb-4 bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] empty-icon">
<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="w-20 h-20 rounded-[20px] flex items-center justify-center mb-4 bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] empty-icon">
<BankOutlined />
</div>
<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>
</div>
@ -166,21 +163,9 @@
</div>
<!-- 添加/编辑班级模态框 -->
<a-modal
v-model:open="modalVisible"
:title="isEdit ? modalEditTitle : modalAddTitle"
@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-modal v-model:open="modalVisible" :title="isEdit ? modalEditTitle : modalAddTitle" @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-input v-model:value="formState.name" placeholder="请输入班级名称">
<template #prefix>
@ -211,12 +196,7 @@
</a-select>
</a-form-item>
<a-form-item label="班主任" name="teacherId">
<a-select
v-model:value="formState.teacherId"
placeholder="请选择班主任"
:loading="teachersLoading"
allow-clear
>
<a-select v-model:value="formState.teacherId" placeholder="请选择班主任" :loading="teachersLoading" allow-clear>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
@ -226,15 +206,12 @@
</a-modal>
<!-- 班级学生列表模态框 -->
<a-modal
v-model:open="studentsModalVisible"
:title="studentsModalTitle"
:footer="null"
width="700px"
>
<a-modal v-model:open="studentsModalVisible" :title="studentsModalTitle" :footer="null" width="700px">
<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 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%)]">
<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
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" />
</div>
<div class="flex-1">
@ -250,15 +227,10 @@
</div>
<div class="grid grid-cols-2 gap-3 students-list" v-if="!studentsLoading && classStudents.length > 0">
<div
v-for="student in classStudents"
:key="student.id"
class="flex items-center gap-3 p-3 bg-[#F8F9FA] rounded-xl student-item"
>
<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%)]'"
>
<div v-for="student in classStudents" :key="student.id"
class="flex items-center gap-3 p-3 bg-[#F8F9FA] rounded-xl student-item">
<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" />
<GirlOutlined v-else class="text-base text-white" />
</div>
@ -272,8 +244,10 @@
</div>
</div>
<div class="flex flex-col items-center py-10 empty-students" 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%)]">
<div class="flex flex-col items-center py-10 empty-students"
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]" />
</div>
<p class="text-[#636E72] mt-2">该班级暂无学生</p>
@ -284,36 +258,24 @@
<p class="text-[#636E72] mt-2">加载学生列表...</p>
</div>
<div class="flex justify-center mt-5 pt-4 border-t border-[#F0F0F0] pagination-wrapper" v-if="studentsPagination.total > studentsPagination.pageSize">
<a-pagination
v-model:current="studentsPagination.current"
v-model:pageSize="studentsPagination.pageSize"
:total="studentsPagination.total"
:show-total="(total: number) => `共 ${total} 条`"
size="small"
@change="handleStudentsPageChange"
/>
<div class="flex justify-center mt-5 pt-4 border-t border-[#F0F0F0] pagination-wrapper"
v-if="studentsPagination.total > studentsPagination.pageSize">
<a-pagination v-model:current="studentsPagination.current" v-model:pageSize="studentsPagination.pageSize"
:total="studentsPagination.total" :show-total="(total: number) => `共 ${total} 条`" size="small"
@change="handleStudentsPageChange" />
</div>
</div>
</a-modal>
<!-- 班级教师管理模态框 -->
<a-modal
v-model:open="teachersModalVisible"
:title="`管理 ${currentClass?.name || ''} 教师团队`"
:footer="null"
width="600px"
>
<a-modal v-model:open="teachersModalVisible" :title="`管理 ${currentClass?.name || ''} 教师团队`" :footer="null"
width="600px">
<div class="p-0">
<!-- 添加教师表单 -->
<div class="p-4 bg-[#F8F9FA] rounded-xl mb-4 add-teacher-form">
<div class="flex items-center gap-3 form-row">
<a-select
v-model:value="teacherFormState.teacherId"
placeholder="选择教师"
class="w-[150px]"
:loading="teachersLoading"
>
<a-select v-model:value="teacherFormState.teacherId" placeholder="选择教师" class="w-[150px]"
:loading="teachersLoading">
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
@ -332,23 +294,18 @@
<!-- 教师列表 -->
<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">
<span class="font-500 text-[#2D3436]">{{ teacher.teacherName }}</span>
<a-select
v-model:value="teacher.role"
size="small"
class="w-20 ml-2"
@change="handleUpdateTeacherRole(teacher)"
>
<a-select v-model:value="teacher.role" size="small" class="w-20 ml-2"
@change="handleUpdateTeacherRole(teacher)">
<a-select-option value="MAIN">主班</a-select-option>
<a-select-option value="ASSIST">配班</a-select-option>
<a-select-option value="CARE">保育员</a-select-option>
</a-select>
<a-checkbox
v-model:checked="teacher.isPrimary"
@change="handleUpdateTeacherRole(teacher)"
>班主任</a-checkbox>
<a-checkbox v-model:checked="teacher.isPrimary"
@change="handleUpdateTeacherRole(teacher)">班主任</a-checkbox>
</div>
<a-button type="link" danger size="small" @click="handleRemoveTeacher(teacher.teacherId)">
移除
@ -370,7 +327,6 @@ import {
HomeOutlined,
BankOutlined,
PlusOutlined,
SearchOutlined,
EditOutlined,
DeleteOutlined,
TeamOutlined,
@ -758,6 +714,7 @@ onMounted(() => {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #4facfe;
}
@ -784,13 +741,16 @@ onMounted(() => {
.class-grid {
grid-template-columns: 1fr !important;
}
.students-list {
grid-template-columns: 1fr;
}
.search-box-wrapper {
flex-direction: column;
gap: 12px;
}
.search-box :deep(.ant-input-search) {
width: 100% !important;
}

View File

@ -20,7 +20,7 @@
</div>
<a-spin :spinning="loading">
<div class="py-6 px-6 max-w-[1400px] mx-auto">
<div class="px-4 py-4 md:px-6 md:py-6">
<!-- 封面和基本信息 -->
<div class="mb-6 text-center" v-if="course.coverImagePath">
<img :src="getFileUrl(course.coverImagePath)" alt="课程封面"

View File

@ -1,5 +1,5 @@
<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="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4">
@ -149,11 +149,14 @@
</template>
<div class="flex flex-col gap-5">
<div class="auth-search">
<a-input-search v-model:value="searchKeyword" placeholder="输入课程名称搜索..." @search="searchCourses" size="large">
<template #prefix>
<SearchOutlined />
</template>
</a-input-search>
<a-input-search
v-model:value="searchKeyword"
placeholder="输入课程名称搜索..."
@search="searchCourses"
size="large"
allow-clear
:enter-button="false"
/>
</div>
<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 { useRouter } from 'vue-router';
import {
SearchOutlined,
BookOutlined,
ReadOutlined,
StarFilled,

View File

@ -1,5 +1,5 @@
<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="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]"
@search="handleFilter"
allow-clear
:enter-button="false"
/>
</div>
</div>

View File

@ -1,5 +1,5 @@
<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="flex justify-between items-center max-md:flex-col max-md:gap-4 max-md:items-start">
@ -41,6 +41,7 @@
class="w-[200px]"
@search="handleFilter"
allow-clear
:enter-button="false"
/>
</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">

View File

@ -1,5 +1,5 @@
<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="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">
<div class="search-box">
<a-input-search v-model:value="searchKeyword" placeholder="搜索家长姓名/手机号/账号" class="w-[280px]"
@search="handleSearch" allow-clear>
<template #prefix>
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
@search="handleSearch" allow-clear :enter-button="false" />
</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"
@ -79,7 +75,7 @@
<TeamOutlined class="text-sm text-[#FF8C42]" />
<span class="text-[#636E72]">
<span v-if="parent.childrenCount > 0">关联 <strong class="text-[#FF8C42]">{{ parent.childrenCount
}}</strong>
}}</strong>
个孩子</span>
<span v-else class="text-[#B2BEC3] italic">未关联孩子</span>
</span>
@ -246,7 +242,7 @@
<div class="flex gap-3 mb-4 select-search-bar">
<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
@change="handleStudentSearch">
<a-select-option v-for="cls in classOptions" :key="cls.id" :value="cls.id">
@ -316,7 +312,6 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import {
SearchOutlined,
IdcardOutlined,
PlusOutlined,
PhoneOutlined,
@ -692,11 +687,6 @@ onMounted(() => {
</script>
<style scoped>
.search-box :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #FF8C42;
}

View File

@ -24,7 +24,7 @@
</div>
<a-spin :spinning="loading">
<div class="p-6 max-w-[1200px] mx-auto">
<div class="px-4 py-4 md:px-6 md:py-6">
<!-- 基本信息 -->
<div class="bg-white rounded-xl overflow-hidden shadow-[0_2px_8px_rgba(0,0,0,0.06)] mb-6">
<div class="py-4 px-6 border-b border-[#f0f0f0] flex justify-between items-center">

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

View File

@ -1,5 +1,5 @@
<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="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4">
@ -19,48 +19,43 @@
</div>
<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="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 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="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 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">
<a-select
v-model:value="selectedClassId"
placeholder="选择班级"
class="w-[150px]"
@change="handleClassChange"
allow-clear
>
<a-select v-model:value="selectedClassId" placeholder="选择班级" class="w-[150px]" @change="handleClassChange"
allow-clear>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
</a-select>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索学生姓名/家长"
class="w-[220px] search-input-wrap"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
<a-input-search v-model:value="searchKeyword" placeholder="搜索学生姓名/家长" class="w-[220px] search-input-wrap"
@search="handleSearch" allow-clear :enter-button="false" />
</div>
<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" />
批量导入
</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" />
添加学生
</a-button>
@ -68,17 +63,14 @@
</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
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 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%)]'"
>
<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
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 />
</div>
<div class="flex-1 min-w-0">
@ -90,11 +82,10 @@
<div class="py-4 px-5 card-body">
<div class="flex items-center gap-2 mb-2 text-[13px]">
<CalendarOutlined class="text-sm text-[#f5576c] w-5" />
<span class="text-[#636E72] flex-1">{{ calculateAge(student.birthDate) || '--' }}{{ student.birthDate ? '岁' : '' }}</span>
<span
class="py-0.5 px-2 rounded-[10px] text-[11px]"
:class="normalizeGender(student.gender) === '男' ? 'bg-[#E3F2FD] text-[#1976D2]' : 'bg-[#FCE4EC] text-[#E91E63]'"
>
<span class="text-[#636E72] flex-1">{{ calculateAge(student.birthDate) || '--' }}{{ student.birthDate ? '岁'
: '' }}</span>
<span class="py-0.5 px-2 rounded-[10px] text-[11px]"
:class="normalizeGender(student.gender) === '男' ? 'bg-[#E3F2FD] text-[#1976D2]' : 'bg-[#FCE4EC] text-[#E91E63]'">
{{ normalizeGender(student.gender) }}
</span>
</div>
@ -108,21 +99,21 @@
</div>
<div class="flex items-center gap-2 text-[13px]">
<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 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 /> 编辑
</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 /> 调班
</a-button>
<a-popconfirm
title="确定要删除这位学生吗?"
@confirm="handleDelete(student.id)"
>
<a-popconfirm title="确定要删除这位学生吗?" @confirm="handleDelete(student.id)">
<a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto">
<DeleteOutlined /> 删除
</a-button>
@ -132,12 +123,15 @@
</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="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%)]">
<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="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 />
</div>
<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>
</div>
@ -150,55 +144,41 @@
<!-- 分页 -->
<div class="flex justify-center py-6 bg-white rounded-2xl pagination-wrapper" v-if="students.length > 0">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
:total="pagination.total"
:show-size-changer="true"
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
/>
<a-pagination v-model:current="pagination.current" v-model:pageSize="pagination.pageSize"
:total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange" />
</div>
<!-- 添加/编辑学生模态框 -->
<a-modal
v-model:open="modalVisible"
@ok="handleModalOk"
@cancel="handleModalCancel"
:confirm-loading="submitting"
:width="520"
>
<a-modal v-model:open="modalVisible" @ok="handleModalOk" @cancel="handleModalCancel" :confirm-loading="submitting"
:width="520">
<template #title>
<span class="flex items-center gap-2">
<component :is="isEdit ? EditOutlined : PlusOutlined" class="text-[#f5576c]" />
{{ isEdit ? '编辑学生' : '添加学生' }}
</span>
</template>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form ref="formRef" :model="formState" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="姓名" name="name">
<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-form-item>
<a-form-item label="性别" name="gender">
<a-radio-group v-model:value="formState.gender">
<a-radio value="男"><UserOutlined class="mr-1 text-[#4FC3F7]" /> 男孩</a-radio>
<a-radio value="女"><UserOutlined class="mr-1 text-[#f5576c]" /> 女孩</a-radio>
<a-radio value="男">
<UserOutlined class="mr-1 text-[#4FC3F7]" /> 男孩
</a-radio>
<a-radio value="女">
<UserOutlined class="mr-1 text-[#f5576c]" /> 女孩
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="出生日期" name="birthDate">
<a-date-picker
v-model:value="formState.birthDate"
class="w-full"
value-format="YYYY-MM-DD"
placeholder="选择出生日期"
/>
<a-date-picker v-model:value="formState.birthDate" class="w-full" value-format="YYYY-MM-DD"
placeholder="选择出生日期" />
</a-form-item>
<a-form-item label="所在班级" name="classId">
<a-select v-model:value="formState.classId" placeholder="请选择班级" :loading="classesLoading">
@ -209,26 +189,24 @@
</a-form-item>
<a-form-item label="家长姓名" name="parentName">
<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-form-item>
<a-form-item label="家长电话" name="parentPhone">
<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-form-item>
</a-form>
</a-modal>
<!-- 学生调班模态框 -->
<a-modal
v-model:open="transferModalVisible"
title="学生调班"
:confirm-loading="transferSubmitting"
@ok="handleTransferSubmit"
@cancel="transferModalVisible = false"
width="480px"
>
<a-modal v-model:open="transferModalVisible" title="学生调班" :confirm-loading="transferSubmitting"
@ok="handleTransferSubmit" @cancel="transferModalVisible = false" width="480px">
<div class="py-2 transfer-modal-content">
<div class="py-3 px-4 bg-[#F8F9FA] rounded-lg mb-4 current-info">
<span>当前学生</span>
@ -237,22 +215,14 @@
</div>
<a-form layout="vertical">
<a-form-item label="目标班级" required>
<a-select
v-model:value="transferTargetClassId"
placeholder="请选择目标班级"
:loading="classesLoading"
>
<a-select v-model:value="transferTargetClassId" placeholder="请选择目标班级" :loading="classesLoading">
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }} ({{ cls.grade }})
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="调班原因">
<a-textarea
v-model:value="transferReason"
placeholder="请输入调班原因(选填)"
:rows="3"
/>
<a-textarea v-model:value="transferReason" placeholder="请输入调班原因(选填)" :rows="3" />
</a-form-item>
</a-form>
@ -261,7 +231,8 @@
<HistoryOutlined /> 调班历史
</div>
<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-[#B2BEC3]"></span>
<span class="text-[#2D3436] font-500">{{ h.toClass.name }}</span>
@ -273,11 +244,7 @@
</a-modal>
<!-- 批量导入模态框 -->
<a-modal
v-model:open="importModalVisible"
:footer="null"
width="560px"
>
<a-modal v-model:open="importModalVisible" :footer="null" width="560px">
<template #title>
<span class="flex items-center gap-2">
<DownloadOutlined class="text-[#f5576c]" />
@ -295,17 +262,15 @@
</ol>
</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" />
下载导入模板
</a-button>
<a-upload-dragger
:before-upload="beforeUpload"
:show-upload-list="false"
accept=".xlsx,.xls,.csv"
class="upload-area rounded-xl"
>
<a-upload-dragger :before-upload="beforeUpload" :show-upload-list="false" accept=".xlsx,.xls,.csv"
class="upload-area rounded-xl">
<div class="py-6 text-center upload-content">
<FileAddOutlined class="block text-[48px] text-[#f5576c] mb-3" />
<p class="text-sm text-[#636E72] mb-1">点击或拖拽文件到此区域上传</p>
@ -321,26 +286,15 @@
<div v-if="importFile" class="mt-2 default-class-select">
<label class="block mb-2 text-[13px] text-[#636E72]">默认班级用于未指定班级的学生</label>
<a-select
v-model:value="importDefaultClassId"
placeholder="选择默认班级"
class="w-full"
allow-clear
>
<a-select v-model:value="importDefaultClassId" placeholder="选择默认班级" class="w-full" allow-clear>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
</a-select>
</div>
<a-button
type="primary"
: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"
>
<a-button type="primary" :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">
<RocketOutlined class="mr-2" />
开始导入
@ -348,16 +302,21 @@
<span v-else>导入中...</span>
</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">
<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>
</div>
<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>
</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">
{{ error.row }} {{ error.message }}
</p>
@ -371,7 +330,6 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import {
SearchOutlined,
TeamOutlined,
UserOutlined,
EditOutlined,
@ -792,11 +750,11 @@ onMounted(() => {
</script>
<style scoped>
.filters :deep(.ant-select-selector),
.filters :deep(.ant-input-affix-wrapper) {
.filters :deep(.ant-select-selector) {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.filters :deep(.ant-select-selector:hover),
.filters :deep(.ant-input-affix-wrapper:hover) {
border-color: #f093fb;
@ -812,8 +770,8 @@ onMounted(() => {
border: 2px dashed #E0E0E0;
background: #FAFAFA;
}
.upload-area :deep(.ant-upload-drag:hover) {
border-color: #f093fb;
}
</style>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<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="flex justify-between items-center max-md:flex-col max-md:items-start max-md:gap-4">
@ -26,46 +26,38 @@
</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="search-box">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索教师姓名/手机号/账号"
class="w-[280px]"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
<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="">
<a-input-search v-model:value="searchKeyword" placeholder="搜索教师姓名/手机号/账号" class="w-[280px]"
@search="handleSearch" allow-clear :enter-button="false" />
</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" />
添加教师
</a-button>
</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
v-for="teacher in teachers"
:key="teacher.id"
<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 v-for="teacher in teachers" :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="teacher.status !== 'ACTIVE' ? 'opacity-70' : ''"
>
<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-xl flex items-center justify-center bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
:class="teacher.status !== 'ACTIVE' ? 'opacity-70' : ''">
<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-xl flex items-center justify-center bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
<SolutionOutlined class="text-2xl text-white" />
</div>
<div class="flex-1 min-w-0">
<div class="text-base font-600 text-[#2D3436]">{{ teacher.name }}</div>
<div class="text-xs text-[#636E72] mt-0.5">@{{ teacher.loginAccount }}</div>
</div>
<span
class="py-1 px-3 rounded-[20px] text-xs font-500"
:class="teacher.status === 'ACTIVE' ? 'bg-[#E8F5E9] text-[#43A047]' : 'bg-[#FFEBEE] text-[#E53935]'"
>
<span class="py-1 px-3 rounded-[20px] text-xs font-500"
:class="teacher.status === 'ACTIVE' ? 'bg-[#E8F5E9] text-[#43A047]' : 'bg-[#FFEBEE] text-[#E53935]'">
{{ teacher.status === 'ACTIVE' ? '在职' : '离职' }}
</span>
</div>
@ -82,30 +74,32 @@
<div class="flex items-center gap-2 mb-2 text-[13px]">
<BankOutlined class="text-sm text-[#667eea]" />
<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 }}
<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 v-else class="text-[#B2BEC3] italic">未分配班级</span>
</span>
</div>
<div class="flex items-center gap-2 text-[13px]">
<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 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 /> 编辑
</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 /> 重置密码
</a-button>
<a-popconfirm
title="确定要删除这位教师吗?"
@confirm="handleDelete(teacher.id)"
>
<a-popconfirm title="确定要删除这位教师吗?" @confirm="handleDelete(teacher.id)">
<a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto">
<DeleteOutlined /> 删除
</a-button>
@ -115,10 +109,12 @@
</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" />
<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>
</div>
@ -131,25 +127,14 @@
<!-- 分页 -->
<div class="flex justify-center py-6 bg-white rounded-2xl pagination-wrapper" v-if="teachers.length > 0">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
:total="pagination.total"
:show-size-changer="true"
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
/>
<a-pagination v-model:current="pagination.current" v-model:pageSize="pagination.pageSize"
:total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange" />
</div>
<!-- 添加/编辑教师模态框 -->
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑教师' : '添加教师'"
@ok="handleModalOk"
@cancel="handleModalCancel"
:confirm-loading="submitting"
:width="520"
>
<a-modal v-model:open="modalVisible" :title="isEdit ? '编辑教师' : '添加教师'" @ok="handleModalOk"
@cancel="handleModalCancel" :confirm-loading="submitting" :width="520">
<template #title>
<span class="flex items-center gap-2 modal-title">
<EditOutlined v-if="isEdit" class="text-[#667eea]" />
@ -157,49 +142,44 @@
{{ isEdit ? '编辑教师' : '添加教师' }}
</span>
</template>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form ref="formRef" :model="formState" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="姓名" name="name">
<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-form-item>
<a-form-item label="手机号" name="phone">
<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-form-item>
<a-form-item label="邮箱" name="email">
<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-form-item>
<a-form-item label="登录账号" name="loginAccount">
<a-input
v-model:value="formState.loginAccount"
placeholder="请输入登录账号"
:disabled="isEdit"
>
<template #prefix><KeyOutlined class="text-[#B2BEC3]" /></template>
<a-input v-model:value="formState.loginAccount" placeholder="请输入登录账号" :disabled="isEdit">
<template #prefix>
<KeyOutlined class="text-[#B2BEC3]" />
</template>
</a-input>
</a-form-item>
<a-form-item v-if="!isEdit" label="密码" name="password">
<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-form-item>
<a-form-item label="负责班级" name="classIds">
<a-select
v-model:value="formState.classIds"
mode="multiple"
placeholder="请选择负责的班级"
:loading="classesLoading"
>
<a-select v-model:value="formState.classIds" mode="multiple" placeholder="请选择负责的班级" :loading="classesLoading">
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
@ -209,12 +189,7 @@
</a-modal>
<!-- 重置密码确认模态框 -->
<a-modal
v-model:open="resetPasswordVisible"
@ok="confirmResetPassword"
:confirm-loading="resetting"
:width="400"
>
<a-modal v-model:open="resetPasswordVisible" @ok="confirmResetPassword" :confirm-loading="resetting" :width="400">
<template #title>
<span class="flex items-center gap-2 modal-title">
<KeyOutlined class="text-[#667eea]" />
@ -228,7 +203,8 @@
</div>
<div v-if="newPassword" class="new-password-box">
<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>
</div>
</div>
@ -240,7 +216,6 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import {
SearchOutlined,
SolutionOutlined,
PlusOutlined,
PhoneOutlined,
@ -472,6 +447,7 @@ onMounted(() => {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #FF8C42;
}

View File

@ -1,11 +1,11 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import UnoCSS from 'unocss/vite';
import { resolve } from 'path';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import viteCompression from 'vite-plugin-compression';
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import UnoCSS from "unocss/vite";
import { resolve } from "path";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
import viteCompression from "vite-plugin-compression";
export default defineConfig({
plugins: [
@ -13,14 +13,14 @@ export default defineConfig({
UnoCSS(),
AutoImport({
imports: [
'vue',
'vue-router',
'pinia',
"vue",
"vue-router",
"pinia",
{
'ant-design-vue': ['message', 'notification', 'Modal'],
"ant-design-vue": ["message", "notification", "Modal"],
},
],
dts: 'src/auto-imports.d.ts',
dts: "src/auto-imports.d.ts",
}),
Components({
resolvers: [
@ -28,32 +28,32 @@ export default defineConfig({
importStyle: false,
}),
],
dts: 'src/components.d.ts',
dts: "src/components.d.ts",
}),
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz',
algorithm: "gzip",
ext: ".gz",
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
"@": resolve(__dirname, "src"),
},
},
server: {
port: 5173,
host: true,
proxy: {
'/api': {
target: 'http://localhost:3000',
"/api": {
target: "http://8.148.151.56:8080",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'),
rewrite: (path) => path.replace(/^\/api/, "/api"),
},
'/uploads': {
target: 'http://localhost:3000',
"/uploads": {
target: "http://8.148.151.56:8080",
changeOrigin: true,
},
},
@ -62,10 +62,16 @@ export default defineConfig({
rollupOptions: {
output: {
manualChunks: {
'ant-design-vue': ['ant-design-vue', '@ant-design/icons-vue'],
'echarts': ['echarts'],
'fullcalendar': ['@fullcalendar/vue3', '@fullcalendar/core', '@fullcalendar/daygrid', '@fullcalendar/timegrid', '@fullcalendar/interaction'],
'dayjs': ['dayjs'],
"ant-design-vue": ["ant-design-vue", "@ant-design/icons-vue"],
echarts: ["echarts"],
fullcalendar: [
"@fullcalendar/vue3",
"@fullcalendar/core",
"@fullcalendar/daygrid",
"@fullcalendar/timegrid",
"@fullcalendar/interaction",
],
dayjs: ["dayjs"],
},
},
},

1
test.txt Normal file
View File

@ -0,0 +1 @@
test