web大作业开发记录04

教师页前端代码:



AdminHome.vue:

<template>
  <div class="admin-home-container">
    <div class="dashboard-header">
      <h1>学生数据统计板</h1>
      <p class="welcome-text">欢迎,{{ userInfo?.name || '老师' }}!</p>
    </div>

    <div class="stats-grid">
      <!-- 统计卡片 -->
      <div class="overview-cards">
        <el-row :gutter="20">
          <el-col :span="8">
            <el-card class="stat-card">
              <div class="stat-item">
                <div class="stat-icon students-icon">
                  <img src="@/assets/user.png" alt="学生" class="icon-image" />
                </div>
                <div class="stat-content">
                  <div class="stat-header">
                    <div class="stat-info">
                      <div class="stat-number">{{ totalStudents }}</div>
                      <div class="stat-label">总学生数</div>
                    </div>
                    <!-- 将下拉框移到右侧 -->
                    <div class="class-selector">
                      <el-select 
                        v-model="selectedClass" 
                        placeholder="选择班级" 
                        @change="onClassChange"
                        size="small"
                        style="width: 120px;"
                      >
                        <el-option label="所有班级" value="all"></el-option>
                        <el-option 
                          v-for="className in classList" 
                          :key="className" 
                          :label="className" 
                          :value="className"
                        ></el-option>
                      </el-select>
                    </div>
                  </div>
                </div>
              </div>
            </el-card>
          </el-col>
          <el-col :span="8">
            <el-card class="stat-card">
              <div class="stat-item">
                <div class="stat-icon active-goals-icon">
                  <img src="@/assets/goal.png" alt="活跃目标" class="icon-image" />
                </div>
                <div class="stat-content">
                  <div class="stat-number">{{ activeGoalsCount }}</div>
                  <div class="stat-label">活跃周目标学生</div>
                </div>
              </div>
            </el-card>
          </el-col>
          <el-col :span="8">
            <el-card class="stat-card">
              <div class="stat-item">
                <div class="stat-icon blogs-icon">
                  <img src="@/assets/blog.png" alt="博客" class="icon-image" />
                </div>
                <div class="stat-content">
                  <div class="stat-number">{{ totalBlogsThisWeek }}</div>
                  <div class="stat-label">本周博客总数</div>
                </div>
              </div>
            </el-card>
          </el-col>
        </el-row>
      </div>

      <!-- 图表区域 -->
      <div class="charts-section">
        <el-row :gutter="20">
          <!-- 学生周目标活跃情况饼图 -->
          <el-col :span="12">
            <el-card class="chart-card">
              <template #header>
                <div class="card-header">
                  <span>学生周目标活跃情况{{ selectedClass !== 'all' ? ` - ${selectedClass}` : '' }}</span>
                </div>
              </template>
              <div ref="goalsActiveChart" class="chart-container"></div>
            </el-card>
          </el-col>

          <!-- 学生本周博客发布情况条形图 -->
          <el-col :span="12">
            <el-card class="chart-card">
              <template #header>
                <div class="card-header">
                  <span>本周博客发布情况{{ selectedClass !== 'all' ? ` - ${selectedClass}` : '' }}</span>
                </div>
              </template>
              <div ref="blogsDistributionChart" class="chart-container"></div>
            </el-card>
          </el-col>
        </el-row>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted, nextTick, watch } from 'vue'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import { getAllStudents } from '@/api/user'
import { getStudentsGoalsActiveStats, getStudentsBlogStats } from '@/api/admin'

// 响应式数据
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
const totalStudents = ref(0)
const activeGoalsCount = ref(0)
const totalBlogsThisWeek = ref(0)
const loading = ref(false)

// 班级相关数据
const selectedClass = ref('all')
const classList = ref([])
const allStudentsData = ref([])

// 图表引用
const goalsActiveChart = ref(null)
const blogsDistributionChart = ref(null)

// 统计数据
const goalsActiveStats = ref({})
const blogsDistributionStats = ref({})

// 获取统计数据
const fetchStatistics = async () => {
  loading.value = true
  try {
    // 获取所有学生
    const studentsResponse = await getAllStudents()
    if (studentsResponse.code === 200) {
      allStudentsData.value = studentsResponse.data
      // 提取班级列表
      const classSet = new Set()
      studentsResponse.data.forEach(student => {
        if (student.className) {
          classSet.add(student.className)
        }
      })
      classList.value = Array.from(classSet).sort()
      
      // 更新统计数据
      updateStatistics()
    }
  } catch (error) {
    console.error('获取统计数据失败:', error)
    ElMessage.error('获取统计数据失败')
  } finally {
    loading.value = false
  }
}

// 根据选择的班级更新统计数据
const updateStatistics = async () => {
  try {
    let filteredStudents = allStudentsData.value
    
    // 如果选择了特定班级,过滤学生数据
    if (selectedClass.value !== 'all') {
      filteredStudents = allStudentsData.value.filter(student => 
        student.className === selectedClass.value
      )
    }
    
    // 更新总学生数
    totalStudents.value = filteredStudents.length
    
    // 获取学生周目标活跃统计(根据班级过滤)
    const goalsStatsResponse = await getStudentsGoalsActiveStats(selectedClass.value !== 'all' ? selectedClass.value : null)
    if (goalsStatsResponse.code === 200) {
      goalsActiveStats.value = goalsStatsResponse.data
      activeGoalsCount.value = goalsStatsResponse.data.activeCount || 0
    }

    // 获取学生本周博客发布统计(根据班级过滤)
    const blogsStatsResponse = await getStudentsBlogStats(selectedClass.value !== 'all' ? selectedClass.value : null)
    if (blogsStatsResponse.code === 200) {
      blogsDistributionStats.value = blogsStatsResponse.data
      totalBlogsThisWeek.value = blogsStatsResponse.data.totalBlogs || 0
    }

    // 等待DOM更新后初始化图表
    await nextTick()
    initCharts()
  } catch (error) {
    console.error('更新统计数据失败:', error)
    ElMessage.error('更新统计数据失败')
  }
}

