|
@@ -0,0 +1,737 @@
|
|
|
+<template>
|
|
|
+ <div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
|
|
+ <!-- 顶部导航 -->
|
|
|
+ <div class="bg-white dark:bg-gray-800 shadow">
|
|
|
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">培训管理</h1>
|
|
|
+ <div class="flex space-x-4">
|
|
|
+ <button @click="showAddPlanModal = true" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 flex items-center">
|
|
|
+ <PlusCircle class="w-5 h-5 mr-1" />
|
|
|
+ 创建培训计划
|
|
|
+ </button>
|
|
|
+ <button @click="showAddCourseModal = true" class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 flex items-center">
|
|
|
+ <BookOpen class="w-5 h-5 mr-1" />
|
|
|
+ 添加课程
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 主要内容区域 -->
|
|
|
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
|
+ <!-- 培训统计卡片 -->
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
|
|
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 flex items-center">
|
|
|
+ <div class="mr-4">
|
|
|
+ <Clock class="w-10 h-10 text-blue-600 dark:text-blue-400" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="text-sm font-medium text-gray-500 dark:text-gray-400">进行中培训</div>
|
|
|
+ <div class="mt-1 text-3xl font-bold text-blue-600 dark:text-blue-400">{{ ongoingTrainings }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 flex items-center">
|
|
|
+ <div class="mr-4">
|
|
|
+ <CheckCircle class="w-10 h-10 text-green-600 dark:text-green-400" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="text-sm font-medium text-gray-500 dark:text-gray-400">已完成培训</div>
|
|
|
+ <div class="mt-1 text-3xl font-bold text-green-600 dark:text-green-400">{{ completedTrainings }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 flex items-center">
|
|
|
+ <div class="mr-4">
|
|
|
+ <Users class="w-10 h-10 text-yellow-600 dark:text-yellow-400" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="text-sm font-medium text-gray-500 dark:text-gray-400">总参与人数</div>
|
|
|
+ <div class="mt-1 text-3xl font-bold text-yellow-600 dark:text-yellow-400">{{ totalParticipants }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 flex items-center">
|
|
|
+ <div class="mr-4">
|
|
|
+ <Star class="w-10 h-10 text-purple-600 dark:text-purple-400" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="text-sm font-medium text-gray-500 dark:text-gray-400">平均评分</div>
|
|
|
+ <div class="mt-1 text-3xl font-bold text-purple-600 dark:text-purple-400">{{ averageRating }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 搜索和筛选 -->
|
|
|
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">搜索</label>
|
|
|
+ <div class="relative">
|
|
|
+ <Search class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ v-model="searchText"
|
|
|
+ placeholder="培训名称/课程名称"
|
|
|
+ class="w-full pl-10 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">培训类型</label>
|
|
|
+ <select v-model="filters.type" class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ <option value="">所有类型</option>
|
|
|
+ <option v-for="type in trainingTypes" :key="type.id" :value="type.id">{{ type.name }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">状态</label>
|
|
|
+ <select v-model="filters.status" class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ <option value="">所有状态</option>
|
|
|
+ <option v-for="status in statusTypes" :key="status.id" :value="status.id">{{ status.name }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">时间范围</label>
|
|
|
+ <select v-model="filters.timeRange" class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ <option value="">所有时间</option>
|
|
|
+ <option value="week">本周</option>
|
|
|
+ <option value="month">本月</option>
|
|
|
+ <option value="quarter">本季度</option>
|
|
|
+ <option value="year">本年</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 培训计划列表 -->
|
|
|
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden mb-6">
|
|
|
+ <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
|
|
+ <h2 class="text-lg font-medium text-gray-900 dark:text-white flex items-center">
|
|
|
+ <ClipboardList class="w-5 h-5 mr-2" />
|
|
|
+ 培训计划列表
|
|
|
+ </h2>
|
|
|
+ </div>
|
|
|
+ <div class="overflow-x-auto">
|
|
|
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
+ <thead class="bg-gray-50 dark:bg-gray-700">
|
|
|
+ <tr>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">培训名称</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">类型</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">开始时间</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">结束时间</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">参与人数</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">状态</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
+ <tr v-for="plan in filteredPlans" :key="plan.id">
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">
|
|
|
+ <div class="text-sm font-medium text-gray-900 dark:text-white">{{ plan.name }}</div>
|
|
|
+ <div class="text-sm text-gray-500 dark:text-gray-400">{{ plan.description }}</div>
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
|
+ {{ getTrainingTypeName(plan.type) }}
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
|
+ {{ formatDate(plan.startTime) }}
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
|
+ {{ formatDate(plan.endTime) }}
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
|
+ {{ plan.participants.length }}
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">
|
|
|
+ <span :class="getPlanStatusClass(plan.status)" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full">
|
|
|
+ {{ getPlanStatusName(plan.status) }}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
|
+ <button @click="viewPlan(plan)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 mr-3 flex items-center">
|
|
|
+ <Eye class="w-4 h-4 mr-1" />
|
|
|
+ 查看
|
|
|
+ </button>
|
|
|
+ <button @click="editPlan(plan)" class="text-yellow-600 hover:text-yellow-900 dark:text-yellow-400 dark:hover:text-yellow-300 mr-3 flex items-center">
|
|
|
+ <Edit class="w-4 h-4 mr-1" />
|
|
|
+ 编辑
|
|
|
+ </button>
|
|
|
+ <button @click="deletePlan(plan)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 flex items-center">
|
|
|
+ <Trash2 class="w-4 h-4 mr-1" />
|
|
|
+ 删除
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ <tr v-if="filteredPlans.length === 0">
|
|
|
+ <td colspan="7" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
|
|
|
+ 没有找到匹配的培训计划
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 课程列表 -->
|
|
|
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
|
|
+ <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
|
|
+ <h2 class="text-lg font-medium text-gray-900 dark:text-white flex items-center">
|
|
|
+ <BookOpen class="w-5 h-5 mr-2" />
|
|
|
+ 课程列表
|
|
|
+ </h2>
|
|
|
+ </div>
|
|
|
+ <div class="overflow-x-auto">
|
|
|
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
+ <thead class="bg-gray-50 dark:bg-gray-700">
|
|
|
+ <tr>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">课程名称</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">类型</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">难度</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">时长</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">讲师</th>
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
+ <tr v-for="course in filteredCourses" :key="course.id">
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">
|
|
|
+ <div class="text-sm font-medium text-gray-900 dark:text-white">{{ course.name }}</div>
|
|
|
+ <div class="text-sm text-gray-500 dark:text-gray-400">{{ course.description }}</div>
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
|
+ {{ getTrainingTypeName(course.type) }}
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
|
+ {{ getDifficultyLevelName(course.difficulty) }}
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
|
+ {{ course.duration }}小时
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
|
|
+ {{ course.instructor }}
|
|
|
+ </td>
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
|
+ <button @click="viewCourse(course)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 mr-3 flex items-center">
|
|
|
+ <Eye class="w-4 h-4 mr-1" />
|
|
|
+ 查看
|
|
|
+ </button>
|
|
|
+ <button @click="editCourse(course)" class="text-yellow-600 hover:text-yellow-900 dark:text-yellow-400 dark:hover:text-yellow-300 mr-3 flex items-center">
|
|
|
+ <Edit class="w-4 h-4 mr-1" />
|
|
|
+ 编辑
|
|
|
+ </button>
|
|
|
+ <button @click="deleteCourse(course)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 flex items-center">
|
|
|
+ <Trash2 class="w-4 h-4 mr-1" />
|
|
|
+ 删除
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ <tr v-if="filteredCourses.length === 0">
|
|
|
+ <td colspan="6" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
|
|
|
+ 没有找到匹配的课程
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 创建培训计划模态框 -->
|
|
|
+ <div v-if="showAddPlanModal" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
|
|
|
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
|
|
|
+ <div class="flex justify-between items-center mb-4">
|
|
|
+ <h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
|
|
+ <ClipboardList class="w-5 h-5 mr-2" />
|
|
|
+ 创建培训计划
|
|
|
+ </h2>
|
|
|
+ <button @click="showAddPlanModal = false" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
|
|
|
+ <X class="w-5 h-5" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <form @submit.prevent="addPlan">
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">培训名称</label>
|
|
|
+ <input type="text" v-model="newPlan.name" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">培训类型</label>
|
|
|
+ <select v-model="newPlan.type" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ <option v-for="type in trainingTypes" :key="type.id" :value="type.id">{{ type.name }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">开始时间</label>
|
|
|
+ <input type="datetime-local" v-model="newPlan.startTime" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">结束时间</label>
|
|
|
+ <input type="datetime-local" v-model="newPlan.endTime" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">培训描述</label>
|
|
|
+ <textarea v-model="newPlan.description" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"></textarea>
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">选择课程</label>
|
|
|
+ <div class="space-y-2 max-h-40 overflow-y-auto p-2 border border-gray-300 dark:border-gray-600 rounded-md">
|
|
|
+ <div v-for="course in courses" :key="course.id" class="flex items-center">
|
|
|
+ <input type="checkbox" :value="course.id" v-model="newPlan.courses" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600">
|
|
|
+ <span class="ml-2 text-sm text-gray-900 dark:text-white">{{ course.name }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-end space-x-3">
|
|
|
+ <button type="button" @click="showAddPlanModal = false" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center">
|
|
|
+ <X class="w-4 h-4 mr-1" />
|
|
|
+ 取消
|
|
|
+ </button>
|
|
|
+ <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center">
|
|
|
+ <Save class="w-4 h-4 mr-1" />
|
|
|
+ 创建
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 添加课程模态框 -->
|
|
|
+ <div v-if="showAddCourseModal" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
|
|
|
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
|
|
|
+ <div class="flex justify-between items-center mb-4">
|
|
|
+ <h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center">
|
|
|
+ <BookOpen class="w-5 h-5 mr-2" />
|
|
|
+ 添加课程
|
|
|
+ </h2>
|
|
|
+ <button @click="showAddCourseModal = false" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
|
|
|
+ <X class="w-5 h-5" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <form @submit.prevent="addCourse">
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">课程名称</label>
|
|
|
+ <input type="text" v-model="newCourse.name" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">课程类型</label>
|
|
|
+ <select v-model="newCourse.type" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ <option v-for="type in trainingTypes" :key="type.id" :value="type.id">{{ type.name }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">难度级别</label>
|
|
|
+ <select v-model="newCourse.difficulty" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ <option v-for="level in difficultyLevels" :key="level.id" :value="level.id">{{ level.name }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">课程时长(小时)</label>
|
|
|
+ <input type="number" v-model="newCourse.duration" required min="1" class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">讲师</label>
|
|
|
+ <input type="text" v-model="newCourse.instructor" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
|
+ </div>
|
|
|
+ <div class="mb-4">
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">课程描述</label>
|
|
|
+ <textarea v-model="newCourse.description" required class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"></textarea>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-end space-x-3">
|
|
|
+ <button type="button" @click="showAddCourseModal = false" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center">
|
|
|
+ <X class="w-4 h-4 mr-1" />
|
|
|
+ 取消
|
|
|
+ </button>
|
|
|
+ <button type="submit" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 flex items-center">
|
|
|
+ <Save class="w-4 h-4 mr-1" />
|
|
|
+ 添加
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed } from 'vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
+import {
|
|
|
+ PlusCircle,
|
|
|
+ BookOpen,
|
|
|
+ Clock,
|
|
|
+ CheckCircle,
|
|
|
+ Users,
|
|
|
+ Star,
|
|
|
+ Search,
|
|
|
+ ClipboardList,
|
|
|
+ Eye,
|
|
|
+ Edit,
|
|
|
+ Trash2,
|
|
|
+ X,
|
|
|
+ Save
|
|
|
+} from 'lucide-vue-next'
|
|
|
+
|
|
|
+// 路由
|
|
|
+const router = useRouter()
|
|
|
+
|
|
|
+// 培训计划数据
|
|
|
+const plans = ref([
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: '新员工入职培训',
|
|
|
+ type: 1,
|
|
|
+ startTime: '2024-03-01T09:00:00',
|
|
|
+ endTime: '2024-03-05T17:00:00',
|
|
|
+ description: '新员工入职培训计划,包含公司文化介绍和工作流程培训',
|
|
|
+ courses: [1, 2],
|
|
|
+ participants: [1, 2, 3, 4, 5],
|
|
|
+ status: 1,
|
|
|
+ rating: 4.5
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: '技术提升培训',
|
|
|
+ type: 2,
|
|
|
+ startTime: '2024-03-10T09:00:00',
|
|
|
+ endTime: '2024-03-12T17:00:00',
|
|
|
+ description: '技术提升培训计划,包含前端和后端开发进阶课程',
|
|
|
+ courses: [3, 4],
|
|
|
+ participants: [6, 7, 8, 9],
|
|
|
+ status: 2,
|
|
|
+ rating: 4.8
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: '管理技能培训',
|
|
|
+ type: 3,
|
|
|
+ startTime: '2024-04-01T09:00:00',
|
|
|
+ endTime: '2024-04-03T17:00:00',
|
|
|
+ description: '管理技能培训计划,针对新晋升的团队领导',
|
|
|
+ courses: [5],
|
|
|
+ participants: [10, 11, 12],
|
|
|
+ status: 1,
|
|
|
+ rating: 4.2
|
|
|
+ }
|
|
|
+])
|
|
|
+
|
|
|
+// 课程数据
|
|
|
+const courses = ref([
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: '公司文化介绍',
|
|
|
+ type: 1,
|
|
|
+ difficulty: 1,
|
|
|
+ duration: 2,
|
|
|
+ instructor: '张三',
|
|
|
+ description: '介绍公司文化和价值观,帮助新员工快速融入团队'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: '工作流程培训',
|
|
|
+ type: 1,
|
|
|
+ difficulty: 1,
|
|
|
+ duration: 3,
|
|
|
+ instructor: '李四',
|
|
|
+ description: '介绍公司工作流程和规范,包括考勤、请假等制度'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: '前端开发进阶',
|
|
|
+ type: 2,
|
|
|
+ difficulty: 3,
|
|
|
+ duration: 8,
|
|
|
+ instructor: '王五',
|
|
|
+ description: '前端开发高级技术培训,包括性能优化和组件设计'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ name: '后端开发进阶',
|
|
|
+ type: 2,
|
|
|
+ difficulty: 3,
|
|
|
+ duration: 8,
|
|
|
+ instructor: '赵六',
|
|
|
+ description: '后端开发高级技术培训,包括系统架构和数据库优化'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 5,
|
|
|
+ name: '团队管理基础',
|
|
|
+ type: 3,
|
|
|
+ difficulty: 2,
|
|
|
+ duration: 6,
|
|
|
+ instructor: '钱七',
|
|
|
+ description: '团队管理基础培训,包括沟通技巧和冲突处理'
|
|
|
+ }
|
|
|
+])
|
|
|
+
|
|
|
+// 培训类型
|
|
|
+const trainingTypes = ref([
|
|
|
+ { id: 1, name: '入职培训' },
|
|
|
+ { id: 2, name: '技术培训' },
|
|
|
+ { id: 3, name: '管理培训' },
|
|
|
+ { id: 4, name: '产品培训' },
|
|
|
+ { id: 5, name: '销售培训' }
|
|
|
+])
|
|
|
+
|
|
|
+// 难度级别
|
|
|
+const difficultyLevels = ref([
|
|
|
+ { id: 1, name: '初级' },
|
|
|
+ { id: 2, name: '中级' },
|
|
|
+ { id: 3, name: '高级' }
|
|
|
+])
|
|
|
+
|
|
|
+// 状态类型
|
|
|
+const statusTypes = ref([
|
|
|
+ { id: 1, name: '进行中' },
|
|
|
+ { id: 2, name: '已完成' },
|
|
|
+ { id: 3, name: '已取消' }
|
|
|
+])
|
|
|
+
|
|
|
+// 筛选条件
|
|
|
+const searchText = ref('')
|
|
|
+const filters = ref({
|
|
|
+ type: '',
|
|
|
+ status: '',
|
|
|
+ timeRange: ''
|
|
|
+})
|
|
|
+
|
|
|
+// 模态框状态
|
|
|
+const showAddPlanModal = ref(false)
|
|
|
+const showAddCourseModal = ref(false)
|
|
|
+
|
|
|
+// 新培训计划表单
|
|
|
+const newPlan = ref({
|
|
|
+ name: '',
|
|
|
+ type: '',
|
|
|
+ startTime: '',
|
|
|
+ endTime: '',
|
|
|
+ description: '',
|
|
|
+ courses: []
|
|
|
+})
|
|
|
+
|
|
|
+// 新课程表单
|
|
|
+const newCourse = ref({
|
|
|
+ name: '',
|
|
|
+ type: '',
|
|
|
+ difficulty: '',
|
|
|
+ duration: '',
|
|
|
+ instructor: '',
|
|
|
+ description: ''
|
|
|
+})
|
|
|
+
|
|
|
+// 计算属性
|
|
|
+const filteredPlans = computed(() => {
|
|
|
+ return plans.value.filter(item => {
|
|
|
+ const matchesSearch = !searchText.value ||
|
|
|
+ item.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
|
|
+ item.description.toLowerCase().includes(searchText.value.toLowerCase())
|
|
|
+ const matchesType = !filters.value.type || item.type === parseInt(filters.value.type)
|
|
|
+ const matchesStatus = !filters.value.status || item.status === parseInt(filters.value.status)
|
|
|
+ const matchesTimeRange = !filters.value.timeRange || checkTimeRange(item.startTime, filters.value.timeRange)
|
|
|
+ return matchesSearch && matchesType && matchesStatus && matchesTimeRange
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+const filteredCourses = computed(() => {
|
|
|
+ return courses.value.filter(item => {
|
|
|
+ const matchesSearch = !searchText.value ||
|
|
|
+ item.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
|
|
+ item.description.toLowerCase().includes(searchText.value.toLowerCase())
|
|
|
+ const matchesType = !filters.value.type || item.type === parseInt(filters.value.type)
|
|
|
+ return matchesSearch && matchesType
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+const ongoingTrainings = computed(() => {
|
|
|
+ return plans.value.filter(item => item.status === 1).length
|
|
|
+})
|
|
|
+
|
|
|
+const completedTrainings = computed(() => {
|
|
|
+ return plans.value.filter(item => item.status === 2).length
|
|
|
+})
|
|
|
+
|
|
|
+const totalParticipants = computed(() => {
|
|
|
+ return plans.value.reduce((total, plan) => total + plan.participants.length, 0)
|
|
|
+})
|
|
|
+
|
|
|
+const averageRating = computed(() => {
|
|
|
+ const ratings = plans.value.map(plan => plan.rating || 0).filter(rating => rating > 0)
|
|
|
+ const sum = ratings.reduce((total, rating) => total + rating, 0)
|
|
|
+ return ratings.length > 0 ? (sum / ratings.length).toFixed(1) : '0.0'
|
|
|
+})
|
|
|
+
|
|
|
+// 格式化日期
|
|
|
+const formatDate = (date) => {
|
|
|
+ return new Date(date).toLocaleDateString('zh-CN', {
|
|
|
+ year: 'numeric',
|
|
|
+ month: '2-digit',
|
|
|
+ day: '2-digit',
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 检查时间范围
|
|
|
+const checkTimeRange = (date, range) => {
|
|
|
+ const now = new Date()
|
|
|
+ const targetDate = new Date(date)
|
|
|
+
|
|
|
+ switch (range) {
|
|
|
+ case 'week': {
|
|
|
+ const weekStart = new Date(now)
|
|
|
+ weekStart.setDate(now.getDate() - now.getDay())
|
|
|
+ weekStart.setHours(0, 0, 0, 0)
|
|
|
+ const weekEnd = new Date(weekStart)
|
|
|
+ weekEnd.setDate(weekStart.getDate() + 7)
|
|
|
+ return targetDate >= weekStart && targetDate < weekEnd
|
|
|
+ }
|
|
|
+ case 'month': {
|
|
|
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
|
+ const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
|
|
+ return targetDate >= monthStart && targetDate <= monthEnd
|
|
|
+ }
|
|
|
+ case 'quarter': {
|
|
|
+ const quarterMonth = Math.floor(now.getMonth() / 3) * 3
|
|
|
+ const quarterStart = new Date(now.getFullYear(), quarterMonth, 1)
|
|
|
+ const quarterEnd = new Date(now.getFullYear(), quarterMonth + 3, 0)
|
|
|
+ return targetDate >= quarterStart && targetDate <= quarterEnd
|
|
|
+ }
|
|
|
+ case 'year': {
|
|
|
+ const yearStart = new Date(now.getFullYear(), 0, 1)
|
|
|
+ const yearEnd = new Date(now.getFullYear(), 11, 31)
|
|
|
+ return targetDate >= yearStart && targetDate <= yearEnd
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return true
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取培训类型名称
|
|
|
+const getTrainingTypeName = (typeId) => {
|
|
|
+ const type = trainingTypes.value.find(t => t.id === typeId)
|
|
|
+ return type ? type.name : '未知类型'
|
|
|
+}
|
|
|
+
|
|
|
+// 获取难度级别名称
|
|
|
+const getDifficultyLevelName = (levelId) => {
|
|
|
+ const level = difficultyLevels.value.find(l => l.id === levelId)
|
|
|
+ return level ? level.name : '未知级别'
|
|
|
+}
|
|
|
+
|
|
|
+// 获取计划状态名称
|
|
|
+const getPlanStatusName = (statusId) => {
|
|
|
+ const status = statusTypes.value.find(s => s.id === statusId)
|
|
|
+ return status ? status.name : '未知状态'
|
|
|
+}
|
|
|
+
|
|
|
+// 获取计划状态样式
|
|
|
+const getPlanStatusClass = (statusId) => {
|
|
|
+ const classes = {
|
|
|
+ 1: 'bg-blue-100 text-blue-800 dark:bg-blue-700 dark:text-blue-300',
|
|
|
+ 2: 'bg-green-100 text-green-800 dark:bg-green-700 dark:text-green-300',
|
|
|
+ 3: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
|
|
+ }
|
|
|
+ return classes[statusId] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
|
|
+}
|
|
|
+
|
|
|
+// 添加培训计划
|
|
|
+const addPlan = () => {
|
|
|
+ const item = {
|
|
|
+ id: plans.value.length + 1,
|
|
|
+ ...newPlan.value,
|
|
|
+ participants: [],
|
|
|
+ status: 1,
|
|
|
+ rating: 0
|
|
|
+ }
|
|
|
+ plans.value.push(item)
|
|
|
+ showAddPlanModal.value = false
|
|
|
+ resetNewPlan()
|
|
|
+}
|
|
|
+
|
|
|
+// 重置新培训计划表单
|
|
|
+const resetNewPlan = () => {
|
|
|
+ newPlan.value = {
|
|
|
+ name: '',
|
|
|
+ type: '',
|
|
|
+ startTime: '',
|
|
|
+ endTime: '',
|
|
|
+ description: '',
|
|
|
+ courses: []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 添加课程
|
|
|
+const addCourse = () => {
|
|
|
+ const item = {
|
|
|
+ id: courses.value.length + 1,
|
|
|
+ ...newCourse.value
|
|
|
+ }
|
|
|
+ courses.value.push(item)
|
|
|
+ showAddCourseModal.value = false
|
|
|
+ resetNewCourse()
|
|
|
+}
|
|
|
+
|
|
|
+// 重置新课程表单
|
|
|
+const resetNewCourse = () => {
|
|
|
+ newCourse.value = {
|
|
|
+ name: '',
|
|
|
+ type: '',
|
|
|
+ difficulty: '',
|
|
|
+ duration: '',
|
|
|
+ instructor: '',
|
|
|
+ description: ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 查看培训计划
|
|
|
+const viewPlan = (item) => {
|
|
|
+ // 实现查看培训计划逻辑
|
|
|
+ console.log('查看培训计划:', item)
|
|
|
+ // 这里可以跳转到培训详情页面
|
|
|
+ // router.push(`/training/${item.id}`)
|
|
|
+}
|
|
|
+
|
|
|
+// 编辑培训计划
|
|
|
+const editPlan = (item) => {
|
|
|
+ // 实现编辑培训计划逻辑
|
|
|
+ console.log('编辑培训计划:', item)
|
|
|
+ // 这里可以打开编辑模态框,预填充当前数据
|
|
|
+ newPlan.value = { ...item }
|
|
|
+ showAddPlanModal.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 删除培训计划
|
|
|
+const deletePlan = (item) => {
|
|
|
+ if (confirm(`确定要删除培训计划"${item.name}"吗?`)) {
|
|
|
+ const index = plans.value.findIndex(p => p.id === item.id)
|
|
|
+ if (index !== -1) {
|
|
|
+ plans.value.splice(index, 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 查看课程
|
|
|
+const viewCourse = (item) => {
|
|
|
+ // 实现查看课程逻辑
|
|
|
+ console.log('查看课程:', item)
|
|
|
+ // 这里可以跳转到课程详情页面
|
|
|
+ // router.push(`/course/${item.id}`)
|
|
|
+}
|
|
|
+
|
|
|
+// 编辑课程
|
|
|
+const editCourse = (item) => {
|
|
|
+ // 实现编辑课程逻辑
|
|
|
+ console.log('编辑课程:', item)
|
|
|
+ // 这里可以打开编辑模态框,预填充当前数据
|
|
|
+ newCourse.value = { ...item }
|
|
|
+ showAddCourseModal.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 删除课程
|
|
|
+const deleteCourse = (item) => {
|
|
|
+ if (confirm(`确定要删除课程"${item.name}"吗?`)) {
|
|
|
+ const index = courses.value.findIndex(c => c.id === item.id)
|
|
|
+ if (index !== -1) {
|
|
|
+ courses.value.splice(index, 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|