web大作业开发记录02






home.vue:

<template>
  <div class="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="6">
            <el-card class="stat-card">
              <!-- 总目标数卡片 -->
              <div class="stat-item">
                <div class="stat-icon goals-icon">
                  <img src="@/assets/goal.png" alt="目标" class="icon-image" />
                </div>
                <div class="stat-content">
                  <div class="stat-number">{{ goalsStats.totalGoals || 0 }}</div>
                  <div class="stat-label">总目标数</div>
                </div>
              </div>

            </el-card>
          </el-col>
          <el-col :span="6">
            <el-card class="stat-card">
              <!-- 已完成目标卡片 -->
              <div class="stat-item">
                <div class="stat-icon completed-icon">
                  <img src="@/assets/check.png" alt="完成" class="icon-image" />
                </div>
                <div class="stat-content">
                  <div class="stat-number">{{ goalsStats.completedGoals || 0 }}</div>
                  <div class="stat-label">已完成目标</div>
                </div>
              </div>
            </el-card>
          </el-col>
          <el-col :span="6">
            <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">{{ blogStats.totalBlogs || 0 }}</div>
                  <div class="stat-label">博客总数</div>
                </div>
              </div>
            </el-card>
          </el-col>
          <el-col :span="6">
            <el-card class="stat-card">
              <!-- 平均完成率卡片 -->
              <div class="stat-item">
                <div class="stat-icon rate-icon">
                  <img src="@/assets/TrendCharts.png" alt="趋势" class="icon-image" />
                </div>
                <div class="stat-content">
                  <div class="stat-number">{{ averageCompletionRate }}%</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>月度目标完成率趋势</span>
                </div>
              </template>
              <div ref="goalsChart" class="chart-container"></div>
            </el-card>
          </el-col>

          <!-- 博客发布统计 -->
          <el-col :span="12">
            <el-card class="chart-card">
              <template #header>
                <div class="card-header">
                  <span>月度博客发布统计</span>
                </div>
              </template>
              <div ref="blogsChart" class="chart-container"></div>
            </el-card>
          </el-col>
        </el-row>

        <el-row :gutter="20" style="margin-top: 20px;">
          <!-- 博客分类分布 -->
          <el-col :span="12">
            <el-card class="chart-card">
              <template #header>
                <div class="card-header">
                  <span>博客分类分布</span>
                </div>
              </template>
              <div ref="categoryChart" class="chart-container"></div>
            </el-card>
          </el-col>

          <!-- 周度博客发布热力图 -->
          <el-col :span="12">
            <el-card class="chart-card">
              <template #header>
                <div class="card-header">
                  <span>周度发布活跃度</span>
                </div>
              </template>
              <div ref="heatmapChart" class="chart-container"></div>
            </el-card>
          </el-col>
        </el-row>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
// 移除这一行: import { Target, Check, Document, TrendCharts } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
import { getGoalsStatistics } from '@/api/goals'
import { getBlogStatistics } from '@/api/blogLink'

// 响应式数据
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
const goalsStats = ref({})
const blogStats = ref({})
const loading = ref(false)

// 图表引用
const goalsChart = ref(null)
const blogsChart = ref(null)
const categoryChart = ref(null)
const heatmapChart = ref(null)

// 计算平均完成率
const averageCompletionRate = computed(() => {
  const monthlyCompletion = goalsStats.value.monthlyCompletion || {}
  const rates = Object.values(monthlyCompletion)
  if (rates.length === 0) return 0
  const average = rates.reduce((sum, rate) => sum + rate, 0) / rates.length
  return Math.round(average)
})

