Compare commits

..

No commits in common. "develop" and "master" have entirely different histories.

16 changed files with 493 additions and 321 deletions

View File

@ -11,22 +11,35 @@ declare module 'vue' {
AAvatar: typeof import('ant-design-vue/es')['Avatar'] AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge'] ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
ACard: typeof import('ant-design-vue/es')['Card'] ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col'] 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'] ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown'] ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty'] AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form'] AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem'] AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image'] AImage: typeof import('ant-design-vue/es')['Image']
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch'] AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout'] ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider'] 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'] AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
@ -35,20 +48,34 @@ declare module 'vue' {
APagination: typeof import('ant-design-vue/es')['Pagination'] APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
AProgress: typeof import('ant-design-vue/es')['Progress'] 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'] 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'] ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space'] ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin'] ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic'] 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'] ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table'] ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane'] ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs'] ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag'] 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'] ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText'] 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'] FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
FileUploader: typeof import('./components/course/FileUploader.vue')['default'] FileUploader: typeof import('./components/course/FileUploader.vue')['default']
LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default'] LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default']

View File

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

View File

@ -1,11 +1,10 @@
<template> <template>
<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="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] p-0">
<!-- 页面头部 --> <!-- 页面头部 -->
<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 <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%)]">
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>
@ -27,36 +26,48 @@
</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)] search-box-wrapper">
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">
<div class=""> <a-input-search
<a-input-search v-model:value="searchKeyword" placeholder="搜索班级名称" class="w-[250px]" @search="handleSearch" v-model:value="searchKeyword"
allow-clear :enter-button="false" /> placeholder="搜索班级名称"
class="w-[250px]"
@search="handleSearch"
allow-clear
>
<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,#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">
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));" <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">
v-if="!loading && classes.length > 0"> <div
<div v-for="cls in classes" :key="cls.id" 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="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 class="w-12 h-12 rounded-xl flex items-center justify-center icon-wrapper" <div
: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="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" /> <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 class="py-0.5 px-2.5 rounded-[10px] text-[11px] font-500 grade-badge" <span
:class="getGradeKey(cls.grade) === 'small' ? 'bg-[#E8F5E9] text-[#43A047]' : getGradeKey(cls.grade) === 'middle' ? 'bg-[#E3F2FD] text-[#1976D2]' : 'bg-[#FFF8F0] text-[#FF8C42]'"> 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 }} {{ cls.grade }}
</span> </span>
</div> </div>
@ -65,15 +76,13 @@
<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 <div class="w-6 h-6 rounded-md flex items-center justify-center bg-[linear-gradient(135deg,#F0F4F8_0%,#E8EDF2_100%)]">
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" {{ 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>
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>
@ -82,8 +91,7 @@
</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 <div class="w-6 h-6 rounded-md flex items-center justify-center bg-[linear-gradient(135deg,#F0F4F8_0%,#E8EDF2_100%)]">
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]">
@ -91,8 +99,7 @@
</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 <div class="w-6 h-6 rounded-md flex items-center justify-center bg-[linear-gradient(135deg,#F0F4F8_0%,#E8EDF2_100%)]">
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]">
@ -103,30 +110,29 @@
<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 <div class="h-full bg-[linear-gradient(90deg,#4facfe_0%,#00f2fe_100%)] rounded transition-[width] duration-300" :style="{ width: getProgressWidth(cls.studentCount) }"></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)" <a-button type="link" size="small" @click="handleViewStudents(cls)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
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)" <a-button type="link" size="small" @click="handleManageTeachers(cls)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
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)" <a-button type="link" size="small" @click="handleEdit(cls)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<EditOutlined /> <EditOutlined />
编辑 编辑
</a-button> </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-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 />
@ -143,15 +149,12 @@
</div> </div>
<!-- 空状态 --> <!-- 空状态 -->
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" <div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && classes.length === 0">
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="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" <a-button type="primary" class="!bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] !border-0" @click="showAddModal">
@click="showAddModal">
创建第一个班级 创建第一个班级
</a-button> </a-button>
</div> </div>
@ -163,9 +166,21 @@
</div> </div>
<!-- 添加/编辑班级模态框 --> <!-- 添加/编辑班级模态框 -->
<a-modal v-model:open="modalVisible" :title="isEdit ? modalEditTitle : modalAddTitle" @ok="handleModalOk" <a-modal
@cancel="handleModalCancel" :confirm-loading="submitting" :width="480"> v-model:open="modalVisible"
<a-form ref="formRef" :model="formState" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }"> :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-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>
@ -196,7 +211,12 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="班主任" name="teacherId"> <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"> <a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }} {{ teacher.name }}
</a-select-option> </a-select-option>
@ -206,12 +226,15 @@
</a-modal> </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="p-0">
<div <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">
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="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">
@ -227,10 +250,15 @@
</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 v-for="student in classStudents" :key="student.id" <div
class="flex items-center gap-3 p-3 bg-[#F8F9FA] rounded-xl student-item"> v-for="student in classStudents"
<div class="w-9 h-9 rounded-full flex items-center justify-center student-avatar" :key="student.id"
:class="student.gender === '男' ? 'bg-[linear-gradient(135deg,#4FC3F7_0%,#29B6F6_100%)]' : 'bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]'"> 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" /> <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>
@ -244,10 +272,8 @@
</div> </div>
</div> </div>
<div class="flex flex-col items-center py-10 empty-students" <div class="flex flex-col items-center py-10 empty-students" v-if="!studentsLoading && classStudents.length === 0">
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="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>
@ -258,24 +284,36 @@
<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" <div class="flex justify-center mt-5 pt-4 border-t border-[#F0F0F0] pagination-wrapper" v-if="studentsPagination.total > studentsPagination.pageSize">
v-if="studentsPagination.total > studentsPagination.pageSize"> <a-pagination
<a-pagination v-model:current="studentsPagination.current" v-model:pageSize="studentsPagination.pageSize" v-model:current="studentsPagination.current"
:total="studentsPagination.total" :show-total="(total: number) => `共 ${total} 条`" size="small" v-model:pageSize="studentsPagination.pageSize"
@change="handleStudentsPageChange" /> :total="studentsPagination.total"
:show-total="(total: number) => `共 ${total} 条`"
size="small"
@change="handleStudentsPageChange"
/>
</div> </div>
</div> </div>
</a-modal> </a-modal>
<!-- 班级教师管理模态框 --> <!-- 班级教师管理模态框 -->
<a-modal v-model:open="teachersModalVisible" :title="`管理 ${currentClass?.name || ''} 教师团队`" :footer="null" <a-modal
width="600px"> v-model:open="teachersModalVisible"
: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 v-model:value="teacherFormState.teacherId" placeholder="选择教师" class="w-[150px]" <a-select
:loading="teachersLoading"> 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"> <a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }} {{ teacher.name }}
</a-select-option> </a-select-option>
@ -294,18 +332,23 @@
<!-- 教师列表 --> <!-- 教师列表 -->
<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" <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">
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 v-model:value="teacher.role" size="small" class="w-20 ml-2" <a-select
@change="handleUpdateTeacherRole(teacher)"> 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="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 v-model:checked="teacher.isPrimary" <a-checkbox
@change="handleUpdateTeacherRole(teacher)">班主任</a-checkbox> v-model:checked="teacher.isPrimary"
@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)">
移除 移除
@ -327,6 +370,7 @@ import {
HomeOutlined, HomeOutlined,
BankOutlined, BankOutlined,
PlusOutlined, PlusOutlined,
SearchOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
TeamOutlined, TeamOutlined,
@ -714,7 +758,6 @@ 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;
} }
@ -741,16 +784,13 @@ 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

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

