|
@@ -0,0 +1,888 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <el-drawer
|
|
|
|
|
+ :visible.sync="visible"
|
|
|
|
|
+ direction="rtl"
|
|
|
|
|
+ size="720px"
|
|
|
|
|
+ append-to-body
|
|
|
|
|
+ :with-header="false"
|
|
|
|
|
+ :wrapper-closable="false"
|
|
|
|
|
+ custom-class="repeat-course-history-drawer"
|
|
|
|
|
+ @closed="handleClosed"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="rch-drawer" v-loading="loading">
|
|
|
|
|
+ <!-- 顶部客户信息区 -->
|
|
|
|
|
+ <div class="rch-hero">
|
|
|
|
|
+ <button type="button" class="rch-close" @click="visible = false">
|
|
|
|
|
+ <i class="el-icon-close"></i>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <div class="rch-hero-inner">
|
|
|
|
|
+ <div class="rch-avatar-wrap">
|
|
|
|
|
+ <img :src="currentRow.avatar || defaultAvatar" alt="" class="rch-avatar" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="rch-hero-info">
|
|
|
|
|
+ <div class="rch-name-row">
|
|
|
|
|
+ <h3 class="rch-name">{{ currentRow.name || '-' }}</h3>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p v-if="currentRow.remark" class="rch-remark">
|
|
|
|
|
+ <i class="el-icon-edit-outline"></i>{{ currentRow.remark }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="rch-stats">
|
|
|
|
|
+ <div class="rch-stat">
|
|
|
|
|
+ <span class="rch-stat-num">{{ projectList.length }}</span>
|
|
|
|
|
+ <span class="rch-stat-label">看课项目</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="rch-stat-divider"></div>
|
|
|
|
|
+ <div class="rch-stat">
|
|
|
|
|
+ <span class="rch-stat-num">{{ mockSales.length }}</span>
|
|
|
|
|
+ <span class="rch-stat-label">关联销售</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="rch-stat-divider"></div>
|
|
|
|
|
+ <div class="rch-stat">
|
|
|
|
|
+ <span class="rch-stat-num">{{ filteredCourses.length }}</span>
|
|
|
|
|
+ <span class="rch-stat-label">课程进度</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 可滚动内容区 -->
|
|
|
|
|
+ <div class="rch-body">
|
|
|
|
|
+ <!-- 看课项目 -->
|
|
|
|
|
+ <section class="rch-panel">
|
|
|
|
|
+ <div class="rch-panel-head">
|
|
|
|
|
+ <span class="rch-panel-icon rch-panel-icon--project"><i class="el-icon-folder-opened"></i></span>
|
|
|
|
|
+ <span class="rch-panel-title">看课项目</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="rch-project-chips" v-if="projectList.length">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="item in projectList"
|
|
|
|
|
+ :key="item.projectId"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="rch-chip"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'rch-chip--repeat': item.isRepeat == 1,
|
|
|
|
|
+ 'rch-chip--active': Number(selectedProjectId) === Number(item.projectId)
|
|
|
|
|
+ }"
|
|
|
|
|
+ @click="selectProject(item.projectId)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ item.projectName }}
|
|
|
|
|
+ <i v-if="item.isRepeat == 1" class="el-icon-warning-outline rch-chip-warn"></i>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="rch-empty">暂无看课项目</div>
|
|
|
|
|
+ <p v-if="showEquivalentHint" class="rch-hint">
|
|
|
|
|
+ <i class="el-icon-info"></i>百膳食养与安康食养为同一项目,课程数据已合并展示
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 关联销售 -->
|
|
|
|
|
+ <section class="rch-panel">
|
|
|
|
|
+ <div class="rch-panel-head">
|
|
|
|
|
+ <span class="rch-panel-icon rch-panel-icon--sales"><i class="el-icon-user"></i></span>
|
|
|
|
|
+ <span class="rch-panel-title">关联销售</span>
|
|
|
|
|
+ <span class="rch-panel-count">{{ mockSales.length }} 人</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="rch-sales-grid" v-if="mockSales.length">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(item, idx) in mockSales"
|
|
|
|
|
+ :key="idx"
|
|
|
|
|
+ class="rch-sales-item"
|
|
|
|
|
+ :class="{ 'rch-sales-item--locked': item.hasPermission === false }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="rch-sales-top">
|
|
|
|
|
+ <span class="rch-sales-name">{{ item.qwUserName }}</span>
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-if="item.status != null"
|
|
|
|
|
+ class="rch-tag"
|
|
|
|
|
+ :class="salesStatusClass(item.status)"
|
|
|
|
|
+ >{{ salesStatusLabel(item.status) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="rch-sales-meta">
|
|
|
|
|
+ <span><i class="el-icon-office-building"></i>{{ item.corpName || '-' }}</span>
|
|
|
|
|
+ <span><i class="el-icon-time"></i>{{ item.addTime || '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span v-if="item.hasPermission === false" class="rch-lock-tag">无权查看</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="rch-empty">暂无关联销售数据</div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 课程学习进度 -->
|
|
|
|
|
+ <section class="rch-panel">
|
|
|
|
|
+ <div class="rch-panel-head">
|
|
|
|
|
+ <span class="rch-panel-icon rch-panel-icon--course"><i class="el-icon-reading"></i></span>
|
|
|
|
|
+ <span class="rch-panel-title">课程学习进度</span>
|
|
|
|
|
+ <span v-if="selectedProjectId" class="rch-panel-count">{{ filteredCourses.length }} 门</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="rch-course-grid" v-if="filteredCourses.length">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(course, idx) in filteredCourses"
|
|
|
|
|
+ :key="course.courseId || idx"
|
|
|
|
|
+ class="rch-course-card"
|
|
|
|
|
+ :class="{ 'rch-course-card--done': course.finished }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="rch-course-top">
|
|
|
|
|
+ <div class="rch-course-title">
|
|
|
|
|
+ <span class="rch-course-idx">{{ idx + 1 }}</span>
|
|
|
|
|
+ <span class="rch-course-name">{{ course.courseName }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="rch-tag"
|
|
|
|
|
+ :class="course.finished ? 'rch-tag--success' : 'rch-tag--warning'"
|
|
|
|
|
+ >{{ course.finished ? '已完课' : '学习中' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="rch-course-progress">
|
|
|
|
|
+ <div class="rch-progress-track">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="rch-progress-fill"
|
|
|
|
|
+ :style="{ width: course.percentage + '%' }"
|
|
|
|
|
+ :class="{ 'rch-progress-fill--done': course.finished }"
|
|
|
|
|
+ ></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="rch-progress-label">
|
|
|
|
|
+ <strong>{{ course.watchedCount }}</strong>/{{ course.totalCount }} 节 · {{ course.percentage }}%
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="course.latestSection" class="rch-course-latest">
|
|
|
|
|
+ <i class="el-icon-video-play"></i>
|
|
|
|
|
+ <span>{{ course.latestSection }}</span>
|
|
|
|
|
+ <time v-if="course.latestTime">{{ course.latestTime }}</time>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="course.qwUserName || course.corpName" class="rch-course-source">
|
|
|
|
|
+ <span v-if="course.qwUserName"><i class="el-icon-user"></i>{{ course.qwUserName }}</span>
|
|
|
|
|
+ <span v-if="course.corpName"><i class="el-icon-office-building"></i>{{ course.corpName }}</span>
|
|
|
|
|
+ <span v-if="course.hasPermission === false" class="rch-lock-tag rch-lock-tag--inline">无权查看</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else-if="selectedProjectId" class="rch-empty">该项目下暂无课程学习记录</div>
|
|
|
|
|
+ <div v-else class="rch-empty">请先选择看课项目</div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 详细看课记录 -->
|
|
|
|
|
+ <section class="rch-panel rch-panel--detail">
|
|
|
|
|
+ <div class="rch-panel-head rch-panel-head--clickable" @click="detailExpanded = !detailExpanded">
|
|
|
|
|
+ <span class="rch-panel-icon rch-panel-icon--log"><i class="el-icon-document"></i></span>
|
|
|
|
|
+ <span class="rch-panel-title">详细看课记录</span>
|
|
|
|
|
+ <span v-if="detailExpanded && total" class="rch-panel-count">{{ total }} 条</span>
|
|
|
|
|
+ <i :class="detailExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" class="rch-expand-icon"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-collapse-transition>
|
|
|
|
|
+ <div v-show="detailExpanded" class="rch-detail-body">
|
|
|
|
|
+ <div class="rch-filter-bar">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="queryParams.courseId"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ placeholder="筛选课程"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ class="rch-filter-select"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="c in filteredCourses"
|
|
|
|
|
+ :key="c.courseId"
|
|
|
|
|
+ :label="c.courseName"
|
|
|
|
|
+ :value="c.courseId"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-search" @click="handleQueryWatchLog">查询</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-table :data="list" size="small" class="rch-table" stripe>
|
|
|
|
|
+ <el-table-column label="企微主体" prop="corpName" min-width="110" show-overflow-tooltip>
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <span :class="{ 'rch-no-perm': scope.row.hasPermission === false }">{{ scope.row.corpName }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="所属企微" prop="qwUserName" min-width="90" show-overflow-tooltip>
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <span :class="{ 'rch-no-perm': scope.row.hasPermission === false }">{{ scope.row.qwUserName }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="项目" prop="projectName" min-width="90" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="课程" prop="courseName" min-width="110" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="小节" prop="videoName" min-width="90" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="记录时间" prop="createTime" min-width="140" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="完课" prop="logType" width="72" align="center">
|
|
|
|
|
+ <template slot-scope="scope">
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="rch-tag rch-tag--mini"
|
|
|
|
|
+ :class="scope.row.logType == 2 ? 'rch-tag--success' : 'rch-tag--muted'"
|
|
|
|
|
+ >{{ scope.row.logType == 2 ? '已完课' : '未完课' }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="完课时间" prop="finishTime" min-width="140" show-overflow-tooltip />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <pagination
|
|
|
|
|
+ v-show="total > 0"
|
|
|
|
|
+ :total="total"
|
|
|
|
|
+ :page.sync="queryParams.pageNum"
|
|
|
|
|
+ :limit.sync="queryParams.pageSize"
|
|
|
|
|
+ @pagination="fetchWatchLogList"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-collapse-transition>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-drawer>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { getRepeatCourseHistory, getWatchLogList } from '@/api/qw/externalContact'
|
|
|
|
|
+
|
|
|
|
|
+const SALES_STATUS = { 0: '正常', 1: '离职待接替', 2: '正在接替', 3: '流失', 4: '删除' }
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'RepeatCourseHistoryDrawer',
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ visible: false,
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ list: [],
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ detailExpanded: false,
|
|
|
|
|
+ currentRow: {},
|
|
|
|
|
+ projectList: [],
|
|
|
|
|
+ selectedProjectId: null,
|
|
|
|
|
+ mockSales: [],
|
|
|
|
|
+ mockCourses: [],
|
|
|
|
|
+ projectOptions: [],
|
|
|
|
|
+ queryParams: {
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ fsUserId: null,
|
|
|
|
|
+ projectId: null,
|
|
|
|
|
+ courseId: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ defaultAvatar: require('@/assets/image/profile.jpg'),
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ filteredCourses() {
|
|
|
|
|
+ const courses = this.mockCourses || []
|
|
|
|
|
+ const selectedId = this.selectedProjectId
|
|
|
|
|
+ if (selectedId == null || selectedId === '') return []
|
|
|
|
|
+ const matchIds = this.getMatchProjectIds(selectedId)
|
|
|
|
|
+ return courses.filter(c => matchIds.includes(Number(c.projectId)))
|
|
|
|
|
+ },
|
|
|
|
|
+ showEquivalentHint() {
|
|
|
|
|
+ const ids = new Set(this.projectList.map(p => Number(p.projectId)))
|
|
|
|
|
+ return ids.has(1) && ids.has(28)
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ created() {
|
|
|
|
|
+ this.getDicts('sys_course_project').then(res => {
|
|
|
|
|
+ this.projectOptions = res.data || []
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ open(row) {
|
|
|
|
|
+ if (!row || !row.fsUserId) return
|
|
|
|
|
+ this.queryParams.fsUserId = row.fsUserId
|
|
|
|
|
+ this.visible = true
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ this.detailExpanded = false
|
|
|
|
|
+ this.projectList = []
|
|
|
|
|
+ this.selectedProjectId = null
|
|
|
|
|
+ this.list = []
|
|
|
|
|
+ this.total = 0
|
|
|
|
|
+ this.currentRow = {
|
|
|
|
|
+ name: row.name,
|
|
|
|
|
+ avatar: row.avatar,
|
|
|
|
|
+ remark: row.remark,
|
|
|
|
|
+ isRepeat: row.isRepeat,
|
|
|
|
|
+ }
|
|
|
|
|
+ getRepeatCourseHistory(row.fsUserId).then(res => {
|
|
|
|
|
+ const data = res.data
|
|
|
|
|
+ if (data) {
|
|
|
|
|
+ this.currentRow = {
|
|
|
|
|
+ name: data.name || row.name,
|
|
|
|
|
+ avatar: data.avatar || row.avatar,
|
|
|
|
|
+ remark: data.remark || row.remark,
|
|
|
|
|
+ isRepeat: data.userRepeat != null ? data.userRepeat : row.isRepeat,
|
|
|
|
|
+ }
|
|
|
|
|
+ this.projectList = this.buildDisplayProjectList(data.projectList)
|
|
|
|
|
+ this.mockSales = (data.salesList || []).map(s => ({
|
|
|
|
|
+ qwUserName: s.qwUserName,
|
|
|
|
|
+ corpName: s.corpName,
|
|
|
|
|
+ addTime: s.addTime,
|
|
|
|
|
+ status: s.status,
|
|
|
|
|
+ hasPermission: s.hasPermission,
|
|
|
|
|
+ }))
|
|
|
|
|
+ this.mockCourses = (data.courseList || []).map(c => ({
|
|
|
|
|
+ courseId: c.courseId,
|
|
|
|
|
+ courseName: c.courseName,
|
|
|
|
|
+ projectId: c.projectId != null ? Number(c.projectId) : null,
|
|
|
|
|
+ watchedCount: c.watchedCount,
|
|
|
|
|
+ totalCount: c.totalCount,
|
|
|
|
|
+ percentage: c.percentage,
|
|
|
|
|
+ latestSection: c.latestSection,
|
|
|
|
|
+ latestTime: c.latestTime,
|
|
|
|
|
+ finished: c.finished,
|
|
|
|
|
+ qwUserName: c.qwUserName,
|
|
|
|
|
+ corpName: c.corpName,
|
|
|
|
|
+ hasPermission: c.hasPermission,
|
|
|
|
|
+ }))
|
|
|
|
|
+ if (this.projectList.length > 0) {
|
|
|
|
|
+ this.selectedProjectId = this.projectList[0].projectId
|
|
|
|
|
+ this.queryParams.projectId = this.selectedProjectId
|
|
|
|
|
+ }
|
|
|
|
|
+ this.fetchWatchLogList()
|
|
|
|
|
+ }
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ }).catch(() => {
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ handleClosed() {
|
|
|
|
|
+ this.queryParams.courseId = null
|
|
|
|
|
+ this.queryParams.pageNum = 1
|
|
|
|
|
+ },
|
|
|
|
|
+ getMatchProjectIds(projectId) {
|
|
|
|
|
+ const pid = Number(projectId)
|
|
|
|
|
+ return pid === 1 || pid === 28 ? [1, 28] : [pid]
|
|
|
|
|
+ },
|
|
|
|
|
+ getProjectLabelById(projectId) {
|
|
|
|
|
+ const id = Number(projectId)
|
|
|
|
|
+ const found = this.projectOptions.find(o => Number(o.dictValue) === id)
|
|
|
|
|
+ return found ? found.dictLabel : `项目${id}`
|
|
|
|
|
+ },
|
|
|
|
|
+ buildDisplayProjectList(projectList) {
|
|
|
|
|
+ const equivalentMap = { 1: 28, 28: 1 }
|
|
|
|
|
+ const list = (projectList || []).map(p => ({
|
|
|
|
|
+ projectId: Number(p.projectId),
|
|
|
|
|
+ projectName: p.projectName || this.getProjectLabelById(p.projectId),
|
|
|
|
|
+ isRepeat: p.isRepeat,
|
|
|
|
|
+ }))
|
|
|
|
|
+ const idSet = new Set(list.map(p => p.projectId))
|
|
|
|
|
+ list.forEach(item => {
|
|
|
|
|
+ const eqId = equivalentMap[item.projectId]
|
|
|
|
|
+ if (eqId != null && !idSet.has(eqId)) {
|
|
|
|
|
+ list.push({
|
|
|
|
|
+ projectId: eqId,
|
|
|
|
|
+ projectName: this.getProjectLabelById(eqId),
|
|
|
|
|
+ isRepeat: item.isRepeat,
|
|
|
|
|
+ })
|
|
|
|
|
+ idSet.add(eqId)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ return list.sort((a, b) => a.projectId - b.projectId)
|
|
|
|
|
+ },
|
|
|
|
|
+ selectProject(projectId) {
|
|
|
|
|
+ const pid = Number(projectId)
|
|
|
|
|
+ this.selectedProjectId = pid
|
|
|
|
|
+ this.queryParams.projectId = pid
|
|
|
|
|
+ this.queryParams.courseId = null
|
|
|
|
|
+ this.queryParams.pageNum = 1
|
|
|
|
|
+ this.detailExpanded = true
|
|
|
|
|
+ this.fetchWatchLogList()
|
|
|
|
|
+ },
|
|
|
|
|
+ handleQueryWatchLog() {
|
|
|
|
|
+ this.queryParams.pageNum = 1
|
|
|
|
|
+ this.fetchWatchLogList()
|
|
|
|
|
+ },
|
|
|
|
|
+ fetchWatchLogList() {
|
|
|
|
|
+ const params = {
|
|
|
|
|
+ pageNum: this.queryParams.pageNum,
|
|
|
|
|
+ pageSize: this.queryParams.pageSize,
|
|
|
|
|
+ fsUserId: this.queryParams.fsUserId,
|
|
|
|
|
+ courseId: this.queryParams.courseId || undefined,
|
|
|
|
|
+ projectId: this.queryParams.projectId || undefined,
|
|
|
|
|
+ }
|
|
|
|
|
+ getWatchLogList(params).then(e => {
|
|
|
|
|
+ this.list = e.rows || []
|
|
|
|
|
+ this.total = e.total || 0
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ salesStatusLabel(status) {
|
|
|
|
|
+ return SALES_STATUS[status] || '未知'
|
|
|
|
|
+ },
|
|
|
|
|
+ salesStatusClass(status) {
|
|
|
|
|
+ if (status === 0) return 'rch-tag--success'
|
|
|
|
|
+ if (status === 1 || status === 2) return 'rch-tag--warning'
|
|
|
|
|
+ return 'rch-tag--danger'
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style>
|
|
|
|
|
+.repeat-course-history-drawer .el-drawer__body {
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.rch-drawer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: #f1f5f9;
|
|
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== Hero ===== */
|
|
|
|
|
+.rch-hero {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ padding: 20px 24px 0;
|
|
|
|
|
+ color: #1e293b;
|
|
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-close {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 16px;
|
|
|
|
|
+ right: 16px;
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: #f1f5f9;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ transition: background 0.2s, color 0.2s;
|
|
|
|
|
+ z-index: 2;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-close:hover {
|
|
|
|
|
+ background: #e2e8f0;
|
|
|
|
|
+ color: #334155;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-hero-inner {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ padding-right: 40px;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-avatar-wrap {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-avatar {
|
|
|
|
|
+ width: 56px;
|
|
|
|
|
+ height: 56px;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ border: 3px solid #e2e8f0;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+.rch-name-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-name {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: -0.02em;
|
|
|
|
|
+ line-height: 1.3;
|
|
|
|
|
+ color: #0f172a;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-remark {
|
|
|
|
|
+ margin: 6px 0 0;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-remark i {
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-stats {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+ border-radius: 12px 12px 0 0;
|
|
|
|
|
+ padding: 14px 0;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-stat {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-stat-num {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+ color: #0f172a;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-stat-label {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-stat-divider {
|
|
|
|
|
+ width: 1px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ background: #e2e8f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== Body ===== */
|
|
|
|
|
+.rch-body {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-body::-webkit-scrollbar {
|
|
|
|
|
+ width: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-body::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: #cbd5e1;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== Panel ===== */
|
|
|
|
|
+.rch-panel {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
|
|
|
|
|
+}
|
|
|
|
|
+.rch-panel-head {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-panel-head--clickable {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-panel-head--clickable + .rch-detail-body,
|
|
|
|
|
+.rch-panel--detail .rch-panel-head--clickable {
|
|
|
|
|
+ margin-bottom: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-panel-icon {
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-panel-icon--project { background: #eef2ff; color: #6366f1; }
|
|
|
|
|
+.rch-panel-icon--sales { background: #ecfdf5; color: #10b981; }
|
|
|
|
|
+.rch-panel-icon--course { background: #fff7ed; color: #f59e0b; }
|
|
|
|
|
+.rch-panel-icon--log { background: #f0f9ff; color: #0ea5e9; }
|
|
|
|
|
+.rch-panel-title {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1e293b;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-panel-count {
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #94a3b8;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-expand-icon {
|
|
|
|
|
+ margin-left: 4px;
|
|
|
|
|
+ color: #94a3b8;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== Project chips ===== */
|
|
|
|
|
+.rch-project-chips {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-chip {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ padding: 6px 14px;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ border: 1.5px solid #e2e8f0;
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+ color: #475569;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-chip:hover {
|
|
|
|
|
+ border-color: #6366f1;
|
|
|
|
|
+ color: #6366f1;
|
|
|
|
|
+ background: #eef2ff;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-chip--active {
|
|
|
|
|
+ background: #6366f1;
|
|
|
|
|
+ border-color: #6366f1;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.35);
|
|
|
|
|
+}
|
|
|
|
|
+.rch-chip--repeat:not(.rch-chip--active) {
|
|
|
|
|
+ border-color: #fecaca;
|
|
|
|
|
+ color: #dc2626;
|
|
|
|
|
+ background: #fef2f2;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-chip--repeat.rch-chip--active {
|
|
|
|
|
+ background: #dc2626;
|
|
|
|
|
+ border-color: #dc2626;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(220, 38, 38, 0.35);
|
|
|
|
|
+}
|
|
|
|
|
+.rch-chip-warn {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-hint {
|
|
|
|
|
+ margin: 10px 0 0;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ background: #f0f9ff;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #0369a1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== Sales ===== */
|
|
|
|
|
+.rch-sales-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-sales-item {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ padding: 12px 14px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
|
|
+ background: #fafbfc;
|
|
|
|
|
+ transition: box-shadow 0.2s, border-color 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-sales-item:hover {
|
|
|
|
|
+ border-color: #cbd5e1;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
|
|
|
|
+}
|
|
|
|
|
+.rch-sales-item--locked {
|
|
|
|
|
+ opacity: 0.72;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-sales-top {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-sales-name {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1e293b;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-sales-meta {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-sales-meta span {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 5px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-sales-meta i {
|
|
|
|
|
+ color: #94a3b8;
|
|
|
|
|
+ width: 14px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== Tags ===== */
|
|
|
|
|
+.rch-tag {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-tag--mini {
|
|
|
|
|
+ padding: 1px 6px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-tag--success { background: #dcfce7; color: #15803d; }
|
|
|
|
|
+.rch-tag--warning { background: #fef3c7; color: #b45309; }
|
|
|
|
|
+.rch-tag--danger { background: #fee2e2; color: #b91c1c; }
|
|
|
|
|
+.rch-tag--muted { background: #f1f5f9; color: #64748b; }
|
|
|
|
|
+.rch-lock-tag {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 10px;
|
|
|
|
|
+ right: 10px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #94a3b8;
|
|
|
|
|
+ background: #f1f5f9;
|
|
|
|
|
+ padding: 1px 6px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-lock-tag--inline {
|
|
|
|
|
+ position: static;
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== Courses ===== */
|
|
|
|
|
+.rch-course-grid {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-card {
|
|
|
|
|
+ padding: 14px 16px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ transition: box-shadow 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-card:hover {
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-card--done {
|
|
|
|
|
+ border-left: 3px solid #10b981;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-top {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-idx {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ width: 22px;
|
|
|
|
|
+ height: 22px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: #6366f1;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-card--done .rch-course-idx {
|
|
|
|
|
+ background: #10b981;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-name {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1e293b;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-progress {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-progress-track {
|
|
|
|
|
+ height: 6px;
|
|
|
|
|
+ background: #e2e8f0;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-progress-fill {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ background: linear-gradient(90deg, #6366f1, #818cf8);
|
|
|
|
|
+ transition: width 0.4s ease;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-progress-fill--done {
|
|
|
|
|
+ background: linear-gradient(90deg, #10b981, #34d399);
|
|
|
|
|
+}
|
|
|
|
|
+.rch-progress-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-progress-label strong {
|
|
|
|
|
+ color: #1e293b;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-latest {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6366f1;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-latest i {
|
|
|
|
|
+ color: #818cf8;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-latest time {
|
|
|
|
|
+ color: #94a3b8;
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-source {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #94a3b8;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-course-source span {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== Detail ===== */
|
|
|
|
|
+.rch-detail-body {
|
|
|
|
|
+ padding-top: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-filter-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-filter-select {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ max-width: 320px;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-table {
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+.rch-no-perm {
|
|
|
|
|
+ color: #cbd5e1;
|
|
|
|
|
+ font-style: italic;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== Empty ===== */
|
|
|
|
|
+.rch-empty {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #94a3b8;
|
|
|
|
|
+ padding: 24px 0;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|