// 班级选择变化处理
const onClassChange = () => {
  updateStatistics()
}

// 初始化图表
const initCharts = () => {
  initGoalsActiveChart()
  initBlogsDistributionChart()
}

// 学生周目标活跃情况饼图
const initGoalsActiveChart = () => {
  if (!goalsActiveChart.value) return
  
  const chart = echarts.init(goalsActiveChart.value)
  const stats = goalsActiveStats.value
  
  const option = {
    tooltip: {
      trigger: 'item',
      formatter: '{b}: {c}人 ({d}%)'
    },
    legend: {
      orient: 'vertical',
      left: 'left'
    },
    series: [
      {
        name: '周目标活跃情况',
        type: 'pie',
        radius: '50%',
        data: [
          { value: stats.activeCount || 0, name: '有活跃周目标', itemStyle: { color: '#67C23A' } },
          { value: stats.inactiveCount || 0, name: '无活跃周目标', itemStyle: { color: '#F56C6C' } }
        ],
        emphasis: {
          itemStyle: {
            shadowBlur: 10,
            shadowOffsetX: 0,
            shadowColor: 'rgba(0, 0, 0, 0.5)'
          }
        }
      }
    ]
  }
  
  chart.setOption(option)
}

// 学生本周博客发布情况条形图
const initBlogsDistributionChart = () => {
  if (!blogsDistributionChart.value) return

  const chart = echarts.init(blogsDistributionChart.value)
  const stats = blogsDistributionStats.value

  const option = {
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'shadow'
      },
      formatter: '{b}: {c}人'
    },
    xAxis: {
      type: 'category',
      // 修改:X轴标签与后端数据结构匹配
      data: ['未发布', '1-4篇', '5篇及以上'],
      axisLabel: {
        interval: 0,
        rotate: 0
      }
    },
    yAxis: {
      type: 'value',
      name: '人数',
      minInterval: 1
    },
    series: [
      {
        name: '博客发布情况',
        type: 'bar',
        data: [
          { value: stats.noBlog || 0, itemStyle: { color: '#F56C6C' } },
          { value: stats.lessThan5 || 0, itemStyle: { color: '#E6A23C' } },
          { value: stats.moreThan5 || 0, itemStyle: { color: '#67C23A' } }
        ],
        barWidth: '60%'
      }
    ]
  }

  chart.setOption(option)
}

// 组件挂载时获取数据
onMounted(() => {
  fetchStatistics()
})
</script>

<style scoped>
.admin-home-container {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
}

.dashboard-header {
  text-align: center;
  margin-bottom: 30px;
}

.dashboard-header h1 {
  color: #303133;
  font-size: 28px;
  margin-bottom: 10px;
}

.welcome-text {
  color: #606266;
  font-size: 16px;
}

.overview-cards {
  margin-bottom: 20px;
}

.stat-card {
  border-radius: 8px;
  transition: all 0.3s;
}

.stat-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.stat-item {
  display: flex;
  align-items: center;
  padding: 10px;
}

.stat-icon {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 12px;
}

.icon-image {
  width: 24px;
  height: 24px;
  display: block;
}

.students-icon {
  background: linear-gradient(135deg, #667eea 0%, #c09fe1 100%);
}

.active-goals-icon {
  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}

.blogs-icon {
  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}

.stat-content {
  flex: 1;
}

.stat-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
}

.stat-info {
  flex: 1;
}

.stat-number {
  font-size: 24px;
  font-weight: bold;
  color: #303133;
  margin-bottom: 5px;
}

.stat-label {
  font-size: 14px;
  color: #909399;
}

.class-selector {
  flex-shrink: 0;
  margin-left: 12px;
}

.class-selector {
  margin-top: 8px;
}

.chart-card {
  border-radius: 8px;
}

.card-header {
  font-weight: bold;
  color: #303133;
}

.chart-container {
  height: 400px;
  width: 100%;
}
</style>

AdminManage.vue:

<template>
  <div class="page-container">
    <el-card class="base-card">
      <template #header>
        <div class="card-header">
          <h2>学生管理</h2>
          <div class="card-actions">
            <el-button type="primary" @click="showAddDialog">添加学生</el-button>
          </div>
        </div>
      </template>

      <!-- 搜索表单 -->
      <div class="search-form">
        <el-form :model="searchForm">
          <div class="search-grid">
            <!-- 第一行 -->
            <div class="search-form-content">
              <el-form-item label="学号" class="search-item">
                <el-input
                  v-model="searchForm.studentId"
                  placeholder="请输入学号"
                  clearable
                  @clear="handleSearch"
                  class="search-input"
                ></el-input>
              </el-form-item>

              <el-form-item label="姓名" class="search-item">
                <el-input
                  v-model="searchForm.name"
                  placeholder="请输入姓名"
                  clearable
                  @clear="handleSearch"
                  class="search-input"
                ></el-input>
              </el-form-item>
            </div>

            <!-- 第二行 -->
            <div class="search-form-content">
              <el-form-item label="班级" class="search-item">
                <el-select
                  v-model="searchForm.className"
                  placeholder="请选择班级"
                  clearable
                  @clear="handleSearch"
                  class="search-select"
                >
                  <el-option
                    v-for="item in classList"
                    :key="item"
                    :label="item"
                    :value="item"
                  ></el-option>
                </el-select>
              </el-form-item>

              <!-- 空白占位,保持布局对齐 -->
              <div class="search-item"></div>
            </div>

            <!-- 操作按钮单独一行 -->
            <div class="search-action">
              <el-button type="primary" @click="handleSearch">搜索</el-button>
              <el-button @click="resetSearch">重置</el-button>
            </div>
          </div>
        </el-form>
      </div>

      <!-- 学生列表 -->
      <el-table
        :data="students"
        style="width: 100%"
        :row-class-name="tableRowClassName"
        border
        stripe
        highlight-current-row
        class="base-table"
      >
        <el-table-column prop="studentId" label="学号" width="120" sortable></el-table-column>
        <el-table-column prop="name" label="姓名" width="100" sortable></el-table-column>
        <el-table-column prop="className" label="班级" min-width="150" sortable></el-table-column>
        <el-table-column prop="phoneNumber" label="手机号" width="130"></el-table-column>
        <el-table-column prop="gender" label="性别" width="70" align="center">
          <template #default="scope">
            <el-tag size="small" :type="scope.row.gender === '男' ? 'primary' : 'success'">
              {{ scope.row.gender }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="points" label="积分" width="80" align="center" sortable>
          <template #default="scope">
            <el-tag size="small" effect="plain" :type="scope.row.points > 0 ? 'success' : 'info'">
              {{ scope.row.points }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" min-width="260" fixed="right" align="center">
          <template #default="scope">
            <div class="operation-buttons">
              <el-button
                type="primary"
                size="small"
                @click="handleEdit(scope.row)"
              >
                编辑
              </el-button>
              <el-button
                type="warning"
                size="small"
                @click="handleResetPassword(scope.row)"
              >
                重置密码
              </el-button>
              <el-button
                type="danger"
                size="small"
                @click="handleDelete(scope.row)"
              >
                删除
              </el-button>
            </div>
          </template>
        </el-table-column>
      </el-table>
    </el-card>

    <!-- 添加学生对话框 -->
    <el-dialog
      title="添加学生"
      v-model="dialogVisible"
      width="500px"
    >
      <el-form :model="studentForm" :rules="rules" ref="studentFormRef" label-width="80px">
        <el-form-item label="学号" prop="studentId">
          <el-input v-model="studentForm.studentId" placeholder="请输入学号"></el-input>
        </el-form-item>
        <el-form-item label="姓名" prop="name">
          <el-input v-model="studentForm.name" placeholder="请输入姓名"></el-input>
        </el-form-item>
        <el-form-item label="班级" prop="className">
          <el-input v-model="studentForm.className" placeholder="请输入班级"></el-input>
        </el-form-item>
        <el-form-item label="手机号" prop="phoneNumber">
          <el-input v-model="studentForm.phoneNumber" placeholder="请输入手机号"></el-input>
        </el-form-item>
        <el-form-item label="性别" prop="gender">
          <el-select v-model="studentForm.gender" placeholder="请选择性别" style="width: 100%">
            <el-option label="男" value="男"></el-option>
            <el-option label="女" value="女"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input v-model="studentForm.password" type="password" placeholder="请输入密码"></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="handleDialogClose">取消</el-button>
          <el-button type="primary" @click="handleSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>

    <!-- 编辑学生对话框 -->
    <el-dialog
      title="编辑学生信息"
      v-model="editDialogVisible"
      width="500px"
    >
      <el-form :model="editForm" :rules="editRules" ref="editFormRef" label-width="80px">
        <el-form-item label="学号" prop="studentId">
          <el-input v-model="editForm.studentId" disabled></el-input>
        </el-form-item>
        <el-form-item label="姓名" prop="name">
          <el-input v-model="editForm.name" placeholder="请输入姓名"></el-input>
        </el-form-item>
        <el-form-item label="班级" prop="className">
          <el-input v-model="editForm.className" placeholder="请输入班级"></el-input>
        </el-form-item>
        <el-form-item label="手机号" prop="phoneNumber">
          <el-input v-model="editForm.phoneNumber" placeholder="请输入手机号"></el-input>
        </el-form-item>
        <el-form-item label="性别" prop="gender">
          <el-select v-model="editForm.gender" placeholder="请选择性别" style="width: 100%">
            <el-option label="男" value="男"></el-option>
            <el-option label="女" value="女"></el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="editDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleEditSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getAllStudents } from '@/api/user'
import { addStudent, updateStudent, deleteStudent, resetPassword } from '@/api/admin'

const dialogVisible = ref(false)
const editDialogVisible = ref(false)
const studentFormRef = ref(null)
const editFormRef = ref(null)

const studentForm = reactive({
  studentId: '',
  name: '',
  className: '',
  phoneNumber: '',
  gender: '',
  password: ''
})

const editForm = reactive({
  studentId: '',
  name: '',
  className: '',
  phoneNumber: '',
  gender: ''
})

const searchForm = reactive({
  studentId: '',
  name: '',
  className: ''
})

const rules = {
  studentId: [{ required: true, message: '请输入学号', trigger: 'blur' }],
  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
  className: [{ required: true, message: '请输入班级', trigger: 'blur' }],
  phoneNumber: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
  ],
  gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
  password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}

const editRules = {
  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
  className: [{ required: true, message: '请输入班级', trigger: 'blur' }],
  phoneNumber: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
  ],
  gender: [{ required: true, message: '请选择性别', trigger: 'change' }]
}