View File

@ -1,5 +1,5 @@
<template> <template>
<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="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 --> <!-- 页面头部 -->
<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,14 +149,11 @@
</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 <a-input-search v-model:value="searchKeyword" placeholder="输入课程名称搜索..." @search="searchCourses" size="large">
v-model:value="searchKeyword" <template #prefix>
placeholder="输入课程名称搜索..." <SearchOutlined />
@search="searchCourses" </template>
size="large" </a-input-search>
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"
@ -209,6 +206,7 @@
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 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6"> <div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 --> <!-- 页面头部 -->
<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,7 +55,6 @@
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 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6"> <div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 --> <!-- 页面头部 -->
<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,7 +41,6 @@
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 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6"> <div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 --> <!-- 页面头部 -->
<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,7 +30,11 @@
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 :enter-button="false" /> @search="handleSearch" allow-clear>
<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"
@ -75,7 +79,7 @@
<TeamOutlined class="text-sm text-[#FF8C42]" /> <TeamOutlined class="text-sm text-[#FF8C42]" />
<span class="text-[#636E72]"> <span class="text-[#636E72]">
<span v-if="parent.childrenCount > 0">关联 <strong class="text-[#FF8C42]">{{ parent.childrenCount <span v-if="parent.childrenCount > 0">关联 <strong class="text-[#FF8C42]">{{ parent.childrenCount
}}</strong> }}</strong>
个孩子</span> 个孩子</span>
<span v-else class="text-[#B2BEC3] italic">未关联孩子</span> <span v-else class="text-[#B2BEC3] italic">未关联孩子</span>
</span> </span>
@ -242,7 +246,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 :enter-button="false" /> @search="handleStudentSearch" allow-clear />
<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">
@ -312,6 +316,7 @@
<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,
@ -687,6 +692,11 @@ 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

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

