Files
zhuzi-admin/src/views/schools/components/SchoolTree.vue
2025-09-15 23:55:27 +08:00

550 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>