550 lines
15 KiB
Vue
550 lines
15 KiB
Vue
<template>
|
||
<div class="school-tree">
|
||
<div v-if="!data.length && !loading" class="empty-state">
|
||
<a-empty
|
||
image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
|
||
:image-style="{ height: '100px' }"
|
||
description="暂无学校数据"
|
||
>
|
||
<template #description>
|
||
<span style="color: #8c8c8c">暂无学校数据,请添加学校或调整筛选条件</span>
|
||
</template>
|
||
</a-empty>
|
||
</div>
|
||
|
||
<div v-else class="tree-content">
|
||
<div v-for="school in data" :key="school.id" class="school-item">
|
||
<!-- 学校卡片 -->
|
||
<a-card class="school-card" :hoverable="true">
|
||
<template #title>
|
||
<div class="school-header">
|
||
<div class="school-info">
|
||
<HomeOutlined class="school-icon" />
|
||
<span class="school-name">{{ school.name }}</span>
|
||
<a-tag :color="getSchoolTypeColor(school.type)" size="small" class="type-tag">
|
||
{{ getSchoolTypeText(school.type) }}
|
||
</a-tag>
|
||
<a-tag color="geekblue" size="small">{{ school.district }}</a-tag>
|
||
</div>
|
||
<div class="school-actions">
|
||
<a-dropdown :trigger="['click']" placement="bottomRight">
|
||
<a-button size="small" type="text">
|
||
<MoreOutlined />
|
||
</a-button>
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item key="add-grade" @click="$emit('addGrade', school.id)">
|
||
<PlusOutlined />
|
||
添加年级
|
||
</a-menu-item>
|
||
<a-menu-item key="edit" @click="$emit('editSchool', school)">
|
||
<EditOutlined />
|
||
编辑学校
|
||
</a-menu-item>
|
||
<a-menu-divider />
|
||
<a-menu-item key="delete" danger @click="handleDeleteSchool(school)">
|
||
<DeleteOutlined />
|
||
删除学校
|
||
</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
</a-dropdown>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #extra>
|
||
<div class="school-stats">
|
||
<a-statistic
|
||
title="学生总数"
|
||
:value="school.studentCount || 0"
|
||
:value-style="{ fontSize: '16px' }"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 学校基本信息 -->
|
||
<div class="school-details">
|
||
<div v-if="school.address" class="detail-item">
|
||
<EnvironmentOutlined />
|
||
<span>{{ school.address }}</span>
|
||
</div>
|
||
<div v-if="school.principal" class="detail-item">
|
||
<UserOutlined />
|
||
<span>校长:{{ school.principal }}</span>
|
||
</div>
|
||
<div v-if="school.phone" class="detail-item">
|
||
<PhoneOutlined />
|
||
<span>{{ school.phone }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 年级列表 -->
|
||
<div v-if="school.grades?.length" class="grades-section">
|
||
<a-divider orientation="left">年级班级</a-divider>
|
||
<div class="grades-container">
|
||
<div v-for="grade in school.grades" :key="grade.id" class="grade-item">
|
||
<a-card size="small" class="grade-card">
|
||
<template #title>
|
||
<div class="grade-header">
|
||
<div class="grade-info">
|
||
<BookOutlined class="grade-icon" />
|
||
<span class="grade-name">{{ grade.name }}</span>
|
||
<a-tag size="small" color="orange">{{ grade.level }}年级</a-tag>
|
||
</div>
|
||
<div class="grade-actions">
|
||
<a-dropdown :trigger="['click']" placement="bottomRight">
|
||
<a-button size="small" type="text">
|
||
<MoreOutlined />
|
||
</a-button>
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item key="add-class" @click="$emit('addClass', school.id, grade.id)">
|
||
<PlusOutlined />
|
||
添加班级
|
||
</a-menu-item>
|
||
<a-menu-item key="edit" @click="$emit('editGrade', grade)">
|
||
<EditOutlined />
|
||
编辑年级
|
||
</a-menu-item>
|
||
<a-menu-divider />
|
||
<a-menu-item key="delete" danger @click="handleDeleteGrade(grade)">
|
||
<DeleteOutlined />
|
||
删除年级
|
||
</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
</a-dropdown>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 班级列表 -->
|
||
<div v-if="grade.classes?.length" class="classes-container">
|
||
<div v-for="cls in grade.classes" :key="cls.id" class="class-item">
|
||
<a-tag
|
||
:color="getClassColor(cls.id)"
|
||
class="class-tag"
|
||
@click="handleClassClick(cls)"
|
||
>
|
||
<TeamOutlined />
|
||
{{ cls.name }}
|
||
<span v-if="cls.studentCount" class="student-count">
|
||
({{ cls.studentCount }}人)
|
||
</span>
|
||
<span v-if="cls.teacherName" class="teacher-name">
|
||
- {{ cls.teacherName }}
|
||
</span>
|
||
|
||
<a-dropdown :trigger="['click']" @click.stop>
|
||
<MoreOutlined class="class-more" />
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item key="edit" @click="$emit('editClass', cls)">
|
||
<EditOutlined />
|
||
编辑班级
|
||
</a-menu-item>
|
||
<a-menu-divider />
|
||
<a-menu-item key="delete" danger @click="handleDeleteClass(cls)">
|
||
<DeleteOutlined />
|
||
删除班级
|
||
</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
</a-dropdown>
|
||
</a-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 无班级提示 -->
|
||
<div v-else class="no-classes">
|
||
<a-button
|
||
type="dashed"
|
||
size="small"
|
||
@click="$emit('addClass', school.id, grade.id)"
|
||
style="width: 100%"
|
||
>
|
||
<PlusOutlined />
|
||
添加班级
|
||
</a-button>
|
||
</div>
|
||
</a-card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 无年级提示 -->
|
||
<div v-else class="no-grades">
|
||
<a-empty
|
||
:image="simpleImage"
|
||
:image-style="{ height: '40px' }"
|
||
description="暂无年级"
|
||
>
|
||
<a-button type="primary" size="small" @click="$emit('addGrade', school.id)">
|
||
<PlusOutlined />
|
||
添加年级
|
||
</a-button>
|
||
</a-empty>
|
||
</div>
|
||
</a-card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { Modal } from 'ant-design-vue';
|
||
import {
|
||
HomeOutlined,
|
||
BookOutlined,
|
||
TeamOutlined,
|
||
MoreOutlined,
|
||
PlusOutlined,
|
||
EditOutlined,
|
||
DeleteOutlined,
|
||
EnvironmentOutlined,
|
||
UserOutlined,
|
||
PhoneOutlined
|
||
} from '@ant-design/icons-vue';
|
||
import type { School, Grade, Class } from '@/apis/schools';
|
||
|
||
// Props定义
|
||
interface Props {
|
||
data: School[];
|
||
loading?: boolean;
|
||
}
|
||
|
||
// 事件定义
|
||
interface Emits {
|
||
addGrade: [schoolId: string];
|
||
addClass: [schoolId: string, gradeId: string];
|
||
editSchool: [school: School];
|
||
editGrade: [grade: Grade];
|
||
editClass: [cls: Class];
|
||
deleteSchool: [school: School];
|
||
deleteGrade: [grade: Grade];
|
||
deleteClass: [cls: Class];
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
loading: false
|
||
});
|
||
|
||
const emit = defineEmits<Emits>();
|
||
|
||
// 简单图标(用于空状态)
|
||
const simpleImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgdmlld0JveD0iMCAwIDEyOCAxMjgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik02NCA5NkM4MC4xNjA0IDk2IDkzIDgzLjE2MDQgOTMgNjdTODAuMTYwNCAzOCA2NCAzOFMzNSA1MC44Mzk2IDM1IDY3UzQ3LjgzOTYgOTYgNjQgOTZaIiBzdHJva2U9IiNEOUQ5RDkiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K';
|
||
|
||
// 获取学校类型颜色
|
||
const getSchoolTypeColor = (type: string) => {
|
||
const colorMap: Record<string, string> = {
|
||
primary: 'green',
|
||
junior: 'blue',
|
||
senior: 'purple',
|
||
vocational: 'orange'
|
||
};
|
||
return colorMap[type] || 'default';
|
||
};
|
||
|
||
// 获取学校类型文本
|
||
const getSchoolTypeText = (type: string) => {
|
||
const textMap: Record<string, string> = {
|
||
primary: '小学',
|
||
junior: '初中',
|
||
senior: '高中',
|
||
vocational: '职校'
|
||
};
|
||
return textMap[type] || '未知';
|
||
};
|
||
|
||
// 获取班级颜色(基于ID生成固定颜色)
|
||
const getClassColor = (classId: string) => {
|
||
const colors = ['blue', 'green', 'orange', 'red', 'purple', 'cyan', 'geekblue', 'magenta'];
|
||
const hash = classId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||
return colors[hash % colors.length];
|
||
};
|
||
|
||
// 处理班级点击
|
||
const handleClassClick = (cls: Class) => {
|
||
console.log('班级点击:', cls);
|
||
// 可以在这里添加班级详情查看逻辑
|
||
};
|
||
|
||
// 删除学校确认
|
||
const handleDeleteSchool = (school: School) => {
|
||
Modal.confirm({
|
||
title: '确认删除学校',
|
||
content: `确定要删除学校"${school.name}"吗?此操作将同时删除该学校下的所有年级和班级,且不可恢复。`,
|
||
okText: '确定删除',
|
||
okType: 'danger',
|
||
cancelText: '取消',
|
||
onOk() {
|
||
emit('deleteSchool', school);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 删除年级确认
|
||
const handleDeleteGrade = (grade: Grade) => {
|
||
Modal.confirm({
|
||
title: '确认删除年级',
|
||
content: `确定要删除年级"${grade.name}"吗?此操作将同时删除该年级下的所有班级,且不可恢复。`,
|
||
okText: '确定删除',
|
||
okType: 'danger',
|
||
cancelText: '取消',
|
||
onOk() {
|
||
emit('deleteGrade', grade);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 删除班级确认
|
||
const handleDeleteClass = (cls: Class) => {
|
||
Modal.confirm({
|
||
title: '确认删除班级',
|
||
content: `确定要删除班级"${cls.name}"吗?此操作不可恢复。`,
|
||
okText: '确定删除',
|
||
okType: 'danger',
|
||
cancelText: '取消',
|
||
onOk() {
|
||
emit('deleteClass', cls);
|
||
}
|
||
});
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.school-tree {
|
||
.empty-state {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 300px;
|
||
}
|
||
|
||
.tree-content {
|
||
.school-item {
|
||
margin-bottom: 24px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.school-card {
|
||
border: 2px solid #f0f0f0;
|
||
border-radius: 12px;
|
||
|
||
&:hover {
|
||
border-color: #1890ff;
|
||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
|
||
}
|
||
|
||
:deep(.ant-card-head) {
|
||
border-bottom: 1px solid #f0f0f0;
|
||
background: linear-gradient(135deg, #f6f9fc 0%, #ffffff 100%);
|
||
}
|
||
}
|
||
|
||
.school-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
width: 100%;
|
||
|
||
.school-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1;
|
||
|
||
.school-icon {
|
||
color: #1890ff;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.school-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #262626;
|
||
}
|
||
|
||
.type-tag {
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.school-actions {
|
||
margin-left: 16px;
|
||
}
|
||
}
|
||
|
||
.school-details {
|
||
margin-bottom: 16px;
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
color: #8c8c8c;
|
||
font-size: 14px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.grades-section {
|
||
.grades-container {
|
||
display: grid;
|
||
gap: 16px;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
}
|
||
}
|
||
|
||
.grade-card {
|
||
border: 1px solid #e8e8e8;
|
||
border-radius: 8px;
|
||
|
||
&:hover {
|
||
border-color: #40a9ff;
|
||
box-shadow: 0 2px 8px rgba(64, 169, 255, 0.15);
|
||
}
|
||
|
||
:deep(.ant-card-head) {
|
||
background: #fafafa;
|
||
border-bottom: 1px solid #e8e8e8;
|
||
min-height: auto;
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
:deep(.ant-card-body) {
|
||
padding: 12px;
|
||
}
|
||
}
|
||
|
||
.grade-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
width: 100%;
|
||
|
||
.grade-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex: 1;
|
||
|
||
.grade-icon {
|
||
color: #fa8c16;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.grade-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #262626;
|
||
}
|
||
}
|
||
}
|
||
|
||
.classes-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
|
||
.class-item {
|
||
.class-tag {
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
position: relative;
|
||
|
||
&:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.student-count {
|
||
opacity: 0.8;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.teacher-name {
|
||
opacity: 0.7;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.class-more {
|
||
margin-left: 4px;
|
||
opacity: 0.6;
|
||
|
||
&:hover {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.no-classes {
|
||
text-align: center;
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.no-grades {
|
||
text-align: center;
|
||
padding: 24px 0;
|
||
border: 2px dashed #d9d9d9;
|
||
border-radius: 8px;
|
||
background: #fafafa;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.school-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
|
||
.school-info {
|
||
width: 100%;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.school-actions {
|
||
margin-left: 0;
|
||
align-self: flex-end;
|
||
}
|
||
}
|
||
|
||
.grades-container {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.grade-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 4px;
|
||
|
||
.grade-actions {
|
||
align-self: flex-end;
|
||
}
|
||
}
|
||
|
||
.classes-container {
|
||
.class-item .class-tag {
|
||
.teacher-name {
|
||
display: none;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|