View File

@ -3,8 +3,7 @@
<!-- 页面头部 --> <!-- 页面头部 -->
<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 <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)]">
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>
@ -12,7 +11,11 @@
<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 type="primary" class="w-full md:w-auto" @click="handleCreate"> <a-button
type="primary"
class="w-full md:w-auto"
@click="handleCreate"
>
<PlusOutlined /> 创建校本课程包 <PlusOutlined /> 创建校本课程包
</a-button> </a-button>
</div> </div>
@ -20,8 +23,7 @@
<!-- 统计概览 --> <!-- 统计概览 -->
<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 <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">
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>
@ -30,8 +32,7 @@
</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 <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">
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>
@ -40,8 +41,7 @@
</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 <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">
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,14 +57,25 @@
<span>校本课程包列表</span> <span>校本课程包列表</span>
</template> </template>
<template #extra> <template #extra>
<div class=""> <div class="search-box">
<a-input-search v-model:value="searchKeyword" placeholder="搜索课程包名称" class="w-[220px]" @search="handleSearch" <a-input-search
allow-clear :enter-button="false" /> v-model:value="searchKeyword"
placeholder="搜索课程包名称"
class="w-[220px]"
@search="handleSearch"
allow-clear
/>
</div> </div>
</template> </template>
<a-table :columns="columns" :data-source="filteredData" :loading="loading" row-key="id" <a-table
:pagination="{ pageSize: 10 }" :scroll="{ x: true }"> :columns="columns"
: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">
@ -76,8 +87,11 @@
</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 v-if="record.sourceCourse?.coverImagePath" :src="getFileUrl(record.sourceCourse.coverImagePath)" <img
class="w-10 h-10 object-cover rounded" /> 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]"> <div v-else class="w-10 h-10 bg-[#f0f0f0] rounded flex items-center justify-center text-[#999]">
<BookOutlined /> <BookOutlined />
</div> </div>
@ -135,8 +149,13 @@
</a-card> </a-card>
<!-- 预约弹窗 --> <!-- 预约弹窗 -->
<a-modal v-model:open="reserveModalVisible" title="预约校本课程包" width="500px" @ok="handleReserve" <a-modal
:confirmLoading="reserveLoading"> v-model:open="reserveModalVisible"
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>
@ -161,21 +180,35 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="预约时间" required> <a-form-item label="预约时间" required>
<a-date-picker v-model:value="reserveForm.scheduledDate" show-time format="YYYY-MM-DD HH:mm" <a-date-picker
placeholder="选择预约时间" class="w-full" /> v-model:value="reserveForm.scheduledDate"
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 v-if="conflictInfo" :type="conflictInfo.hasConflict ? 'error' : 'success'" <a-alert
:message="conflictInfo.message" show-icon /> v-if="conflictInfo"
:type="conflictInfo.hasConflict ? 'error' : 'success'"
:message="conflictInfo.message"
show-icon
/>
</div> </div>
</a-modal> </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 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>
@ -186,8 +219,14 @@
<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 :columns="reservationColumns" :data-source="upcomingReservations" :loading="reservationLoading" <a-table
row-key="id" size="small" :pagination="false"> :columns="reservationColumns"
: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>
@ -204,8 +243,14 @@
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="history" tab="历史记录"> <a-tab-pane key="history" tab="历史记录">
<a-table :columns="reservationColumns" :data-source="historyReservations" :loading="reservationLoading" <a-table
row-key="id" size="small" :pagination="{ pageSize: 5 }"> :columns="reservationColumns"
: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 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6"> <div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 --> <!-- 页面头部 -->
<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,43 +19,48 @@
</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"> <span class="text-xs text-white/80 flex items-center gap-1"><UserOutlined class="text-sm text-[#4FC3F7]" /> 男生</span>
<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"> <span class="text-xs text-white/80 flex items-center gap-1"><UserOutlined class="text-sm text-[#FFD93D]" /> 女生</span>
<UserOutlined class="text-sm text-[#FFD93D]" /> 女生
</span>
</div> </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">
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 v-model:value="selectedClassId" placeholder="选择班级" class="w-[150px]" @change="handleClassChange" <a-select
allow-clear> 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"> <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 v-model:value="searchKeyword" placeholder="搜索学生姓名/家长" class="w-[220px] search-input-wrap" <a-input-search
@search="handleSearch" allow-clear :enter-button="false" /> 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>
</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 <a-button class="rounded-xl h-10 border-2 border-[#f093fb] text-[#f5576c] hover:bg-[#FFF0F5] hover:border-[#f5576c] hover:text-[#f5576c]" @click="showImportModal">
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" <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">
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>
@ -63,14 +68,17 @@
</div> </div>
<!-- 学生卡片网格 --> <!-- 学生卡片网格 -->
<div class="grid gap-5 mb-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(280px,1fr))]" <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">
v-if="!loading && students.length > 0"> <div
<div v-for="student in students" :key="student.id" v-for="student in students"
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]"> :key="student.id"
<div 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="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" <div class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]">
:class="normalizeGender(student.gender) === '男' ? 'bg-[linear-gradient(135deg,#4FC3F7_0%,#29B6F6_100%)]' : 'bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]'"> <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">
@ -82,10 +90,11 @@
<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 class="text-[#636E72] flex-1">{{ calculateAge(student.birthDate) || '--' }}{{ student.birthDate ? '岁' : '' }}</span>
: '' }}</span> <span
<span class="py-0.5 px-2 rounded-[10px] text-[11px]" 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>
@ -99,21 +108,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 class="text-[#636E72]">参与课程 <strong class="text-[#f5576c]">{{ student.lessonCount || 0 }}</strong> </span>
</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)" <a-button type="link" size="small" @click="handleEdit(student)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
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)" <a-button type="link" size="small" @click="handleTransfer(student)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<SwapOutlined /> 调班 <SwapOutlined /> 调班
</a-button> </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"> <a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto">
<DeleteOutlined /> 删除 <DeleteOutlined /> 删除
</a-button> </a-button>
@ -123,15 +132,12 @@
</div> </div>
<!-- 空状态 --> <!-- 空状态 -->
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" <div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && students.length === 0">
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="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" <a-button type="primary" class="!bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] !border-0 rounded-xl" @click="showAddModal">
@click="showAddModal">
添加第一位学生 添加第一位学生
</a-button> </a-button>
</div> </div>
@ -144,41 +150,55 @@
<!-- 分页 --> <!-- 分页 -->
<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 v-model:current="pagination.current" v-model:pageSize="pagination.pageSize" <a-pagination
:total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`" v-model:current="pagination.current"
@change="handlePageChange" /> v-model:pageSize="pagination.pageSize"
:total="pagination.total"
:show-size-changer="true"
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
/>
</div> </div>
<!-- 添加/编辑学生模态框 --> <!-- 添加/编辑学生模态框 -->
<a-modal v-model:open="modalVisible" @ok="handleModalOk" @cancel="handleModalCancel" :confirm-loading="submitting" <a-modal
:width="520"> v-model:open="modalVisible"
@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 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-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><UserOutlined class="text-[#B2BEC3]" /></template>
<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="男"> <a-radio value="男"><UserOutlined class="mr-1 text-[#4FC3F7]" /> 男孩</a-radio>
<UserOutlined class="mr-1 text-[#4FC3F7]" /> 男孩 <a-radio value="女"><UserOutlined class="mr-1 text-[#f5576c]" /> 女孩</a-radio>
</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 v-model:value="formState.birthDate" class="w-full" value-format="YYYY-MM-DD" <a-date-picker
placeholder="选择出生日期" /> v-model:value="formState.birthDate"
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">
@ -189,24 +209,26 @@
</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> <template #prefix><TeamOutlined class="text-[#B2BEC3]" /></template>
<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> <template #prefix><PhoneOutlined class="text-[#B2BEC3]" /></template>
<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 v-model:open="transferModalVisible" title="学生调班" :confirm-loading="transferSubmitting" <a-modal
@ok="handleTransferSubmit" @cancel="transferModalVisible = false" width="480px"> 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-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>
@ -215,14 +237,22 @@
</div> </div>
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item label="目标班级" required> <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"> <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 v-model:value="transferReason" placeholder="请输入调班原因(选填)" :rows="3" /> <a-textarea
v-model:value="transferReason"
placeholder="请输入调班原因(选填)"
:rows="3"
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -231,8 +261,7 @@
<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" <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">
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>
@ -244,7 +273,11 @@
</a-modal> </a-modal>
<!-- 批量导入模态框 --> <!-- 批量导入模态框 -->
<a-modal v-model:open="importModalVisible" :footer="null" width="560px"> <a-modal
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]" />
@ -262,15 +295,17 @@
</ol> </ol>
</div> </div>
<a-button <a-button class="rounded-xl border-2 border-dashed border-[#f093fb] text-[#f5576c] hover:border-[#f5576c] hover:text-[#f5576c] hover:bg-[#FFF0F5]" @click="downloadTemplate">
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 :before-upload="beforeUpload" :show-upload-list="false" accept=".xlsx,.xls,.csv" <a-upload-dragger
class="upload-area rounded-xl"> :before-upload="beforeUpload"
: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>
@ -286,15 +321,26 @@
<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 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"> <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 type="primary" :loading="importing" :disabled="!importFile" @click="handleImport" block <a-button
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"> 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"> <template v-if="!importing">
<RocketOutlined class="mr-2" /> <RocketOutlined class="mr-2" />
开始导入 开始导入
@ -302,21 +348,16 @@
<span v-else>导入中...</span> <span v-else>导入中...</span>
</a-button> </a-button>
<div v-if="importResult" class="p-4 rounded-xl import-result" <div v-if="importResult" class="p-4 rounded-xl import-result" :class="importResult.failed === 0 ? 'bg-[#E8F5E9]' : 'bg-[#FFF8E1]'">
: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" <component :is="importResult.failed === 0 ? CheckCircleOutlined : WarningOutlined" class="text-xl" :class="importResult.failed === 0 ? 'text-[#43A047]' : 'text-[#FF8C42]'" />
: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]' : ''">成功 {{ <span class="text-[13px]" :class="importResult.failed === 0 ? 'text-[#43A047]' : ''">成功 {{ importResult.success }} </span>
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" <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">
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>
@ -330,6 +371,7 @@
<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,
@ -750,11 +792,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;
@ -770,8 +812,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,7 +62,6 @@
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,8 +32,6 @@
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 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6"> <div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 --> <!-- 页面头部 -->
<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,38 +26,46 @@
</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">
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=""> <a-input-search
<a-input-search v-model:value="searchKeyword" placeholder="搜索教师姓名/手机号/账号" class="w-[280px]" v-model:value="searchKeyword"
@search="handleSearch" allow-clear :enter-button="false" /> placeholder="搜索教师姓名/手机号/账号"
class="w-[280px]"
@search="handleSearch"
allow-clear
>
<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" @click="showAddModal">
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))]" <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">
v-if="!loading && teachers.length > 0"> <div
<div v-for="teacher in teachers" :key="teacher.id" 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="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 >
class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]"> <div class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]">
<div <div class="w-12 h-12 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
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 class="py-1 px-3 rounded-[20px] text-xs font-500" <span
:class="teacher.status === 'ACTIVE' ? 'bg-[#E8F5E9] text-[#43A047]' : 'bg-[#FFEBEE] text-[#E53935]'"> 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' ? '在职' : '离职' }} {{ teacher.status === 'ACTIVE' ? '在职' : '离职' }}
</span> </span>
</div> </div>
@ -74,32 +82,30 @@
<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 <span v-if="teacher.classNames && (Array.isArray(teacher.classNames) ? teacher.classNames.length > 0 : teacher.classNames)">
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">{{ <span v-if="Array.isArray(teacher.classNames) && teacher.classNames.length > 2">{{ teacher.classNames.length }}个班级</span>
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 class="text-[#636E72]">授课 <strong class="text-[#FF8C42]">{{ teacher.lessonCount || 0 }}</strong> </span>
</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)" <a-button type="link" size="small" @click="handleEdit(teacher)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
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)" <a-button type="link" size="small" @click="handleResetPassword(teacher)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<KeyOutlined /> 重置密码 <KeyOutlined /> 重置密码
</a-button> </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"> <a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto">
<DeleteOutlined /> 删除 <DeleteOutlined /> 删除
</a-button> </a-button>
@ -109,12 +115,10 @@
</div> </div>
<!-- 空状态 --> <!-- 空状态 -->
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" <div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && teachers.length === 0">
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" <a-button type="primary" class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] !border-0 rounded-xl" @click="showAddModal">
@click="showAddModal">
添加第一位教师 添加第一位教师
</a-button> </a-button>
</div> </div>
@ -127,14 +131,25 @@
<!-- 分页 --> <!-- 分页 -->
<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 v-model:current="pagination.current" v-model:pageSize="pagination.pageSize" <a-pagination
:total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`" v-model:current="pagination.current"
@change="handlePageChange" /> v-model:pageSize="pagination.pageSize"
:total="pagination.total"
:show-size-changer="true"
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
/>
</div> </div>
<!-- 添加/编辑教师模态框 --> <!-- 添加/编辑教师模态框 -->
<a-modal v-model:open="modalVisible" :title="isEdit ? '编辑教师' : '添加教师'" @ok="handleModalOk" <a-modal
@cancel="handleModalCancel" :confirm-loading="submitting" :width="520"> v-model:open="modalVisible"
: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]" />
@ -142,44 +157,49 @@
{{ isEdit ? '编辑教师' : '添加教师' }} {{ isEdit ? '编辑教师' : '添加教师' }}
</span> </span>
</template> </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-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><UserOutlined class="text-[#B2BEC3]" /></template>
<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> <template #prefix><PhoneOutlined class="text-[#B2BEC3]" /></template>
<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> <template #prefix><MailOutlined class="text-[#B2BEC3]" /></template>
<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 v-model:value="formState.loginAccount" placeholder="请输入登录账号" :disabled="isEdit"> <a-input
<template #prefix> v-model:value="formState.loginAccount"
<KeyOutlined class="text-[#B2BEC3]" /> placeholder="请输入登录账号"
</template> :disabled="isEdit"
>
<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> <template #prefix><LockOutlined class="text-[#B2BEC3]" /></template>
<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 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"> <a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }} {{ cls.name }}
</a-select-option> </a-select-option>
@ -189,7 +209,12 @@
</a-modal> </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> <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]" />
@ -203,8 +228,7 @@
</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 <div class="p-4 rounded-xl font-bold text-2xl text-white bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] password-display">
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>
@ -216,6 +240,7 @@
<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,
@ -447,7 +472,6 @@ 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;
} }

View File

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

View File

@ -1 +0,0 @@
test