Files
012-kaopeilian/frontend/src/views/manager/course-management.vue.current
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

3234 lines
88 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="course-management-container">
<div class="page-header">
<h1 class="page-title">课程管理</h1>
<div class="header-actions">
<el-button type="primary" @click="createCourse">
<el-icon class="el-icon--left"><Plus /></el-icon>
新建课程
</el-button>
</div>
</div>
<!-- 搜索和筛选 -->
<div class="filter-section card">
<el-form :inline="true" :model="filterForm" class="filter-form">
<el-form-item label="关键词">
<el-input v-model="filterForm.keyword" placeholder="搜索课程名称" clearable />
</el-form-item>
<el-form-item label="课程分类">
<el-select v-model="filterForm.category" placeholder="请选择" clearable>
<el-option label="基础课程" value="basic" />
<el-option label="进阶课程" value="advanced" />
<el-option label="产品培训" value="product" />
<el-option label="管理课程" value="management" />
<el-option label="高级课程" value="senior" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filterForm.status" placeholder="请选择" clearable>
<el-option label="已发布" value="published" />
<el-option label="草稿" value="draft" />
<el-option label="处理中" value="processing" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon class="el-icon--left"><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 课程统计 -->
<div class="stats-section">
<div class="stat-card card" v-for="stat in courseStats" :key="stat.label">
<div class="stat-icon" :style="{ backgroundColor: stat.bgColor }">
<el-icon :size="24" :color="stat.color">
<component :is="iconComponents[stat.icon]" />
</el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</div>
<!-- 课程列表 -->
<div class="course-list card">
<div class="list-header">
<h3>课程列表</h3>
<div class="view-toggle">
<el-button-group>
<el-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
<el-icon><Grid /></el-icon>
</el-button>
<el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
<el-icon><List /></el-icon>
</el-button>
</el-button-group>
</div>
</div>
<!-- 卡片视图 -->
<div v-if="viewMode === 'card'" class="course-grid">
<div class="course-card" v-for="course in courseList" :key="course.id">
<div class="course-cover">
<div class="course-placeholder">
<el-icon :size="48" color="#c0c4cc"><Files /></el-icon>
</div>
<div class="course-status" :class="course.status">
{{ getStatusText(course.status) }}
</div>
</div>
<div class="course-info">
<h4 class="course-title">{{ course.name }}</h4>
<p class="course-desc">{{ course.description }}</p>
<div class="course-meta">
<span><el-icon><Files /></el-icon> {{ course.materialCount }} 个资料</span>
</div>
<div class="course-actions">
<el-button link type="primary" size="small" @click="viewCourseDetail(course)">
详情
</el-button>
<el-button link type="primary" size="small" @click="editCourse(course)">
编辑
</el-button>
<el-button link type="primary" size="small" @click="manageCoursePositions(course)">
岗位分配
</el-button>
<el-button link type="danger" size="small" @click="deleteCourse(course)">
删除
</el-button>
</div>
</div>
</div>
</div>
<!-- 列表视图 -->
<el-table v-else :data="courseList" style="width: 100%" v-loading="loading">
<el-table-column prop="name" label="课程名称" min-width="200" />
<el-table-column prop="category" label="课程分类" width="120">
<template #default="scope">
{{ getCategoryText(scope.row.category) }}
</template>
</el-table-column>
<el-table-column prop="materialCount" label="资料数" width="100">
<template #default="scope">
{{ scope.row.materialCount }} 个
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间" width="180" />
<el-table-column label="操作" width="280" fixed="right">
<template #default="scope">
<el-button link type="primary" size="small" @click="viewCourseDetail(scope.row)">
详情
</el-button>
<el-button link type="primary" size="small" @click="editCourse(scope.row)">
编辑
</el-button>
<el-button link type="primary" size="small" @click="manageCoursePositions(scope.row)">
岗位分配
</el-button>
<el-button link type="danger" size="small" @click="deleteCourse(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 知识点管理弹窗 -->
<el-dialog
v-model="knowledgeDialogVisible"
:title="`${currentCourse?.name} - 知识点管理`"
width="90%"
top="5vh"
>
<div class="knowledge-management">
<!-- 知识点列表 -->
<el-table :data="knowledgeList" style="width: 100%" max-height="500">
<el-table-column type="index" width="60" label="序号" />
<el-table-column prop="title" label="知识点标题" min-width="200" />
<el-table-column prop="content" label="内容摘要" min-width="300">
<template #default="scope">
<div class="content-summary">{{ scope.row.content }}</div>
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="100">
<template #default="scope">
<el-tag size="small">{{ scope.row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="source" label="来源" width="150">
<template #default="scope">
{{ scope.row.source }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button link type="primary" size="small" @click="editKnowledge(scope.row)">
编辑
</el-button>
<el-button link type="danger" size="small" @click="deleteKnowledge(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="knowledgeDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="addKnowledge">
<el-icon class="el-icon--left"><Plus /></el-icon>
添加知识点
</el-button>
</template>
</el-dialog>
<!-- 资料管理弹窗 -->
<el-dialog
v-model="materialDialogVisible"
:title="`${currentCourse?.name} - 资料管理`"
width="80%"
>
<div class="material-management">
<!-- 上传区域 -->
<el-upload
class="upload-area"
drag
multiple
:auto-upload="false"
:file-list="fileList"
@change="handleFileChange"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持格式PDF、Word、PPT、TXT、MD单个文件不超过50MB
</div>
</template>
</el-upload>
<!-- 已上传资料列表 -->
<div class="material-list">
<h4>已上传资料</h4>
<el-table :data="materialList" style="width: 100%">
<el-table-column prop="name" label="文件名" min-width="200">
<template #default="scope">
<div class="file-info">
<el-icon><Document /></el-icon>
<span>{{ scope.row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="size" label="大小" width="100">
<template #default="scope">
{{ formatFileSize(scope.row.size) }}
</template>
</el-table-column>
<el-table-column prop="uploadTime" label="上传时间" width="180" />
<el-table-column prop="status" label="处理状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.status === 'completed' ? 'success' : ''">
{{ scope.row.status === 'completed' ? '已处理' : '处理中' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button link type="primary" size="small" @click="downloadMaterial(scope.row)">
下载
</el-button>
<el-button link type="danger" size="small" @click="deleteMaterial(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer>
<el-button @click="materialDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="uploadFiles" :disabled="fileList.length === 0">
开始上传
</el-button>
</template>
</el-dialog>
<!-- 编辑知识点弹窗 -->
<el-dialog
v-model="knowledgeEditDialogVisible"
:title="isEditKnowledge ? '编辑知识点' : '添加知识点'"
width="700px"
>
<el-form :model="knowledgeForm" label-width="100px">
<el-form-item label="知识点标题" prop="title">
<el-input v-model="knowledgeForm.title" placeholder="请输入知识点标题" />
</el-form-item>
<el-form-item label="知识点内容" prop="content">
<el-input
v-model="knowledgeForm.content"
type="textarea"
placeholder="请输入知识点内容"
:rows="8"
/>
</el-form-item>
<el-form-item label="知识点类型" prop="type">
<el-select v-model="knowledgeForm.type" placeholder="请选择类型">
<el-option label="概念定义" value="concept" />
<el-option label="操作步骤" value="procedure" />
<el-option label="案例分析" value="case" />
<el-option label="注意事项" value="notice" />
<el-option label="技巧方法" value="skill" />
</el-select>
</el-form-item>
<el-form-item label="关联资料" prop="source">
<el-select v-model="knowledgeForm.source" placeholder="请选择关联资料">
<el-option
v-for="material in materialList"
:key="material.id"
:label="material.name"
:value="material.name"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="knowledgeEditDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveKnowledge">保存</el-button>
</template>
</el-dialog>
<!-- 课程详情弹窗 -->
<el-dialog
v-model="courseDetailDialogVisible"
:title="`${currentCourse?.name} - 课程详情`"
width="1100px"
top="5vh"
class="course-detail-dialog"
:close-on-click-modal="false"
:destroy-on-close="true"
:append-to-body="true"
>
<el-tabs v-model="activeTab">
<el-tab-pane label="基本信息" name="basic">
<div class="course-basic-info">
<div class="info-header">
<div class="course-title-section">
<h2>{{ currentCourse?.name }}</h2>
<div class="course-tags">
<el-tag type="primary" size="large" effect="dark">{{ getCategoryText(currentCourse?.category) }}</el-tag>
<el-tag :type="getStatusTagType(currentCourse?.status)" size="large" effect="dark">
{{ getStatusText(currentCourse?.status) }}
</el-tag>
</div>
<div class="course-meta-info">
<span class="meta-item">
<el-icon><User /></el-icon>
创建者:{{ currentCourse?.creator || '系统管理员' }}
</span>
<span class="meta-item">
<el-icon><Calendar /></el-icon>
创建时间:{{ currentCourse?.createTime }}
</span>
</div>
</div>
</div>
<div class="info-content">
<div class="description-section">
<div class="section-header">
<el-icon :size="20" color="#409eff"><EditPen /></el-icon>
<h4>课程描述</h4>
</div>
<div class="description-text">{{ currentCourse?.description || '暂无描述' }}</div>
</div>
<div class="stats-section">
<div class="stat-item">
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<el-icon :size="28" color="#fff"><Files /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ currentCourse?.materialCount || 0 }}</div>
<div class="stat-label">学习资料</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon" style="background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);">
<el-icon :size="28" color="#fff"><List /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ currentCourse?.knowledgeCount || 0 }}</div>
<div class="stat-label">知识点</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
<el-icon :size="28" color="#fff"><Clock /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ currentCourse?.updateTime || '-' }}</div>
<div class="stat-label">更新时间</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon" style="background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);">
<el-icon :size="28" color="#fff"><Trophy /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ currentCourse?.students || 0 }}</div>
<div class="stat-label">学习人数</div>
</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
<!-- 考试设置功能暂时隐藏 -->
<!-- <el-tab-pane label="考试设置" name="exam">
<div class="exam-tab-content">
<el-form :model="examSettings" label-width="120px">
<el-form-item label="考试题数">
<el-input-number
v-model="examSettings.questionCount"
:min="5"
:max="50"
:step="5"
/>
<span style="margin-left: 10px">题</span>
</el-form-item>
<el-form-item label="最大轮次">
<el-input-number
v-model="examSettings.maxRounds"
:min="1"
:max="10"
:step="1"
/>
<span style="margin-left: 10px">轮</span>
</el-form-item>
<el-form-item label="及格分数">
<el-input-number
v-model="examSettings.passingScore"
:min="50"
:max="100"
:step="5"
/>
<span style="margin-left: 10px">分</span>
</el-form-item>
<el-form-item label="考试时长">
<el-input-number
v-model="examSettings.examDuration"
:min="10"
:max="180"
:step="10"
/>
<span style="margin-left: 10px">分钟</span>
</el-form-item>
<el-form-item label="题型分布">
<div class="question-type-distribution">
<div class="type-item">
<span>单选题:</span>
<el-input-number
v-model="examSettings.questionTypes.single"
:min="0"
:max="20"
size="small"
/>
<span> %</span>
</div>
<div class="type-item">
<span>多选题:</span>
<el-input-number
v-model="examSettings.questionTypes.multiple"
:min="0"
:max="20"
size="small"
/>
<span> %</span>
</div>
<div class="type-item">
<span>判断题:</span>
<el-input-number
v-model="examSettings.questionTypes.judge"
:min="0"
:max="20"
size="small"
/>
<span> %</span>
</div>
<div class="type-item">
<span>填空题:</span>
<el-input-number
v-model="examSettings.questionTypes.blank"
:min="0"
:max="20"
size="small"
/>
<span> %</span>
</div>
</div>
<div class="type-total" :class="{ error: totalPercentage !== 100 }">
总计:{{ totalPercentage }}%
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveExamSettings">保存考试设置</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane> -->
<!-- 学习资料与知识点功能暂时隐藏 -->
<!-- <el-tab-pane label="学习资料与知识点" name="materials">
<div class="materials-section">
<div class="section-header">
<h3>学习资料与知识点管理</h3>
<div class="header-actions">
<el-button type="primary" size="small" @click="uploadMaterial">
<el-icon class="el-icon--left"><Upload /></el-icon>
上传资料
</el-button>
<el-button size="small" @click="addKnowledgeManual">
<el-icon class="el-icon--left"><Plus /></el-icon>
手动添加知识点
</el-button>
</div>
</div>
<el-table :data="currentCourseMaterials" style="width: 100%" v-if="currentCourseMaterials?.length > 0" row-class-name="material-row">
<el-table-column type="expand">
<template #default="scope">
<div class="material-knowledge-points">
<div class="kp-stats">
<h4>关联知识点 ({{ scope.row.knowledgePoints?.length || 0 }}个)</h4>
<el-tag v-if="scope.row.status === 'completed'" type="success" size="small">
<el-icon><CircleCheck /></el-icon> AI已分析
</el-tag>
<el-tag v-else-if="scope.row.status === 'analyzing'" type="warning" size="small">
<el-icon class="is-loading"><Loading /></el-icon> AI分析中...
</el-tag>
<el-tag v-else type="info" size="small">
<el-icon><Warning /></el-icon> 待分析
</el-tag>
</div>
<div class="knowledge-points-grid">
<div class="kp-grid-container">
<div
v-for="(kp, index) in scope.row.knowledgePoints"
:key="kp.id"
class="knowledge-point-card"
@click="viewKnowledgeDetail(kp)"
>
<div class="kp-card-header">
<span class="kp-number">{{ index + 1 }}</span>
<el-tag size="small" :type="getKnowledgeTypeTag(kp.type)">{{ kp.type || '概念' }}</el-tag>
</div>
<h5 class="kp-title">{{ kp.title }}</h5>
<p class="kp-summary">{{ kp.content }}</p>
<div class="kp-card-actions">
<el-button
link
type="primary"
size="small"
@click.stop="editKnowledgePoint(scope.row, kp)"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
link
type="danger"
size="small"
@click.stop="deleteKnowledgePoint(scope.row, kp)"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<!-- 添加知识点按钮 -->
<div
v-if="(scope.row.knowledgePoints?.length || 0) < 20"
class="knowledge-point-card add-card"
@click="addKnowledgePointToMaterial(scope.row)"
>
<el-icon :size="18" class="add-icon"><Plus /></el-icon>
<span class="add-text">添加知识点</span>
</div>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="文件名" min-width="200">
<template #default="scope">
<div class="file-info">
<el-icon><Document /></el-icon>
<span>{{ scope.row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="size" label="大小" width="100">
<template #default="scope">
{{ formatFileSize(scope.row.size) }}
</template>
</el-table-column>
<el-table-column prop="knowledgePoints" label="知识点" width="100">
<template #default="scope">
<el-tag>{{ scope.row.knowledgePoints?.length || 0 }} 个</el-tag>
</template>
</el-table-column>
<el-table-column prop="uploadTime" label="上传时间" width="180" />
<el-table-column label="AI分析" width="120">
<template #default="scope">
<el-button
v-if="scope.row.status === 'pending'"
type="primary"
link
size="small"
@click="analyzeWithAI(scope.row)"
>
<el-icon><MagicStick /></el-icon>
AI拆解
</el-button>
<el-button
v-else-if="scope.row.status === 'completed'"
type="success"
link
size="small"
@click="reAnalyzeWithAI(scope.row)"
>
<el-icon><Refresh /></el-icon>
重新分析
</el-button>
<el-text v-else type="warning" size="small">
<el-icon class="is-loading"><Loading /></el-icon>
分析中...
</el-text>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button link type="primary" size="small" @click="downloadMaterial(scope.row)">
下载
</el-button>
<el-button link type="danger" size="small" @click="deleteMaterial(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无学习资料" />
</div>
</el-tab-pane> -->
<!-- 岗位分配 -->
<el-tab-pane label="岗位分配" name="positions">
<div class="position-assignment-content">
<div class="assignment-stats">
<div class="stat-item required">
<div class="stat-value">{{ courseRequiredPositions.length }}</div>
<div class="stat-label">必修岗位</div>
</div>
<div class="stat-item optional">
<div class="stat-value">{{ courseOptionalPositions.length }}</div>
<div class="stat-label">选修岗位</div>
</div>
<div class="stat-item total">
<div class="stat-value">{{ (courseRequiredPositions.length + courseOptionalPositions.length) }}</div>
<div class="stat-label">已分配岗位</div>
</div>
</div>
<el-tabs v-model="positionTabActive" type="border-card">
<!-- 必修岗位 -->
<el-tab-pane label="必修岗位" name="required">
<div class="position-section">
<div class="section-header">
<h4>必修岗位列表</h4>
<el-button type="danger" size="small" @click="showPositionSelector('required')">
<el-icon><Plus /></el-icon>
添加必修岗位
</el-button>
</div>
<div class="position-cards">
<div v-for="position in courseRequiredPositions" :key="position.id" class="position-card">
<div class="card-header">
<div class="position-name">{{ position.name }}</div>
<el-button link type="danger" size="small" @click="removePositionAssignment(position.id, 'required')">
<el-icon><Close /></el-icon>
</el-button>
</div>
<div class="card-content">
<div class="position-desc">{{ position.description }}</div>
<div class="position-meta">
<span><el-icon><User /></el-icon> {{ position.memberCount }} 人</span>
<span><el-icon><Star /></el-icon> 优先级: {{ position.priority || '中' }}</span>
</div>
</div>
</div>
</div>
<el-empty v-if="courseRequiredPositions.length === 0" description="该课程暂无必修岗位" :image-size="80" />
</div>
</el-tab-pane>
<!-- 选修岗位 -->
<el-tab-pane label="选修岗位" name="optional">
<div class="position-section">
<div class="section-header">
<h4>选修岗位列表</h4>
<el-button type="warning" size="small" @click="showPositionSelector('optional')">
<el-icon><Plus /></el-icon>
添加选修岗位
</el-button>
</div>
<div class="position-cards">
<div v-for="position in courseOptionalPositions" :key="position.id" class="position-card">
<div class="card-header">
<div class="position-name">{{ position.name }}</div>
<el-button link type="danger" size="small" @click="removePositionAssignment(position.id, 'optional')">
<el-icon><Close /></el-icon>
</el-button>
</div>
<div class="card-content">
<div class="position-desc">{{ position.description }}</div>
<div class="position-meta">
<span><el-icon><User /></el-icon> {{ position.memberCount }} 人</span>
<span><el-icon><Star /></el-icon> 推荐级别: {{ position.recommendLevel || 3 }} 星</span>
</div>
</div>
</div>
</div>
<el-empty v-if="courseOptionalPositions.length === 0" description="该课程暂无选修岗位" :image-size="80" />
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="courseDetailDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="editCourse(currentCourse)">
<el-icon class="el-icon--left"><Edit /></el-icon>
编辑课程
</el-button>
</template>
</el-dialog>
<!-- 岗位选择器弹窗 -->
<el-dialog
v-model="positionSelectorVisible"
:title="`选择${assignmentType === 'required' ? '必修' : '选修'}岗位`"
width="700px"
>
<div class="position-selector-content">
<div class="selector-header">
<el-input
v-model="positionSearchText"
placeholder="搜索岗位名称"
style="width: 300px"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div class="available-positions">
<div
v-for="position in filteredAvailablePositions"
:key="position.id"
class="position-option"
:class="{ selected: selectedPositions.includes(position.id) }"
@click="togglePositionSelection(position.id)"
>
<div class="position-info">
<div class="position-name">{{ position.name }}</div>
<div class="position-desc">{{ position.description }}</div>
<div class="position-meta">
<span><el-icon><User /></el-icon> {{ position.memberCount }} 人</span>
<span><el-icon><OfficeBuilding /></el-icon> {{ position.parentName || '顶级部门' }}</span>
</div>
</div>
<div class="selection-indicator">
<el-icon v-if="selectedPositions.includes(position.id)"><Check /></el-icon>
</div>
</div>
</div>
<div class="selector-footer">
<div class="selected-info">
已选择 {{ selectedPositions.length }} 个岗位
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelPositionSelection">取消</el-button>
<el-button type="primary" @click="confirmPositionSelection" :disabled="selectedPositions.length === 0">
确认选择
</el-button>
</div>
</template>
</el-dialog>
<!-- 知识点详情查看弹窗 -->
<el-dialog
v-model="knowledgeDetailDialogVisible"
title="知识点详情"
width="800px"
class="knowledge-detail-dialog"
>
<div class="knowledge-detail" v-if="currentKnowledge">
<div class="detail-header">
<div class="title-section">
<h3>{{ currentKnowledge.title }}</h3>
<el-tag :type="getKnowledgeTypeTag(currentKnowledge.type)" size="large">
{{ getTypeLabel(currentKnowledge.type) }}
</el-tag>
</div>
</div>
<div class="detail-content">
<div class="content-section">
<h4>内容描述</h4>
<div class="content-text">{{ currentKnowledge.content }}</div>
</div>
</div>
<div class="detail-footer">
<div class="footer-meta">
<span v-if="currentKnowledge.source">
<el-icon><Document /></el-icon>
来源:{{ currentKnowledge.source }}
</span>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="knowledgeDetailDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="editFromDetail">
<el-icon><Edit /></el-icon>
编辑
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus,
Search,
Grid,
List,
Files,
User,
Calendar,
EditPen,
Clock,
Trophy,
Upload,
CircleCheck,
Loading,
Warning,
Edit,
Delete,
Document,
MagicStick,
Refresh,
Close,
Star,
OfficeBuilding,
Check,
Collection
} from '@element-plus/icons-vue'
const router = useRouter()
// 图标组件映射
const iconComponents = {
Plus,
Search,
Grid,
List,
Files,
User,
Calendar,
EditPen,
Clock,
Trophy,
Upload,
CircleCheck,
Loading,
Warning,
Edit,
Delete,
Document,
MagicStick,
Refresh,
Close,
Star,
OfficeBuilding,
Check,
Collection
}
// 搜索筛选
const filterForm = reactive({
keyword: '',
category: '',
status: ''
})
// 分页
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(8)
const loading = ref(false)
// 视图模式
const viewMode = ref('card')
// 弹窗相关
const knowledgeDialogVisible = ref(false)
const materialDialogVisible = ref(false)
const knowledgeEditDialogVisible = ref(false)
const knowledgeDetailDialogVisible = ref(false)
const courseDetailDialogVisible = ref(false)
const currentCourse = ref<any>(null)
const currentCourseKnowledgePoints = ref<any[]>([])
const currentMaterial = ref<any>(null)
const currentKnowledge = ref<any>(null)
const isEditKnowledge = ref(false)
// 岗位分配相关
const positionTabActive = ref('required')
const positionSelectorVisible = ref(false)
const positionSearchText = ref('')
const assignmentType = ref<'required' | 'optional'>('required')
const selectedPositions = ref<number[]>([])
const courseRequiredPositions = ref<any[]>([])
const courseOptionalPositions = ref<any[]>([])
const availablePositions = ref<any[]>([])
const activeTab = ref('basic')
// 文件列表
const fileList = ref<any[]>([])
// 知识点表单
const knowledgeForm = reactive({
id: '',
title: '',
content: '',
type: 'concept',
source: ''
})
// 考试设置
const examSettings = reactive({
questionCount: 10,
maxRounds: 3,
passingScore: 60,
examDuration: 60,
questionTypes: {
single: 40,
multiple: 20,
judge: 20,
blank: 20
}
})
// 当前课程的资料列表
const currentCourseMaterials = ref<any[]>([])
// 课程统计
const courseStats = ref([
{
label: '课程总数',
value: '12',
icon: 'Collection',
color: '#667eea',
bgColor: 'rgba(102, 126, 234, 0.1)'
},
{
label: '资料总数',
value: '156',
icon: 'Document',
color: '#67c23a',
bgColor: 'rgba(103, 194, 58, 0.1)'
},
{
label: '知识点总数',
value: '823',
icon: 'List',
color: '#e6a23c',
bgColor: 'rgba(230, 162, 60, 0.1)'
},
{
label: 'AI处理中',
value: '3',
icon: 'Loading',
color: '#f56c6c',
bgColor: 'rgba(245, 108, 108, 0.1)'
}
])
// 课程列表数据
const courseList = ref([
{
id: 1,
name: '皮肤管理基础知识',
description: '学习皮肤的基本结构、类型分析和日常护理知识',
category: 'basic',
cover: '',
materialCount: 12,
knowledgeCount: 68,
status: 'published',
updateTime: '2024-03-20 14:30'
},
{
id: 2,
name: '客户沟通与咨询技巧',
description: '掌握与客户沟通的技巧,了解客户需求并提供专业建议',
category: 'basic',
cover: '',
materialCount: 8,
knowledgeCount: 45,
status: 'published',
updateTime: '2024-03-19 10:20'
},
{
id: 3,
name: '美容产品知识大全',
description: '全面了解美容产品的成分、功效和适用人群',
category: 'product',
cover: '',
materialCount: 15,
knowledgeCount: 120,
status: 'published',
updateTime: '2024-03-18 16:45'
},
{
id: 4,
name: '轻医美项目介绍与咨询',
description: '学习各种轻医美项目的特点、效果和注意事项',
category: 'advanced',
cover: '',
materialCount: 6,
knowledgeCount: 38,
status: 'processing',
updateTime: '2024-03-20 09:15'
}
])
// 知识点列表
const knowledgeList = ref([
{
id: 1,
title: '建立信任关系的重要性',
content: '在销售过程中,建立信任是成功的第一步。客户只有信任你,才会愿意听你介绍产品...',
type: '概念定义',
source: '销售技巧基础.pdf'
},
{
id: 2,
title: '有效倾听的技巧',
content: '倾听不仅是听客户说什么,更要理解客户的真实需求和顾虑。有效倾听包括:保持眼神接触、适时点头、复述确认等...',
type: '技巧方法',
source: '客户沟通技巧.docx'
},
{
id: 3,
title: 'SPIN提问法',
content: 'SPIN是Situation(情况)、Problem(问题)、Implication(影响)、Need-payoff(需求回报)的缩写...',
type: '技巧方法',
source: '需求挖掘方法.pptx'
}
])
// 计算题型总百分比
const totalPercentage = computed(() => {
return Object.values(examSettings.questionTypes).reduce((sum, val) => sum + val, 0)
})
// 资料列表
const materialList = ref([
{
id: 1,
name: '销售技巧基础.pdf',
size: 2457600,
uploadTime: '2024-03-15 10:30',
status: 'completed',
knowledgePoints: [
{ id: 'kp1', title: '建立信任关系的重要性', content: '在销售过程中,建立信任是成功的第一步。客户只有信任你,才会愿意听你介绍产品...', type: 'concept', editing: false },
{ id: 'kp2', title: '有效倾听技巧', content: '倾听是销售中最重要的技能之一。通过倾听,你可以了解客户的真实需求...', type: 'skill', editing: false },
{ id: 'kp3', title: 'SPIN销售法则', content: 'SPIN代表情况、问题、暗示和需求回报四个方面是一种有效的销售提问技巧...', type: 'procedure', editing: false },
{ id: 'kp4', title: '处理客户异议的方法', content: '当客户提出异议时,不要急于反驳,而是要理解异议背后的真实原因...', type: 'skill', editing: false },
{ id: 'kp5', title: '成交技巧与时机把握', content: '识别客户的购买信号,选择合适的成交时机,使用适当的成交技巧...', type: 'skill', editing: false }
]
},
{
id: 2,
name: '客户沟通技巧.docx',
size: 1843200,
uploadTime: '2024-03-15 11:20',
status: 'completed',
knowledgePoints: [
{ id: 'kp6', title: '非语言沟通的重要性', content: '研究表明55%的沟通是通过肢体语言进行的38%通过语调只有7%是语言本身...', type: 'concept', editing: false },
{ id: 'kp7', title: '同理心在沟通中的应用', content: '站在客户的角度思考问题,理解他们的感受和需求,是建立良好关系的关键...', type: 'skill', editing: false },
{ id: 'kp8', title: '积极聆听的技巧', content: '积极聆听包括眼神接触、点头认同、适时提问和总结反馈等技巧...', type: 'skill', editing: false },
{ id: 'kp9', title: '清晰表达的要点', content: '使用简洁明了的语言,避免专业术语,确保信息准确传达...', type: 'procedure', editing: false }
]
},
{
id: 3,
name: '需求挖掘方法.pptx',
size: 5242880,
uploadTime: '2024-03-16 14:00',
status: 'pending',
knowledgePoints: []
}
])
/**
* 搜索处理
*/
const handleSearch = () => {
loading.value = true
setTimeout(() => {
loading.value = false
ElMessage.success('搜索完成')
}, 1000)
}
/**
* 重置搜索
*/
const handleReset = () => {
filterForm.keyword = ''
filterForm.category = ''
filterForm.status = ''
handleSearch()
}
/**
* 创建课程
*/
const createCourse = () => {
router.push('/manager/create-course')
}
/**
* 查看知识点
*/
const viewKnowledge = (course: any) => {
currentCourse.value = course
knowledgeDialogVisible.value = true
}
/**
* 管理资料
*/
const manageMaterial = (course: any) => {
currentCourse.value = course
materialDialogVisible.value = true
}
/**
* 编辑课程
*/
const editCourse = (course: any) => {
router.push(`/manager/edit-course/${course.id}`)
}
/**
* 管理课程岗位分配
*/
const manageCoursePositions = (course: any) => {
currentCourse.value = course
activeTab.value = 'positions'
// 加载课程的岗位分配
loadCoursePositions(course.id)
courseDetailDialogVisible.value = true
}
/**
* 删除课程
*/
const deleteCourse = (course: any) => {
ElMessageBox.confirm(
`确定要删除课程"${course.name}"吗?删除后相关的资料和知识点也会被删除。`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
ElMessage.success('删除成功')
}).catch(() => {})
}
/**
* 添加知识点
*/
const addKnowledge = () => {
isEditKnowledge.value = false
knowledgeForm.id = ''
knowledgeForm.title = ''
knowledgeForm.content = ''
knowledgeForm.type = 'concept'
knowledgeForm.source = ''
knowledgeEditDialogVisible.value = true
}
/**
* 编辑知识点
*/
const editKnowledge = (knowledge: any) => {
isEditKnowledge.value = true
knowledgeForm.id = knowledge.id
knowledgeForm.title = knowledge.title
knowledgeForm.content = knowledge.content
knowledgeForm.type = knowledge.type
knowledgeForm.source = knowledge.source
knowledgeEditDialogVisible.value = true
}
/**
* 删除知识点
*/
const deleteKnowledge = (knowledge: any) => {
ElMessageBox.confirm(
`确定要删除知识点"${knowledge.title}"吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
ElMessage.success('删除成功')
}).catch(() => {})
}
/**
* 保存知识点
*/
const saveKnowledge = () => {
if (!knowledgeForm.title || !knowledgeForm.content) {
ElMessage.warning('请填写完整信息')
return
}
if (isEditKnowledge.value) {
// 编辑模式:更新预览列表中的知识点
const index = currentCourseKnowledgePoints.value.findIndex(kp => kp.id === knowledgeForm.id)
if (index > -1) {
currentCourseKnowledgePoints.value[index] = {
...currentCourseKnowledgePoints.value[index],
title: knowledgeForm.title,
content: knowledgeForm.content,
type: knowledgeForm.type,
source: knowledgeForm.source
}
}
// 同时更新资料中的知识点
currentCourseMaterials.value.forEach(material => {
if (material.knowledgePoints) {
const materialIndex = material.knowledgePoints.findIndex((kp: any) => kp.id === knowledgeForm.id)
if (materialIndex > -1) {
material.knowledgePoints[materialIndex] = {
...material.knowledgePoints[materialIndex],
title: knowledgeForm.title,
content: knowledgeForm.content,
type: knowledgeForm.type
}
}
}
})
} else {
// 添加模式:创建新知识点
const newKnowledge = {
id: 'kp_' + Date.now(),
title: knowledgeForm.title,
content: knowledgeForm.content,
type: knowledgeForm.type,
source: knowledgeForm.source,
editing: false
}
// 添加到预览列表
currentCourseKnowledgePoints.value.push(newKnowledge)
// 如果有关联资料,也添加到对应的资料中
if (knowledgeForm.source) {
const material = materialList.value.find(m => m.name === knowledgeForm.source)
if (material) {
if (!material.knowledgePoints) {
material.knowledgePoints = []
}
material.knowledgePoints.push(newKnowledge)
}
}
}
ElMessage.success(isEditKnowledge.value ? '编辑成功' : '添加成功')
knowledgeEditDialogVisible.value = false
// 重置表单
knowledgeForm.id = ''
knowledgeForm.title = ''
knowledgeForm.content = ''
knowledgeForm.type = 'concept'
knowledgeForm.source = ''
}
/**
* 文件改变处理
*/
const handleFileChange = (file: any, fileList: any[]) => {
// 处理文件变化
}
/**
* 上传文件
*/
const uploadFiles = () => {
if (fileList.value.length === 0) {
ElMessage.warning('请选择要上传的文件')
return
}
ElMessage.success('开始上传文件AI将自动分析并提取知识点')
materialDialogVisible.value = false
}
/**
* 下载资料
*/
const downloadMaterial = (material: any) => {
ElMessage.success(`开始下载:${material.name}`)
}
/**
* 上传资料
*/
const uploadMaterial = () => {
if (!currentCourse.value) {
ElMessage.warning('请先选择课程')
return
}
fileList.value = []
materialDialogVisible.value = true
}
/**
* 手动添加知识点
*/
const addKnowledgeManual = () => {
currentMaterial.value = null
isEditKnowledge.value = false
knowledgeForm.id = ''
knowledgeForm.title = ''
knowledgeForm.content = ''
knowledgeForm.type = 'concept'
knowledgeForm.source = ''
knowledgeEditDialogVisible.value = true
}
/**
* 删除资料
*/
const deleteMaterial = (material: any) => {
ElMessageBox.confirm(
`确定要删除资料"${material.name}"吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
ElMessage.success('删除成功')
}).catch(() => {})
}
/**
* 查看知识点详情
*/
const viewKnowledgeDetail = (kp: any) => {
currentKnowledge.value = kp
knowledgeDetailDialogVisible.value = true
}
/**
* AI分析资料
*/
const analyzeWithAI = (material: any) => {
material.status = 'analyzing'
ElMessage.info('AI正在分析文件内容提取知识点...')
// 模拟AI分析过程
setTimeout(() => {
material.status = 'completed'
material.knowledgePoints = [
{
id: 'kp_auto_' + Date.now() + '_1',
title: '核心概念1',
content: '这是AI从文档中提取的第一个核心概念...',
type: 'concept',
source: material.name
},
{
id: 'kp_auto_' + Date.now() + '_2',
title: '操作步骤1',
content: '这是AI从文档中提取的操作步骤...',
type: 'procedure',
source: material.name
},
{
id: 'kp_auto_' + Date.now() + '_3',
title: '案例分析1',
content: '这是AI从文档中提取的案例分析...',
type: 'case',
source: material.name
}
]
ElMessage.success('AI分析完成已提取知识点')
}, 3000)
}
/**
* 重新AI分析
*/
const reAnalyzeWithAI = (material: any) => {
ElMessageBox.confirm(
'重新分析将覆盖现有的AI提取结果是否继续',
'重新分析确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
analyzeWithAI(material)
}).catch(() => {})
}
/**
* 从详情页编辑
*/
const editFromDetail = () => {
const kp = currentKnowledge.value
knowledgeDetailDialogVisible.value = false
// 找到对应的资料
let material = null
for (const m of currentCourseMaterials.value) {
if (m.knowledgePoints) {
const found = m.knowledgePoints.find((k: any) => k.id === kp.id)
if (found) {
material = m
break
}
}
}
if (material) {
currentMaterial.value = material
isEditKnowledge.value = true
knowledgeForm.id = kp.id
knowledgeForm.title = kp.title
knowledgeForm.content = kp.content
knowledgeForm.type = kp.type || 'concept'
knowledgeForm.source = kp.source || material.name
knowledgeEditDialogVisible.value = true
} else {
ElMessage.warning('未找到知识点所属的资料')
}
}
// 注释掉重复的函数定义,使用后面更完整的版本
// editKnowledgePoint, deleteKnowledgePoint, addKnowledgePointToMaterial 在第1490行后有更完整的定义
/**
* 格式化文件大小
*/
const formatFileSize = (size: number) => {
if (size < 1024) {
return size + ' B'
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + ' KB'
} else if (size < 1024 * 1024 * 1024) {
return (size / 1024 / 1024).toFixed(2) + ' MB'
} else {
return (size / 1024 / 1024 / 1024).toFixed(2) + ' GB'
}
}
/**
* 获取分类文本
*/
const getCategoryText = (category: string) => {
const categoryMap: Record<string, string> = {
basic: '基础课程',
advanced: '进阶课程',
product: '产品培训',
management: '管理课程',
senior: '高级课程'
}
return categoryMap[category] || ''
}
/**
* 获取状态文本
*/
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
published: '已发布',
draft: '草稿',
processing: 'AI处理中'
}
return statusMap[status] || ''
}
/**
* 获取状态标签类型
*/
const getStatusTagType = (status: string) => {
const statusMap: Record<string, string> = {
published: 'success',
draft: 'info',
processing: 'warning'
}
return statusMap[status] || ''
}
/**
* 分页大小改变
*/
const handleSizeChange = (val: number) => {
pageSize.value = val
handleSearch()
}
/**
* 当前页改变
*/
const handleCurrentChange = (val: number) => {
currentPage.value = val
handleSearch()
}
/**
* 查看课程详情
*/
const viewCourseDetail = (course: any) => {
currentCourse.value = course
activeTab.value = 'basic'
// 加载课程资料
currentCourseMaterials.value = materialList.value
// 加载课程的岗位分配
loadCoursePositions(course.id)
// 加载课程知识点(从所有资料中汇总)
const allKnowledgePoints: any[] = []
materialList.value.forEach(material => {
if (material.knowledgePoints && material.knowledgePoints.length > 0) {
material.knowledgePoints.forEach((kp: any) => {
allKnowledgePoints.push({
...kp,
source: material.name
})
})
}
})
currentCourseKnowledgePoints.value = allKnowledgePoints
// 加载考试设置(模拟数据)
examSettings.questionCount = 10
examSettings.maxRounds = 3
examSettings.passingScore = 60
examSettings.examDuration = 60
examSettings.questionTypes = {
single: 40,
multiple: 20,
judge: 20,
blank: 20
}
courseDetailDialogVisible.value = true
}
/**
* 保存考试设置
*/
const saveExamSettings = () => {
if (totalPercentage.value !== 100) {
ElMessage.warning('题型分布总和必须为100%')
return
}
ElMessage.success('考试设置保存成功')
}
/**
* 编辑文件关联的知识点
*/
const editKnowledgePoint = (material: any, kp: any) => {
currentMaterial.value = material
isEditKnowledge.value = true
knowledgeForm.id = kp.id
knowledgeForm.title = kp.title
knowledgeForm.content = kp.content
knowledgeForm.type = kp.type || 'concept'
knowledgeForm.source = kp.source || material.name
knowledgeEditDialogVisible.value = true
}
/**
* 保存知识点编辑
*/
const saveKnowledgePoint = (material: any, kp: any) => {
kp.editing = false
ElMessage.success('知识点已更新')
}
/**
* 删除文件关联的知识点
*/
const deleteKnowledgePoint = (material: any, kp: any) => {
ElMessageBox.confirm(
`确定要删除知识点"${kp.title}"吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
const index = material.knowledgePoints.findIndex((k: any) => k.id === kp.id)
if (index > -1) {
material.knowledgePoints.splice(index, 1)
}
ElMessage.success('删除成功')
}).catch(() => {})
}
/**
* AI分析知识点
*/
const analyzeKnowledgePoints = (material: any) => {
ElMessage.info('正在使用AI分析文件内容提取知识点...')
// 模拟AI分析过程
setTimeout(() => {
material.status = 'completed'
material.knowledgePoints = [
{ id: 'kp_auto_1', title: '自动提取的知识点1', content: 'AI分析得出的内容...', editing: false },
{ id: 'kp_auto_2', title: '自动提取的知识点2', content: 'AI分析得出的内容...', editing: false },
{ id: 'kp_auto_3', title: '自动提取的知识点3', content: 'AI分析得出的内容...', editing: false }
]
ElMessage.success('AI分析完成已提取3个知识点')
}, 2000)
}
/**
* 重新分析知识点
*/
const reAnalyzeKnowledgePoints = (material: any) => {
ElMessageBox.confirm(
'重新分析将覆盖现有的知识点,是否继续?',
'重新分析确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
analyzeKnowledgePoints(material)
}).catch(() => {})
}
/**
* 获取知识点类型标签样式
*/
const getKnowledgeTypeTag = (type: string) => {
const typeMap: Record<string, string> = {
'理论知识': 'primary', // 蓝色 - 基础理论
'诊断设计': 'success', // 绿色 - 专业设计
'操作步骤': 'warning', // 橙色 - 操作流程
'沟通话术': 'warning', // 橙色 - 沟通技巧(亮眼)
'案例分析': 'danger', // 红色 - 案例学习
'注意事项': '', // 默认色 - 重要提醒
'技巧方法': 'success', // 绿色 - 实用技巧
'客诉处理': 'danger', // 红色 - 紧急处理
// 兼容旧类型
'concept': 'primary',
'procedure': 'success',
'case': 'warning',
'notice': 'danger',
'skill': 'info'
}
return typeMap[type] || 'info'
}
/**
* 获取知识点类型标签文本
*/
const getTypeLabel = (type: string) => {
const typeMap: Record<string, string> = {
'理论知识': '理论知识',
'诊断设计': '诊断设计',
'操作步骤': '操作步骤',
'沟通话术': '沟通话术',
'案例分析': '案例分析',
'注意事项': '注意事项',
'技巧方法': '技巧方法',
'客诉处理': '客诉处理',
// 兼容旧类型
'concept': '概念定义',
'procedure': '操作步骤',
'case': '案例分析',
'notice': '注意事项',
'skill': '技巧方法'
}
return typeMap[type] || type
}
/**
* 根据文件类型获取图标颜色
*/
const getFileIconColor = (filename: string) => {
const ext = filename.split('.').pop()?.toLowerCase()
const colorMap: Record<string, string> = {
pdf: '#e74c3c',
doc: '#3498db',
docx: '#3498db',
xls: '#27ae60',
xlsx: '#27ae60',
ppt: '#e67e22',
pptx: '#e67e22',
txt: '#95a5a6',
mp4: '#9b59b6',
mp3: '#9b59b6',
jpg: '#1abc9c',
png: '#1abc9c',
jpeg: '#1abc9c'
}
return colorMap[ext || ''] || '#7f8c8d'
}
/**
* 加载课程的岗位分配
*/
const loadCoursePositions = async (courseId: number) => {
try {
// 模拟加载课程的岗位分配
courseRequiredPositions.value = [
{ id: 1, name: '总经理', description: '公司最高管理者', memberCount: 1, priority: '高' },
{ id: 2, name: '销售部', description: '负责产品销售与市场拓展', memberCount: 25, priority: '中' }
]
courseOptionalPositions.value = [
{ id: 3, name: '技术部', description: '负责技术研发与系统维护', memberCount: 30, recommendLevel: 4 },
{ id: 6, name: '前端开发', description: '负责前端应用开发', memberCount: 10, recommendLevel: 3 }
]
} catch (error) {
console.error('加载课程岗位失败:', error)
}
}
/**
* 加载可用岗位
*/
const loadAvailablePositions = async () => {
try {
// 模拟所有可用岗位
availablePositions.value = [
{ id: 1, name: '总经理', description: '公司最高管理者', memberCount: 1, parentName: null },
{ id: 2, name: '销售部', description: '负责产品销售与市场拓展', memberCount: 25, parentName: '总经理' },
{ id: 3, name: '技术部', description: '负责技术研发与系统维护', memberCount: 30, parentName: '总经理' },
{ id: 4, name: '销售经理', description: '管理销售团队,制定销售策略', memberCount: 3, parentName: '销售部' },
{ id: 5, name: '销售专员', description: '执行销售任务,维护客户关系', memberCount: 20, parentName: '销售部' },
{ id: 6, name: '前端开发', description: '负责前端应用开发', memberCount: 10, parentName: '技术部' },
{ id: 7, name: '后端开发', description: '负责后端服务开发', memberCount: 12, parentName: '技术部' },
{ id: 8, name: '测试工程师', description: '负责软件质量保证', memberCount: 8, parentName: '技术部' }
]
} catch (error) {
console.error('加载可用岗位失败:', error)
}
}
// 筛选后的可用岗位
const filteredAvailablePositions = computed(() => {
let filtered = availablePositions.value
// 排除已分配的岗位
const assignedIds = [...courseRequiredPositions.value.map(p => p.id), ...courseOptionalPositions.value.map(p => p.id)]
filtered = filtered.filter(p => !assignedIds.includes(p.id))
// 按关键词搜索
if (positionSearchText.value.trim()) {
const keyword = positionSearchText.value.toLowerCase()
filtered = filtered.filter(position =>
position.name.toLowerCase().includes(keyword) ||
position.description.toLowerCase().includes(keyword)
)
}
return filtered
})
/**
* 显示岗位选择器
*/
const showPositionSelector = (type: 'required' | 'optional') => {
assignmentType.value = type
selectedPositions.value = []
positionSearchText.value = ''
loadAvailablePositions()
positionSelectorVisible.value = true
}
/**
* 切换岗位选择
*/
const togglePositionSelection = (positionId: number) => {
const index = selectedPositions.value.indexOf(positionId)
if (index > -1) {
selectedPositions.value.splice(index, 1)
} else {
selectedPositions.value.push(positionId)
}
}
/**
* 取消岗位选择
*/
const cancelPositionSelection = () => {
positionSelectorVisible.value = false
selectedPositions.value = []
}
/**
* 确认岗位选择
*/
const confirmPositionSelection = () => {
const selectedPositionData = availablePositions.value.filter(p => selectedPositions.value.includes(p.id))
if (assignmentType.value === 'required') {
courseRequiredPositions.value.push(...selectedPositionData.map(p => ({ ...p, priority: '中' })))
ElMessage.success(`已添加 ${selectedPositionData.length} 个必修岗位`)
} else {
courseOptionalPositions.value.push(...selectedPositionData.map(p => ({ ...p, recommendLevel: 3 })))
ElMessage.success(`已添加 ${selectedPositionData.length} 个选修岗位`)
}
positionSelectorVisible.value = false
selectedPositions.value = []
}
/**
* 移除岗位分配
*/
const removePositionAssignment = (positionId: number, type: 'required' | 'optional') => {
if (type === 'required') {
const index = courseRequiredPositions.value.findIndex(p => p.id === positionId)
if (index > -1) {
const position = courseRequiredPositions.value[index]
courseRequiredPositions.value.splice(index, 1)
ElMessage.success(`已移除必修岗位「${position.name}」`)
}
} else {
const index = courseOptionalPositions.value.findIndex(p => p.id === positionId)
if (index > -1) {
const position = courseOptionalPositions.value[index]
courseOptionalPositions.value.splice(index, 1)
ElMessage.success(`已移除选修岗位「${position.name}」`)
}
}
}
</script>
<style lang="scss" scoped>
.course-management-container {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.page-title {
font-size: 24px;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
gap: 12px;
.el-button--primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
}
}
.filter-section {
margin-bottom: 20px;
padding: 20px;
.filter-form {
.el-form-item {
margin-bottom: 0;
}
}
}
.stats-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
.stat-card {
display: flex;
align-items: center;
gap: 16px;
padding: 24px;
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-content {
.stat-value {
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #666;
}
}
}
}
.course-list {
padding: 24px;
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #333;
}
}
.course-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
.course-card {
background: #fff;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.course-cover {
position: relative;
height: 160px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.course-status {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.published {
background: #67c23a;
color: white;
}
&.draft {
background: #909399;
color: white;
}
&.processing {
background: #e6a23c;
color: white;
}
}
}
.course-info {
padding: 20px;
.course-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.course-desc {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 16px;
height: 42px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.course-meta {
display: flex;
gap: 16px;
margin-bottom: 16px;
font-size: 13px;
color: #999;
span {
display: flex;
align-items: center;
gap: 4px;
}
}
.course-actions {
display: flex;
gap: 12px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
}
}
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 24px;
}
}
.knowledge-management {
.content-summary {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
color: #666;
font-size: 13px;
}
}
.material-management {
.upload-area {
margin-bottom: 32px;
}
.material-list {
h4 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
.el-icon {
color: #667eea;
}
}
}
}
// 课程详情弹窗样式
.question-type-distribution {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 12px;
.type-item {
display: flex;
align-items: center;
gap: 8px;
span {
font-size: 14px;
color: #666;
}
}
}
.type-total {
font-size: 14px;
color: #666;
padding: 8px 16px;
background: #f5f7fa;
border-radius: 4px;
display: inline-block;
&.error {
color: #f56c6c;
background: rgba(245, 108, 108, 0.1);
}
}
.knowledge-summary {
font-size: 14px;
color: #666;
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
text-align: center;
}
}
// 响应式
@media (max-width: 1200px) {
.course-management-container {
.stats-section {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media (max-width: 768px) {
.course-management-container {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
.header-actions {
width: 100%;
flex-wrap: wrap;
.el-button {
flex: 1;
}
}
}
.filter-form {
.el-form-item {
display: block;
margin-bottom: 16px !important;
}
}
.stats-section {
grid-template-columns: 1fr;
}
.course-grid {
grid-template-columns: 1fr !important;
}
}
// 资料知识点样式
.material-knowledge-points {
padding: 20px;
background: #f9fafb;
h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 500;
color: #333;
}
.knowledge-points-list {
display: flex;
flex-direction: column;
gap: 16px;
.knowledge-point-item {
background: white;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
.kp-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.kp-number {
font-weight: 600;
color: #409eff;
min-width: 24px;
}
.kp-title {
flex: 1;
font-weight: 500;
color: #333;
font-size: 15px;
}
.kp-actions {
display: flex;
gap: 8px;
}
}
.kp-content {
margin-left: 36px;
p {
margin: 0;
color: #666;
font-size: 14px;
line-height: 1.6;
}
}
}
}
}
// 知识点预览样式 - 优化为卡片网格
.knowledge-preview {
// 卡片网格布局
.knowledge-cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
max-height: 550px;
overflow-y: auto;
padding: 4px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
}
.knowledge-card {
background: white;
border: 1px solid #e4e7ed;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border-color: #409eff;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f2f5;
.card-number {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.el-tag {
height: 28px;
padding: 0 12px;
font-size: 13px;
}
}
.card-content {
margin-bottom: 12px;
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-summary {
font-size: 14px;
color: #666;
line-height: 1.5;
height: 42px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin: 0;
}
}
.card-footer {
padding-top: 12px;
border-top: 1px solid #f0f2f5;
.card-source {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #909399;
.el-icon {
color: #409eff;
}
}
}
}
.knowledge-empty {
padding: 60px 0;
text-align: center;
}
}
}
// 课程详情弹窗样式优化
:deep(.course-detail-dialog) {
.el-dialog__wrapper {
.el-dialog {
border-radius: 12px;
overflow: hidden;
}
}
.el-dialog__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 24px 30px;
margin: 0;
.el-dialog__title {
color: white;
font-size: 20px;
font-weight: 600;
}
.el-dialog__close {
color: white;
font-size: 20px;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
}
}
.el-dialog__body {
padding: 0;
margin: 0;
max-height: calc(85vh - 140px);
overflow-y: auto;
background: transparent;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
}
.el-dialog__footer {
padding: 20px 30px;
margin: 0;
background: #f8f9fa;
border-top: 1px solid #ebeef5;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
}
.el-tabs {
.el-tabs__header {
padding: 0 30px;
margin: 0;
background: #f8f9fa;
border-bottom: 2px solid #e4e7ed;
}
.el-tabs__content {
padding: 0;
background: white;
}
.el-tab-pane {
background: white;
}
}
}
// 课程基本信息样式
.course-basic-info {
padding: 30px;
background: white;
.info-header {
display: flex;
align-items: flex-start;
gap: 24px;
margin-bottom: 30px;
padding-bottom: 24px;
border-bottom: 2px solid #f0f2f5;
.course-cover-wrapper {
.course-cover-mini {
position: relative;
width: 120px;
height: 80px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
&:hover {
opacity: 1;
}
}
}
}
.course-title-section {
flex: 1;
h2 {
margin: 0 0 12px 0;
font-size: 24px;
font-weight: 700;
color: #333;
}
.course-tags {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
.course-meta-info {
display: flex;
gap: 24px;
font-size: 14px;
color: #666;
.meta-item {
display: flex;
align-items: center;
gap: 6px;
.el-icon {
color: #909399;
}
}
}
}
}
.info-content {
.description-section {
margin-bottom: 30px;
.section-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
h4 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
.description-text {
font-size: 15px;
line-height: 1.6;
color: #666;
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #409eff;
}
}
.stats-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
.stat-item {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(64, 158, 255, 0.1);
}
.stat-content {
.stat-value {
font-size: 24px;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
}
}
}
}
}
// 材料和知识点tab样式
.materials-tab-content,
.knowledge-tab-content,
.exam-tab-content {
padding: 30px;
background: white;
.tab-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e4e7ed;
.header-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
color: #333;
}
}
}
// 材料卡片样式
.material-cards {
.material-card {
background: white;
border: 1px solid #e4e7ed;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.material-card-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
.material-icon {
width: 60px;
height: 60px;
background: #f0f2f5;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.material-info {
flex: 1;
.material-name {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.material-meta {
display: flex;
gap: 16px;
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #666;
}
}
}
.material-status {
.el-tag {
padding: 8px 16px;
font-size: 14px;
.el-icon {
margin-right: 4px;
}
}
}
}
.material-card-body {
.knowledge-points-summary {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
.summary-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 14px;
color: #666;
}
.collapse-title {
font-size: 14px;
color: #409eff;
cursor: pointer;
&:hover {
color: #66b1ff;
}
}
.knowledge-points-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
margin-top: 16px;
.kp-card {
display: flex;
gap: 12px;
padding: 12px;
background: white;
border-radius: 8px;
border: 1px solid #e4e7ed;
.kp-index {
width: 24px;
height: 24px;
background: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.kp-content {
flex: 1;
h5 {
margin: 0 0 4px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
font-size: 13px;
color: #666;
line-height: 1.5;
}
}
}
}
}
}
}
}
// 封面上传弹窗样式
.cover-upload-content {
.current-cover {
margin-bottom: 24px;
h4 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.cover-preview {
width: 200px;
height: 150px;
border: 2px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.upload-section {
margin-bottom: 24px;
h4 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.cover-uploader {
width: 100%;
.upload-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
border: 2px dashed #d9d9d9;
border-radius: 8px;
background: #fafafa;
transition: all 0.3s;
&:hover {
border-color: #409eff;
background: #f5f7fa;
}
.el-icon--upload {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
}
.el-upload__text {
color: #606266;
font-size: 14px;
em {
color: #409eff;
font-style: normal;
}
}
.el-upload__tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
}
.new-cover-preview {
position: relative;
width: 100%;
height: 200px;
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
}
}
}
}
.cover-settings {
h4 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
}
}
// 课程封面管理样式
.course-cover {
.cover-actions {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.3s ease;
.el-button {
background: rgba(0, 0, 0, 0.7);
border: none;
color: white;
&:hover {
background: rgba(0, 0, 0, 0.9);
}
}
}
&:hover .cover-actions {
opacity: 1;
}
}
// 列表视图中的封面样式
.table-cover {
position: relative;
width: 60px;
height: 45px;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
color: white;
.el-icon {
font-size: 16px;
}
}
&:hover .cover-mask {
opacity: 1;
}
}
// 岗位分配样式
.position-assignment-content {
.assignment-stats {
display: flex;
gap: 16px;
margin-bottom: 24px;
.stat-item {
flex: 1;
text-align: center;
padding: 16px;
border-radius: 8px;
&.required {
background: rgba(245, 108, 108, 0.1);
.stat-value {
color: #f56c6c;
}
}
&.optional {
background: rgba(230, 162, 60, 0.1);
.stat-value {
color: #e6a23c;
}
}
&.total {
background: rgba(64, 158, 255, 0.1);
.stat-value {
color: #409eff;
}
}
.stat-value {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
}
}
.position-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h4 {
font-size: 14px;
font-weight: 600;
color: #333;
margin: 0;
}
}
.position-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
.position-card {
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fff;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.position-name {
font-weight: 600;
color: #333;
}
}
.card-content {
.position-desc {
font-size: 12px;
color: #666;
margin-bottom: 8px;
line-height: 1.4;
}
.position-meta {
display: flex;
gap: 12px;
font-size: 11px;
color: #999;
span {
display: flex;
align-items: center;
gap: 2px;
}
}
}
}
}
}
}
// 岗位选择器样式
.position-selector-content {
.selector-header {
margin-bottom: 20px;
}
.available-positions {
max-height: 400px;
overflow-y: auto;
border: 1px solid #e4e7ed;
border-radius: 8px;
.position-option {
display: flex;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #f5f7fa;
}
&.selected {
background: rgba(64, 158, 255, 0.1);
border-color: #409eff;
}
&:last-child {
border-bottom: none;
}
.position-info {
flex: 1;
.position-name {
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.position-desc {
font-size: 12px;
color: #666;
margin-bottom: 8px;
line-height: 1.4;
}
.position-meta {
display: flex;
gap: 16px;
font-size: 11px;
color: #999;
span {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.selection-indicator {
width: 24px;
height: 24px;
border: 2px solid #ddd;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
.el-icon {
color: #409eff;
font-size: 14px;
}
}
&.selected .selection-indicator {
background: #409eff;
border-color: #409eff;
.el-icon {
color: white;
}
}
}
}
.selector-footer {
margin-top: 16px;
text-align: center;
.selected-info {
font-size: 14px;
color: #666;
}
}
}
</style>