// 获取统计数据
const fetchStatistics = async () => {
  if (!userInfo.value.studentId) {
    ElMessage.warning('请先登录')
    return
  }

  loading.value = true
  try {
    const [goalsResponse, blogsResponse] = await Promise.all([
      getGoalsStatistics(userInfo.value.studentId),
      getBlogStatistics(userInfo.value.studentId)
    ])

    if (goalsResponse.code === 200) {
      goalsStats.value = goalsResponse.data || {}
    }

    if (blogsResponse.code === 200) {
      blogStats.value = blogsResponse.data || {}
    }

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

// 初始化图表
const initCharts = () => {
  initGoalsChart()
  initBlogsChart()
  initCategoryChart()
  initHeatmapChart()
}

// 目标完成率趋势图
const initGoalsChart = () => {
  if (!goalsChart.value) return
  
  const chart = echarts.init(goalsChart.value)
  const monthlyCompletion = goalsStats.value.monthlyCompletion || {}
  
  const months = Object.keys(monthlyCompletion).sort()
  const rates = months.map(month => monthlyCompletion[month])
  
  const option = {
    tooltip: {
      trigger: 'axis',
      formatter: '{b}: {c}%'
    },
    xAxis: {
      type: 'category',
      data: months,
      axisLabel: {
        rotate: 45
      }
    },
    yAxis: {
      type: 'value',
      min: 0,
      max: 100,
      axisLabel: {
        formatter: '{value}%'
      }
    },
    series: [{
      data: rates,
      type: 'line',
      smooth: true,
      itemStyle: {
        color: '#409EFF'
      },
      areaStyle: {
        color: {
          type: 'linear',
          x: 0,
          y: 0,
          x2: 0,
          y2: 1,
          colorStops: [{
            offset: 0, color: 'rgba(64, 158, 255, 0.3)'
          }, {
            offset: 1, color: 'rgba(64, 158, 255, 0.1)'
          }]
        }
      }
    }]
  }
  
  chart.setOption(option)
}

// 博客发布统计图
const initBlogsChart = () => {
  if (!blogsChart.value) return
  
  const chart = echarts.init(blogsChart.value)
  const monthlyCount = blogStats.value.monthlyCount || {}
  
  const months = Object.keys(monthlyCount).sort()
  const counts = months.map(month => monthlyCount[month])
  
  const option = {
    tooltip: {
      trigger: 'axis',
      formatter: '{b}: {c}篇'
    },
    xAxis: {
      type: 'category',
      data: months,
      axisLabel: {
        rotate: 45
      }
    },
    yAxis: {
      type: 'value',
      axisLabel: {
        formatter: '{value}篇'
      }
    },
    series: [{
      data: counts,
      type: 'bar',
      itemStyle: {
        color: '#67C23A'
      }
    }]
  }
  
  chart.setOption(option)
}

// 博客分类分布饼图
const initCategoryChart = () => {
  if (!categoryChart.value) return
  
  const chart = echarts.init(categoryChart.value)
  const categoryCount = blogStats.value.categoryCount || {}
  
  const data = Object.entries(categoryCount).map(([name, value]) => ({ name, value }))
  
  const option = {
    tooltip: {
      trigger: 'item',
      formatter: '{a} <br/>{b}: {c} ({d}%)'
    },
    series: [{
      name: '博客分类',
      type: 'pie',
      radius: '50%',
      data: data,
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }]
  }
  
  chart.setOption(option)
}

// 周度发布热力图
const initHeatmapChart = () => {
  if (!heatmapChart.value) return
  
  const chart = echarts.init(heatmapChart.value)
  const weeklyCount = blogStats.value.weeklyCount || {}
  
  // 构造热力图数据
  const data = []
  Object.entries(weeklyCount).forEach(([month, weeks]) => {
    Object.entries(weeks).forEach(([week, count]) => {
      data.push([month, week, count])
    })
  })
  
  const option = {
    tooltip: {
      position: 'top',
      formatter: function (params) {
        return params.data[0] + ' ' + params.data[1] + ': ' + params.data[2] + '篇'
      }
    },
    grid: {
      height: '50%',
      top: '10%'
    },
    xAxis: {
      type: 'category',
      data: [...new Set(data.map(item => item[0]))].sort(),
      splitArea: {
        show: true
      }
    },
    yAxis: {
      type: 'category',
      data: [...new Set(data.map(item => item[1]))].sort(),
      splitArea: {
        show: true
      }
    },
    visualMap: {
      min: 0,
      max: Math.max(...data.map(item => item[2]), 1),
      calculable: true,
      orient: 'horizontal',
      left: 'center',
      bottom: '15%'
    },
    series: [{
      name: '发布数量',
      type: 'heatmap',
      data: data,
      label: {
        show: true
      },
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }]
  }
  
  chart.setOption(option)
}

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

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

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

.dashboard-header h1 {
  color: #303133;
  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;
}

.goals-icon {
  background: linear-gradient(135deg, #91a1e4 0%, #ba89ef 100%);
}

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

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

.rate-icon {
  background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}

.stat-content {
  flex: 1;
}

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

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

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

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

.chart-container {
  width: 100%;
  height: 300px;
}

.charts-section {
  margin-top: 20px;
}
</style>

goals.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="showAddGoalDialog">添加学习目标</el-button>
            <el-button @click="showGoalsList = !showGoalsList">
              {{ showGoalsList ? '返回当前目标' : '查看所有目标' }}
            </el-button>
          </div>
        </div>
      </template>

      <!-- 目标列表视图 -->
      <div class="search-form" v-if="showGoalsList">
        <!-- 修改搜索表单布局 -->
        <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.title"
                  placeholder="请输入目标标题"
                  clearable
                  @clear="handleSearch"
                  class="search-input"
                ></el-input>
              </el-form-item>

              <el-form-item label="时间范围" class="search-item">
                <el-date-picker
                  v-model="searchForm.dateRange"
                  type="daterange"
                  range-separator="至"
                  start-placeholder="开始日期"
                  end-placeholder="结束日期"
                  value-format="YYYY-MM-DD"
                  @clear="handleSearch"
                  class="search-date-picker"
                ></el-date-picker>
              </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>

        <el-table class="base-table" :data="goals" @row-click="selectGoal" style="width: 100%">
          <el-table-column prop="goal.title" label="目标标题" />
          <el-table-column label="时间范围" width="220">
            <template #default="scope">
              {{ formatDate(scope.row.goal.startDate) }} 至 {{ formatDate(scope.row.goal.endDate) }}
            </template>
          </el-table-column>
          <el-table-column label="状态" width="100">
            <template #default="scope">
              <el-tag :type="getStatusType(scope.row.goal.status)">
                {{ getStatusText(scope.row.goal.status) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="100" align="center">
            <template #default="scope">
              <el-button
                type="danger"
                link
                @click.stop="handleDeleteGoal(scope.row.goal.goalId)">
                <img src="../assets/delete.png" alt="" class="icon"/>
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>

      <!-- 目标详情视图 -->
      <div v-else-if="currentGoal" class="goal-detail">
        <div class="goal-header">
          <div class="goal-title-container">
            <span class="goal-title">{{ currentGoal.goal.title }}</span>
            <el-button 
              v-if="isActiveGoal"
              type="primary" 
              link 
              @click="showEditDialog"
              class="edit-button">
              <img src="../assets/edit.png" alt="" class="icon"/>
            </el-button>
          </div>
        </div>

        <!-- 添加编辑目标对话框 -->
        <el-dialog
            title="编辑周目标"
            v-model="editDialogVisible"
            width="500px"
        >
          <el-form :model="editForm" ref="editFormRef" label-width="100px" :rules="rules">
            <el-form-item label="目标标题" prop="title">
              <el-input v-model="editForm.title" placeholder="请输入目标标题"></el-input>
            </el-form-item>

            <el-form-item label="现有子目标">
              <div v-for="subgoal in currentGoal.subGoals.incomplete" :key="subgoal.subgoalId" class="subgoal-input">
                <div class="subgoal-actions">
                    <img src="../assets/move.png" alt="" class="icon" @click="handleDeleteSubGoal(subgoal.subgoalId)"/>
                </div>
                <el-input v-model="subgoal.content"></el-input>
              </div>
              <div v-for="subgoal in currentGoal.subGoals.completed" :key="subgoal.subgoalId" class="subgoal-input">
                <div class="subgoal-actions">
                  <img src="../assets/move.png" alt="" class="icon" @click="handleDeleteSubGoal(subgoal.subgoalId)"/>
                </div>
                <el-input v-model="subgoal.content" disabled></el-input>
              </div>
            </el-form-item>

            <el-form-item label="新增子目标">
              <div v-for="(newSubgoal, index) in editForm.newSubGoals" :key="index" class="subgoal-input">
                <div class="subgoal-actions">
                  <img src="../assets/move.png" alt="" class="icon" @click="removeNewSubGoal(index)"/>
                </div>
                <el-input
                  v-model="editForm.newSubGoals[index]"
                  placeholder="请输入子目标描述">
                </el-input>
              </div>
              <div class="subgoal-input">
                <div class="subgoal-actions">
                  <img src="../assets/add.png" alt="" class="icon" @click="addNewSubGoal"/>
                </div>
              </div>
            </el-form-item>

          </el-form>
          <template #footer>
            <el-button @click="editDialogVisible = false">取消</el-button>
            <el-button type="primary" @click="submitEdit">保存修改</el-button>
          </template>
        </el-dialog>

        <!-- 修改任务清单区域,添加卡片样式 -->
        <el-card class="task-list-card">
          <div class="task-list-header">
            <h4>任务清单</h4>
            <div class="header-actions" v-if="isActiveGoal">
              <el-button
                  type="primary"
                  @click="completeSelectedSubGoals"
                  v-if="checkedSubGoals.length > 0">
                完成选中的任务
              </el-button>
              <el-button
                  type="success"
                  @click="handleCompleteGoal"
                  v-if="canCompleteGoal">
                完成本周目标
              </el-button>
            </div>
          </div>
          
          <div class="task-list">
            <!-- 未完成的子目标 -->
            <div class="task-section">
              <el-checkbox-group v-model="checkedSubGoals" @change="handleSubGoalsChange">
                <div v-for="subgoal in currentGoal.subGoals.incomplete" 
                     :key="subgoal.subgoalId" 
                     class="task-item">
                  <div class="task-content">
                    <el-checkbox 
                      :label="subgoal.subgoalId"
                      :disabled="!isActiveGoal">
                      {{ subgoal.content }}
                    </el-checkbox>
                  </div>
                  <el-tag size="small" type="danger">未完成</el-tag>
                </div>
              </el-checkbox-group>
            </div>
        
            <!-- 已完成的子目标 -->
            <div class="task-section completed">
              <div v-for="subgoal in currentGoal.subGoals.completed" 
                   :key="subgoal.subgoalId" 
                   class="task-item completed">
                <div class="task-content">
                  <el-checkbox checked disabled>{{ subgoal.content }}</el-checkbox>
                </div>
                <el-tag size="small" type="success">已完成</el-tag>
              </div>
            </div>
          </div>

        </el-card>

        <!-- 修改时间信息区域,添加卡片样式 -->
        <el-card class="info-card">
          <div class="info-header">
            <h4>基本信息</h4>
            <el-tag :type="getStatusType(currentGoal.goal.status)">
              {{ getStatusText(currentGoal.goal.status) }}
            </el-tag>
          </div>
          <div class="goal-info">
            <div class="info-content">
              <div class="date-info">
                <p>开始日期: {{ formatDate(currentGoal.goal.startDate) }}</p>
                <p>结束日期: {{ formatDate(currentGoal.goal.endDate) }}</p>
              </div>
              <div v-if="currentGoal.goal.status === 1" class="remaining-time-container">
                <div class="remaining-time-box" :class="{ 'urgent': getRemainingTime === '已到期' }">
                  <span class="remaining-label">剩余时间</span>
                  <span class="remaining-value">{{ getRemainingTime }}</span>
                </div>
              </div>
            </div>
          </div>
        </el-card>

        <!-- 移动到最后的进度图表 -->
        <el-card class="chart-card">
          <div class="chart-header">
            <h4>目标完成进度</h4>
          </div>
          <div class="chart-container" ref="pieChart"></div>
        </el-card>
      </div>
      
      <!-- 空状态 -->
      <el-empty v-else description="暂无学习目标"></el-empty>
    </el-card>

    <!-- 添加目标对话框 -->
    <el-dialog
        title="添加周目标"
        v-model="addGoalDialogVisible"
        width="500px"
    >
      <el-form :model="goalForm" ref="goalFormRef" label-width="100px" :rules="rules">
        <el-form-item label="目标标题" prop="title">
          <el-input v-model="goalForm.title" placeholder="请输入目标标题"></el-input>
        </el-form-item>

        <el-form-item label="子目标">
          <div v-for="(subgoal, index) in goalForm.subGoals" :key="index" class="subgoal-input">
            <div class="subgoal-actions">
                <img src="../assets/move.png" alt="" class="icon" @click="removeSubGoal(index)"/>
            </div>
            <el-input v-model="goalForm.subGoals[index]" placeholder="请输入子目标描述"></el-input>
              <img v-if="index === goalForm.subGoals.length - 1" src="../assets/add.png" alt="" class="icon" @click="addSubGoal"/>
          </div>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="addGoalDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitGoal">确定</el-button>
      </template>
    </el-dialog>

  </div>
</template>

<script setup>
const showGoalsList = ref(false)

// 添加选择目标方法
const selectGoal = (row) => {
  const index = goals.value.findIndex(g => g.goal.goalId === row.goal.goalId)
  if (index !== -1) {
    currentIndex.value = index
    showGoalsList.value = false
    checkedSubGoals.value = []
  }
}

import { ref, onMounted, reactive, computed, watch, nextTick, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getGoalsList, addGoal, updateGoalsStatus, batchUpdateSubGoals, completeGoal, 
         updateGoalTitle, deleteSubGoal, addSubGoalToExisting, deleteGoal, searchGoals } from '@/api/goals'

// 搜索表单数据
const searchForm = reactive({
  title: '',
  dateRange: null
})

// 搜索方法
const handleSearch = async () => {
  try {
    const params = {
      studentId: userInfo.value.studentId,
      title: searchForm.title,
      startDate: searchForm.dateRange ? searchForm.dateRange[0] : null,
      endDate: searchForm.dateRange ? searchForm.dateRange[1] : null
    }

    // 验证搜索条件
    if (!params.title && (!params.startDate || !params.endDate)) {
      ElMessage.warning('请至少输入标题或完整的时间范围')
      return
    }
    const res = await searchGoals(params)
    if (res.code === 200) {
      goals.value = res.data
    } else {
      ElMessage.error(res.msg || '搜索失败')
    }
  } catch (error) {
    console.error('搜索失败:', error)
    ElMessage.error('搜索失败')
  }
}

// 重置搜索
const resetSearch = async () => {
  searchForm.title = ''
  searchForm.dateRange = null
  await fetchGoals()
}
const userInfo = ref({})
const goals = ref([])
const currentIndex = ref(0)
const checkedSubGoals = ref([])
const addGoalDialogVisible = ref(false)
const goalFormRef = ref(null)

// 当前目标
const currentGoal = computed(() => {
  return goals.value[currentIndex.value] || null
})

// 是否为活跃目标(本周目标)
const isActiveGoal = computed(() => {
  if (!currentGoal.value) return false
  return currentGoal.value.goal.status === 1 // 1 表示进行中
})

// 表单数据
const goalForm = reactive({
  title: '',
  subGoals: ['']
})

// 表单验证规则
const rules = {
  title: [{ required: true, message: '请输入目标标题', trigger: 'blur' }]
}

// 获取状态文本
const getStatusText = (status) => {
  const statusMap = {
    0: '未开始',
    1: '进行中',
    2: '已完成'
  }
  return statusMap[status] || '未知状态'
}

// 获取状态类型
const getStatusType = (status) => {
  const typeMap = {
    0: 'info',
    1: 'warning',
    2: 'success'
  }
  return typeMap[status] || 'info'
}

// 完成选中的子目标
const completeSelectedSubGoals = async () => {
  try {
    if (!checkedSubGoals.value || checkedSubGoals.value.length === 0) {
      ElMessage.warning('请选择要完成的子目标')
      return
    }

    const res = await batchUpdateSubGoals({
      subgoalIds: checkedSubGoals.value,
      completed: true
    })
    
    if (res.code === 200) {
      ElMessage.success('更新成功')
      fetchGoals() // 重新获取数据
      checkedSubGoals.value = [] // 清空选中项
    } else {
      ElMessage.error(res.msg || '更新失败')
    }
  } catch (error) {
    console.error('更新子目标状态失败:', error)
    ElMessage.error('操作失败')
  }
}

// 修改获取目标列表方法
const fetchGoals = async () => {
  try {
    const studentId = userInfo.value.studentId
    if (!studentId) {
      ElMessage.warning('请先登录')
      return
    }
    
    // 先获取并更新目标状态
    await updateGoalsStatus(studentId)
    
    // 获取最新的目标列表
    const res = await getGoalsList(studentId)
    if (res.code === 200) {
      goals.value = res.data.sort((a, b) => {
        if (a.goal.status !== b.goal.status) {
          const statusOrder = { 1: 0, 0: 1, 2: 2 }
          return statusOrder[a.goal.status] - statusOrder[b.goal.status]
        }
        return new Date(b.goal.startDate) - new Date(a.goal.startDate)
      })

      currentIndex.value = 0

      if (goals.value.length === 0) {
        ElMessage.info('暂无学习目标,请添加新的目标')
      } else if (!goals.value.some(g => g.goal.status === 1)) {
        ElMessage.info('当前没有进行中的周目标,可以添加新的目标')
      }
    }
  } catch (error) {
    console.error('获取目标失败:', error)
    ElMessage.error('获取目标失败')
  }
}

// 子目标状态变更处理方法
const handleSubGoalsChange = (value) => {
  // value 是当前选中的子目标 ID 数组
  checkedSubGoals.value = value;
}

// 添加子目标方法
const addSubGoal = () => {
  goalForm.subGoals.push('');
}

// 移除子目标方法
const removeSubGoal = (index) => {
  if (goalForm.subGoals.length > 1) {
    goalForm.subGoals.splice(index, 1);
  } else {
    ElMessage.warning('至少需要保留一个子目标');
  }
}

// 显示添加目标对话框
const showAddGoalDialog = async () => {
  // 检查是否存在活跃目标
  const hasActiveGoal = goals.value.some(g => g.goal.status === 1)
  if (hasActiveGoal) {
    ElMessage.warning('当前已有进行中的周目标,无法添加新目标')
    return
  }
  
  goalForm.title = ''
  goalForm.subGoals = ['']
  addGoalDialogVisible.value = true
}

// 提交目标
// 添加日期工具函数
const dateUtils = {
  getWeekStart() {
    const now = new Date()
    const day = now.getDay() || 7
    const monday = new Date(now)
    monday.setDate(now.getDate() - day + 1)
    monday.setHours(0, 0, 0, 0)
    return monday
  },
  
  getWeekEnd() {
    const now = new Date()
    const day = now.getDay() || 7
    const sunday = new Date(now)
    sunday.setDate(now.getDate() - day + 7)
    sunday.setHours(23, 59, 59, 999)
    return sunday
  }
}

const submitGoal = async () => {
  try {
    await goalFormRef.value.validate()

    const filteredSubGoals = goalForm.subGoals.filter(sg => sg.trim() !== '')
    if (filteredSubGoals.length === 0) {
      ElMessage.warning('请至少添加一个子目标')
      return
    }

    const params = {
      studentId: userInfo.value.studentId,
      title: goalForm.title,
      subGoals: filteredSubGoals,
      startDate: formatDate(dateUtils.getWeekStart()),
      endDate: formatDate(dateUtils.getWeekEnd())
    }

    const res = await addGoal(params)
    if (res.code === 200) {
      ElMessage.success('添加目标成功')
      addGoalDialogVisible.value = false
      fetchGoals()
    } else {
      ElMessage.error(res.msg || '添加目标失败')
    }
  } catch (error) {
    console.error('提交目标失败:', error)
    ElMessage.error('请填写完整信息')
  }
}

// 格式化日期
const formatDate = (dateStr) => {
  if (!dateStr) return ''
  const date = new Date(dateStr)
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}

// 修改 onMounted 钩子
onMounted(async () => {
  const storedUserInfo = localStorage.getItem('userInfo')
  if (storedUserInfo) {
    userInfo.value = JSON.parse(storedUserInfo)
    await fetchGoals()
    nextTick(() => {
      initChart()
    })
  }
})
// 添加是否可以完成目标的计算属性
const canCompleteGoal = computed(() => {
  if (!currentGoal.value || !isActiveGoal.value) return false
  
  // 检查是否所有子目标都已完成
  const allSubGoals = [
    ...(currentGoal.value.subGoals.incomplete || []),
    ...(currentGoal.value.subGoals.completed || [])
  ]
  const completedCount = currentGoal.value.subGoals.completed?.length || 0
  
  // 允许在所有子目标完成时,或用户主动选择提前完成时显示按钮
  return isActiveGoal.value && (completedCount === allSubGoals.length || allSubGoals.length > 0)
})

// 添加完成目标的方法
const handleCompleteGoal = async () => {
  // 获取未完成的子目标数量
  const incompleteCount = currentGoal.value.subGoals.incomplete?.length || 0
  
  let shouldComplete = true
  
  // 如果还有未完成的子目标,显示确认对话框
  if (incompleteCount > 0) {
    try {
      await ElMessageBox.confirm(
        '还有未完成的子目标,确定要提前完成本周目标吗?',
        '提示',
        {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }
      )
    } catch (e) {
      shouldComplete = false
    }
  }
  
  if (shouldComplete) {
    try {
      const res = await completeGoal(currentGoal.value.goal.goalId)
      if (res.code === 200) {
        ElMessage.success('目标已完成')
        await fetchGoals() // 重新获取数据
      } else {
        ElMessage.error(res.msg || '操作失败')
      }
    } catch (error) {
      console.error('完成目标失败:', error)
      ElMessage.error('操作失败')
    }
  }
}

// 添加编辑相关的响应式变量
const editDialogVisible = ref(false)
const editFormRef = ref(null)
const editForm = reactive({
  title: '',
  newSubGoals: []
})

// 显示编辑对话框
const showEditDialog = () => {
  editForm.title = currentGoal.value.goal.title
  editForm.newSubGoals = []
  editDialogVisible.value = true
}

// 删除子目标
const handleDeleteSubGoal = async (subgoalId) => {
  try {
    await ElMessageBox.confirm('确定要删除这个子目标吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })
    
    const res = await deleteSubGoal(subgoalId)
    if (res.code === 200) {
      ElMessage.success('删除成功')
      await fetchGoals()
    } else {
      ElMessage.error(res.msg || '删除失败')
    }
  } catch (error) {
    if (error !== 'cancel') {
      console.error('删除子目标失败:', error)
      ElMessage.error('删除失败')
    }
  }
}

// 添加新子目标输入框
const addNewSubGoal = () => {
  // 如果没有新增子目标,先添加一个空的
  if (editForm.newSubGoals.length === 0) {
    editForm.newSubGoals.push('')
  } else {
    // 检查最后一个子目标是否为空
    const lastSubGoal = editForm.newSubGoals[editForm.newSubGoals.length - 1]
    if (lastSubGoal.trim() !== '') {
      editForm.newSubGoals.push('')
    } else {
      ElMessage.warning('请先填写当前子目标')
    }
  }
}

// 移除新子目标输入框
const removeNewSubGoal = (index) => {
  editForm.newSubGoals.splice(index, 1)
}

// 提交编辑
const submitEdit = async () => {
  try {
    await editFormRef.value.validate()
    
    // 更新标题
    if (editForm.title !== currentGoal.value.goal.title) {
      const titleRes = await updateGoalTitle({
        goalId: currentGoal.value.goal.goalId,
        title: editForm.title
      })
      if (titleRes.code !== 200) {
        ElMessage.error(titleRes.msg || '更新标题失败')
        return
      }
    }
    
    // 添加新子目标
    const newSubGoals = editForm.newSubGoals.filter(sg => sg.trim() !== '')
    for (const content of newSubGoals) {
      const addRes = await addSubGoalToExisting({
        goalId: currentGoal.value.goal.goalId,
        content
      })
      if (addRes.code !== 200) {
        ElMessage.error(addRes.msg || '添加子目标失败')
        return
      }
    }
    
    ElMessage.success('修改成功')
    editDialogVisible.value = false
    await fetchGoals()
  } catch (error) {
    console.error('提交修改失败:', error)
    ElMessage.error('保存修改失败')
  }
}
// 删除目标
const handleDeleteGoal = async (goalId) => {
  try {
    await ElMessageBox.confirm(
      '确定要删除这个目标吗?相关的子目标也会被删除。',
      '提示',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )
    const res = await deleteGoal(goalId)
    if (res.code === 200) {
      ElMessage.success('删除成功')
      await fetchGoals()
    } else {
      ElMessage.error(res.msg || '删除失败')
    }
  } catch (error) {
    if (error !== 'cancel') {
      console.error('删除目标失败:', error)
      ElMessage.error('删除失败')
    }
  }
}

// 计算剩余时间
const getRemainingTime = computed(() => {
  if (!currentGoal.value || currentGoal.value.goal.status !== 1) return null

  const now = new Date()
  const endDate = new Date(currentGoal.value.goal.endDate)
  const timeDiff = endDate - now

  if (timeDiff <= 0) return '已到期'

  const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24))
  const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))

  return `${days}天${hours}小时`
})

// 图表实例
const pieChart = ref(null)
let chartInstance = null

// 计算完成进度
const progressData = computed(() => {
  if (!currentGoal.value) return { completed: 0, incomplete: 0 }

  const completed = currentGoal.value.subGoals.completed?.length || 0
  const incomplete = currentGoal.value.subGoals.incomplete?.length || 0
  const total = completed + incomplete

  return {
    completed,
    incomplete,
    total,
    percentage: total > 0 ? Math.round((completed / total) * 100) : 0
  }
})

// 初始化图表
const initChart = () => {
  if (!pieChart.value) return

  // 检查是否已经有图表实例,如果有则先销毁
  if (chartInstance) {
    chartInstance.dispose()
    chartInstance = null
  }

  // 确保当前有目标数据且不在"查看所有目标"视图
  if (currentGoal.value && !showGoalsList.value) {
    chartInstance = echarts.init(pieChart.value)
    updateChart()

    // 添加窗口大小变化监听器
    window.addEventListener('resize', handleResize)
  }
}

// 更新图表数据
const updateChart = () => {
  if (!chartInstance || !currentGoal.value) return

  const { completed, incomplete, percentage } = progressData.value

  const option = {
    tooltip: {
      trigger: 'item',
      formatter: '{b}: {c} ({d}%)'
    },
    legend: {
      orient: 'horizontal',
      bottom: 0
    },
    series: [
      {
        name: '目标进度',
        type: 'pie',
        radius: ['60%', '80%'],
        avoidLabelOverlap: false,
        label: {
          show: false,
          position: 'center'
        },
        emphasis: {
          label: {
            show: true,
            fontSize: '20',
            fontWeight: 'bold',
            formatter: `{c}\n完成率: ${percentage}%`
          }
        },
        data: [
          { value: completed, name: '已完成', itemStyle: { color: '#67C23A' } },
          { value: incomplete, name: '未完成', itemStyle: { color: '#909399' } }
        ]
      }
    ]
  }

  chartInstance.setOption(option)
}

// 监听目标变化,更新图表
watch(() => currentGoal.value, () => {
  nextTick(() => {
    initChart()
  })
})

// 监听 showGoalsList 变化
watch(showGoalsList, (newVal) => {
  if (!newVal) { // 当从"查看所有目标"返回"当前目标"时
    nextTick(() => {
      // 确保 DOM 更新完成后再重新初始化图表
      initChart()
    })
  }
})

// 监听窗口大小变化
window.addEventListener('resize', () => {
  if (chartInstance) {
    chartInstance.resize()
  }
})

// 处理窗口大小变化
const handleResize = () => {
  if (chartInstance) {
    chartInstance.resize()
  }
}

// 在组件卸载时清理
onUnmounted(() => {
  if (chartInstance) {
    chartInstance.dispose()
    window.removeEventListener('resize', handleResize)
  }
  window.removeEventListener('resize', () => {
    if (chartInstance) {
      chartInstance.resize()
    }
  })
})

</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;
}

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

.search-form-content {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.search-form-item {
  flex: 1;
  min-width: 200px;
}

.search-form-item .label {
  display: block;
  margin-bottom: 8px;
  font-size: 14px;
  color: #606266;
}

.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;
}

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

/* 对话框样式 */
.base-dialog :deep(.el-dialog__header) {
  border-bottom: 1px solid #ebeef5;
  padding-bottom: 15px;
  margin-bottom: 15px;
}

.base-dialog :deep(.el-dialog__body) {
  padding: 20px;
}

/* 添加Honor.vue样式的搜索表单样式 */
.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-input {
  width: 100%;
}

.search-date-picker {
  width: 100%;
}

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

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

  .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;
  }
}