const allStudents = ref([])
const students = ref([])

// 获取班级列表
const classList = computed(() => {
  const classes = [...new Set(allStudents.value.map(student => student.className))]
  return classes.filter(className => className)
})

// 显示添加对话框
const showAddDialog = () => {
  dialogVisible.value = true
  Object.assign(studentForm, {
    studentId: '',
    name: '',
    className: '',
    phoneNumber: '',
    gender: '',
    password: ''
  })
}

// 关闭添加对话框
const handleDialogClose = () => {
  dialogVisible.value = false
}

// 获取学生列表
const fetchStudents = async () => {
  try {
    const res = await getAllStudents()
    if (res.code === 200) {
      allStudents.value = res.data
      students.value = res.data
    } else {
      ElMessage.error(res.msg || '获取学生列表失败')
    }
  } catch (error) {
    console.error('获取学生列表失败:', error)
    ElMessage.error('获取学生列表失败')
  }
}

// 添加学生
const handleSubmit = async () => {
  try {
    await studentFormRef.value.validate()
    
    const res = await addStudent(studentForm)
    if (res.code === 200) {
      ElMessage.success('添加成功')
      dialogVisible.value = false
      fetchStudents()
    } else {
      ElMessage.error(res.msg || '添加失败')
    }
  } catch (error) {
    console.error('添加失败:', error)
    ElMessage.error('请填写完整信息')
  }
}

// 编辑学生
const handleEdit = (row) => {
  Object.assign(editForm, {
    studentId: row.studentId,
    name: row.name,
    className: row.className,
    phoneNumber: row.phoneNumber,
    gender: row.gender
  })
  editDialogVisible.value = true
}

// 提交编辑
const handleEditSubmit = async () => {
  try {
    await editFormRef.value.validate()
    
    const res = await updateStudent(editForm)
    if (res.code === 200) {
      ElMessage.success('更新成功')
      editDialogVisible.value = false
      fetchStudents()
    } else {
      ElMessage.error(res.msg || '更新失败')
    }
  } catch (error) {
    console.error('更新失败:', error)
    ElMessage.error('请填写完整信息')
  }
}

