trainingSignIn.vue 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183
  1. <!-- 培训详情-扫码签到跳转至此 -->
  2. <template>
  3. <view class="training-signin-page">
  4. <!-- 顶部导航 -->
  5. <!-- <view class="topbar">
  6. <view class="topbar-back" @click="goBack">
  7. <text class="back-icon">‹</text>
  8. <text>返回</text>
  9. </view>
  10. <view class="topbar-title">培训详情</view>
  11. <view v-if="userType === 1" class="teacher-topbar">
  12. <text class="publish-label" :style="{ color: isPublished ? '#52c41a' : '#8c8c8c' }">{{ isPublished ? '已发布' : '未发布' }}</text>
  13. <view class="publish-toggle" :class="{ active: isPublished }" @click="togglePublish">
  14. <view class="publish-knob"></view>
  15. </view>
  16. </view>
  17. </view> -->
  18. <!-- 滚动内容 -->
  19. <scroll-view scroll-y @scrolltolower="scrollGet" class="scroll-content">
  20. <!-- 状态标签 -->
  21. <view class="status-bar">
  22. <text class="status-tag" :class="statusClass">{{ statusText }}</text>
  23. <text class="tag tag-blue">{{ detail.trainingModeName || '知识讲座' }}</text>
  24. </view>
  25. <!-- 标题 -->
  26. <view class="title">{{ detail.trainingName }}</view>
  27. <!-- 信息卡片 -->
  28. <view class="info-card">
  29. <view class="info-row">
  30. <text class="info-label">培训时间</text>
  31. <text class="info-value">{{ detail.startDate }} 至 {{ detail.endDate }}</text>
  32. </view>
  33. <view class="info-row">
  34. <text class="info-label">培训地点</text>
  35. <!-- <text class="info-value">📍 {{ detail.locationText || '-' }}</text> -->
  36. <text class="info-value">{{ detail.locationText || '-' }}</text>
  37. </view>
  38. <view class="info-row">
  39. <text class="info-label">组织单位</text>
  40. <!-- <text class="info-value">{{ detail.categoryName || '-' }}</text> -->
  41. <text class="info-value">{{ detail.orgDeptName || '-' }}</text>
  42. </view>
  43. <view class="info-row">
  44. <text class="info-label">主讲人</text>
  45. <text class="info-value">{{ detail.speakerName || '-' }}</text>
  46. </view>
  47. <view class="info-row">
  48. <text class="info-label">培训时长</text>
  49. <!-- <text class="info-value">{{ trainingDurationMinutes ? trainingDurationMinutes + ' 分钟' : '-' }}</text> -->
  50. <text class="info-value">{{ detail.trainingDurationMinutes ? detail.trainingDurationMinutes + ' 分钟' : '-' }}</text>
  51. </view>
  52. <view class="info-row">
  53. <text class="info-label">可得学时</text>
  54. <text class="info-value info-value-blue">+ {{ detail.creditHours || 0 }} 学时</text>
  55. </view>
  56. <view class="info-row last">
  57. <text class="info-label">可得积分</text>
  58. <text class="info-value info-value-orange">+ {{ detail.points || 0 }} 奖励分</text>
  59. </view>
  60. </view>
  61. <!-- 签到二维码(老师端 + 进行中) -->
  62. <view v-if="userType == 1 && trainingStatus === 'active'" class="qr-section">
  63. <text class="qr-title">签到二维码</text>
  64. <view class="qr-box" @click="openQrModal">
  65. <uqrcode
  66. v-show="false"
  67. ref="qrcode"
  68. canvas-id="qrcode"
  69. :value="qrValue"
  70. :size="200"
  71. @complete="onQrcodeComplete">
  72. </uqrcode>
  73. <!-- 用于展示生成的二维码图片 -->
  74. <image v-if="qrImagePath" :src="qrImagePath" mode="widthFix" style="width: 200px; height: 200px;"></image>
  75. <!-- <image class="qr-image" src="/static/qr-placeholder.png" mode="aspectFit"></image> -->
  76. </view>
  77. <text class="qr-hint">学员扫码签到 · 点击可放大</text>
  78. </view>
  79. <!-- 签到人数统计(老师端) -->
  80. <view v-if="userType == 1" class="signin-stats">
  81. <text class="stats-title">签到统计</text>
  82. <view class="stats-grid">
  83. <view class="stats-card stats-card-blue">
  84. <text class="stats-num">{{ detail.totalCount || 0 }}</text>
  85. <text class="stats-label">总人数</text>
  86. </view>
  87. <view class="stats-card stats-card-green">
  88. <text class="stats-num">{{ detail.signedCount || 0 }}</text>
  89. <text class="stats-label">已签到</text>
  90. </view>
  91. <view class="stats-card stats-card-orange">
  92. <text class="stats-num">{{ detail.notSignedCount || 0 }}</text>
  93. <text class="stats-label">未签到</text>
  94. </view>
  95. </view>
  96. <!-- 进度条 -->
  97. <view class="progress-bar">
  98. <view class="progress-fill" :style="{ width: (detail.signRate || 0) + '%' }"></view>
  99. </view>
  100. <text class="progress-text">签到率 {{ detail.signRate || 0 }}%</text>
  101. </view>
  102. <!-- 签到区域(学员端 + 进行中) -->
  103. <view v-if="userType == 2 && !signType" class="signin-section">
  104. <text class="signin-title">现场签到</text>
  105. <view class="signin-btn" @click="openScanModal">
  106. <text class="signin-icon"></text>
  107. <text class="signin-btn-text">点击签到</text>
  108. </view>
  109. <view class="signin-status" :class="{ signed: isSigned }">
  110. {{ isSigned ? '✓ 已签到' : '● 未签到' }}
  111. </view>
  112. </view>
  113. <!-- 培训证明上传区域(学员端 + 进行中) -->
  114. <view v-if="userType == 2 && !upType && proofRequired" class="upload-section">
  115. <view class="upload-header">
  116. <text class="upload-icon">🖼️</text>
  117. <text class="upload-title">上传培训证明</text>
  118. <text class="upload-hint">(如签到截图、现场照片等)</text>
  119. </view>
  120. <!-- 已选图片预览 -->
  121. <view class="upload-preview">
  122. <view v-for="(img, idx) in previewImages" :key="idx" class="preview-item">
  123. <image class="preview-img" :src="baseUrl+img" mode="aspectFill" @click="previewImage(img)"></image>
  124. <view class="preview-del" @click.stop="removeImage(idx)">×</view>
  125. </view>
  126. </view>
  127. <!-- 上传按钮 -->
  128. <view class="upload-actions">
  129. <view v-if="previewImages.length < 5" class="upload-btn" @click="chooseImage">
  130. <text class="upload-btn-icon">+</text>
  131. <text>选择图片</text>
  132. </view>
  133. <view v-if="previewImages.length > 0" class="submit-btn" @click="submitProof">提交证明</view>
  134. </view>
  135. <text class="upload-tip">支持 JPG、PNG 格式,单张不超过 10MB,最多上传 5 张</text>
  136. </view>
  137. <!-- 已完成状态展示 -->
  138. <view v-if="signType">
  139. <!-- 完成状态提示 -->
  140. <view class="done-banner">
  141. <text class="done-icon">✅</text>
  142. <view class="done-info">
  143. <!-- <text class="done-title">培训已完成</text>
  144. <text class="done-desc">已签到并获得 {{ detail.creditHours || 0 }} 学时 · +{{ detail.points || 0 }} 奖励分</text> -->
  145. <text class="done-title">签到已完成</text>
  146. <!-- <text class="done-desc">已签到并获得 {{ detail.creditHours || 0 }} 学时 · +{{ detail.points || 0 }} 奖励分</text> -->
  147. </view>
  148. </view>
  149. </view>
  150. <view v-if="upType">
  151. <!-- 已上传培训证明 -->
  152. <view v-if="previewImages.length > 0" class="done-images-card">
  153. <view class="done-images-header">
  154. <text class="done-images-icon">🖼️</text>
  155. <text class="done-images-title">培训证明图片</text>
  156. </view>
  157. <view class="done-images-grid">
  158. <view v-for="(img, idx) in previewImages" :key="idx" class="done-image-item" @click="previewImage(img)">
  159. <image class="done-image" :src="img" mode="aspectFill"></image>
  160. </view>
  161. </view>
  162. <text class="done-images-tip">共 {{ previewImages.length }} 张</text>
  163. </view>
  164. </view>
  165. <!-- 签到名单(老师端) -->
  166. <view v-if="userType == 1" class="roster-section">
  167. <!-- Tab -->
  168. <view class="roster-tabs">
  169. <view class="roster-tab" :class="{ active: activeRosterTab === 1 }" @click="switchRosterTab(1)">
  170. 已签到 <text class="tab-badge tab-badge-green">{{signedCount}}</text>
  171. </view>
  172. <view class="roster-tab" :class="{ active: activeRosterTab === 0 }" @click="switchRosterTab(0)">
  173. 未签到 <text class="tab-badge tab-badge-orange">{{notSignedCount}}</text>
  174. </view>
  175. </view>
  176. <!-- 已签到名单 -->
  177. <view v-if="activeRosterTab === 1" class="roster-panel">
  178. <view v-for="(item, idx) in signedList" :key="idx" class="roster-item roster-item-green">
  179. <view class="roster-avatar" style="background: linear-gradient(135deg, #1677ff, #4096ff);">{{ item.userName[0] }}</view>
  180. <view class="roster-info">
  181. <text class="roster-name">{{ item.userName }}</text>
  182. <text class="roster-meta">{{ item.account }} · {{ item.deptName }}</text>
  183. </view>
  184. <view class="roster-status">
  185. <text class="status-text-green">✓ 已签到</text>
  186. <text class="status-time">{{ item.signTime }}</text>
  187. </view>
  188. </view>
  189. <text class="roster-more">…… 共{{signedCount}}人已签到 ……</text>
  190. </view>
  191. <!-- 未签到名单 -->
  192. <view v-if="activeRosterTab === 0" class="roster-panel">
  193. <view v-for="(item, idx) in unsignedList" :key="idx" class="roster-item roster-item-orange">
  194. <view class="roster-avatar" style="background: linear-gradient(135deg, #1677ff, #4096ff);">{{ item.userName[0] }}</view>
  195. <view class="roster-info">
  196. <text class="roster-name">{{ item.userName }}</text>
  197. <text class="roster-meta">{{ item.account }} · {{ item.deptName }}</text>
  198. </view>
  199. <view class="roster-status">
  200. <text class="status-text-orange">● 未签到</text>
  201. </view>
  202. </view>
  203. <text class="roster-more">…… 共{{notSignedCount}}人未签到 ……</text>
  204. </view>
  205. </view>
  206. </scroll-view>
  207. </view>
  208. </template>
  209. <script>
  210. import { config } from '@/api/request/config.js'
  211. import {
  212. examElTrainingTaskProgressDetail,examElTrainingTaskSignInList,
  213. examUserTrainingDetail,examUserTrainingSignIn,examUserTrainingProofUpload,
  214. } from '@/pages_basics/api/index.js'
  215. export default {
  216. data() {
  217. return {
  218. baseUrl:config.base_url,
  219. id:'',
  220. userType: 0, // 1老师 2学生(后续自行调试切换)
  221. detail: {},
  222. isPublished: true,
  223. activeRosterTab: 1,
  224. previewImages: [], // 待上传图片预览
  225. currentUserId: null, // TODO: 当前登录用户ID,需从登录态获取
  226. // 已签到名单
  227. signedList: [],
  228. // 未签到名单
  229. unsignedList: [],
  230. signedCount:0,
  231. notSignedCount:0,
  232. page:1,
  233. pageSize:10,
  234. total:0,
  235. getDataType:false,
  236. //二维码
  237. qrValue: 'https://uniapp.dcloud.io/', // 二维码内容
  238. qrImagePath: '' ,// 生成的二维码图片路径
  239. //签到状态
  240. signType:false,
  241. //上传状态
  242. upType:false,
  243. //开启签到上传状态
  244. proofRequired:false,
  245. }
  246. },
  247. computed: {
  248. // 培训状态:直接取接口返回的 status(1-未开始 2-进行中 3-已结束)
  249. trainingStatus() {
  250. const status = this.detail.status
  251. if (status === 1) return 'upcoming'
  252. if (status === 3) return 'done'
  253. return 'active'
  254. },
  255. // 状态标签文字
  256. statusText() {
  257. return this.detail.statusName || '● 进行中'
  258. },
  259. // 状态标签样式类
  260. statusClass() {
  261. const map = {
  262. upcoming: 'status-upcoming',
  263. active: 'status-active',
  264. done: 'status-done'
  265. }
  266. return map[this.trainingStatus] || 'status-active'
  267. },
  268. // 格式化培训时间显示
  269. formatTime() {
  270. const { startDate, endDate } = this.detail
  271. if (!startDate || !endDate) return '-'
  272. const datePart = startDate.split(' ')[0]
  273. const startTime = startDate.split(' ')[1].substring(0, 5)
  274. const endTime = endDate.split(' ')[1].substring(0, 5)
  275. return `${datePart} ${startTime} – ${endTime}`
  276. },
  277. // 培训时长(分钟):根据起止时间计算
  278. trainingDurationMinutes() {
  279. const { startDate, endDate } = this.detail
  280. if (!startDate || !endDate) return 0
  281. const start = new Date(startDate.replace(' ', 'T')).getTime()
  282. const end = new Date(endDate.replace(' ', 'T')).getTime()
  283. const diff = end - start
  284. return diff > 0 ? Math.round(diff / (1000 * 60)) : 0
  285. },
  286. // 当前用户在 records 中的记录
  287. myRecord() {
  288. if (!this.detail.records || !this.currentUserId) return null
  289. return this.detail.records.find(r => r.userId === this.currentUserId) || null
  290. },
  291. // 是否已签到(线下 status === 1 表示已签到)
  292. isSigned() {
  293. return this.myRecord && this.myRecord.status === 1
  294. },
  295. },
  296. onLoad(option) {
  297. this.qrValue = uni.getStorageSync('codeOnlineAdd') +'?code='+option.code+'&type=offlineTraining',
  298. this.userType = uni.getStorageSync('userType');
  299. if (option.code) {
  300. this.$set(this,'id',option.code);
  301. this.examElTrainingTaskProgressDetail()
  302. this.examElTrainingTaskSignInList()
  303. if(this.userType == 2){
  304. this.examUserTrainingDetail();
  305. }
  306. }
  307. },
  308. methods: {
  309. onQrcodeComplete(res) {
  310. if (res.success) {
  311. // 生成成功后,调用 toTempFilePath 获取图片临时路径
  312. this.$refs.qrcode.toTempFilePath({
  313. success: (res) => {
  314. // console.log('二维码图片路径:', res.tempFilePath);
  315. this.qrImagePath = res.tempFilePath; // 将路径赋值给 data,用于页面展示
  316. },
  317. fail: (err) => {
  318. console.error('获取图片路径失败:', err);
  319. }
  320. });
  321. }
  322. },
  323. //查询培训详情
  324. async examElTrainingTaskProgressDetail() {
  325. const {
  326. data
  327. } = await examElTrainingTaskProgressDetail({
  328. id: this.id
  329. })
  330. if (data.code == 200) {
  331. this.detail = data.data || {};
  332. }
  333. },
  334. //查询用户培训信息
  335. async examUserTrainingDetail() {
  336. const {
  337. data
  338. } = await examUserTrainingDetail({
  339. id: this.id
  340. })
  341. if (data.code == 200) {
  342. this.$set(this,'signType',data.data.signStatus==1?true:false);
  343. this.$set(this,'upType',data.data.proofImages[0]?true:false);
  344. this.$set(this,'proofRequired',data.data.proofRequired==1?true:false);
  345. this.$set(this,'previewImages',data.data.proofImages);
  346. }
  347. },
  348. //滚动加载事件
  349. scrollGet() {
  350. let self = this;
  351. console.log('self.total',self.total);
  352. console.log('self.pageSize',self.pageSize);
  353. console.log('self.page',self.page);
  354. if (self.total / self.pageSize <= self.page) {
  355. this.$set(this, 'getDataType', true);
  356. } else {
  357. this.page += 1;
  358. this.$nextTick(() => {
  359. this.examElTrainingTaskSignInList();
  360. })
  361. }
  362. },
  363. async examElTrainingTaskSignInList() {
  364. let self = this;
  365. const {
  366. data
  367. } = await examElTrainingTaskSignInList({
  368. id: this.id,
  369. signStatus:this.activeRosterTab,
  370. page:this.page,
  371. pageSize:this.pageSize,
  372. })
  373. if (data.code == 200) {
  374. this.$set(this,'signedCount',data.data.signedCount);
  375. this.$set(this,'notSignedCount',data.data.notSignedCount);
  376. if(self.page == 1){
  377. if(this.activeRosterTab == 1){
  378. this.signedList = data.data.records;
  379. }else{
  380. this.unsignedList = data.data.records;
  381. }
  382. this.total = data.data.pageTotal;
  383. if (data.data.total / self.pageSize <= self.page) {
  384. this.$set(this, 'getDataType', true);
  385. }
  386. }else{
  387. if(this.activeRosterTab == 1){
  388. this.signedList = [...this.signedList, ...data.data.records]
  389. }else{
  390. this.unsignedList = [...this.unsignedList, ...data.data.records]
  391. }
  392. this.total = data.data.pageTotal;
  393. if (data.data.total / self.pageSize <= self.page) {
  394. this.$set(this, 'getDataType', true);
  395. }
  396. }
  397. }
  398. },
  399. // 返回上一页
  400. goBack() {
  401. uni.navigateBack()
  402. },
  403. // 发布开关切换(老师端)
  404. togglePublish() {
  405. this.isPublished = !this.isPublished
  406. uni.showToast({
  407. title: this.isPublished ? '培训已发布,学员可见' : '培训已取消发布',
  408. icon: 'none'
  409. })
  410. },
  411. // 打开扫码
  412. openScanModal() {
  413. this.examUserTrainingSignIn()
  414. },
  415. async examUserTrainingSignIn() {
  416. const {
  417. data
  418. } = await examUserTrainingSignIn({
  419. taskId: this.id
  420. })
  421. if (data.code == 200) {
  422. uni.showToast({ title: data.message, icon: 'none' })
  423. this.examUserTrainingDetail();
  424. }
  425. },
  426. // 扫码完成:更新本地 records 中当前用户状态
  427. completeScan() {
  428. if (this.myRecord) {
  429. this.$set(this.myRecord, 'status', 1)
  430. this.$set(this.myRecord, 'statusName', '已签到')
  431. }
  432. uni.showToast({ title: '签到成功!', icon: 'success' })
  433. },
  434. // 打开二维码放大
  435. openQrModal() {
  436. if (!this.qrImagePath) return;
  437. uni.previewImage({
  438. current: this.qrImagePath, // 当前显示图片的链接
  439. urls: [this.qrImagePath], // 需要预览的图片链接列表(这里只有一张,所以包在数组里)
  440. fail: (err) => {
  441. console.error('预览失败', err);
  442. }
  443. });
  444. },
  445. // 选择图片
  446. chooseImage() {
  447. let self = this;
  448. const remaining = 5 - this.previewImages.length
  449. if (remaining <= 0) {
  450. uni.showToast({ title: '最多上传 5 张图片', icon: 'none' })
  451. return
  452. }
  453. uni.chooseImage({
  454. count: remaining,
  455. sizeType: ['compressed'],
  456. sourceType: ['album', 'camera'],
  457. success: (res) => {
  458. const tempFiles = res.tempFiles || []
  459. const oversized = tempFiles.filter(f => f.size > 10 * 1024 * 1024)
  460. if (oversized.length) {
  461. uni.showToast({ title: '图片大小不能超过 10MB', icon: 'none' })
  462. return
  463. }
  464. let tempFilePaths = res.tempFilePaths[0];
  465. self.uploadImg(tempFilePaths);
  466. }
  467. })
  468. },
  469. async uploadImg(tempFilePaths){
  470. let self = this;
  471. uni.showLoading({
  472. title: '上传中',
  473. mask: true
  474. });
  475. uni.uploadFile({
  476. url: config.base_url + '/system/file/upload', //仅为示例,非真实的接口地址
  477. header:{'Authorization':uni.getStorageSync('token')},
  478. filePath: tempFilePaths,
  479. name: 'file',
  480. formData: {
  481. 'user': 'test'
  482. },
  483. success: (uploadFileRes) => {
  484. let res = JSON.parse(uploadFileRes.data);
  485. if(res.code == 200){
  486. this.previewImages.push(res.data.url);
  487. }else{
  488. uni.showToast({
  489. title: res.msg,
  490. icon:"none",
  491. mask:true,
  492. duration: 2000
  493. });
  494. }
  495. },
  496. fail: err => {},
  497. complete: () => {
  498. uni.hideLoading()
  499. }
  500. });
  501. },
  502. // 移除预览图
  503. removeImage(idx) {
  504. this.previewImages.splice(idx, 1)
  505. },
  506. // 提交证明
  507. submitProof() {
  508. if (!this.previewImages.length) {
  509. uni.showToast({ title: '请先选择图片', icon: 'none' })
  510. return
  511. }
  512. this.examUserTrainingProofUpload();
  513. },
  514. //上传培训照片
  515. async examUserTrainingProofUpload() {
  516. const {
  517. data
  518. } = await examUserTrainingProofUpload({
  519. taskId: this.id,
  520. proofImagePath:this.previewImages+'',
  521. })
  522. if (data.code == 200) {
  523. uni.showToast({ title: data.message, icon: 'none' })
  524. this.examUserTrainingDetail();
  525. }
  526. },
  527. // 预览图片
  528. previewImage(url) {
  529. const urls = this.previewImages.length > 0 ? this.previewImages : this.myProofImages
  530. uni.previewImage({
  531. current: url,
  532. urls: urls.length > 0 ? urls : [url]
  533. })
  534. },
  535. // 名单Tab切换
  536. switchRosterTab(idx) {
  537. this.activeRosterTab = idx
  538. this.examElTrainingTaskSignInList();
  539. }
  540. }
  541. }
  542. </script>
  543. <style lang="stylus" scoped>
  544. // ========== 基础变量 ==========
  545. $blue = #1857d4
  546. $blue-light = #eef3fd
  547. $green = #10a37f
  548. $green-dark = #0d8f6f
  549. $green-light = #edfaf6
  550. $orange = #e67e22
  551. $orange-light = #fef6ec
  552. $red = #e53e3e
  553. $gray = #8896a7
  554. $gray-light = #f4f6fa
  555. $gray-border = #dde3ed
  556. $text-primary = #1a2233
  557. $text-hint = #8896a7
  558. $bg = #f0f4fb
  559. $white = #ffffff
  560. // ========== 页面布局 ==========
  561. .training-signin-page
  562. display flex
  563. flex-direction column
  564. height 100vh
  565. background $bg
  566. // ========== 顶部导航 ==========
  567. .topbar
  568. background $white
  569. padding 13px 16px
  570. display flex
  571. align-items center
  572. justify-content space-between
  573. border-bottom 1px solid $gray-border
  574. flex-shrink 0
  575. box-shadow 0 1px 6px rgba(24, 87, 212, .07)
  576. .topbar-back
  577. display flex
  578. align-items center
  579. gap 4px
  580. color $blue
  581. font-size 13px
  582. padding 5px 10px
  583. border 1px solid rgba(24, 87, 212, .25)
  584. border-radius 8px
  585. background #f5f8ff
  586. font-weight 500
  587. .back-icon
  588. font-size 18px
  589. line-height 1
  590. .topbar-title
  591. font-size 16px
  592. font-weight 700
  593. color $text-primary
  594. letter-spacing .3px
  595. // 老师端发布开关
  596. .teacher-topbar
  597. display flex
  598. align-items center
  599. gap 6px
  600. .publish-label
  601. font-size 11px
  602. font-weight 500
  603. white-space nowrap
  604. .publish-toggle
  605. width 40px
  606. height 22px
  607. border-radius 11px
  608. background #d9d9d9
  609. position relative
  610. transition background .2s
  611. &.active
  612. background #52c41a
  613. .publish-knob
  614. left auto
  615. right 4px
  616. .publish-knob
  617. position absolute
  618. top 4px
  619. left 4px
  620. width 14px
  621. height 14px
  622. border-radius 50%
  623. background #fff
  624. transition left .2s, right .2s
  625. // ========== 滚动内容区 ==========
  626. .scroll-content
  627. flex 1
  628. overflow-y scroll;
  629. padding 12px
  630. box-sizing border-box
  631. // ========== 状态标签 ==========
  632. .status-bar
  633. display flex
  634. align-items center
  635. gap 8px
  636. margin-bottom 12px
  637. .status-tag
  638. font-size 11px
  639. border-radius 10px
  640. padding 3px 10px
  641. font-weight 500
  642. .status-active
  643. background #fff7e6
  644. color #fa8c16
  645. border 1px solid #ffd591
  646. .status-done
  647. background #f6ffed
  648. color #52c41a
  649. border 1px solid #b7eb8f
  650. .status-upcoming
  651. background #f5f5f5
  652. color #8c8c8c
  653. border 1px solid #d9d9d9
  654. .tag
  655. display inline-flex
  656. align-items center
  657. padding 2px 8px
  658. border-radius 20px
  659. font-size 11px
  660. font-weight 500
  661. .tag-blue
  662. background $blue-light
  663. color $blue
  664. // ========== 标题 ==========
  665. .title
  666. font-size 17px
  667. font-weight 700
  668. color #1a1a1a
  669. margin-bottom 16px
  670. // ========== 信息卡片 ==========
  671. .info-card
  672. border 1px solid #f0f0f0
  673. border-radius 10px
  674. overflow hidden
  675. margin-bottom 14px
  676. background $white
  677. .info-row
  678. display flex
  679. border-bottom 1px solid #f5f5f5
  680. &.last
  681. border-bottom none
  682. .info-label
  683. padding 10px 14px
  684. color #888
  685. background #fafafa
  686. width 30%
  687. font-size 13px
  688. white-space nowrap
  689. flex-shrink 0
  690. .info-value
  691. padding 10px 14px
  692. color #333
  693. font-size 13px
  694. font-weight 500
  695. flex 1
  696. .info-value-blue
  697. color #1677ff
  698. font-weight 600
  699. .info-value-orange
  700. color #fa8c16
  701. font-weight 600
  702. // ========== 二维码区域 ==========
  703. .qr-section
  704. border 1.5px solid #ffadd2
  705. border-radius 12px
  706. padding 16px 14px
  707. background linear-gradient(135deg, #fff0f6, #fff7e6)
  708. margin-bottom 14px
  709. text-align center
  710. .qr-title
  711. font-size 13px
  712. font-weight 600
  713. color #c41d7f
  714. margin-bottom 12px
  715. display block
  716. .qr-box
  717. display inline-block
  718. background #fff
  719. border-radius 12px
  720. padding 10px
  721. border 1.5px solid #ffadd2
  722. .qr-image
  723. width 120px
  724. height 120px
  725. display block
  726. .qr-hint
  727. font-size 12px
  728. color #ad6494
  729. margin-top 8px
  730. display block
  731. // ========== 签到统计 ==========
  732. .signin-stats
  733. margin-bottom 14px
  734. .stats-title
  735. font-size 13px
  736. font-weight 600
  737. color #1a1a1a
  738. margin-bottom 8px
  739. display block
  740. .stats-grid
  741. display flex
  742. gap 10px
  743. .stats-card
  744. flex 1
  745. border-radius 12px
  746. padding 14px 10px
  747. text-align center
  748. .stats-card-blue
  749. background linear-gradient(135deg, #e6f4ff, #bae0ff)
  750. border 1px solid #91caff
  751. .stats-num
  752. color #0958d9
  753. .stats-label
  754. color #4096ff
  755. .stats-card-green
  756. background linear-gradient(135deg, #f6ffed, #d9f7be)
  757. border 1px solid #b7eb8f
  758. .stats-num
  759. color #389e0d
  760. .stats-label
  761. color #52c41a
  762. .stats-card-orange
  763. background linear-gradient(135deg, #fff7e6, #ffe7ba)
  764. border 1px solid #ffd591
  765. .stats-num
  766. color #d46b08
  767. .stats-label
  768. color #fa8c16
  769. .stats-num
  770. font-size 28px
  771. font-weight 800
  772. line-height 1.1
  773. display block
  774. .stats-label
  775. font-size 11px
  776. margin-top 4px
  777. font-weight 500
  778. display block
  779. // 进度条
  780. .progress-bar
  781. margin-top 10px
  782. background #f0f0f0
  783. border-radius 20px
  784. height 8px
  785. overflow hidden
  786. .progress-fill
  787. height 100%
  788. background linear-gradient(90deg, #52c41a, #73d13d)
  789. border-radius 20px
  790. transition width .4s
  791. .progress-text
  792. font-size 11px
  793. color #888
  794. margin-top 4px
  795. text-align right
  796. display block
  797. // ========== 签到区域 ==========
  798. .signin-section
  799. border 1px solid #ffadd2
  800. border-radius 10px
  801. padding 14px
  802. background linear-gradient(135deg, #fff0f6, #fff7e6)
  803. text-align center
  804. margin-bottom 14px
  805. .signin-title
  806. font-size 13px
  807. font-weight 600
  808. color #c41d7f
  809. margin-bottom 10px
  810. display block
  811. .signin-btn
  812. width 72px
  813. height 72px
  814. border-radius 12px
  815. border 1.5px solid #eb2f96
  816. background #fff
  817. color #c41d7f
  818. display inline-flex
  819. flex-direction column
  820. align-items center
  821. justify-content center
  822. gap 4px
  823. margin-bottom 6px
  824. .signin-icon
  825. font-size 28px
  826. line-height 1
  827. .signin-btn-text
  828. font-size 10px
  829. font-weight 500
  830. .signin-status
  831. font-size 11px
  832. font-weight 500
  833. color #fa8c16
  834. &.signed
  835. color #52c41a
  836. // ========== 上传区域 ==========
  837. .upload-section
  838. border 1px solid #91caff
  839. border-radius 10px
  840. padding 14px
  841. background linear-gradient(135deg, #e6f4ff, #f0f9ff)
  842. margin-bottom 14px
  843. .upload-header
  844. display flex
  845. align-items center
  846. gap 6px
  847. margin-bottom 10px
  848. .upload-icon
  849. font-size 15px
  850. .upload-title
  851. font-size 13px
  852. font-weight 600
  853. color #1677ff
  854. .upload-hint
  855. font-size 11px
  856. color #999
  857. margin-left 2px
  858. .upload-preview
  859. display flex
  860. flex-wrap wrap
  861. gap 8px
  862. margin-bottom 10px
  863. .preview-item
  864. position relative
  865. width 80px
  866. height 80px
  867. border-radius 8px
  868. overflow hidden
  869. border 1px solid #91caff
  870. flex-shrink 0
  871. .preview-img
  872. width 100%
  873. height 100%
  874. display block
  875. .preview-del
  876. position absolute
  877. top 2px
  878. right 2px
  879. width 20px
  880. height 20px
  881. border-radius 50%
  882. background rgba(0, 0, 0, .5)
  883. color #fff
  884. font-size 14px
  885. line-height 20px
  886. text-align center
  887. .upload-actions
  888. display flex
  889. align-items center
  890. gap 10px
  891. .upload-btn
  892. display inline-flex
  893. align-items center
  894. gap 6px
  895. padding 8px 16px
  896. border-radius 8px
  897. border 1.5px dashed #4096ff
  898. background #fff
  899. color #1677ff
  900. font-size 13px
  901. font-weight 500
  902. .upload-btn-icon
  903. font-size 16px
  904. .submit-btn
  905. padding 8px 20px
  906. border-radius 8px
  907. background #1677ff
  908. color #fff
  909. font-size 13px
  910. font-weight 500
  911. .upload-tip
  912. font-size 11px
  913. color #aaa
  914. margin-top 8px
  915. display block
  916. // ========== 已完成区域 ==========
  917. .done-banner
  918. border 1px solid #b7eb8f
  919. border-radius 10px
  920. padding 12px 14px
  921. background #f6ffed
  922. display flex
  923. align-items center
  924. gap 10px
  925. margin-bottom 14px
  926. .done-icon
  927. font-size 22px
  928. .done-info
  929. flex 1
  930. .done-title
  931. font-size 13px
  932. font-weight 600
  933. color #389e0d
  934. display block
  935. .done-desc
  936. font-size 12px
  937. color #73d13d
  938. margin-top 2px
  939. display block
  940. .done-images-card
  941. border 1px solid #f0f0f0
  942. border-radius 10px
  943. padding 14px
  944. background $white
  945. .done-images-header
  946. display flex
  947. align-items center
  948. gap 6px
  949. margin-bottom 12px
  950. .done-images-icon
  951. font-size 15px
  952. .done-images-title
  953. font-size 13px
  954. font-weight 600
  955. color #1a1a1a
  956. .done-images-grid
  957. display flex
  958. flex-wrap wrap
  959. gap 8px
  960. .done-image-item
  961. position relative
  962. width 80px
  963. height 80px
  964. border-radius 8px
  965. overflow hidden
  966. border 1px solid #f0f0f0
  967. .done-image
  968. width 100%
  969. height 100%
  970. display block
  971. .done-images-tip
  972. font-size 11px
  973. color #bbb
  974. margin-top 10px
  975. display block
  976. // ========== 签到名单 ==========
  977. .roster-section
  978. margin-top 14px
  979. .roster-tabs
  980. display flex
  981. border-bottom 2px solid $gray-border
  982. margin-bottom 12px
  983. .roster-tab
  984. flex 1
  985. text-align center
  986. padding 9px 4px 8px
  987. font-size 13px
  988. color #888
  989. border-bottom 2px solid transparent
  990. margin-bottom -2px
  991. &.active
  992. color #1677ff
  993. font-weight 600
  994. border-bottom-color #1677ff
  995. .tab-badge
  996. border-radius 10px
  997. padding 1px 7px
  998. font-size 11px
  999. font-weight 600
  1000. color #fff
  1001. .tab-badge-green
  1002. background #52c41a
  1003. .tab-badge-orange
  1004. background #fa8c16
  1005. .roster-panel
  1006. display flex
  1007. flex-direction column
  1008. gap 8px
  1009. .roster-item
  1010. display flex
  1011. align-items center
  1012. gap 10px
  1013. border-radius 10px
  1014. padding 10px 12px
  1015. .roster-item-green
  1016. background #f6ffed
  1017. border 1px solid #d9f7be
  1018. .roster-item-orange
  1019. background #fff7e6
  1020. border 1px solid #ffe7ba
  1021. .roster-avatar
  1022. width 36px
  1023. height 36px
  1024. border-radius 50%
  1025. display flex
  1026. align-items center
  1027. justify-content center
  1028. color #fff
  1029. font-size 15px
  1030. font-weight 700
  1031. flex-shrink 0
  1032. .roster-info
  1033. flex 1
  1034. .roster-name
  1035. font-size 13px
  1036. font-weight 600
  1037. display block
  1038. .roster-meta
  1039. font-size 11px
  1040. color #888
  1041. margin-top 2px
  1042. display block
  1043. .roster-status
  1044. text-align right
  1045. .status-text-green
  1046. font-size 11px
  1047. color #52c41a
  1048. font-weight 500
  1049. display block
  1050. .status-text-orange
  1051. font-size 11px
  1052. color #fa8c16
  1053. font-weight 500
  1054. display block
  1055. .status-time
  1056. font-size 10px
  1057. color #aaa
  1058. margin-top 2px
  1059. display block
  1060. .roster-more
  1061. text-align center
  1062. padding 8px 0
  1063. color $text-hint
  1064. font-size 12px
  1065. display block
  1066. </style>