.goal-card h2 {
  margin: 0 0 20px;
  color: #303133;
  font-size: 24px;
  border-bottom: 2px solid #409EFF;
  padding-bottom: 10px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 25px;
  padding: 0 10px;
}

.card-header h2 {
  margin: 0;
  color: #303133;
  font-size: 24px;
}

.card-actions {
  display: flex;
  gap: 15px;
}

.goal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding: 15px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.goal-title-container {
  display: flex;
  align-items: center;
  gap: 10px;
}

.goal-title {
  font-size: 18px;
  font-weight: 600;
  color: #303133;
}

.info-card,
.task-list-card,
.chart-card {
  margin-bottom: 20px;
}

.info-header,
.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-bottom: 15px;
  margin-bottom: 15px;
  border-bottom: 1px solid #ebeef5;
}

.task-list-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-bottom: 15px;
  margin-bottom: 15px;
  border-bottom: 1px solid #ebeef5;
}

.header-actions {
  display: flex;
  gap: 15px;
}

.info-header h4,
.task-list-header h4,
.chart-header h4 {
  margin: 0;
  color: #303133;
  font-size: 16px;
  font-weight: 600;
}

.goal-info {
  padding: 0;
  background-color: #fff;
}

.goal-info p {
  margin: 8px 0;
  color: #606266;
  font-size: 14px;
}

