information.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. import OSticky from '@/components/o-sticky'
  2. import {
  3. Button,
  4. closeToast,
  5. DatePicker,
  6. Grid,
  7. GridItem,
  8. Icon,
  9. Image,
  10. List,
  11. Picker,
  12. Popover,
  13. Popup,
  14. showFailToast,
  15. showLoadingToast,
  16. showSuccessToast,
  17. showToast
  18. } from 'vant'
  19. import { computed, defineComponent, nextTick, onMounted, reactive } from 'vue'
  20. import styles from './information.module.less'
  21. import iconSaveImage from '../images/icon-save-image.png'
  22. import iconWechat from '../images/icon-wechat.png'
  23. import OQrcode from '@/components/o-qrcode'
  24. import request from '@/helpers/request'
  25. import { useRoute } from 'vue-router'
  26. import { CountUp } from 'countup.js'
  27. import OEmpty from '@/components/o-empty'
  28. import dayjs from 'dayjs'
  29. import { promisefiyPostMessage, postMessage } from '@/helpers/native-message'
  30. import html2canvas from 'html2canvas'
  31. export default defineComponent({
  32. name: 'detail-information',
  33. props: {
  34. termTimes: {
  35. type: Object,
  36. default: {}
  37. }
  38. },
  39. setup(props) {
  40. const startTime = computed(() => props.termTimes.start)
  41. const endTime = computed(() => props.termTimes.end)
  42. const route = useRoute()
  43. const state = reactive({
  44. timeShow: false,
  45. currentData: [dayjs().year() + ''],
  46. actionText: '上学期',
  47. actionType: 'up',
  48. actionTerm: [
  49. { text: '上学期', color: 'var(--van-primary-color)', value: 'up' },
  50. { text: '下学期', value: 'down' }
  51. ],
  52. oPopover: false,
  53. check: [],
  54. checkboxRefs: [] as any,
  55. showQrcode: false,
  56. qrcodeUrl: '',
  57. isLoading: false,
  58. list: [] as any,
  59. listState: {
  60. dataShow: true, // 判断是否有数据
  61. loading: false,
  62. finished: false
  63. },
  64. params: {
  65. startTime: dayjs(dayjs().year() + startTime.value).format('YYYY-MM-DD HH:mm:ss'),
  66. endTime: dayjs(dayjs().year() + endTime.value)
  67. .subtract(1, 'day')
  68. .format('YYYY-MM-DD HH:mm:ss'),
  69. page: 1,
  70. rows: 20
  71. },
  72. statistics: {} as any,
  73. orchestraInfo: {} as any // 乐团详情
  74. })
  75. // 选择学期
  76. const onSelect = (val: any) => {
  77. state.actionTerm.forEach((item: any) => {
  78. item.color = null
  79. })
  80. val.color = 'var(--van-primary-color)'
  81. state.actionText = val.text
  82. state.actionType = val.value
  83. if (val.value === 'up') {
  84. state.params.startTime = dayjs(Number(state.currentData[0]) - 1 + startTime.value).format(
  85. 'YYYY-MM-DD HH:mm:ss'
  86. )
  87. state.params.endTime = dayjs(Number(state.currentData[0]) + endTime.value)
  88. .subtract(1, 'day')
  89. .format('YYYY-MM-DD HH:mm:ss')
  90. } else if (val.value === 'down') {
  91. state.params.startTime = dayjs(Number(state.currentData[0]) + endTime.value).format(
  92. 'YYYY-MM-DD HH:mm:ss'
  93. )
  94. state.params.endTime = dayjs(Number(state.currentData[0]) + startTime.value)
  95. .subtract(1, 'day')
  96. .format('YYYY-MM-DD HH:mm:ss')
  97. }
  98. onSearch()
  99. }
  100. const onConfirmDate = (date: any) => {
  101. state.currentData = date.selectedValues
  102. if (state.actionType === 'up') {
  103. state.params.startTime = dayjs(Number(state.currentData[0]) - 1 + startTime.value).format(
  104. 'YYYY-MM-DD HH:mm:ss'
  105. )
  106. state.params.endTime = dayjs(Number(state.currentData[0]) + endTime.value)
  107. .subtract(1, 'day')
  108. .format('YYYY-MM-DD HH:mm:ss')
  109. } else if (state.actionType === 'down') {
  110. state.params.startTime = dayjs(Number(state.currentData[0]) + endTime.value).format(
  111. 'YYYY-MM-DD HH:mm:ss'
  112. )
  113. state.params.endTime = dayjs(Number(state.currentData[0]) + startTime.value)
  114. .subtract(1, 'day')
  115. .format('YYYY-MM-DD HH:mm:ss')
  116. }
  117. state.timeShow = false
  118. onSearch()
  119. }
  120. const getDetails = async () => {
  121. try {
  122. const { data } = await request.get('/api-school/orchestra/detail/' + route.query.id)
  123. state.orchestraInfo = data || {}
  124. } catch {
  125. //
  126. }
  127. }
  128. const getStatistics = async () => {
  129. try {
  130. const { data } = await request.post('/api-school/classGroup/statistics', {
  131. data: {
  132. orchestraId: route.query.id,
  133. startTime: state.params.startTime,
  134. endTime: state.params.endTime
  135. }
  136. })
  137. state.statistics = data || {}
  138. initNumCountUp()
  139. } catch {
  140. //
  141. }
  142. }
  143. // 班级列表
  144. const getList = async () => {
  145. try {
  146. if (state.isLoading) return
  147. state.isLoading = true
  148. const res = await request.post('/api-school/classGroup/page', {
  149. data: {
  150. ...state.params,
  151. orchestraId: route.query.id
  152. }
  153. })
  154. state.listState.loading = false
  155. const result = res.data || {}
  156. // 处理重复请求数据
  157. if (state.list.length > 0 && result.current === 1) {
  158. return
  159. }
  160. const rows = result.rows || []
  161. state.list = state.list.concat(rows)
  162. state.listState.finished = result.current >= result.pages
  163. state.params.page = result.current + 1
  164. state.listState.dataShow = state.list.length > 0
  165. state.isLoading = false
  166. } catch {
  167. state.listState.dataShow = false
  168. state.listState.finished = true
  169. state.isLoading = false
  170. }
  171. }
  172. const onSearch = () => {
  173. state.params.page = 1
  174. state.list = []
  175. state.listState.dataShow = true // 判断是否有数据
  176. state.listState.loading = false
  177. state.listState.finished = false
  178. getList()
  179. }
  180. const initNumCountUp = () => {
  181. nextTick(() => {
  182. // 在读学生
  183. const statistics = state.statistics
  184. new CountUp('currentStudentNum', statistics.studentNum || 0).start()
  185. new CountUp('time1', statistics.attendanceRate || 0).start()
  186. new CountUp('time2', statistics.homeworkRate || 0).start()
  187. new CountUp('time3', statistics.homeworkQualifiedRate || 0).start()
  188. })
  189. }
  190. const imgs = reactive({
  191. saveLoading: false,
  192. image: null as any,
  193. shareLoading: false
  194. })
  195. const onSaveImg = async () => {
  196. // 判断是否在保存中...
  197. if (imgs.saveLoading) {
  198. return
  199. }
  200. imgs.saveLoading = true
  201. // 判断是否已经生成图片
  202. if (imgs.image) {
  203. saveImg()
  204. } else {
  205. const container: any = document.getElementById(`preview-container`)
  206. html2canvas(container, {
  207. allowTaint: true,
  208. useCORS: true,
  209. backgroundColor: null
  210. })
  211. .then(async (canvas) => {
  212. const url = canvas.toDataURL('image/png')
  213. imgs.image = url
  214. saveImg()
  215. })
  216. .catch(() => {
  217. closeToast()
  218. imgs.saveLoading = false
  219. })
  220. }
  221. }
  222. const onShare = () => {
  223. if (imgs.shareLoading) {
  224. return
  225. }
  226. imgs.shareLoading = true
  227. if (imgs.image) {
  228. openShare()
  229. } else {
  230. const container: any = document.getElementById(`preview-container`)
  231. html2canvas(container, {
  232. allowTaint: true,
  233. useCORS: true,
  234. backgroundColor: null
  235. })
  236. .then(async (canvas) => {
  237. const url = canvas.toDataURL('image/png')
  238. imgs.image = url
  239. openShare()
  240. })
  241. .catch(() => {
  242. closeToast()
  243. imgs.shareLoading = false
  244. })
  245. }
  246. }
  247. const openShare = () => {
  248. const image = imgs.image
  249. setTimeout(() => {
  250. imgs.shareLoading = false
  251. }, 100)
  252. if (image) {
  253. postMessage(
  254. {
  255. api: 'shareTripartite',
  256. content: {
  257. title: '',
  258. desc: '',
  259. image,
  260. video: '',
  261. type: 'image',
  262. // button: ['copy']
  263. shareType: 'wechat'
  264. }
  265. },
  266. (res: any) => {
  267. if (res && res.content) {
  268. showToast(res.content.message || (res.content.status ? '分享成功' : '分享失败'))
  269. }
  270. }
  271. )
  272. }
  273. }
  274. const saveImg = async () => {
  275. showLoadingToast({ message: '图片生成中...', forbidClick: true })
  276. setTimeout(() => {
  277. imgs.saveLoading = false
  278. }, 100)
  279. const res = await promisefiyPostMessage({
  280. api: 'savePicture',
  281. content: {
  282. base64: imgs.image
  283. }
  284. })
  285. if (res?.content?.status === 'success') {
  286. showSuccessToast('保存成功')
  287. state.showQrcode = false
  288. } else {
  289. showFailToast('保存失败')
  290. }
  291. }
  292. onMounted(() => {
  293. getDetails()
  294. getStatistics()
  295. getList()
  296. })
  297. return () => (
  298. <>
  299. <div style={{ padding: '12px 13px 16px', background: '#F8F8F8' }}>
  300. <div class={styles.searchBand} onClick={() => (state.timeShow = true)}>
  301. {state.currentData[0]}年 <Icon name={state.timeShow ? 'arrow-up' : 'arrow-down'} />
  302. </div>
  303. <Popover
  304. v-model:show={state.oPopover}
  305. actions={state.actionTerm}
  306. showArrow={false}
  307. placement="bottom"
  308. offset={[0, 12]}
  309. style={{ zIndex: '9999' }}
  310. onSelect={onSelect}
  311. >
  312. {{
  313. reference: () => (
  314. <div class={styles.searchBand} style="margin-left: 16px">
  315. {state.actionText} <Icon name={state.oPopover ? 'arrow-up' : 'arrow-down'} />
  316. </div>
  317. )
  318. }}
  319. </Popover>
  320. </div>
  321. <Grid border={false} class={styles.gridContainer}>
  322. <GridItem>
  323. <p class={[styles.title, styles.red]}>
  324. <span id="currentStudentNum">{state.statistics.studentNum || 0}</span>
  325. <i>名</i>
  326. </p>
  327. <p class={styles.name}>在读学生</p>
  328. </GridItem>
  329. <GridItem>
  330. <p class={[styles.title, styles.red]}>
  331. <span id="time1">{state.statistics.attendanceRate || 0}</span>%
  332. </p>
  333. <p class={styles.name}>到课率</p>
  334. </GridItem>
  335. <GridItem>
  336. <p class={[styles.title, styles.red]}>
  337. <span id="time2">{state.statistics.homeworkRate || 0}</span>%
  338. </p>
  339. <p class={styles.name}>作业提交率</p>
  340. </GridItem>
  341. <GridItem>
  342. <p class={[styles.title, styles.red]}>
  343. <span id="time3">{state.statistics.homeworkQualifiedRate || 0}</span>%
  344. </p>
  345. <p class={styles.name}>练习合格率</p>
  346. </GridItem>
  347. </Grid>
  348. {state.listState.dataShow ? (
  349. <List
  350. v-model:loading={state.listState.loading}
  351. finished={state.listState.finished}
  352. finishedText=" "
  353. class={[styles.liveList]}
  354. onLoad={getList}
  355. immediateCheck={false}
  356. >
  357. {state.list.map((item: any) => (
  358. <div class={[styles.gridContainer, styles.gridClass]}>
  359. <div class={styles.className}>
  360. <i class={styles.line}></i>
  361. {item.name}
  362. </div>
  363. <Grid border={false} columnNum={3}>
  364. <GridItem>
  365. <p class={styles.title}>{item.preStudentNum || 0}</p>
  366. <p class={styles.name}>在读学生</p>
  367. </GridItem>
  368. <GridItem>
  369. <p class={[styles.title, styles.teacher, 'van-ellipsis']}>
  370. {item.teacherName || '-'}
  371. </p>
  372. <p class={styles.name}>伴学指导</p>
  373. </GridItem>
  374. <GridItem>
  375. <p class={styles.title}>
  376. {item.completeCourseScheduleNum || 0}/{item.courseScheduleNum || 0}
  377. </p>
  378. <p class={styles.name}>课时</p>
  379. </GridItem>
  380. </Grid>
  381. </div>
  382. ))}
  383. </List>
  384. ) : (
  385. <OEmpty btnStatus={false} tips="暂无班级" />
  386. )}
  387. {/* */}
  388. {state.orchestraInfo.canSignUp && (
  389. <OSticky position="bottom">
  390. <div class={'btnGroup'}>
  391. <Button
  392. round
  393. block
  394. type="primary"
  395. onClick={() => {
  396. state.showQrcode = true
  397. state.qrcodeUrl =
  398. window.location.origin + '/orchestra-student/#/preApply?id=' + route.query.id
  399. }}
  400. >
  401. 报名二维码
  402. </Button>
  403. </div>
  404. </OSticky>
  405. )}
  406. <Popup
  407. v-model:show={state.showQrcode}
  408. position="bottom"
  409. style={{ background: 'transparent' }}
  410. // safeAreaInsetBottom={true}
  411. >
  412. <div class={styles.codeContainer}>
  413. <div class={styles.codeImg} id="preview-container">
  414. <div class={styles.codeContent}>
  415. <h2 class={styles.codeTitle}>乐团报名</h2>
  416. <div class={[styles.codeName, 'van-ellipsis']}>{state.orchestraInfo.name}</div>
  417. <div class={styles.codeQr}>
  418. <OQrcode text={state.qrcodeUrl} size={'400'} />
  419. </div>
  420. <div style={{ textAlign: 'center' }}>
  421. <span class={styles.codeBtnText}>扫描上方二维码完成资料填写</span>
  422. </div>
  423. <div class={styles.codeTips}>二维码将在两小时后失效,请及时登记</div>
  424. </div>
  425. </div>
  426. <div class={styles.codeBottom}>
  427. <Icon
  428. name="cross"
  429. size={22}
  430. class={styles.close}
  431. color="#666"
  432. onClick={() => (state.showQrcode = false)}
  433. />
  434. <h3 class={styles.title}>
  435. <i></i>分享方式
  436. </h3>
  437. <Grid columnNum={2} border={false}>
  438. <GridItem onClick={onSaveImg}>
  439. {{
  440. icon: () => <Image class={styles.shareImg} src={iconSaveImage} />,
  441. text: () => <div class={styles.shareText}>保存图片</div>
  442. }}
  443. </GridItem>
  444. <GridItem onClick={onShare}>
  445. {{
  446. icon: () => <Image class={styles.shareImg} src={iconWechat} />,
  447. text: () => <div class={styles.shareText}>微信</div>
  448. }}
  449. </GridItem>
  450. </Grid>
  451. </div>
  452. </div>
  453. </Popup>
  454. <Popup v-model:show={state.timeShow} position="bottom" round>
  455. <DatePicker
  456. v-model={state.currentData}
  457. columnsType={['year']}
  458. onConfirm={onConfirmDate}
  459. onCancel={() => (state.timeShow = false)}
  460. />
  461. </Popup>
  462. </>
  463. )
  464. }
  465. })