Compare commits

..

No commits in common. "b6e46ba21eb04e6dc06732887a25fb21bed80aa2" and "0777d2901ab589c9c2ce9baaeeaf33b9230053b2" have entirely different histories.

5 changed files with 477 additions and 190 deletions

View File

@ -28,8 +28,17 @@
<!-- 操作栏 -->
<div class="action-bar">
<div class="search-box">
<a-input-search v-model:value="searchKeyword" placeholder="搜索班级名称" style="width: 250px;" @search="handleSearch"
allow-clear />
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索班级名称"
style="width: 250px;"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined style="color: #B2BEC3;" />
</template>
</a-input-search>
</div>
<a-button type="primary" class="add-btn" @click="showAddModal">
<PlusOutlined class="btn-icon" />
@ -39,7 +48,12 @@
<!-- 班级卡片网格 -->
<div class="class-grid" v-if="!loading && classes.length > 0">
<div v-for="cls in classes" :key="cls.id" class="class-card" :class="'grade-' + getGradeKey(cls.grade)">
<div
v-for="cls in classes"
:key="cls.id"
class="class-card"
:class="'grade-' + getGradeKey(cls.grade)"
>
<div class="card-header">
<div class="class-icon">
<component :is="getGradeIcon(cls.grade)" class="icon-component" />
@ -108,7 +122,11 @@
<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="disabled-btn">
<DeleteOutlined />
@ -142,9 +160,21 @@
</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>
@ -175,7 +205,12 @@
</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>
@ -185,7 +220,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="students-modal-content">
<div class="class-info-header">
<div class="class-emoji">
@ -204,7 +244,11 @@
</div>
<div class="students-list" v-if="!studentsLoading && classStudents.length > 0">
<div v-for="student in classStudents" :key="student.id" class="student-item">
<div
v-for="student in classStudents"
:key="student.id"
class="student-item"
>
<div class="student-avatar" :class="student.gender === '男' ? 'boy' : 'girl'">
<BoyOutlined v-if="student.gender === ''" class="student-gender-icon" />
<GirlOutlined v-else class="student-gender-icon" />
@ -232,22 +276,35 @@
</div>
<div class="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" />
<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="teachers-modal-content">
<!-- 添加教师表单 -->
<div class="add-teacher-form">
<div class="form-row">
<a-select v-model:value="teacherFormState.teacherId" placeholder="选择教师" style="width: 150px;"
:loading="teachersLoading">
<a-select
v-model:value="teacherFormState.teacherId"
placeholder="选择教师"
style="width: 150px;"
:loading="teachersLoading"
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
@ -269,14 +326,20 @@
<div v-for="teacher in classTeachers" :key="teacher.teacherId" class="teacher-item">
<div class="teacher-info">
<span class="teacher-name">{{ teacher.teacherName }}</span>
<a-select v-model:value="teacher.role" size="small" style="width: 80px; margin-left: 8px;"
@change="handleUpdateTeacherRole(teacher)">
<a-select
v-model:value="teacher.role"
size="small"
style="width: 80px; margin-left: 8px;"
@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)">
移除
@ -298,6 +361,7 @@ import {
HomeOutlined,
BankOutlined,
PlusOutlined,
SearchOutlined,
EditOutlined,
DeleteOutlined,
TeamOutlined,
@ -769,6 +833,10 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.search-box :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #4facfe;
@ -813,17 +881,9 @@ onMounted(() => {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.class-card.grade-small {
border-top: 4px solid #43e97b;
}
.class-card.grade-middle {
border-top: 4px solid #4facfe;
}
.class-card.grade-big {
border-top: 4px solid #FF8C42;
}
.class-card.grade-small { border-top: 4px solid #43e97b; }
.class-card.grade-middle { border-top: 4px solid #4facfe; }
.class-card.grade-big { border-top: 4px solid #FF8C42; }
.class-card .card-header {
display: flex;
@ -842,17 +902,9 @@ onMounted(() => {
justify-content: center;
}
.class-card.grade-small .class-icon {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.class-card.grade-middle .class-icon {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.class-card.grade-big .class-icon {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
}
.class-card.grade-small .class-icon { background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); }
.class-card.grade-middle .class-icon { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
.class-card.grade-big .class-icon { background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%); }
.icon-component {
font-size: 24px;
@ -880,20 +932,9 @@ onMounted(() => {
font-weight: 500;
}
.grade-badge.grade-small {
background: #E8F5E9;
color: #43A047;
}
.grade-badge.grade-middle {
background: #E3F2FD;
color: #1976D2;
}
.grade-badge.grade-big {
background: #FFF8F0;
color: #FF8C42;
}
.grade-badge.grade-small { background: #E8F5E9; color: #43A047; }
.grade-badge.grade-middle { background: #E3F2FD; color: #1976D2; }
.grade-badge.grade-big { background: #FFF8F0; color: #FF8C42; }
.card-body {
padding: 16px 20px;

View File

@ -30,16 +30,30 @@
<div class="grade-tabs">
<span class="tab-label">年级筛选</span>
<div class="tab-buttons">
<div v-for="grade in gradeOptions" :key="grade.value" class="grade-tab"
:class="{ active: selectedGrade === grade.value }" @click="selectedGrade = grade.value">
<div
v-for="grade in gradeOptions"
:key="grade.value"
class="grade-tab"
:class="{ active: selectedGrade === grade.value }"
@click="selectedGrade = grade.value"
>
{{ grade.label }}
</div>
</div>
</div>
<div class="action-row">
<div class="search-box">
<a-input-search v-model:value="searchKeyword" placeholder="搜索课程名称" style="width: 280px;"
@search="handleSearch" allow-clear />
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索课程名称"
style="width: 280px;"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined style="color: #B2BEC3;" />
</template>
</a-input-search>
</div>
<a-button type="primary" class="auth-btn" @click="showAuthModal">
<StarFilled class="btn-icon" />
@ -50,8 +64,12 @@
<!-- 课程卡片网格 -->
<div class="course-grid" v-if="!loading && filteredCourses.length > 0">
<div v-for="course in filteredCourses" :key="course.id" class="course-card"
:class="{ 'unauthorized': !course.authorized }">
<div
v-for="course in filteredCourses"
:key="course.id"
class="course-card"
:class="{ 'unauthorized': !course.authorized }"
>
<div class="card-cover">
<div class="cover-placeholder" v-if="!course.pictureUrl">
<ReadOutlined class="cover-icon" />
@ -69,12 +87,20 @@
<p class="course-book">{{ course.pictureBookName }}</p>
<div class="course-tags">
<span v-for="tag in course.gradeTags.slice(0, 2)" :key="tag" class="tag grade"
:style="getGradeTagStyle(translateGradeTag(tag))">
<span
v-for="tag in course.gradeTags.slice(0, 2)"
:key="tag"
class="tag grade"
:style="getGradeTagStyle(translateGradeTag(tag))"
>
{{ translateGradeTag(tag) }}
</span>
<span v-for="tag in course.domainTags.slice(0, 2)" :key="tag" class="tag domain"
:style="getDomainTagStyle(translateDomainTag(tag))">
<span
v-for="tag in course.domainTags.slice(0, 2)"
:key="tag"
class="tag domain"
:style="getDomainTagStyle(translateDomainTag(tag))"
>
{{ translateDomainTag(tag) }}
</span>
</div>
@ -96,12 +122,21 @@
<FileTextOutlined />
详情
</a-button>
<a-button v-if="!course.authorized" type="link" size="small" class="auth-action"
@click="handleAuthorize(course)">
<a-button
v-if="!course.authorized"
type="link"
size="small"
class="auth-action"
@click="handleAuthorize(course)"
>
<StarFilled />
授权
</a-button>
<a-popconfirm v-else title="确定要取消授权吗?" @confirm="handleRevoke(course)">
<a-popconfirm
v-else
title="确定要取消授权吗?"
@confirm="handleRevoke(course)"
>
<a-button type="link" size="small" danger>
<StopOutlined />
取消
@ -129,8 +164,13 @@
</div>
<!-- 授权课程模态框 -->
<a-modal v-model:open="authModalVisible" width="800px" class="auth-modal" @ok="handleAuthModalOk"
@cancel="authModalVisible = false">
<a-modal
v-model:open="authModalVisible"
width="800px"
class="auth-modal"
@ok="handleAuthModalOk"
@cancel="authModalVisible = false"
>
<template #title>
<span class="modal-title">
<StarFilled class="modal-title-icon" />
@ -139,12 +179,26 @@
</template>
<div class="auth-content">
<div class="auth-search">
<a-input-search v-model:value="searchKeyword" placeholder="输入课程名称搜索..." @search="searchCourses" size="large" />
<a-input-search
v-model:value="searchKeyword"
placeholder="输入课程名称搜索..."
@search="searchCourses"
size="large"
>
<template #prefix>
<SearchOutlined />
</template>
</a-input-search>
</div>
<div class="available-courses" v-if="!authLoading && availableCourses.length > 0">
<div v-for="course in availableCourses" :key="course.id" class="available-course-item"
:class="{ 'selected': selectedCourseIds.includes(course.id) }" @click="toggleCourseSelection(course.id)">
<div
v-for="course in availableCourses"
:key="course.id"
class="available-course-item"
:class="{ 'selected': selectedCourseIds.includes(course.id) }"
@click="toggleCourseSelection(course.id)"
>
<div class="course-checkbox">
<CheckCircleOutlined v-if="selectedCourseIds.includes(course.id)" class="checkbox-check" />
</div>
@ -186,6 +240,7 @@
import { ref, reactive, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import {
SearchOutlined,
BookOutlined,
ReadOutlined,
StarFilled,
@ -574,6 +629,10 @@ onMounted(() => {
gap: 12px;
}
.search-box :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #43e97b;
@ -798,6 +857,9 @@ onMounted(() => {
gap: 20px;
}
.auth-search :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
}
.available-courses {
display: grid;

View File

@ -28,8 +28,17 @@
<!-- 操作栏 -->
<div class="action-bar">
<div class="search-box">
<a-input-search v-model:value="searchKeyword" placeholder="搜索家长姓名/手机号/账号" style="width: 280px;"
@search="handleSearch" allow-clear />
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索家长姓名/手机号/账号"
style="width: 280px;"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined style="color: #B2BEC3;" />
</template>
</a-input-search>
</div>
<a-button type="primary" class="add-btn" @click="showAddModal">
<PlusOutlined class="btn-icon" />
@ -39,8 +48,12 @@
<!-- 家长卡片列表 -->
<div class="parent-grid" v-if="!loading && parents.length > 0">
<div v-for="parent in parents" :key="parent.id" class="parent-card"
:class="{ 'inactive': parent.status !== 'ACTIVE' }">
<div
v-for="parent in parents"
:key="parent.id"
class="parent-card"
:class="{ 'inactive': parent.status !== 'ACTIVE' }"
>
<div class="card-header">
<div class="parent-avatar">
<IdcardOutlined class="avatar-icon" />
@ -84,7 +97,10 @@
<a-button type="link" size="small" @click="handleResetPassword(parent)">
<KeyOutlined /> 重置
</a-button>
<a-popconfirm title="确定要删除这位家长吗?" @confirm="handleDelete(parent.id)">
<a-popconfirm
title="确定要删除这位家长吗?"
@confirm="handleDelete(parent.id)"
>
<a-button type="link" size="small" danger>
<DeleteOutlined /> 删除
</a-button>
@ -110,14 +126,26 @@
<!-- 分页 -->
<div class="pagination-wrapper" v-if="parents.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" class="parent-modal">
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑家长' : '添加家长'"
@ok="handleModalOk"
@cancel="handleModalCancel"
:confirm-loading="submitting"
:width="520"
class="parent-modal"
>
<template #title>
<span class="modal-title">
<EditOutlined v-if="isEdit" class="modal-title-icon" />
@ -125,47 +153,53 @@
{{ 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 style="color: #B2BEC3;" />
</template>
<template #prefix><UserOutlined style="color: #B2BEC3;" /></template>
</a-input>
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input v-model:value="formState.phone" placeholder="请输入手机号">
<template #prefix>
<PhoneOutlined style="color: #B2BEC3;" />
</template>
<template #prefix><PhoneOutlined style="color: #B2BEC3;" /></template>
</a-input>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formState.email" placeholder="请输入邮箱(可选)">
<template #prefix>
<MailOutlined style="color: #B2BEC3;" />
</template>
<template #prefix><MailOutlined style="color: #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 style="color: #B2BEC3;" />
</template>
<a-input
v-model:value="formState.loginAccount"
placeholder="请输入登录账号"
:disabled="isEdit"
>
<template #prefix><KeyOutlined style="color: #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 style="color: #B2BEC3;" />
</template>
<template #prefix><LockOutlined style="color: #B2BEC3;" /></template>
</a-input-password>
</a-form-item>
</a-form>
</a-modal>
<!-- 管理孩子模态框 -->
<a-modal v-model:open="childrenModalVisible" title="管理关联孩子" :width="650" :footer="null" class="children-modal">
<a-modal
v-model:open="childrenModalVisible"
title="管理关联孩子"
:width="650"
:footer="null"
class="children-modal"
>
<template #title>
<span class="modal-title">
<TeamOutlined class="modal-title-icon" />
@ -185,7 +219,10 @@
<div class="list-header">
<span>已关联孩子 ({{ parentChildren.length }})</span>
</div>
<a-list :data-source="parentChildren" :loading="childrenLoading">
<a-list
:data-source="parentChildren"
:loading="childrenLoading"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
@ -203,7 +240,10 @@
</template>
</a-list-item-meta>
<template #actions>
<a-popconfirm title="确定要解除关联吗?" @confirm="handleRemoveChild(item.id)">
<a-popconfirm
title="确定要解除关联吗?"
@confirm="handleRemoveChild(item.id)"
>
<a-button type="link" size="small" danger>
<DisconnectOutlined /> 解除
</a-button>
@ -219,8 +259,13 @@
</a-modal>
<!-- 选择学生弹窗 -->
<a-modal v-model:open="selectStudentModalVisible" title="选择孩子" :width="800" :footer="null"
class="select-student-modal">
<a-modal
v-model:open="selectStudentModalVisible"
title="选择孩子"
:width="800"
:footer="null"
class="select-student-modal"
>
<template #title>
<span class="modal-title">
<UserAddOutlined class="modal-title-icon" />
@ -230,10 +275,20 @@
<!-- 搜索和筛选 -->
<div class="select-search-bar">
<a-input-search v-model:value="studentSearchKeyword" placeholder="搜索学生姓名" style="width: 240px;"
@search="handleStudentSearch" allow-clear />
<a-select v-model:value="studentClassFilter" placeholder="按班级筛选" style="width: 160px;" allow-clear
@change="handleStudentSearch">
<a-input-search
v-model:value="studentSearchKeyword"
placeholder="搜索学生姓名"
style="width: 240px;"
@search="handleStudentSearch"
allow-clear
/>
<a-select
v-model:value="studentClassFilter"
placeholder="按班级筛选"
style="width: 160px;"
allow-clear
@change="handleStudentSearch"
>
<a-select-option v-for="cls in classOptions" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
@ -241,9 +296,17 @@
</div>
<!-- 学生表格 -->
<a-table :columns="studentTableColumns" :data-source="studentTableData" :loading="studentsLoading"
:pagination="studentPagination" :row-selection="studentRowSelection" row-key="id" size="small"
@change="handleStudentTableChange" style="margin-top: 16px;">
<a-table
:columns="studentTableColumns"
:data-source="studentTableData"
:loading="studentsLoading"
:pagination="studentPagination"
:row-selection="studentRowSelection"
row-key="id"
size="small"
@change="handleStudentTableChange"
style="margin-top: 16px;"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'gender'">
{{ record.gender === 'MALE' ? '男' : record.gender === 'FEMALE' ? '女' : '-' }}
@ -275,7 +338,12 @@
</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="modal-title">
<KeyOutlined class="modal-title-icon" />
@ -301,6 +369,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import {
SearchOutlined,
IdcardOutlined,
PlusOutlined,
PhoneOutlined,
@ -767,6 +836,10 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.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

@ -38,14 +38,28 @@
<!-- 操作栏 -->
<div class="action-bar">
<div class="filters">
<a-select v-model:value="selectedClassId" placeholder="选择班级" style="width: 150px;" @change="handleClassChange"
allow-clear>
<a-select
v-model:value="selectedClassId"
placeholder="选择班级"
style="width: 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="搜索学生姓名/家长" style="width: 220px;"
@search="handleSearch" allow-clear />
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索学生姓名/家长"
style="width: 220px;"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined style="color: #B2BEC3;" />
</template>
</a-input-search>
</div>
<div class="actions">
<a-button class="import-btn" @click="showImportModal">
@ -61,7 +75,11 @@
<!-- 学生卡片网格 -->
<div class="student-grid" v-if="!loading && students.length > 0">
<div v-for="student in students" :key="student.id" class="student-card">
<div
v-for="student in students"
:key="student.id"
class="student-card"
>
<div class="card-header">
<div class="student-avatar" :class="normalizeGender(student.gender) === '男' ? 'boy' : 'girl'">
<UserOutlined class="avatar-icon" />
@ -75,8 +93,7 @@
<div class="card-body">
<div class="info-row">
<CalendarOutlined class="info-icon" />
<span class="info-value">{{ calculateAge(student.birthDate) || '--' }}{{ student.birthDate ? '岁' : ''
}}</span>
<span class="info-value">{{ calculateAge(student.birthDate) || '--' }}{{ student.birthDate ? '岁' : '' }}</span>
<span class="gender-tag" :class="normalizeGender(student.gender) === '男' ? 'boy' : 'girl'">
{{ normalizeGender(student.gender) }}
</span>
@ -102,7 +119,10 @@
<a-button type="link" size="small" @click="handleTransfer(student)">
<SwapOutlined /> 调班
</a-button>
<a-popconfirm title="确定要删除这位学生吗?" @confirm="handleDelete(student.id)">
<a-popconfirm
title="确定要删除这位学生吗?"
@confirm="handleDelete(student.id)"
>
<a-button type="link" size="small" danger>
<DeleteOutlined /> 删除
</a-button>
@ -130,21 +150,37 @@
<!-- 分页 -->
<div class="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="modal-title">
<component :is="isEdit ? EditOutlined : PlusOutlined" class="modal-title-icon" />
{{ 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>
@ -154,17 +190,17 @@
</a-form-item>
<a-form-item label="性别" name="gender">
<a-radio-group v-model:value="formState.gender">
<a-radio value="男">
<UserOutlined class="gender-icon boy" /> 男孩
</a-radio>
<a-radio value="女">
<UserOutlined class="gender-icon girl" /> 女孩
</a-radio>
<a-radio value="男"><UserOutlined class="gender-icon boy" /> 男孩</a-radio>
<a-radio value="女"><UserOutlined class="gender-icon girl" /> 女孩</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="出生日期" name="birthDate">
<a-date-picker v-model:value="formState.birthDate" style="width: 100%;" value-format="YYYY-MM-DD"
placeholder="选择出生日期" />
<a-date-picker
v-model:value="formState.birthDate"
style="width: 100%;"
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">
@ -191,8 +227,14 @@
</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="transfer-modal-content">
<div class="current-info">
<span>当前学生</span>
@ -201,14 +243,22 @@
</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>
@ -230,7 +280,11 @@
</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="modal-title">
<DownloadOutlined class="modal-title-icon" />
@ -253,8 +307,12 @@
下载导入模板
</a-button>
<a-upload-dragger :before-upload="beforeUpload" :show-upload-list="false" accept=".xlsx,.xls,.csv"
class="upload-area">
<a-upload-dragger
:before-upload="beforeUpload"
:show-upload-list="false"
accept=".xlsx,.xls,.csv"
class="upload-area"
>
<div class="upload-content">
<FileAddOutlined class="upload-icon" />
<p class="upload-text">点击或拖拽文件到此区域上传</p>
@ -270,15 +328,26 @@
<div v-if="importFile" class="default-class-select">
<label>默认班级用于未指定班级的学生</label>
<a-select v-model:value="importDefaultClassId" placeholder="选择默认班级" style="width: 100%;" allow-clear>
<a-select
v-model:value="importDefaultClassId"
placeholder="选择默认班级"
style="width: 100%;"
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="import-submit-btn">
<a-button
type="primary"
:loading="importing"
:disabled="!importFile"
@click="handleImport"
block
class="import-submit-btn"
>
<template v-if="!importing">
<RocketOutlined class="btn-icon" />
开始导入
@ -310,6 +379,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import {
SearchOutlined,
TeamOutlined,
UserOutlined,
EditOutlined,
@ -847,9 +917,9 @@ onMounted(() => {
}
.filters :deep(.ant-select-selector),
{
border-radius: 12px;
border: 2px solid #F0F0F0;
.filters :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.filters :deep(.ant-select-selector:hover),
@ -1320,8 +1390,7 @@ border: 2px solid #F0F0F0;
gap: 12px;
}
.filters,
.actions {
.filters, .actions {
width: 100%;
flex-wrap: wrap;
}

View File

@ -28,8 +28,17 @@
<!-- 操作栏 -->
<div class="action-bar">
<div class="search-box">
<a-input-search v-model:value="searchKeyword" placeholder="搜索教师姓名/手机号/账号" style="width: 280px;"
@search="handleSearch" allow-clear />
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索教师姓名/手机号/账号"
style="width: 280px;"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined style="color: #B2BEC3;" />
</template>
</a-input-search>
</div>
<a-button type="primary" class="add-btn" @click="showAddModal">
<PlusOutlined class="btn-icon" />
@ -39,8 +48,12 @@
<!-- 教师卡片列表 -->
<div class="teacher-grid" v-if="!loading && teachers.length > 0">
<div v-for="teacher in teachers" :key="teacher.id" class="teacher-card"
:class="{ 'inactive': teacher.status !== 'ACTIVE' }">
<div
v-for="teacher in teachers"
:key="teacher.id"
class="teacher-card"
:class="{ 'inactive': teacher.status !== 'ACTIVE' }"
>
<div class="card-header">
<div class="teacher-avatar">
<SolutionOutlined class="avatar-icon" />
@ -66,11 +79,9 @@
<div class="info-row">
<BankOutlined class="info-icon" />
<span class="info-value 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="no-class">未分配班级</span>
</span>
@ -88,7 +99,10 @@
<a-button type="link" size="small" @click="handleResetPassword(teacher)">
<KeyOutlined /> 重置密码
</a-button>
<a-popconfirm title="确定要删除这位教师吗?" @confirm="handleDelete(teacher.id)">
<a-popconfirm
title="确定要删除这位教师吗?"
@confirm="handleDelete(teacher.id)"
>
<a-button type="link" size="small" danger>
<DeleteOutlined /> 删除
</a-button>
@ -114,14 +128,26 @@
<!-- 分页 -->
<div class="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" class="teacher-modal">
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑教师' : '添加教师'"
@ok="handleModalOk"
@cancel="handleModalCancel"
:confirm-loading="submitting"
:width="520"
class="teacher-modal"
>
<template #title>
<span class="modal-title">
<EditOutlined v-if="isEdit" class="modal-title-icon" />
@ -129,44 +155,49 @@
{{ 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 style="color: #B2BEC3;" />
</template>
<template #prefix><UserOutlined style="color: #B2BEC3;" /></template>
</a-input>
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input v-model:value="formState.phone" placeholder="请输入手机号">
<template #prefix>
<PhoneOutlined style="color: #B2BEC3;" />
</template>
<template #prefix><PhoneOutlined style="color: #B2BEC3;" /></template>
</a-input>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formState.email" placeholder="请输入邮箱(可选)">
<template #prefix>
<MailOutlined style="color: #B2BEC3;" />
</template>
<template #prefix><MailOutlined style="color: #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 style="color: #B2BEC3;" />
</template>
<a-input
v-model:value="formState.loginAccount"
placeholder="请输入登录账号"
:disabled="isEdit"
>
<template #prefix><KeyOutlined style="color: #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 style="color: #B2BEC3;" />
</template>
<template #prefix><LockOutlined style="color: #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>
@ -176,7 +207,12 @@
</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="modal-title">
<KeyOutlined class="modal-title-icon" />
@ -202,6 +238,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import {
SearchOutlined,
SolutionOutlined,
PlusOutlined,
PhoneOutlined,
@ -520,6 +557,11 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.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;
}