.task-list {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.task-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.task-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 15px;
  background-color: #f8f9fa;
  border-radius: 6px;
  transition: all 0.3s;
}

.task-item:hover {
  background-color: #f0f2f5;
}

.task-item.completed {
  opacity: 0.8;
  background-color: #f0f7f0;
}

.task-content {
  flex: 1;
}

.task-item:hover {
  opacity: 1;
}

.subgoal-input {
  display: flex;
  align-items: center;
  margin-bottom: 15px;
  gap: 15px;
  padding: 5px;
}

.search-form {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f5f7fa;
  border-radius: 8px;
  border: 1px solid #ebeef5;
}

.goal-info {
  padding: 0;
  background-color: #fff;
}

.info-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.date-info {
  flex: 1;
}

.date-info p {
  margin: 8px 0;
  color: #606266;
  font-size: 14px;
}

.remaining-time-container {
  padding-left: 40px;
  border-left: 1px solid #ebeef5;
}

.remaining-label {
  font-size: 14px;
  color: #606266;
  margin-bottom: 8px;
}

.remaining-value {
  font-size: 20px;
  font-weight: 600;
  color: #409EFF;
}

.remaining-time-box.urgent .remaining-value {
  color: #F56C6C;
}

.remaining-time .urgent {
  color: #F56C6C;
}