// 重置密码
const handleResetPassword = async (row) => {
  try {
    await ElMessageBox.confirm(`确定要重置学生 ${row.name} 的密码吗?密码将重置为默认密码123456`, '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })

    const res = await resetPassword(row.studentId)
    if (res.code === 200) {
      ElMessage.success('密码重置成功')
    } else {
      ElMessage.error(res.msg || '密码重置失败')
    }
  } catch (error) {
    if (error !== 'cancel') {
      console.error('密码重置失败:', error)
      ElMessage.error('密码重置失败')
    }
  }
}

// 删除学生
const handleDelete = async (row) => {
  try {
    await ElMessageBox.confirm(`确定要删除学生 ${row.name} 吗?`, '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })

    const res = await deleteStudent(row.studentId)
    if (res.code === 200) {
      ElMessage.success('删除成功')
      fetchStudents()
    } else {
      ElMessage.error(res.msg || '删除失败')
    }
  } catch (error) {
    if (error !== 'cancel') {
      console.error('删除失败:', error)
      ElMessage.error('删除失败')
    }
  }
}

// 搜索
const handleSearch = () => {
  let filteredStudents = allStudents.value
  
  if (searchForm.studentId) {
    filteredStudents = filteredStudents.filter(student => 
      student.studentId.includes(searchForm.studentId)
    )
  }
  
  if (searchForm.name) {
    filteredStudents = filteredStudents.filter(student => 
      student.name.includes(searchForm.name)
    )
  }
  
  if (searchForm.className) {
    filteredStudents = filteredStudents.filter(student => 
      student.className === searchForm.className
    )
  }
  
  students.value = filteredStudents
}

// 重置搜索
const resetSearch = () => {
  Object.assign(searchForm, {
    studentId: '',
    name: '',
    className: ''
  })
  students.value = allStudents.value
}

// 表格行样式
const tableRowClassName = ({ row, rowIndex }) => {
  return rowIndex % 2 === 0 ? 'even-row' : 'odd-row'
}

// 页面加载时获取数据
onMounted(() => {
  fetchStudents()
})
</script>

<style scoped>
/* 全局基础样式 */
:root {
  --primary-color: #409EFF;
  --success-color: #67C23A;
  --warning-color: #E6A23C;
  --danger-color: #F56C6C;
  --info-color: #909399;
  --border-radius-base: 4px;
  --box-shadow-base: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  --transition-base: all 0.3s;
}

/* 页面容器基础样式 */
.page-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

/* 卡片基础样式 */
.base-card {
  margin-bottom: 30px;
  box-shadow: var(--box-shadow-base);
  border-radius: var(--border-radius-base);
  transition: var(--transition-base);
}

.base-card:hover {
  box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
}

/* 卡片头部样式 */
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 25px;
  padding: 0 10px;
}

.card-header h2 {
  margin: 0;
  font-size: 24px;
  color: #303133;
  border-bottom: 2px solid var(--primary-color);
  padding-bottom: 10px;
}

/* 按钮组样式 */
.card-actions {
  display: flex;
  gap: 15px;
}

/* 搜索表单样式 - 参考Honor.vue */
.search-form {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f5f7fa;
  border-radius: var(--border-radius-base);
  border: 1px solid #ebeef5;
}

.search-grid {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.search-form-content {
  display: flex;
  gap: 20px;
  align-items: flex-end;
}

.search-item {
  margin-bottom: 0;
  display: flex;
  align-items: center;
  flex: 1;
  min-width: 300px;
}

.search-item :deep(.el-form-item__label) {
  width: 80px;
  text-align: right;
  padding-right: 12px;
  color: #606266;
  font-weight: normal;
}

.search-item :deep(.el-form-item__content) {
  flex: 1;
  min-width: 200px;
}

.search-select {
  width: 100%;
}

.search-input {
  width: 100%;
}

/* 搜索按钮样式 - 与Honor.vue一致 */
.search-action {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  padding-top: 8px;
  border-top: 1px solid #e6e6e6;
  margin-top: 8px;
}

/* 表格样式优化 */
.base-table {
  width: 100%;
  margin-top: 20px;
}

.base-table :deep(.el-table__row) {
  cursor: pointer;
  transition: background-color 0.3s;
}

.base-table :deep(.el-table__row:hover) {
  background-color: #f5f7fa !important;
}

.base-table :deep(.el-table__header-wrapper) {
  background-color: #fafafa;
}

.base-table :deep(.el-table__header) {
  font-weight: 600;
  color: #303133;
}

/* 操作按钮样式优化 */
.operation-buttons {
  display: flex;
  gap: 8px;
  justify-content: center;
  flex-wrap: wrap;
}

.operation-buttons .el-button {
  margin: 0;
  min-width: 70px;
}

/* 对话框样式 */
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

/* 响应式调整 */
@media (max-width: 768px) {
  .search-item {
    min-width: 100%;
  }

  .search-form-content {
    flex-direction: column;
    gap: 12px;
  }

  .search-action {
    justify-content: center;
  }

  .card-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 15px;
  }

  .card-actions {
    width: 100%;
    justify-content: flex-end;
  }

  .operation-buttons {
    flex-direction: column;
    gap: 4px;
  }

  .operation-buttons .el-button {
    width: 100%;
  }
}

/* 表格行样式 */
:deep(.el-table__row.even-row) {
  background-color: #fafafa;
}

:deep(.el-table__row.odd-row) {
  background-color: #ffffff;
}

/* 标签样式优化 */
.el-tag {
  font-weight: 500;
}
</style>

AdminBlog.vue:

<template>
  <div class="page-container">
    <el-card class="base-card">
      <template #header>
        <div class="card-header">
          <h2>学生博客管理</h2>
        </div>
      </template>

      <!-- 搜索表单 -->
      <div class="search-form">
        <el-form :model="searchForm">
          <div class="search-grid">
            <!-- 第一行 -->
            <div class="search-form-content">
              <el-form-item label="学号" class="search-item">
                <el-input
                  v-model="searchForm.studentId"
                  placeholder="请输入学号"
                  clearable
                  @clear="handleSearch"
                  class="search-input"
                ></el-input>
              </el-form-item>

              <el-form-item label="姓名" class="search-item">
                <el-input
                  v-model="searchForm.name"
                  placeholder="请输入姓名"
                  clearable
                  @clear="handleSearch"
                  class="search-input"
                ></el-input>
              </el-form-item>
            </div>

            <!-- 第二行 -->
            <div class="search-form-content">
              <el-form-item label="班级" class="search-item">
                <el-select
                  v-model="searchForm.className"
                  placeholder="请选择班级"
                  clearable
                  @clear="handleSearch"
                  class="search-select"
                >
                  <el-option
                    v-for="item in classList"
                    :key="item"
                    :label="item"
                    :value="item"
                  ></el-option>
                </el-select>
              </el-form-item>
              
            </div>

            <!-- 操作按钮单独一行 -->
            <div class="search-action">
              <el-button type="primary" @click="handleSearch">搜索</el-button>
              <el-button @click="resetSearch">重置</el-button>
            </div>
          </div>
        </el-form>
      </div>

      <!-- 未提交博客的学生 -->
      <div class="section-header">
        <h3 style="color: #F56C6C;">本周未提交博客的学生 ({{ filteredNoBlogStudents.length }}人)</h3>
      </div>
      
      <el-table
        :data="filteredNoBlogStudents"
        style="width: 100%"
        border
        stripe
        highlight-current-row
        class="base-table"
      >
        <el-table-column prop="studentId" label="学号" width="120" sortable>
          <template #default="scope">
            <el-button type="text" @click="viewStudentDetail(scope.row)" class="student-link">
              {{ scope.row.studentId }}
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="name" label="姓名" width="100" sortable>
          <template #default="scope">
            <el-button type="text" @click="viewStudentDetail(scope.row)" class="student-link">
              {{ scope.row.name }}
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="className" label="班级" min-width="150" sortable></el-table-column>
        <el-table-column prop="weekBlogCount" label="本周博客篇数" width="120" align="center">
          <template #default="scope">
            <el-button 
              type="text" 
              @click="viewWeekBlogs(scope.row)"
              :disabled="scope.row.weekBlogCount === 0"
              class="blog-count-link"
            >
              <el-tag size="small" type="danger">{{ scope.row.weekBlogCount }}</el-tag>
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="allBlogCount" label="所有博客篇数" width="120" align="center">
          <template #default="scope">
            <el-button
                type="text"
                @click="viewAllBlogs(scope.row)"
                :disabled="scope.row.allBlogCount === 0"
                class="blog-count-link"
            >
              <el-tag size="small" effect="plain">{{ scope.row.allBlogCount }}</el-tag>
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="points" label="积分" width="80" align="center" sortable>
          <template #default="scope">
            <el-tag size="small" effect="plain" :type="scope.row.points > 0 ? 'success' : 'info'">
              {{ scope.row.points }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="120" align="center">
          <template #default="scope">
            <el-button
              type="warning"
              size="small"
              @click="showScoreDialog(scope.row)"
            >
              评分
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分隔空白 -->
      <div class="section-divider"></div>

      <!-- 已提交博客的学生 -->
      <div class="section-header">
        <h3 style="color: #67C23A;">已提交博客的学生 ({{ filteredHasBlogStudents.length }}人)</h3>
      </div>
      
      <el-table
        :data="filteredHasBlogStudents"
        style="width: 100%"
        border
        stripe
        highlight-current-row
        class="base-table"
      >
        <el-table-column prop="studentId" label="学号" width="120" sortable>
          <template #default="scope">
            <el-button type="text" @click="viewStudentDetail(scope.row)" class="student-link">
              {{ scope.row.studentId }}
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="name" label="姓名" width="100" sortable>
          <template #default="scope">
            <el-button type="text" @click="viewStudentDetail(scope.row)" class="student-link">
              {{ scope.row.name }}
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="className" label="班级" min-width="150" sortable></el-table-column>
        <el-table-column prop="weekBlogCount" label="本周博客篇数" width="120" align="center">
          <template #default="scope">
            <el-button 
              type="text" 
              @click="viewWeekBlogs(scope.row)"
              :disabled="scope.row.weekBlogCount === 0"
              class="blog-count-link"
            >
              <el-tag size="small" type="success">{{ scope.row.weekBlogCount }}</el-tag>
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="allBlogCount" label="所有博客篇数" width="120" align="center">
          <template #default="scope">
            <el-button 
              type="text" 
              @click="viewAllBlogs(scope.row)"
              :disabled="scope.row.allBlogCount === 0"
              class="blog-count-link"
            >
              <el-tag size="small" effect="plain">{{ scope.row.allBlogCount }}</el-tag>
            </el-button>
          </template>
        </el-table-column>
        <el-table-column prop="points" label="积分" width="80" align="center" sortable>
          <template #default="scope">
            <el-tag size="small" effect="plain" :type="scope.row.points > 0 ? 'success' : 'info'">
              {{ scope.row.points }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="120" align="center">
          <template #default="scope">
            <el-button
              type="warning"
              size="small"
              @click="showScoreDialog(scope.row)"
            >
              评分
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>

    <!-- 学生详情对话框 -->
    <el-dialog
      :title="`${selectedStudent.name}的详细信息`"
      v-model="detailDialogVisible"
      width="50%"
      top="5vh"
    >
      <div class="student-detail">
        <div class="detail-row">
          <span class="detail-label">学号:</span>
          <span class="detail-value">{{ selectedStudent.studentId }}</span>
        </div>
        <div class="detail-row">
          <span class="detail-label">姓名:</span>
          <span class="detail-value">{{ selectedStudent.name }}</span>
        </div>
        <div class="detail-row">
          <span class="detail-label">班级:</span>
          <span class="detail-value">{{ selectedStudent.className }}</span>
        </div>
        <div class="detail-row">
          <span class="detail-label">积分:</span>
          <span class="detail-value">{{ selectedStudent.points }}</span>
        </div>
        <div class="detail-row">
          <span class="detail-label">本周博客数:</span>
          <span class="detail-value">{{ selectedStudent.weekBlogCount }}</span>
        </div>
        <div class="detail-row">
          <span class="detail-label">总博客数:</span>
          <span class="detail-value">{{ selectedStudent.allBlogCount }}</span>
        </div>
      </div>
      
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关闭</el-button>
        </span>
      </template>
    </el-dialog>

    <!-- 博客列表对话框 -->
    <el-dialog
      :title="blogListTitle"
      v-model="blogListDialogVisible"
      width="50%"
      top="5vh"
    >
      <div v-loading="blogListLoading">
        <div v-if="blogList.length === 0" class="no-data">
          <el-empty description="暂无博客"></el-empty>
        </div>
        <div v-else>
          <el-table
            :data="blogList"
            style="width: 100%"
            border
            stripe
          >
            <el-table-column prop="title" label="博客标题" min-width="200">
              <template #default="scope">
                <el-link 
                  :href="scope.row.url" 
                  target="_blank" 
                  type="primary"
                  class="blog-title-link"
                >
                  {{ scope.row.title }}
                </el-link>
              </template>
            </el-table-column>
            <el-table-column prop="url" label="博客链接" min-width="250">
              <template #default="scope">
                <el-link 
                  :href="scope.row.url" 
                  target="_blank" 
                  type="info"
                  class="blog-url-link"
                >
                  {{ scope.row.url }}
                </el-link>
              </template>
            </el-table-column>
            <el-table-column prop="publishTime" label="发布时间" width="180" align="center">
              <template #default="scope">
                <el-tag size="small" effect="plain">
                  {{ formatDate(scope.row.publishTime) }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
      
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="blogListDialogVisible = false">关闭</el-button>
        </span>
      </template>
    </el-dialog>

    <!-- 评分对话框 -->
    <el-dialog
      title="学生评分"
      v-model="scoreDialogVisible"
      width="400px"
    >
      <el-form :model="scoreForm" :rules="scoreRules" ref="scoreFormRef" label-width="100px">
        <el-form-item label="学生姓名">
          <el-input v-model="scoreForm.studentName" disabled></el-input>
        </el-form-item>
        <el-form-item label="当前积分">
          <el-input v-model="scoreForm.currentPoints" disabled></el-input>
        </el-form-item>
        <el-form-item label="评分积分" prop="points">
          <el-input-number
            v-model="scoreForm.points"
            :min="-1000"
            :max="1000"
            placeholder="请输入积分(可为负数)"
            style="width: 100%"
          ></el-input-number>
        </el-form-item>
        <el-form-item label="评分说明">
          <el-input
            v-model="scoreForm.remark"
            type="textarea"
            :rows="3"
            placeholder="请输入评分说明(可选)"
          ></el-input>
        </el-form-item>
      </el-form>
      
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="scoreDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleScore">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { getAllStudents } from '@/api/user'
import { getStudentsBlogManageData, scoreStudent } from '@/api/admin'
import { getWeekBlogsByStudentId, getBlogLinksByStudentId } from '@/api/blogLink'

const noBlogStudents = ref([])
const hasBlogStudents = ref([])
const allStudents = ref([])
const detailDialogVisible = ref(false)
const scoreDialogVisible = ref(false)
const blogListDialogVisible = ref(false)
const selectedStudent = ref({})
const scoreFormRef = ref()
const blogList = ref([])
const blogListLoading = ref(false)
const blogListTitle = ref('')

// 搜索表单
const searchForm = reactive({
  studentId: '',
  name: '',
  className: '',
  blogStatus: ''
})

// 评分表单
const scoreForm = reactive({
  studentId: '',
  studentName: '',
  currentPoints: 0,
  points: 0,
  remark: ''
})

// 评分表单验证规则
const scoreRules = {
  points: [
    { required: true, message: '请输入评分积分', trigger: 'blur' },
    { type: 'number', message: '积分必须为数字' }
  ]
}

// 获取班级列表
const classList = computed(() => {
  const classes = [...new Set(allStudents.value.map(student => student.className))]
  return classes.filter(className => className)
})

// 过滤后的未提交博客学生
const filteredNoBlogStudents = computed(() => {
  return filterStudents(noBlogStudents.value)
})

// 过滤后的已提交博客学生
const filteredHasBlogStudents = computed(() => {
  return filterStudents(hasBlogStudents.value)
})

// 学生过滤函数
const filterStudents = (students) => {
  return students.filter(student => {
    const matchStudentId = !searchForm.studentId || student.studentId.includes(searchForm.studentId)
    const matchName = !searchForm.name || student.name.includes(searchForm.name)
    const matchClassName = !searchForm.className || student.className === searchForm.className
    
    let matchBlogStatus = true
    if (searchForm.blogStatus === 'submitted') {
      matchBlogStatus = student.weekBlogCount > 0
    } else if (searchForm.blogStatus === 'not_submitted') {
      matchBlogStatus = student.weekBlogCount === 0
    }
    
    return matchStudentId && matchName && matchClassName && matchBlogStatus
  })
}

// 格式化日期
const formatDate = (dateString) => {
  if (!dateString) return ''
  const date = new Date(dateString)
  return date.toLocaleString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit'
  })
}

// 获取所有学生数据
const fetchAllStudents = async () => {
  try {
    const res = await getAllStudents()
    if (res.code === 200) {
      allStudents.value = res.data
    } else {
      ElMessage.error(res.msg || '获取学生列表失败')
    }
  } catch (error) {
    console.error('获取学生列表失败:', error)
    ElMessage.error('获取学生列表失败')
  }
}

// 获取学生博客管理数据
const fetchData = async () => {
  try {
    const res = await getStudentsBlogManageData()
    if (res.code === 200) {
      noBlogStudents.value = res.data.noBlogStudents
      hasBlogStudents.value = res.data.hasBlogStudents
    } else {
      ElMessage.error(res.msg || '获取数据失败')
    }
  } catch (error) {
    console.error('获取数据失败:', error)
    ElMessage.error('获取数据失败')
  }
}

// 搜索处理
const handleSearch = () => {
  // 搜索逻辑已通过computed属性实现
}

// 重置搜索
const resetSearch = () => {
  searchForm.studentId = ''
  searchForm.name = ''
  searchForm.className = ''
  searchForm.blogStatus = ''
}

// 查看学生详情
const viewStudentDetail = (student) => {
  selectedStudent.value = student
  detailDialogVisible.value = true
}

// 查看学生本周博客列表
const viewWeekBlogs = async (student) => {
  if (student.weekBlogCount === 0) {
    ElMessage.info('该学生本周暂无博客')
    return
  }
  
  selectedStudent.value = student
  blogListTitle.value = `${student.name}的本周博客列表`
  blogListDialogVisible.value = true
  blogListLoading.value = true
  blogList.value = []
  
  try {
    const res = await getWeekBlogsByStudentId(student.studentId)
    if (res.code === 200) {
      blogList.value = res.data || []
    } else {
      ElMessage.error(res.msg || '获取博客列表失败')
    }
  } catch (error) {
    console.error('获取博客列表失败:', error)
    ElMessage.error('获取博客列表失败')
  } finally {
    blogListLoading.value = false
  }
}

// 查看学生所有博客列表
const viewAllBlogs = async (student) => {
  if (student.allBlogCount === 0) {
    ElMessage.info('该学生暂无博客')
    return
  }
  
  selectedStudent.value = student
  blogListTitle.value = `${student.name}的所有博客列表`
  blogListDialogVisible.value = true
  blogListLoading.value = true
  blogList.value = []
  
  try {
    const res = await getBlogLinksByStudentId(student.studentId)
    if (res.code === 200) {
      blogList.value = res.data || []
    } else {
      ElMessage.error(res.msg || '获取博客列表失败')
    }
  } catch (error) {
    console.error('获取博客列表失败:', error)
    ElMessage.error('获取博客列表失败')
  } finally {
    blogListLoading.value = false
  }
}

// 显示评分对话框
const showScoreDialog = (student) => {
  scoreForm.studentId = student.studentId
  scoreForm.studentName = student.name
  scoreForm.currentPoints = student.points
  scoreForm.points = student.weekBlogCount // 默认一篇博客一分
  scoreForm.remark = ''
  scoreDialogVisible.value = true
}

// 处理评分
const handleScore = async () => {
  try {
    await scoreFormRef.value.validate()
    
    const res = await scoreStudent({
      studentId: scoreForm.studentId,
      points: scoreForm.points,
      remark: scoreForm.remark
    })
    
    if (res.code === 200) {
      ElMessage.success('评分成功')
      scoreDialogVisible.value = false
      // 重新获取数据
      await fetchData()
    } else {
      ElMessage.error(res.msg || '评分失败')
    }
  } catch (error) {
    console.error('评分失败:', error)
    ElMessage.error('评分失败')
  }
}

// 页面加载时获取数据
onMounted(() => {
  fetchAllStudents()
  fetchData()
})
</script>

<style scoped>
/* 全局基础样式 */
:root {
  --primary-color: #409EFF;
  --success-color: #67C23A;
  --warning-color: #E6A23C;
  --danger-color: #F56C6C;
  --info-color: #909399;
  --border-radius-base: 4px;
  --box-shadow-base: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  --transition-base: all 0.3s;
}

/* 页面容器基础样式 */
.page-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

/* 卡片基础样式 */
.base-card {
  box-shadow: var(--box-shadow-base);
  border-radius: var(--border-radius-base);
  transition: var(--transition-base);
}

.base-card:hover {
  box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
}

/* 卡片头部样式 */
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 25px;
  padding: 0 10px;
}

.card-header h2 {
  margin: 0;
  font-size: 24px;
  color: #303133;
  border-bottom: 2px solid var(--primary-color);
  padding-bottom: 10px;
}

/* 搜索表单样式 */
.search-form {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: var(--border-radius-base);
  border: 1px solid #e9ecef;
}

.search-grid {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.search-form-content {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  align-items: end;
}

.search-item {
  margin-bottom: 0;
}

.search-input,
.search-select {
  width: 100%;
}

.search-action {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  padding-top: 10px;
  border-top: 1px solid #e9ecef;
}

/* 区块头部样式 */
.section-header {
  margin: 30px 0 20px 0;
  padding: 0 10px;
}

.section-header h3 {
  margin: 0;
  font-size: 18px;
  border-bottom: 2px solid currentColor;
  padding-bottom: 8px;
  display: inline-block;
}

/* 分隔空白 */
.section-divider {
  height: 40px;
  border-bottom: 1px solid #e9ecef;
  margin: 30px 0;
}

/* 表格样式 */
.base-table {
  width: 100%;
  margin-bottom: 20px;
}

.base-table :deep(.el-table__row) {
  cursor: pointer;
  transition: background-color 0.3s;
}

.base-table :deep(.el-table__row:hover) {
  background-color: #f5f7fa !important;
}

.base-table :deep(.el-table__header-wrapper) {
  background-color: #fafafa;
}

.base-table :deep(.el-table__header) {
  font-weight: 600;
  color: #303133;
}

/* 博客链接样式 */
.blog-link {
  color: var(--primary-color);
  text-decoration: none;
  font-weight: 500;
  transition: all 0.3s;
}

.blog-link:hover {
  color: #66b1ff;
  text-decoration: underline;
}

/* 对话框样式 */
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

/* 响应式调整 */
@media (max-width: 768px) {
  .search-form-content {
    grid-template-columns: 1fr;
  }
  
  .search-action {
    justify-content: center;
  }
}

/* 博客数量链接样式 */
.blog-count-link {
  padding: 0;
  border: none;
  background: none;
}

.blog-count-link:hover:not(:disabled) {
  background: none;
}

.blog-count-link:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

/* 博客标题链接样式 */
.blog-title-link {
  font-weight: 500;
  word-break: break-all;
}

/* 博客URL链接样式 */
.blog-url-link {
  font-size: 12px;
  word-break: break-all;
}

/* 无数据样式 */
.no-data {
  text-align: center;
  padding: 40px 0;
}

/* 学生详情样式 */
.student-detail {
  padding: 20px 0;
}

.detail-row {
  display: flex;
  margin-bottom: 15px;
  align-items: center;
}

.detail-label {
  font-weight: 600;
  color: #606266;
  width: 120px;
  flex-shrink: 0;
}

.detail-value {
  color: #303133;
  flex: 1;
}
</style>

posted @ 2025-06-05 22:04  vivi_vimi  阅读(15)  评论(0)    收藏  举报