|
|
@@ -6,6 +6,18 @@
|
|
|
<div class="title-box">
|
|
|
<div class="course-title">{{ classInfo.name || '课程现场' }}</div>
|
|
|
<div class="banner-text">{{ banner }}</div>
|
|
|
+ <!-- 课程进行时间(突出显示) -->
|
|
|
+ <div class="timer-row" v-if="showTimer">
|
|
|
+ <div class="timer-box" v-if="startAt">
|
|
|
+ <span class="timer-label">课程进行中</span>
|
|
|
+ <span class="timer-value">{{ elapsedText }}</span>
|
|
|
+ <span class="step-pill">{{ currentStepTitle }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="timer-box idle" v-else>
|
|
|
+ <span class="timer-label">课程未开始</span>
|
|
|
+ <span class="timer-idle">等待助教开始课程</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div class="clock">
|
|
|
<div class="time">{{ now }}</div>
|
|
|
@@ -13,10 +25,19 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 环节步骤条 -->
|
|
|
+ <div class="agenda-bar" v-if="config.agenda && config.agenda.length > 0">
|
|
|
+ <div class="step-item" v-for="(ag, i) in config.agenda" :key="i"
|
|
|
+ :class="{ active: i === currentStep, done: i < currentStep }">
|
|
|
+ <div class="step-dot"></div>
|
|
|
+ <div class="step-name">{{ ag.title }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 主体 -->
|
|
|
<div class="screen-body">
|
|
|
<!-- 左侧:积分榜 -->
|
|
|
- <div class="panel scoreboard-panel">
|
|
|
+ <div class="panel scoreboard-panel" v-if="config.showScoreboard !== 0">
|
|
|
<div class="panel-title">
|
|
|
<span>小组积分榜</span>
|
|
|
<span class="panel-sub">GROUP RANKING</span>
|
|
|
@@ -34,9 +55,9 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 右侧:签到 + 完课进度 -->
|
|
|
+ <!-- 右侧列 -->
|
|
|
<div class="right-col">
|
|
|
- <div class="panel checkin-panel">
|
|
|
+ <div class="panel checkin-panel" v-if="config.showCheckin !== 0">
|
|
|
<div class="panel-title">
|
|
|
<span>签到情况</span>
|
|
|
<span class="stat-badge">{{ checkins.checkedIn || 0 }} / {{ checkins.total || 0 }}</span>
|
|
|
@@ -55,7 +76,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="panel progress-panel">
|
|
|
+ <div class="panel progress-panel" v-if="config.showProgress !== 0">
|
|
|
<div class="panel-title">
|
|
|
<span>完课进度</span>
|
|
|
<span class="stat-badge">{{ progress.eligible || 0 }} / {{ progress.total || 0 }}</span>
|
|
|
@@ -74,6 +95,67 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 中部:小组进展矩阵 -->
|
|
|
+ <div class="panel gp-panel" v-if="config.showGroupProgress !== 0">
|
|
|
+ <div class="panel-title">
|
|
|
+ <span>小组进展</span>
|
|
|
+ <span class="panel-sub">GROUP PROGRESS</span>
|
|
|
+ </div>
|
|
|
+ <div class="gp-table">
|
|
|
+ <div class="gp-head">
|
|
|
+ <div class="gp-cell group">小组</div>
|
|
|
+ <div class="gp-cell">分工确认</div>
|
|
|
+ <div class="gp-cell">任务上传</div>
|
|
|
+ <div class="gp-cell">确认完成</div>
|
|
|
+ <div class="gp-cell">路演材料</div>
|
|
|
+ </div>
|
|
|
+ <div class="gp-row" v-for="g in groupProgress" :key="g.groupId">
|
|
|
+ <div class="gp-cell group">{{ g.groupName }}</div>
|
|
|
+ <div class="gp-cell"><span :class="['status-pill', g.assignDone ? 'ok' : 'no']">{{ g.assignDone ? '已完成' : g.assignConfirmed + '/' + g.memberCount }}</span></div>
|
|
|
+ <div class="gp-cell"><span :class="['status-pill', g.uploadCount > 0 ? 'ok' : 'no']">{{ g.uploadCount > 0 ? g.uploadCount + ' 项' : '未上传' }}</span></div>
|
|
|
+ <div class="gp-cell"><span :class="['status-pill', g.approvedCount > 0 ? 'ok' : 'no']">{{ g.approvedCount > 0 ? '已确认' : '待确认' }}</span></div>
|
|
|
+ <div class="gp-cell"><span :class="['status-pill', g.roadmapUploaded ? 'ok' : 'no']">{{ g.roadmapUploaded ? '已上传' : '未上传' }}</span></div>
|
|
|
+ </div>
|
|
|
+ <div v-if="groupProgress.length === 0" class="empty">暂无小组</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 底部追加:投票结果 + 资料 -->
|
|
|
+ <div class="lower-row">
|
|
|
+ <div class="panel vote-panel" v-if="config.showVote !== 0">
|
|
|
+ <div class="panel-title">
|
|
|
+ <span>路演投票</span>
|
|
|
+ <span class="panel-sub">VOTE</span>
|
|
|
+ </div>
|
|
|
+ <div class="vote-tabs">
|
|
|
+ <span class="vote-tab" :class="{ active: voteRound === 1 }" @click="voteRound = 1">第1轮</span>
|
|
|
+ <span class="vote-tab" :class="{ active: voteRound === 2 }" @click="voteRound = 2">第2轮</span>
|
|
|
+ </div>
|
|
|
+ <div class="vote-bars">
|
|
|
+ <div class="v-row" v-for="b in currentVoteBars" :key="b.groupId">
|
|
|
+ <span class="v-name">{{ b.groupName }}</span>
|
|
|
+ <div class="v-track"><div class="v-fill" :style="{ width: voteWidth(b.count) + '%' }"></div></div>
|
|
|
+ <span class="v-count">{{ b.count }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="currentVoteBars.length === 0" class="empty">暂无投票数据</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel material-panel" v-if="config.showMaterial !== 0">
|
|
|
+ <div class="panel-title">
|
|
|
+ <span>课程资料</span>
|
|
|
+ <span class="panel-sub">MATERIAL</span>
|
|
|
+ </div>
|
|
|
+ <div class="material-list">
|
|
|
+ <div class="m-item" v-for="m in materials" :key="m.id">
|
|
|
+ <span class="m-icon">📄</span>
|
|
|
+ <span class="m-name">{{ m.title }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="materials.length === 0" class="empty">暂无资料</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 底部状态条 -->
|
|
|
@@ -84,36 +166,124 @@
|
|
|
|
|
|
<!-- 悬浮控制条(鼠标移动显示) -->
|
|
|
<div class="control-bar" v-if="showControl">
|
|
|
- <el-select v-model="classId" size="small" @change="fetchData" style="width: 200px">
|
|
|
+ <el-select v-model="classId" size="small" @change="onClassChange" style="width: 200px">
|
|
|
<el-option v-for="c in classList" :key="c.id" :label="c.name" :value="String(c.id)" />
|
|
|
</el-select>
|
|
|
<el-button size="small" @click="toggleFullscreen">{{ isFullscreen ? '退出全屏' : '全屏' }}</el-button>
|
|
|
+ <el-button size="small" @click="startClass">开始课程</el-button>
|
|
|
+ <el-button size="small" @click="openStepEditor">设置环节</el-button>
|
|
|
+ <el-button size="small" @click="openVoteEditor">录入投票</el-button>
|
|
|
<el-button size="small" @click="openBannerEditor">编辑公告</el-button>
|
|
|
+ <el-button size="small" @click="openConfigEditor">投屏设置</el-button>
|
|
|
<el-button size="small" type="text" class="back-link" @click="goBack">返回后台</el-button>
|
|
|
</div>
|
|
|
|
|
|
<!-- 公告编辑弹窗 -->
|
|
|
<el-dialog title="编辑投屏公告" :visible.sync="editBanner" width="480px" append-to-body>
|
|
|
- <div class="banner-hint">显示在投屏顶部标题下方,保存到本地浏览器</div>
|
|
|
+ <div class="banner-hint">显示在投屏顶部标题下方,随投屏配置保存</div>
|
|
|
<el-input v-model="bannerDraft" placeholder="例如:欢迎来到家立方-AI管家成长营" maxlength="60" show-word-limit></el-input>
|
|
|
<div slot="footer">
|
|
|
<el-button @click="editBanner = false">取消</el-button>
|
|
|
<el-button type="primary" @click="saveBanner">保存</el-button>
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
+
|
|
|
+ <!-- 环节设置弹窗 -->
|
|
|
+ <el-dialog title="设置课程环节" :visible.sync="editStep" width="520px" append-to-body>
|
|
|
+ <div class="banner-hint">切换当前进行到的课程环节,投屏顶部同步高亮</div>
|
|
|
+ <div class="step-edit-list">
|
|
|
+ <div class="step-edit-item" v-for="(ag, i) in agendaDraft" :key="i"
|
|
|
+ :class="{ active: i === currentStepDraft }" @click="currentStepDraft = i">
|
|
|
+ <span class="step-edit-dot">{{ i + 1 }}</span>
|
|
|
+ <span class="step-edit-name">{{ ag.title }}</span>
|
|
|
+ <span class="step-edit-min">{{ ag.minutes }} 分钟</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="agendaDraft.length === 0" class="empty">请先在「投屏设置」中配置课程环节</div>
|
|
|
+ </div>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="editStep = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="saveStep">设为当前环节</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 投屏设置弹窗 -->
|
|
|
+ <el-dialog title="投屏设置" :visible.sync="editConfig" width="640px" append-to-body>
|
|
|
+ <el-form label-width="120px">
|
|
|
+ <el-form-item label="显示模块">
|
|
|
+ <el-checkbox v-model="cfgDraft.showScoreboard">小组积分榜</el-checkbox>
|
|
|
+ <el-checkbox v-model="cfgDraft.showCheckin">签到情况</el-checkbox>
|
|
|
+ <el-checkbox v-model="cfgDraft.showProgress">完课进度</el-checkbox>
|
|
|
+ <el-checkbox v-model="cfgDraft.showGroupProgress">小组进展</el-checkbox>
|
|
|
+ <el-checkbox v-model="cfgDraft.showVote">投票结果</el-checkbox>
|
|
|
+ <el-checkbox v-model="cfgDraft.showMaterial">课程资料</el-checkbox>
|
|
|
+ <el-checkbox v-model="cfgDraft.showBanner">公告</el-checkbox>
|
|
|
+ <el-checkbox v-model="cfgDraft.showTimer">进行时间</el-checkbox>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="课程环节">
|
|
|
+ <div class="agenda-editor">
|
|
|
+ <div class="agenda-row" v-for="(ag, i) in cfgDraft.agenda" :key="i">
|
|
|
+ <el-input v-model="ag.title" placeholder="环节名称" size="small" style="width: 260px"></el-input>
|
|
|
+ <el-input v-model.number="ag.minutes" placeholder="分钟" size="small" style="width: 90px"></el-input>
|
|
|
+ <el-button size="small" type="text" @click="removeAgenda(i)">删除</el-button>
|
|
|
+ </div>
|
|
|
+ <el-button size="small" type="text" @click="addAgenda">+ 添加环节</el-button>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="公告">
|
|
|
+ <el-input v-model="cfgDraft.banner" placeholder="投屏顶部公告" maxlength="60"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="editConfig = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="saveConfig">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 投票录入弹窗(线下投票结果) -->
|
|
|
+ <el-dialog title="录入投票结果" :visible.sync="editVote" width="520px" append-to-body>
|
|
|
+ <div class="banner-hint">线下投票结果录入,投屏投票模块同步展示(负数标记为手工录入)</div>
|
|
|
+ <div class="vote-tabs light">
|
|
|
+ <span class="vote-tab" :class="{ active: voteRoundDraft === 1 }" @click="switchVoteRound(1)">第1轮</span>
|
|
|
+ <span class="vote-tab" :class="{ active: voteRoundDraft === 2 }" @click="switchVoteRound(2)">第2轮</span>
|
|
|
+ </div>
|
|
|
+ <div class="vote-edit-list">
|
|
|
+ <div class="vote-edit-row" v-for="g in voteDraftRows" :key="g.groupId">
|
|
|
+ <span class="vote-edit-name">{{ g.groupName }}</span>
|
|
|
+ <el-input-number v-model="g.count" :min="0" :max="999" size="small"></el-input-number>
|
|
|
+ </div>
|
|
|
+ <div v-if="voteDraftRows.length === 0" class="empty">暂无小组</div>
|
|
|
+ </div>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="editVote = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="saveVote">保存投票结果</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import { statsScreen } from '@/api/stats'
|
|
|
+import { screenConfigGet, screenConfigSave, screenStep, screenStart, screenReset, screenVoteRecord } from '@/api/screen'
|
|
|
import { classList } from '@/api/class'
|
|
|
|
|
|
var BANNER_KEY = 'train-screen-banner'
|
|
|
+var CLASS_KEY = 'train-screen-classId'
|
|
|
|
|
|
function pad(n) {
|
|
|
return n < 10 ? '0' + n : '' + n
|
|
|
}
|
|
|
|
|
|
+function parseAgenda(jsonText) {
|
|
|
+ if (!jsonText) return []
|
|
|
+ var arr = []
|
|
|
+ try {
|
|
|
+ arr = JSON.parse(jsonText)
|
|
|
+ } catch (e) {
|
|
|
+ arr = []
|
|
|
+ }
|
|
|
+ return (Array.isArray(arr) ? arr : []).filter(function (a) { return a && a.title })
|
|
|
+}
|
|
|
+
|
|
|
export default {
|
|
|
name: 'Screen',
|
|
|
data: function () {
|
|
|
@@ -124,18 +294,60 @@ export default {
|
|
|
scoreboard: [],
|
|
|
checkins: {},
|
|
|
progress: {},
|
|
|
+ groupProgress: [],
|
|
|
+ materials: [],
|
|
|
+ config: {},
|
|
|
+ startAt: '',
|
|
|
+ voteRound: 1,
|
|
|
+ votes: {},
|
|
|
now: '',
|
|
|
today: '',
|
|
|
banner: '',
|
|
|
bannerDraft: '',
|
|
|
editBanner: false,
|
|
|
+ editStep: false,
|
|
|
+ editConfig: false,
|
|
|
+ editVote: false,
|
|
|
+ voteRoundDraft: 1,
|
|
|
+ voteDraftRows: [],
|
|
|
showControl: false,
|
|
|
isFullscreen: false,
|
|
|
+ currentStepDraft: 0,
|
|
|
+ agendaDraft: [],
|
|
|
+ cfgDraft: {
|
|
|
+ showScoreboard: true, showCheckin: true, showProgress: true,
|
|
|
+ showGroupProgress: true, showVote: true, showMaterial: true,
|
|
|
+ showBanner: true, showTimer: true, agenda: [], banner: ''
|
|
|
+ },
|
|
|
pollTimer: null,
|
|
|
clockTimer: null
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
+ showTimer: function () {
|
|
|
+ return this.config.showTimer !== 0
|
|
|
+ },
|
|
|
+ currentStep: function () {
|
|
|
+ return this.config.currentStep || 0
|
|
|
+ },
|
|
|
+ currentStepTitle: function () {
|
|
|
+ var agenda = this.config.agenda || []
|
|
|
+ var cs = this.currentStep
|
|
|
+ if (agenda.length === 0) return ''
|
|
|
+ if (cs >= agenda.length) return agenda[agenda.length - 1].title
|
|
|
+ return agenda[cs].title
|
|
|
+ },
|
|
|
+ elapsedText: function () {
|
|
|
+ if (!this.startAt) return '00:00:00'
|
|
|
+ var start = new Date(String(this.startAt).replace(/-/g, '/'))
|
|
|
+ var now = new Date()
|
|
|
+ var secs = Math.floor((now.getTime() - start.getTime()) / 1000)
|
|
|
+ if (secs < 0) secs = 0
|
|
|
+ var h = Math.floor(secs / 3600)
|
|
|
+ var m = Math.floor((secs % 3600) / 60)
|
|
|
+ var s = secs % 60
|
|
|
+ return pad(h) + ':' + pad(m) + ':' + pad(s)
|
|
|
+ },
|
|
|
checkinPct: function () {
|
|
|
var total = this.checkins.total || 0
|
|
|
return total > 0 ? Math.round(((this.checkins.checkedIn || 0) / total) * 100) : 0
|
|
|
@@ -149,6 +361,10 @@ export default {
|
|
|
},
|
|
|
rows: function () {
|
|
|
return (this.progress.rows || []).filter(function (u) { return !u.eligible })
|
|
|
+ },
|
|
|
+ currentVoteBars: function () {
|
|
|
+ var key = 'round' + this.voteRound
|
|
|
+ return this.votes[key] || []
|
|
|
}
|
|
|
},
|
|
|
mounted: function () {
|
|
|
@@ -171,6 +387,10 @@ export default {
|
|
|
self.classList = res.data || []
|
|
|
}).catch(function () {})
|
|
|
},
|
|
|
+ onClassChange: function () {
|
|
|
+ localStorage.setItem(CLASS_KEY, this.classId)
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
fetchData: function () {
|
|
|
var self = this
|
|
|
var params = {}
|
|
|
@@ -181,10 +401,23 @@ export default {
|
|
|
self.scoreboard = data.scoreboard || []
|
|
|
self.checkins = data.checkins || {}
|
|
|
self.progress = data.progress || {}
|
|
|
+ self.groupProgress = data.groupProgress || []
|
|
|
+ self.materials = data.materials || []
|
|
|
+ self.votes = data.votes || {}
|
|
|
+ self.config = data.config || {}
|
|
|
+ self.startAt = data.startAt || ''
|
|
|
+ if (self.config.banner !== undefined) {
|
|
|
+ self.banner = self.config.banner || ''
|
|
|
+ }
|
|
|
var cls = data.classList || []
|
|
|
if (cls.length > 0 && !self.classId) {
|
|
|
self.classList = cls
|
|
|
- self.classId = String(cls[0].id)
|
|
|
+ var saved = localStorage.getItem(CLASS_KEY)
|
|
|
+ if (saved && cls.some(function (c) { return String(c.id) === saved })) {
|
|
|
+ self.classId = saved
|
|
|
+ } else {
|
|
|
+ self.classId = String(cls[0].id)
|
|
|
+ }
|
|
|
}
|
|
|
}).catch(function () {})
|
|
|
},
|
|
|
@@ -195,6 +428,120 @@ export default {
|
|
|
this.today = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
|
|
|
' 星期' + week[d.getDay()]
|
|
|
},
|
|
|
+ voteWidth: function (count) {
|
|
|
+ var max = 1
|
|
|
+ this.currentVoteBars.forEach(function (b) {
|
|
|
+ if (b.count > max) max = b.count
|
|
|
+ })
|
|
|
+ return Math.round((count / max) * 100)
|
|
|
+ },
|
|
|
+ startClass: function () {
|
|
|
+ var self = this
|
|
|
+ if (!this.classId) return
|
|
|
+ this.$confirm('确定开始课程?将记录课程开始时间并在投屏上开始计时。', '开始课程', {
|
|
|
+ confirmButtonText: '开始', cancelButtonText: '取消', type: 'info'
|
|
|
+ }).then(function () {
|
|
|
+ return screenStart({ classId: self.classId })
|
|
|
+ }).then(function () {
|
|
|
+ self.fetchData()
|
|
|
+ self.$message.success('课程已开始')
|
|
|
+ }).catch(function () {})
|
|
|
+ },
|
|
|
+ openStepEditor: function () {
|
|
|
+ var agenda = this.config.agenda || []
|
|
|
+ this.agendaDraft = agenda.map(function (a) { return { title: a.title, minutes: a.minutes } })
|
|
|
+ this.currentStepDraft = this.currentStep
|
|
|
+ this.editStep = true
|
|
|
+ },
|
|
|
+ saveStep: function () {
|
|
|
+ var self = this
|
|
|
+ screenStep({ classId: this.classId, step: this.currentStepDraft }).then(function () {
|
|
|
+ self.editStep = false
|
|
|
+ self.fetchData()
|
|
|
+ self.$message.success('已切换当前环节')
|
|
|
+ }).catch(function () {})
|
|
|
+ },
|
|
|
+ openVoteEditor: function () {
|
|
|
+ this.voteRoundDraft = this.voteRound
|
|
|
+ this.loadVoteDraftRows()
|
|
|
+ this.editVote = true
|
|
|
+ },
|
|
|
+ switchVoteRound: function (round) {
|
|
|
+ if (this.voteRoundDraft === round) return
|
|
|
+ this.voteRoundDraft = round
|
|
|
+ this.loadVoteDraftRows()
|
|
|
+ },
|
|
|
+ loadVoteDraftRows: function () {
|
|
|
+ var key = 'round' + this.voteRoundDraft
|
|
|
+ var bars = this.votes[key] || []
|
|
|
+ // 若尚无该轮投票数据,用积分榜的小组作为底稿
|
|
|
+ var rows = bars.length > 0 ? bars : this.scoreboard.map(function (g) {
|
|
|
+ return { groupId: g.groupId, groupName: g.groupName, count: 0 }
|
|
|
+ })
|
|
|
+ this.voteDraftRows = rows.map(function (b) {
|
|
|
+ return { groupId: b.groupId, groupName: b.groupName, count: b.count || 0 }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ saveVote: function () {
|
|
|
+ var self = this
|
|
|
+ var records = this.voteDraftRows.map(function (r) {
|
|
|
+ return { groupId: r.groupId, count: Number(r.count) || 0 }
|
|
|
+ })
|
|
|
+ screenVoteRecord({ classId: this.classId, round: this.voteRoundDraft, records: records }).then(function () {
|
|
|
+ self.editVote = false
|
|
|
+ self.voteRound = self.voteRoundDraft
|
|
|
+ self.fetchData()
|
|
|
+ self.$message.success('投票结果已录入')
|
|
|
+ }).catch(function () {})
|
|
|
+ },
|
|
|
+ openConfigEditor: function () {
|
|
|
+ var cfg = this.config || {}
|
|
|
+ var agenda = (cfg.agenda || []).map(function (a) {
|
|
|
+ return { title: a.title, minutes: a.minutes === undefined ? 10 : a.minutes }
|
|
|
+ })
|
|
|
+ this.cfgDraft = {
|
|
|
+ showScoreboard: cfg.showScoreboard !== 0,
|
|
|
+ showCheckin: cfg.showCheckin !== 0,
|
|
|
+ showProgress: cfg.showProgress !== 0,
|
|
|
+ showGroupProgress: cfg.showGroupProgress !== 0,
|
|
|
+ showVote: cfg.showVote !== 0,
|
|
|
+ showMaterial: cfg.showMaterial !== 0,
|
|
|
+ showBanner: cfg.showBanner !== 0,
|
|
|
+ showTimer: cfg.showTimer !== 0,
|
|
|
+ agenda: agenda,
|
|
|
+ banner: cfg.banner || ''
|
|
|
+ }
|
|
|
+ this.editConfig = true
|
|
|
+ },
|
|
|
+ saveConfig: function () {
|
|
|
+ var self = this
|
|
|
+ var payload = {
|
|
|
+ classId: this.classId,
|
|
|
+ showScoreboard: this.cfgDraft.showScoreboard ? 1 : 0,
|
|
|
+ showCheckin: this.cfgDraft.showCheckin ? 1 : 0,
|
|
|
+ showProgress: this.cfgDraft.showProgress ? 1 : 0,
|
|
|
+ showGroupProgress: this.cfgDraft.showGroupProgress ? 1 : 0,
|
|
|
+ showVote: this.cfgDraft.showVote ? 1 : 0,
|
|
|
+ showMaterial: this.cfgDraft.showMaterial ? 1 : 0,
|
|
|
+ showBanner: this.cfgDraft.showBanner ? 1 : 0,
|
|
|
+ showTimer: this.cfgDraft.showTimer ? 1 : 0,
|
|
|
+ agenda: this.cfgDraft.agenda.map(function (a) {
|
|
|
+ return { title: a.title, minutes: Number(a.minutes) || 10 }
|
|
|
+ }),
|
|
|
+ banner: this.cfgDraft.banner
|
|
|
+ }
|
|
|
+ screenConfigSave(payload).then(function () {
|
|
|
+ self.editConfig = false
|
|
|
+ self.fetchData()
|
|
|
+ self.$message.success('投屏设置已保存')
|
|
|
+ }).catch(function () {})
|
|
|
+ },
|
|
|
+ addAgenda: function () {
|
|
|
+ this.cfgDraft.agenda.push({ title: '', minutes: 10 })
|
|
|
+ },
|
|
|
+ removeAgenda: function (i) {
|
|
|
+ this.cfgDraft.agenda.splice(i, 1)
|
|
|
+ },
|
|
|
toggleFullscreen: function () {
|
|
|
if (this.isFullscreen) {
|
|
|
if (document.exitFullscreen) document.exitFullscreen()
|
|
|
@@ -214,9 +561,24 @@ export default {
|
|
|
this.editBanner = true
|
|
|
},
|
|
|
saveBanner: function () {
|
|
|
- this.banner = this.bannerDraft
|
|
|
- localStorage.setItem(BANNER_KEY, this.banner)
|
|
|
- this.editBanner = false
|
|
|
+ var self = this
|
|
|
+ var payload = { classId: this.classId, banner: this.bannerDraft }
|
|
|
+ var cfg = this.config || {}
|
|
|
+ payload.showScoreboard = cfg.showScoreboard === undefined ? 1 : cfg.showScoreboard
|
|
|
+ payload.showCheckin = cfg.showCheckin === undefined ? 1 : cfg.showCheckin
|
|
|
+ payload.showProgress = cfg.showProgress === undefined ? 1 : cfg.showProgress
|
|
|
+ payload.showGroupProgress = cfg.showGroupProgress === undefined ? 1 : cfg.showGroupProgress
|
|
|
+ payload.showVote = cfg.showVote === undefined ? 1 : cfg.showVote
|
|
|
+ payload.showMaterial = cfg.showMaterial === undefined ? 1 : cfg.showMaterial
|
|
|
+ payload.showBanner = cfg.showBanner === undefined ? 1 : cfg.showBanner
|
|
|
+ payload.showTimer = cfg.showTimer === undefined ? 1 : cfg.showTimer
|
|
|
+ if (cfg.agenda) payload.agenda = cfg.agenda
|
|
|
+ screenConfigSave(payload).then(function () {
|
|
|
+ self.banner = self.bannerDraft
|
|
|
+ self.editBanner = false
|
|
|
+ self.fetchData()
|
|
|
+ self.$message.success('公告已保存')
|
|
|
+ }).catch(function () {})
|
|
|
},
|
|
|
goBack: function () {
|
|
|
this.$router.push('/dashboard')
|
|
|
@@ -495,6 +857,342 @@ export default {
|
|
|
50% { opacity: 0.35; }
|
|
|
}
|
|
|
|
|
|
+/* 课程进行时间(突出显示) */
|
|
|
+.timer-row {
|
|
|
+ margin-top: 14px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.timer-box {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 18px;
|
|
|
+ background: linear-gradient(90deg, rgba(56, 189, 248, 0.18), rgba(129, 140, 248, 0.18));
|
|
|
+ border: 1px solid rgba(56, 189, 248, 0.45);
|
|
|
+ border-radius: 40px;
|
|
|
+ padding: 10px 32px;
|
|
|
+ box-shadow: 0 0 34px rgba(56, 189, 248, 0.28);
|
|
|
+}
|
|
|
+.timer-box.idle {
|
|
|
+ background: rgba(148, 163, 184, 0.12);
|
|
|
+ border-color: rgba(148, 163, 184, 0.4);
|
|
|
+ box-shadow: none;
|
|
|
+}
|
|
|
+.timer-label {
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #f8fafc;
|
|
|
+ letter-spacing: 2px;
|
|
|
+}
|
|
|
+.timer-value {
|
|
|
+ font-size: 60px;
|
|
|
+ font-weight: 800;
|
|
|
+ color: #38bdf8;
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
+ line-height: 1;
|
|
|
+ text-shadow: 0 0 24px rgba(56, 189, 248, 0.5);
|
|
|
+}
|
|
|
+.timer-box.idle .timer-value,
|
|
|
+.timer-idle {
|
|
|
+ font-size: 30px;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.step-pill {
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #c4b5fd;
|
|
|
+ background: rgba(167, 139, 250, 0.2);
|
|
|
+ border: 1px solid rgba(167, 139, 250, 0.4);
|
|
|
+ border-radius: 20px;
|
|
|
+ padding: 4px 16px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 环节步骤条 */
|
|
|
+.agenda-bar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.step-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
+ border: 1px solid rgba(148, 163, 184, 0.2);
|
|
|
+ border-radius: 20px;
|
|
|
+ padding: 6px 16px;
|
|
|
+ opacity: 0.55;
|
|
|
+}
|
|
|
+.step-item.active {
|
|
|
+ opacity: 1;
|
|
|
+ background: rgba(56, 189, 248, 0.2);
|
|
|
+ border-color: #38bdf8;
|
|
|
+ box-shadow: 0 0 16px rgba(56, 189, 248, 0.3);
|
|
|
+}
|
|
|
+.step-item.done {
|
|
|
+ opacity: 0.85;
|
|
|
+ border-color: rgba(52, 211, 153, 0.5);
|
|
|
+}
|
|
|
+.step-dot {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #475569;
|
|
|
+}
|
|
|
+.step-item.active .step-dot {
|
|
|
+ background: #38bdf8;
|
|
|
+}
|
|
|
+.step-item.done .step-dot {
|
|
|
+ background: #34d399;
|
|
|
+}
|
|
|
+.step-name {
|
|
|
+ font-size: 18px;
|
|
|
+ color: #e2e8f0;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* 小组进展矩阵 */
|
|
|
+.gp-panel {
|
|
|
+ flex: 1.4;
|
|
|
+}
|
|
|
+.gp-table {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+ min-height: 0;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+.gp-head,
|
|
|
+.gp-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1.4fr repeat(4, 1fr);
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.gp-head {
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 16px;
|
|
|
+ padding: 0 8px 4px;
|
|
|
+}
|
|
|
+.gp-cell {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 18px;
|
|
|
+ color: #e2e8f0;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+.gp-cell.group {
|
|
|
+ text-align: left;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #f1f5f9;
|
|
|
+}
|
|
|
+.gp-row {
|
|
|
+ background: rgba(30, 58, 95, 0.45);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 10px 8px;
|
|
|
+}
|
|
|
+.status-pill {
|
|
|
+ display: inline-block;
|
|
|
+ min-width: 74px;
|
|
|
+ font-size: 16px;
|
|
|
+ padding: 3px 10px;
|
|
|
+ border-radius: 16px;
|
|
|
+ border: 1px solid rgba(148, 163, 184, 0.4);
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+.status-pill.ok {
|
|
|
+ color: #34d399;
|
|
|
+ border-color: rgba(52, 211, 153, 0.5);
|
|
|
+ background: rgba(52, 211, 153, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+/* 底部区域:投票 + 资料 */
|
|
|
+.lower-row {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1.4fr 1fr;
|
|
|
+ gap: 24px;
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+.vote-panel,
|
|
|
+.material-panel {
|
|
|
+ max-height: 220px;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+.vote-tabs {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.vote-tabs.light {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.vote-tabs.light .vote-tab {
|
|
|
+ color: #475569;
|
|
|
+ border-color: #cbd5e1;
|
|
|
+}
|
|
|
+.vote-tabs.light .vote-tab.active {
|
|
|
+ color: #0284c7;
|
|
|
+ border-color: #38bdf8;
|
|
|
+ background: #e0f2fe;
|
|
|
+}
|
|
|
+.vote-tab {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #94a3b8;
|
|
|
+ padding: 3px 12px;
|
|
|
+ border-radius: 14px;
|
|
|
+ border: 1px solid rgba(148, 163, 184, 0.4);
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+.vote-tab.active {
|
|
|
+ color: #38bdf8;
|
|
|
+ border-color: #38bdf8;
|
|
|
+ background: rgba(56, 189, 248, 0.15);
|
|
|
+}
|
|
|
+.vote-edit-list {
|
|
|
+ max-height: 320px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+.vote-edit-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 8px 4px;
|
|
|
+ border-bottom: 1px solid rgba(148, 163, 184, 0.15);
|
|
|
+}
|
|
|
+.vote-edit-name {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+.vote-bars {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ min-height: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.v-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+.v-name {
|
|
|
+ width: 130px;
|
|
|
+ font-size: 18px;
|
|
|
+ color: #e2e8f0;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+.v-track {
|
|
|
+ flex: 1;
|
|
|
+ height: 16px;
|
|
|
+ background: rgba(148, 163, 184, 0.2);
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.v-fill {
|
|
|
+ height: 100%;
|
|
|
+ background: linear-gradient(90deg, #38bdf8, #a78bfa);
|
|
|
+ border-radius: 8px;
|
|
|
+ transition: width 0.6s ease;
|
|
|
+}
|
|
|
+.v-count {
|
|
|
+ width: 40px;
|
|
|
+ text-align: right;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #f8fafc;
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
+}
|
|
|
+.material-list {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+.m-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ font-size: 20px;
|
|
|
+ padding: 8px 4px;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+.m-icon {
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+.m-name {
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+/* 环节设置弹窗 */
|
|
|
+.step-edit-list {
|
|
|
+ max-height: 360px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+.step-edit-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 12px 14px;
|
|
|
+ border-radius: 10px;
|
|
|
+ border: 1px solid rgba(148, 163, 184, 0.2);
|
|
|
+ margin-bottom: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+.step-edit-item.active {
|
|
|
+ border-color: #38bdf8;
|
|
|
+ background: rgba(56, 189, 248, 0.1);
|
|
|
+}
|
|
|
+.step-edit-dot {
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #475569;
|
|
|
+ color: #e2e8f0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-weight: 700;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.step-edit-item.active .step-edit-dot {
|
|
|
+ background: #38bdf8;
|
|
|
+}
|
|
|
+.step-edit-name {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+.step-edit-min {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+.agenda-editor {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.agenda-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+.banner-hint {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
/* 控制条 */
|
|
|
.control-bar {
|
|
|
position: fixed;
|
|
|
@@ -514,9 +1212,4 @@ export default {
|
|
|
.back-link {
|
|
|
color: #93c5fd !important;
|
|
|
}
|
|
|
-.banner-hint {
|
|
|
- font-size: 13px;
|
|
|
- color: #94a3b8;
|
|
|
- margin-bottom: 8px;
|
|
|
-}
|
|
|
</style>
|