.chart-container {
  height: 300px;
  width: 100%;
  margin: 10px 0;
}

.icon {
  width: 20px;
  height: 20px;
  vertical-align: middle;
}

.remaining-time-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 15px 25px;
  background-color: #ecf5ff;
  border-radius: 8px;
  transition: all 0.3s;
}

.remaining-time-box.urgent {
  background-color: #fef0f0;
}
</style>

blog.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.title"
                  placeholder="请输入博客标题"
                  clearable
                  @clear="handleSearch"
                  class="search-input"
                ></el-input>
              </el-form-item>

              <el-form-item label="分类" class="search-item">
                <el-select
                  v-model="searchForm.category"
                  placeholder="请选择分类"
                  clearable
                  @clear="handleSearch"
                  class="search-select"
                >
                  <el-option
                    v-for="item in categories"
                    :key="item"
                    :label="item"
                    :value="item"
                  ></el-option>
                </el-select>
              </el-form-item>
            </div>

            <!-- 第二行 -->
            <div class="search-form-content">
              <el-form-item label="发布时间" class="search-item">
                <el-date-picker
                  v-model="searchForm.dateRange"
                  type="daterange"
                  range-separator="至"
                  start-placeholder="开始日期"
                  end-placeholder="结束日期"
                  value-format="YYYY-MM-DD"
                  @clear="handleSearch"
                  class="search-date-picker"
                ></el-date-picker>
              </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="blogLinks"
          style="width: 100%"
          :row-class-name="tableRowClassName"
          border
          stripe
          highlight-current-row
          class="base-table"
      >
        <el-table-column label="博客标题" min-width="250">
          <template #default="scope">
            <div class="blog-title-container">
              <el-tooltip :content="scope.row.title" placement="top" :show-after="1000">
                <a :href="scope.row.url" target="_blank" class="blog-title">
                  {{ scope.row.title }}
                </a>
              </el-tooltip>
            </div>
          </template>
        </el-table-column>
        <el-table-column prop="category" label="分类" width="150">
          <template #default="scope">
            <el-tag size="small" effect="plain">{{ scope.row.category }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="发布时间" width="180">
          <template #default="scope">
            <div class="time-container">
              <i class="el-icon-time"></i>
              <span>{{ formatDate(scope.row.publishTime) }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="180" fixed="right">
          <template #default="scope">
            <div class="operation-buttons">
              <el-button
                  type="primary"
                  size="small"
                  @click="handleEdit(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="blogForm" :rules="rules" ref="blogFormRef" label-width="80px">
        <el-form-item label="标题" prop="title">
          <el-input v-model="blogForm.title" placeholder="请输入博客标题"></el-input>
        </el-form-item>

        <el-form-item label="地址" prop="url">
          <el-input v-model="blogForm.url" placeholder="请输入博客链接地址"></el-input>
        </el-form-item>

        <el-form-item label="分类" prop="category">
          <el-select
              v-model="blogForm.category"
              placeholder="请选择分类"
              @change="handleCategoryChange"
          >
            <el-option
                v-for="item in categories"
                :key="item"
                :label="item"
                :value="item"
            ></el-option>
            <el-option
                key="add-new"
                label="添加新分类"
                value="add-new"
            ></el-option>
          </el-select>
          <!-- 新增分类输入框 -->
          <el-input
              v-if="showNewCategoryInput"
              v-model="newCategory"
              placeholder="请输入新分类名称"
              class="new-category-input"
              @blur="handleNewCategory"
          ></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="rules" ref="editFormRef" label-width="80px">
        <el-form-item label="标题" prop="title">
          <el-input v-model="editForm.title" placeholder="请输入博客标题"></el-input>
        </el-form-item>

        <el-form-item label="地址" prop="url">
          <el-input v-model="editForm.url" placeholder="请输入博客链接地址"></el-input>
        </el-form-item>

        <el-form-item label="分类" prop="category">
          <el-select
              v-model="editForm.category"
              placeholder="请选择分类"
              @change="handleEditCategoryChange"
          >
            <el-option
                v-for="item in categories"
                :key="item"
                :label="item"
                :value="item"
            ></el-option>
            <el-option
                key="add-new"
                label="添加新分类"
                value="add-new"
            ></el-option>
          </el-select>
          <el-input
              v-if="showEditCategoryInput"
              v-model="newEditCategory"
              placeholder="请输入新分类名称"
              class="new-category-input"
              @blur="handleNewEditCategory"
          ></el-input>
        </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 } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {addBlogLink, getBlogLinks, getAllCategories, deleteBlogLink, updateBlogLink, searchBlogLinks } from '@/api/blogLink'

const dialogVisible = ref(false)
const blogFormRef = ref(null)

const blogForm = reactive({
  title: '',
  url: '',
  category: ''
})

const rules = {
  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
  url: [
    { required: true, message: '请输入链接地址', trigger: 'blur' },
    { type: 'url', message: '请输入有效的URL地址', trigger: 'blur' }
  ],
  category: [{ required: true, message: '请选择或输入分类', trigger: 'change' }]
}

const showAddDialog = () => {
  dialogVisible.value = true
  blogForm.title = ''
  blogForm.url = ''
  blogForm.category = ''
  showNewCategoryInput.value = false  // 添加这行
  newCategory.value = ''  // 添加这行
}

// 添加对话框关闭的处理方法
const handleDialogClose = () => {
  dialogVisible.value = false
  showNewCategoryInput.value = false
  newCategory.value = ''
}

const blogLinks = ref([])

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

// 获取博客链接列表
const fetchBlogLinks = async () => {
  try {
    const userInfo = JSON.parse(localStorage.getItem('userInfo'))
    if (!userInfo || !userInfo.studentId) {
      ElMessage.warning('请先登录')
      return
    }

    const res = await getBlogLinks(userInfo.studentId)
    if (res.code === 200) {
      blogLinks.value = res.data
    } else {
      ElMessage.error(res.msg || '获取数据失败')
    }
  } catch (error) {
    console.error('获取博客链接失败:', error)
    ElMessage.error('获取数据失败')
  }
}

// 修改初始化分类列表
const categories = ref([])

// 添加获取分类列表的方法
const fetchCategories = async () => {
  try {
    const userInfo = JSON.parse(localStorage.getItem('userInfo'))
    if (!userInfo || !userInfo.studentId) {
      return
    }
    const res = await getAllCategories(userInfo.studentId)
    if (res.code === 200) {
      categories.value = res.data || ['默认']
    }
  } catch (error) {
    console.error('获取分类列表失败:', error)
    categories.value = ['默认']
  }
}

// 修改 onMounted
onMounted(() => {
  fetchBlogLinks()
  fetchCategories()
})

// 修改 handleSubmit,添加成功后刷新分类列表
const handleSubmit = async () => {
  try {
    await blogFormRef.value.validate()

    const userInfo = JSON.parse(localStorage.getItem('userInfo'))
    if (!userInfo || !userInfo.studentId) {
      ElMessage.warning('请先登录')
      return
    }

    const res = await addBlogLink({
      title: blogForm.title,
      url: blogForm.url,
      category: blogForm.category,
      studentId: userInfo.studentId
    })

    if (res.code === 200) {
      ElMessage.success('添加成功')
      dialogVisible.value = false
      fetchBlogLinks()
      fetchCategories() // 刷新分类列表
    } else {
      ElMessage.error(res.msg || '添加失败')
    }
  } catch (error) {
    console.error('提交失败:', error)
    ElMessage.error('请填写完整信息')
  }
}

// 页面加载时获取数据
onMounted(() => {
  fetchBlogLinks()
  fetchCategories()
})

const showNewCategoryInput = ref(false)
const newCategory = ref('')

// 处理分类选择变化
const handleCategoryChange = (value) => {
  if (value === 'add-new') {
    showNewCategoryInput.value = true
    blogForm.category = ''
  }
}

// 处理新分类输入
const handleNewCategory = () => {
  if (newCategory.value.trim()) {
    if (!categories.value.includes(newCategory.value)) {
      categories.value.push(newCategory.value)
    }
    blogForm.category = newCategory.value
  }
  showNewCategoryInput.value = false
  newCategory.value = ''
}

// 添加删除处理方法
const handleDelete = async (row) => {
  try {
    await ElMessageBox.confirm('确定要删除这个博客链接吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })

    const res = await deleteBlogLink(row.id)
    if (res.code === 200) {
      ElMessage.success('删除成功')
      await fetchBlogLinks() // 刷新列表
      await fetchCategories() // 刷新分类列表
    } else {
      ElMessage.error(res.msg || '删除失败')
    }
  } catch (error) {
    if (error !== 'cancel') {
      console.error('删除失败:', error)
      ElMessage.error('删除失败')
    }
  }
}

// 编辑相关的变量
const editDialogVisible = ref(false)
const editFormRef = ref(null)
const showEditCategoryInput = ref(false)
const newEditCategory = ref('')
const editForm = reactive({
  id: null,
  title: '',
  url: '',
  category: ''
})

// 处理编辑按钮点击
const handleEdit = (row) => {
  editForm.id = row.id
  editForm.title = row.title
  editForm.url = row.url
  editForm.category = row.category
  editDialogVisible.value = true
}

// 处理编辑分类变化
const handleEditCategoryChange = (value) => {
  if (value === 'add-new') {
    showEditCategoryInput.value = true
    editForm.category = ''
  }
}

// 处理编辑新分类输入
const handleNewEditCategory = () => {
  if (newEditCategory.value.trim()) {
    if (!categories.value.includes(newEditCategory.value)) {
      categories.value.push(newEditCategory.value)
    }
    editForm.category = newEditCategory.value
  }
  showEditCategoryInput.value = false
  newEditCategory.value = ''
}

// 处理编辑提交
const handleEditSubmit = async () => {
  try {
    await editFormRef.value.validate()

    const res = await updateBlogLink(editForm)
    if (res.code === 200) {
      ElMessage.success('更新成功')
      editDialogVisible.value = false
      await fetchBlogLinks()
      await fetchCategories()
    } else {
      ElMessage.error(res.msg || '更新失败')
    }
  } catch (error) {
    console.error('更新失败:', error)
    ElMessage.error('请填写完整信息')
  }
}

// 添加搜索相关的变量
const searchForm = reactive({
  title: '',
  category: '',
  dateRange: []
})

// 处理搜索
const handleSearch = async () => {
  try {
    const userInfo = JSON.parse(localStorage.getItem('userInfo'))
    if (!userInfo || !userInfo.studentId) {
      ElMessage.warning('请先登录')
      return
    }

    const params = {
      studentId: userInfo.studentId,
      title: searchForm.title || undefined,
      category: searchForm.category || undefined,
      startDate: searchForm.dateRange?.[0] || undefined,
      endDate: searchForm.dateRange?.[1] || undefined
    }

    const res = await searchBlogLinks(params)
    if (res.code === 200) {
      blogLinks.value = res.data
    } else {
      ElMessage.error(res.msg || '搜索失败')
    }
  } catch (error) {
    console.error('搜索失败:', error)
    ElMessage.error('搜索失败')
  }
}

// 重置搜索
const resetSearch = async () => {
  searchForm.title = ''
  searchForm.category = ''
  searchForm.dateRange = []
  await fetchBlogLinks()
}

// 添加表格行样式方法
const tableRowClassName = ({ row, rowIndex }) => {
  return rowIndex % 2 === 0 ? 'even-row' : 'odd-row'
}
</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;
}

/* 搜索表单样式 */
.search-form {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f5f7fa;
  border-radius: 4px;
  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-date-picker {
  width: 100%;
}

.search-input {
  width: 100%;
}

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

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

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

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

.blog-title {
  color: var(--primary-color);
  text-decoration: none;
  font-weight: 500;
  transition: all 0.3s;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

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

.new-category-input {
  margin-top: 10px;
}
</style>

honor.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-select
                    v-model="searchForm.semester"
                    placeholder="选择学期"
                    clearable
                    class="search-select"
                >
                  <el-option
                      v-for="item in semesters"
                      :key="item"
                      :label="item"
                      :value="item"
                  ></el-option>
                </el-select>
              </el-form-item>

              <el-form-item label="奖项类别" class="search-item">
                <el-select
                    v-model="searchForm.category"
                    placeholder="选择类别"
                    clearable
                    class="search-select"
                >
                  <el-option
                      v-for="item in categories"
                      :key="item.code"
                      :label="item.desc"
                      :value="item.code"
                  ></el-option>
                </el-select>
              </el-form-item>
            </div>

            <!-- 第二行 -->
            <div class="search-form-content">
              <el-form-item label="获奖时间" class="search-item">
                <el-date-picker
                    v-model="searchForm.timeRange"
                    type="daterange"
                    range-separator="至"
                    start-placeholder="开始日期"
                    end-placeholder="结束日期"
                    value-format="YYYY-MM-DD"
                    clearable
                    class="search-date-picker"
                ></el-date-picker>
              </el-form-item>

              <el-form-item label="关键词" class="search-item">
                <el-input
                    v-model="searchForm.keyword"
                    placeholder="搜索奖项描述"
                    clearable
                    class="search-input"
                ></el-input>
              </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>
      
      <!-- 现有的表格部分 -->
      <el-table
        :data="honorList"
        class="base-table"
        style="width: 100%" 
        border 
        stripe
        @row-click="handleRowClick"
      >
        <el-table-column prop="semester" label="学期" width="150"></el-table-column>
        <el-table-column prop="category" label="类别" width="120">
          <template #default="scope">
            <el-tag>{{ getCategoryDesc(scope.row.category) }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="description" label="奖项描述" min-width="200">
          <template #default="scope">
            <el-tooltip
              class="box-item"
              effect="dark"
              :content="scope.row.description"
              placement="top-start"
            >
              <span class="truncate-text">{{ scope.row.description }}</span>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column label="获奖时间" width="120">
          <template #default="scope">
            {{ formatDate(scope.row.honorTime) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="180" fixed="right">
          <template #default="scope">
            <el-button type="primary" size="small" @click.stop="editHonor(scope.row)">修改</el-button>
            <el-button type="danger" size="small" @click.stop="deleteHonor(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>

    <!-- 查看荣誉奖项对话框 -->
    <el-dialog
      title="荣誉奖项详情"
      class="base-dialog"
      v-model="viewDialogVisible"
      width="700px"
    >
      <el-descriptions :column="1" border>
        <el-descriptions-item label="学期">{{ currentHonor.semester }}</el-descriptions-item>
        <el-descriptions-item label="奖项类别">{{ getCategoryDesc(currentHonor.category) }}</el-descriptions-item>
        <el-descriptions-item label="获奖时间">{{ formatDate(currentHonor.honorTime) }}</el-descriptions-item>
        <el-descriptions-item label="奖项描述">{{ currentHonor.description }}</el-descriptions-item>
        <el-descriptions-item label="奖项材料">
          <div v-if="currentHonor.material" class="honor-image">
            <el-image 
              :src="currentHonor.material" 
              :preview-src-list="[currentHonor.material]"
              fit="contain" 
              style="max-width: 100%; max-height: 400px;"
              :initial-index="0"
            >
              <template #placeholder>
                <div class="image-slot">
                  加载中<span class="dot">...</span>
                </div>
              </template>
            </el-image>
          </div>
          <div v-else>无材料</div>
        </el-descriptions-item>
      </el-descriptions>
    </el-dialog>

    <!-- 添加荣誉奖项对话框 -->
    <el-dialog
      title="添加荣誉奖项"
      v-model="dialogVisible"
      width="650px"
      :close-on-click-modal="false"
    >
      <el-form :model="honorForm" :rules="rules" ref="honorFormRef" label-width="100px">
        <el-form-item label="学期" prop="semester">
          <el-select v-model="honorForm.semester" placeholder="请选择学期" style="width: 100%">
            <el-option
              v-for="item in semesters"
              :key="item"
              :label="item"
              :value="item"
            ></el-option>
          </el-select>
        </el-form-item>

        <el-form-item label="奖项类别" prop="category">
          <el-select v-model="honorForm.category" placeholder="请选择奖项类别" style="width: 100%">
            <el-option
              v-for="item in categories"
              :key="item.code"
              :label="item.desc"
              :value="item.code"
            ></el-option>
          </el-select>
        </el-form-item>

        <el-form-item label="获奖时间" prop="honorTime">
          <el-date-picker
            v-model="honorForm.honorTime"
            type="date"
            placeholder="选择获奖时间"
            style="width: 100%"
            value-format="YYYY-MM-DD"
          ></el-date-picker>
        </el-form-item>

        <el-form-item label="奖项描述" prop="description">
          <el-input
            v-model="honorForm.description"
            type="textarea"
            :rows="3"
            placeholder="请输入奖项描述"
          ></el-input>
        </el-form-item>

        <el-form-item label="奖项材料">
          <div class="upload-section">
            <input
              type="file"
              ref="fileInput"
              style="display: none"
              accept="image/*"
              @change="handleFileChange"
            />
            <div 
              class="upload-area" 
              @click="triggerFileInput"
              @dragover.prevent
              @drop.prevent="handleDrop"
            >
              <template v-if="fileList.length === 0">
                <el-icon class="upload-icon"><Upload /></el-icon>
                <div class="upload-text">
                  <span>点击选择或拖拽图片到此处</span>
                  <p>支持 jpg、png 格式图片</p>
                </div>
              </template>
              
              <div v-else class="image-preview-container">
                <div v-for="(img, index) in fileList" :key="index" class="image-preview-item">
                  <el-image :src="img.url" fit="cover" />
                  <div class="image-actions">
                    <el-button 
                      type="danger" 
                      icon="Delete" 
                      circle 
                      @click.stop="removeImage(index)"
                    />
                  </div>
                </div>
              </div>
            </div>
          </div>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
    </el-card>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Upload } from '@element-plus/icons-vue'
import { 
  addStudentHonor, 
  getStudentHonorsByStudentId, 
  deleteStudentHonor,
  updateStudentHonor 
} from '@/api/studentHonor'

// 荣誉奖项列表
const honorList = ref([])
// 对话框显示状态
const dialogVisible = ref(false)
const viewDialogVisible = ref(false)
// 表单引用
const honorFormRef = ref(null)
// 当前查看的荣誉奖项
const currentHonor = ref({})
// 文件上传相关
const fileInput = ref(null)
const fileList = ref([])

// 学期列表
const semesters = ref([
  '大一第一学期',
  '大一第二学期',
  '大二第一学期',
  '大二第二学期',
  '大三第一学期',
  '大三第二学期',
  '大四第一学期',
  '大四第二学期'
])

// 奖项类别列表
const categories = ref([
  { code: 'ACADEMIC', desc: '学业学术' },
  { code: 'SOCIAL_PRACTICE', desc: '社会实践' },
  { code: 'COMPETITION', desc: '竞赛获奖' },
  { code: 'INNOVATION', desc: '创新创业' },
  { code: 'VOLUNTEER', desc: '志愿服务' },
  { code: 'LEADERSHIP', desc: '学生干部' },
  { code: 'SPORTS', desc: '体育竞赛' },
  { code: 'ARTS', desc: '文艺表演' },
  { code: 'OTHER', desc: '其他荣誉' }
])

// 表单数据
const honorForm = reactive({
  studentId: '',
  semester: '',
  honorTime: new Date(),
  category: '',
  description: '',
  material: ''
})

// 表单验证规则
const rules = {
  semester: [{ required: true, message: '请选择学期', trigger: 'change' }],
  category: [{ required: true, message: '请选择奖项类别', trigger: 'change' }],
  honorTime: [{ required: true, message: '请选择获奖时间', trigger: 'change' }],
  description: [{ required: true, message: '请输入奖项描述', trigger: 'blur' }]
}

// 格式化日期
const formatDate = (dateTime) => {
  if (!dateTime) return ''
  const date = new Date(dateTime)
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${year}-${month}-${day}`
}

// 获取类别描述
const getCategoryDesc = (code) => {
  const category = categories.value.find(item => item.code === code)
  return category ? category.desc : code
}

// 获取荣誉奖项列表
const fetchHonorList = async () => {
  try {
    const userInfo = JSON.parse(localStorage.getItem('userInfo'))
    if (!userInfo || !userInfo.studentId) {
      ElMessage.warning('请先登录')
      return
    }

    const response = await getStudentHonorsByStudentId(userInfo.studentId)
    if (response.code === 200) {
      honorList.value = response.data || []
    } else {
      ElMessage.error(response.msg || '获取荣誉奖项列表失败')
    }
  } catch (error) {
    console.error('获取荣誉奖项列表失败:', error)
    ElMessage.error('获取荣誉奖项列表失败')
  }
}

// 处理行点击
const handleRowClick = (row) => {
  viewHonor(row)
}

// 查看荣誉奖项
const viewHonor = (row) => {
  currentHonor.value = { ...row }
  viewDialogVisible.value = true
}

// 删除荣誉奖项
const deleteHonor = async (row) => {
  try {
    await ElMessageBox.confirm('确定要删除该荣誉奖项吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })
    
    const response = await deleteStudentHonor(row.id)
    if (response.code === 200) {
      ElMessage.success('删除成功')
      // 关闭详情对话框(如果正在查看)
      if (viewDialogVisible.value && currentHonor.value.id === row.id) {
        viewDialogVisible.value = false
      }
      // 重新获取列表
      fetchHonorList()
    } else {
      ElMessage.error(response.msg || '删除失败')
    }
  } catch (error) {
    if (error !== 'cancel') {
      console.error('删除失败:', error)
      ElMessage.error('删除失败')
    }
  }
}

// 触发文件选择
const triggerFileInput = () => {
  fileInput.value.click()
}

// 处理文件选择
const handleFileChange = (e) => {
  const file = e.target.files[0]
  if (file) {
    processImage(file)
  }
}

// 处理拖拽
const handleDrop = (e) => {
  const file = e.dataTransfer.files[0]
  if (file && file.type.startsWith('image/')) {
    processImage(file)
  } else {
    ElMessage.warning('请上传图片文件')
  }
}

// 处理图片
const processImage = (file) => {
  const reader = new FileReader()
  reader.onload = (e) => {
    const img = new Image()
    img.onload = () => {
      const canvas = document.createElement('canvas')
      canvas.width = img.width
      canvas.height = img.height
      const ctx = canvas.getContext('2d')

      // 绘制原图
      ctx.drawImage(img, 0, 0)

      // 设置水印样式
      ctx.font = '24px Arial'
      ctx.fillStyle = '#000000'
      ctx.globalAlpha = 0.5
      ctx.textAlign = 'right'

      // 添加时间水印
      const timeText = new Date().toLocaleString('zh-CN')
      const x = canvas.width - 20
      const timeY = canvas.height - 20

      // 绘制水印
      ctx.fillText(timeText, x, timeY)

      // 转换为 base64 并添加到列表
      const watermarkedImage = canvas.toDataURL(file.type)
      fileList.value = [{ 
        url: watermarkedImage,
        name: file.name 
      }]
      honorForm.material = watermarkedImage // 将 base64 字符串保存到表单中
    }
    img.src = e.target.result
  }
  reader.readAsDataURL(file)
}

// 移除图片
const removeImage = (index) => {
  fileList.value.splice(index, 1)
  honorForm.material = ''
}

// 编辑荣誉奖项
const editHonor = (row) => {
  // 复制当前行数据到表单
  honorForm.semester = row.semester
  honorForm.category = row.category
  honorForm.honorTime = formatDate(row.honorTime)
  honorForm.description = row.description
  honorForm.material = row.material
  
  // 如果有图片,添加到预览列表
  if (row.material) {
    fileList.value = [{
      url: row.material,
      name: '当前图片'
    }]
  } else {
    fileList.value = []
  }
  
  // 保存当前编辑的记录ID
  honorForm.id = row.id
  honorForm.studentId = row.studentId
  
  // 修改对话框标题
  dialogTitle.value = '编辑荣誉奖项'
  
  // 显示对话框
  dialogVisible.value = true
}

// 添加响应式变量用于对话框标题
const dialogTitle = ref('添加荣誉奖项')

// 修改 showAddDialog 方法
const showAddDialog = () => {
  // 重置表单
  honorForm.id = null
  honorForm.studentId = ''
  honorForm.semester = ''
  honorForm.honorTime = new Date()
  honorForm.category = ''
  honorForm.description = ''
  honorForm.material = ''
  fileList.value = []
  
  // 设置对话框标题
  dialogTitle.value = '添加荣誉奖项'
  
  // 显示对话框
  dialogVisible.value = true
}

// 修改提交表单方法
const submitForm = async () => {
  try {
    await honorFormRef.value.validate()
    
    const userInfo = JSON.parse(localStorage.getItem('userInfo'))
    if (!userInfo || !userInfo.studentId) {
      ElMessage.warning('请先登录')
      return
    }
    
    // 构造提交数据
    const submitData = {
      id: honorForm.id, // 添加ID字段
      studentId: honorForm.id ? honorForm.studentId : userInfo.studentId, // 如果是编辑则使用原studentId
      semester: honorForm.semester,
      honorTime: honorForm.honorTime,
      category: honorForm.category,
      description: honorForm.description,
      material: honorForm.material
    }
    
    let response
    if (honorForm.id) {
      // 更新
      response = await updateStudentHonor(submitData)
    } else {
      // 新增
      response = await addStudentHonor(submitData)
    }
    
    if (response.code === 200) {
      ElMessage.success(honorForm.id ? '更新成功' : '添加成功')
      dialogVisible.value = false
      fetchHonorList()
    } else {
      ElMessage.error(response.msg || (honorForm.id ? '更新失败' : '添加失败'))
    }
  } catch (error) {
    console.error(honorForm.id ? '更新失败:' : '添加失败:', error)
    if (error.response && error.response.data) {
      ElMessage.error(error.response.data.message || (honorForm.id ? '更新失败' : '添加失败'))
    } else {
      ElMessage.error('请填写完整信息')
    }
  }
}

onMounted(() => {
  fetchHonorList()
})

// 搜索表单数据
const searchForm = reactive({
  semester: '',
  category: '',
  timeRange: [],
  keyword: ''
})

// 处理搜索
const handleSearch = () => {
  const filteredList = honorList.value.filter(item => {
    // 学期筛选
    if (searchForm.semester && item.semester !== searchForm.semester) {
      return false
    }
    
    // 类别筛选
    if (searchForm.category && item.category !== searchForm.category) {
      return false
    }
    
    // 时间范围筛选
    if (searchForm.timeRange && searchForm.timeRange.length === 2) {
      const honorDate = new Date(item.honorTime).getTime()
      const startDate = new Date(searchForm.timeRange[0]).getTime()
      const endDate = new Date(searchForm.timeRange[1]).getTime()
      if (honorDate < startDate || honorDate > endDate) {
        return false
      }
    }
    
    // 关键词筛选
    if (searchForm.keyword && !item.description.toLowerCase().includes(searchForm.keyword.toLowerCase())) {
      return false
    }
    
    return true
  })
  
  honorList.value = filteredList
}

// 重置搜索
const resetSearch = async () => {
  searchForm.semester = ''
  searchForm.category = ''
  searchForm.timeRange = []
  searchForm.keyword = ''
  await fetchHonorList()
}
</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;
}

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

.search-form-content {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.search-form-item {
  flex: 1;
  min-width: 200px;
}

.search-form-item .label {
  display: block;
  margin-bottom: 8px;
  font-size: 14px;
  color: #606266;
}

.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;
}

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

/* 对话框样式 */
.base-dialog :deep(.el-dialog__header) {
  border-bottom: 1px solid #ebeef5;
  padding-bottom: 15px;
  margin-bottom: 15px;
}

.base-dialog :deep(.el-dialog__body) {
  padding: 20px;
}

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

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

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

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

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.upload-section {
  width: 100%;
}

.upload-icon {
  font-size: 48px;
  color: #8c939d;
  margin-bottom: 10px;
}

.upload-text {
  text-align: center;
  color: #606266;
}

.image-preview-item {
  position: relative;
  width: 150px;
  height: 150px;
  border-radius: 4px;
  overflow: hidden;
}

.image-preview-item .el-image {
  width: 100%;
  height: 100%;
}

.image-actions {
  position: absolute;
  top: 5px;
  right: 5px;
  display: flex;
  gap: 5px;
}

.truncate-text {
  display: inline-block;
  max-width: 300px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.honor-detail-dialog :deep(.el-dialog__body) {
  padding: 20px;
}

.honor-image {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  min-height: 200px;
  background-color: #f5f7fa;
  border-radius: 4px;
}

.image-slot {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  color: #909399;
  font-size: 14px;
}

.dot {
  animation: dot 1.5s infinite;
  display: inline-block;
  width: 20px;
  text-align: left;
}

@keyframes dot {
  0% { content: '.'; }
  33% { content: '..'; }
  66% { content: '...'; }
}

:deep(.el-table__row) {
  cursor: pointer;
}

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

.search-form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.search-form :deep(.el-form-item) {
  margin-bottom: 0;
  margin-right: 10px;
}

.search-form :deep(.el-form-item__label) {
  font-weight: normal;
}

.search-form {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.search-item {
  margin-bottom: 0;
  display: flex;
  align-items: center;
}

.search-form {
  width: 100%;
}

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

.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-date-picker {
  width: 100%;
}

.search-input {
  width: 100%;
}

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

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

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

.upload-area {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  background-color: #fafafa;
  transition: border-color 0.3s;
  padding: 20px;
  min-height: 180px;
}

.upload-area:hover {
  border-color: var(--primary-color);
}

.image-preview-container {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  width: 100%;
}
</style>

posted @ 2025-05-20 20:36  vivi_vimi  阅读(16)  评论(0)    收藏  举报