- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
3234 lines
88 KiB
Plaintext
3234 lines
88 KiB
Plaintext
<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>
|