index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import {
  2. Button,
  3. Cell,
  4. DatetimePicker,
  5. Field,
  6. Popup,
  7. Radio,
  8. RadioGroup,
  9. Stepper,
  10. Tag,
  11. Toast
  12. } from 'vant'
  13. import { computed, defineComponent, onMounted, reactive, ref } from 'vue'
  14. import styles from './index.module.less'
  15. import iconTips from '../images/icon_tips.png'
  16. import { formatterDate } from '@/helpers/utils'
  17. import request from '@/helpers/request'
  18. import OrganSearch from '@/student/practice-class/model/organ-search'
  19. import dayjs from 'dayjs'
  20. import ColHeader from '@/components/col-header'
  21. import ColPopup from '@/components/col-popup'
  22. import SelectStudents, { IStudent } from './select-students'
  23. import CourseSchedule from './course-schedule'
  24. const fieldProps = {
  25. 'is-link': true,
  26. readonly: true,
  27. 'arrow-direction': 'down'
  28. }
  29. export default defineComponent({
  30. name: 'ClassArrangement',
  31. setup() {
  32. const dateShow = ref(false)
  33. const timeShow = ref(false)
  34. const voiceShow = ref(false)
  35. const selectStudentShow = ref(false)
  36. const confirmShow = ref(false)
  37. // 参数
  38. const params = reactive({
  39. courseName: '', // 课程名称
  40. classNum: 1, // 课时数
  41. singleClssTime: 45, //单课时长
  42. studentIds: [], //学员id集合
  43. timeList: [] as any, // 上课时间
  44. date: '',
  45. time: '',
  46. subjectId: 0,
  47. subjectName: '',
  48. week: '', // 周几
  49. isSkipHolidays: true
  50. })
  51. const startTime = ref('')
  52. const endTime = ref('')
  53. // 上课时间
  54. const timeScope = computed(() => {
  55. // if (startTime.value && endTime.value) {
  56. // let start = startTime.value.split(':')
  57. // let end = endTime.value.split(':')
  58. // let min = parseInt(start[0]) * 60 + parseInt(start[1])
  59. // let max = parseInt(end[0]) * 60 + parseInt(end[1])
  60. // // params.singleClssTime = max - min
  61. // return startTime.value + ' ~ ' + endTime.value
  62. // }
  63. // params.singleClssTime = 0
  64. return startTime.value
  65. })
  66. // 学员
  67. const students = ref<IStudent[]>([])
  68. // 设置学员
  69. const onSetStudents = (result: IStudent[]) => {
  70. students.value = result
  71. selectStudentShow.value = false
  72. }
  73. const onDeleteStudent = index => {
  74. const n = students.value.splice(index, 1)[0]
  75. studentRef?.value.onDelete(n)
  76. }
  77. const studentRef = ref('') as any
  78. // 训练声部
  79. const subjectList = ref<[]>([]) // 声部分类
  80. const getSubjectSelect = async () => {
  81. try {
  82. const res = await request.get('/api-teacher/subject/subjectSelect')
  83. subjectList.value = res.data || []
  84. } catch {}
  85. }
  86. //上课时间
  87. const startClassTime = ref<string>('')
  88. const endClassTime = ref<string>('')
  89. const getClassTime = async () => {
  90. try {
  91. const res = await request.get(
  92. '/api-teacher/sysConfig/queryByParamNameList',
  93. {
  94. params: {
  95. paramNames: 'course_start_setting,course_end_setting'
  96. }
  97. }
  98. )
  99. if (res.code === 200) {
  100. for (let i = 0, len = res.data.length; i < len; i++) {
  101. if (res.data[i].paramName === 'course_start_setting') {
  102. startClassTime.value = res.data[i].paramValue
  103. }
  104. if (res.data[i].paramName === 'course_end_setting') {
  105. endClassTime.value = res.data[i].paramValue
  106. }
  107. }
  108. }
  109. } catch (error) {}
  110. }
  111. onMounted(() => {
  112. getSubjectSelect()
  113. getClassTime()
  114. })
  115. //检查上课时间是否满足后台设置的最晚时间
  116. const checkClassTimeIsSatisfyLastTime = () => {
  117. const baseTime = dayjs()
  118. const _endTime = baseTime.set('hour', Number(startTime.value.split(':')[0]))
  119. .set('minute', Number(startTime.value.split(':')[1]))
  120. .add(params.singleClssTime, 'minute')
  121. const _endClassTime = baseTime.set('hour', Number(endClassTime.value.split(':')[0]))
  122. .set('minute', Number(endClassTime.value.split(':')[1]))
  123. // console.log(_endTime.format('HH:mm'),_endClassTime.format('HH:mm'))
  124. return {
  125. isOk: _endTime.isBefore(_endClassTime),
  126. _endClassTime: _endClassTime.format('HH:mm')
  127. }
  128. }
  129. //开始时间
  130. const onSetTime = async (time: string) => {
  131. console.log(time)
  132. params.time = time
  133. timeShow.value = false
  134. }
  135. const holidays = ref('') // 节假日
  136. const getHolidays = async (year: string) => {
  137. try {
  138. let result = await request.get(
  139. '/api-teacher/courseSchedule/selectHoliday',
  140. { params: { year } }
  141. )
  142. holidays.value = result?.data?.holidaysFestivalsJson || ''
  143. } catch (error) {}
  144. }
  145. // 计算日期
  146. const calcDate = () => {
  147. const timeList = [] as any
  148. const curriculumList = [] as any
  149. let total = 0
  150. let index = 0
  151. const selectWeekIndex = week[params.week]
  152. const endTime = dayjs()
  153. .set('hour', Number(startTime.value.split(':')[0]))
  154. .set('minute', Number(startTime.value.split(':')[1]))
  155. .add(params.singleClssTime, 'minute')
  156. .format('HH:mm')
  157. while (total < params.classNum) {
  158. const time = dayjs(params.date).add(index, 'day')
  159. index++
  160. const weekIndex = time.get('day')
  161. if (weekIndex !== selectWeekIndex) {
  162. continue
  163. }
  164. const year_month_date = time.format('YYYY-MM-DD')
  165. if (params.isSkipHolidays) {
  166. if (
  167. ![6, 0].includes(time.get('day')) &&
  168. !holidays.value.includes(year_month_date)
  169. ) {
  170. total++
  171. timeList.push({
  172. startTime: year_month_date + ` ${startTime.value}`,
  173. endTime: year_month_date + ` ${endTime}`
  174. })
  175. curriculumList.push(
  176. year_month_date + ` ${startTime.value} ~ ${endTime}`
  177. )
  178. }
  179. } else {
  180. total++
  181. timeList.push({
  182. startTime: year_month_date + ` ${startTime.value}`,
  183. endTime: year_month_date + ` ${endTime}`
  184. })
  185. curriculumList.push(
  186. year_month_date + ` ${startTime.value} ~ ${endTime}`
  187. )
  188. }
  189. }
  190. return { timeList, curriculumList }
  191. }
  192. const curriculum = ref<string[]>([])
  193. // 设置排课数据
  194. const setParmas = () => {
  195. if (!params.courseName) {
  196. Toast('请填写课程名称')
  197. return
  198. }
  199. if (!params.subjectId) {
  200. Toast('请选择训练声部')
  201. return
  202. }
  203. if (!params.singleClssTime) {
  204. Toast('请填写单课时时长')
  205. return
  206. }
  207. if (!params.date) {
  208. Toast('请选择开始日期')
  209. return
  210. }
  211. if (!params.week) {
  212. Toast('请选择循环周次')
  213. return
  214. }
  215. if (!startTime.value) {
  216. Toast('请选择上课时间')
  217. return
  218. }
  219. let checkData = checkClassTimeIsSatisfyLastTime()
  220. if (!checkData.isOk) {
  221. Toast(`上课结束时间不能晚于${checkData._endClassTime}`)
  222. return
  223. }
  224. if (!students.value.length) {
  225. Toast('请选择上课学员')
  226. return
  227. }
  228. const { timeList, curriculumList } = calcDate()
  229. params.timeList = timeList
  230. curriculum.value = curriculumList
  231. // console.log(curriculumList)
  232. confirmShow.value = true
  233. }
  234. // 排课
  235. const onCourseSchedule = async () => {
  236. try {
  237. let { code, data } = await request.post(
  238. '/api-teacher/courseSchedule/arrangeCourse',
  239. {
  240. data: {
  241. classNum: params.classNum, //课时数
  242. consumeTime: Math.ceil(
  243. students.value.length * params.classNum * params.singleClssTime
  244. ), //消耗时长
  245. courseName: params.courseName, //课程名称
  246. singleClssTime: params.singleClssTime, //单课时长
  247. studentIds: students.value.map(n => n.userId), //学员id集合
  248. subjectId: params.subjectId, //声部id
  249. timeList: params.timeList
  250. }
  251. }
  252. )
  253. if (code === 200) {
  254. confirmShow.value = false
  255. Toast('排课成功')
  256. }
  257. } catch (error) {}
  258. }
  259. const week = {
  260. 周一: 1,
  261. 周二: 2,
  262. 周三: 3,
  263. 周四: 4,
  264. 周五: 5,
  265. 周六: 6,
  266. 周日: 0
  267. }
  268. // console.log(Object.values(week))
  269. return () => {
  270. const minStartHour = startClassTime.value.split(':')[0] || ''
  271. const minStartMimute = startClassTime.value.split(':')[1] || ''
  272. return (
  273. <>
  274. <ColHeader />
  275. <div class={styles.container}>
  276. <Field
  277. label="课程名称"
  278. placeholder="请输入课程名称"
  279. v-model={params.courseName}
  280. />
  281. <Field
  282. label="训练声部"
  283. placeholder="请选择训练声部"
  284. {...fieldProps}
  285. modelValue={params.subjectName}
  286. onClick={() => (voiceShow.value = true)}
  287. />
  288. <Cell style={{ padding: 0 }}>
  289. <Field
  290. style={{ margin: 0 }}
  291. border={false}
  292. label="上课学员"
  293. placeholder="请选择上课学员"
  294. {...fieldProps}
  295. onClick={() => (selectStudentShow.value = true)}
  296. />
  297. {students.value.length ? (
  298. <div class={styles.tags}>
  299. {students.value.map((n: IStudent, index: number) => (
  300. <Tag closeable onClose={() => onDeleteStudent(index)}>
  301. {n.userName}
  302. </Tag>
  303. ))}
  304. </div>
  305. ) : null}
  306. </Cell>
  307. <Field
  308. label="课时数"
  309. placeholder="请输入课时数"
  310. v-slots={{
  311. input: () => <Stepper v-model={params.classNum}></Stepper>
  312. }}
  313. />
  314. <Field
  315. class={styles.singleClssTime}
  316. type="number"
  317. label="单课时时长"
  318. placeholder="请输入课程时长"
  319. modelValue={params.singleClssTime}
  320. onUpdate:modelValue={t => {
  321. if (Math.abs(t) > 60) {
  322. Toast('时长不能大于60分钟')
  323. return
  324. }
  325. if (Math.abs(t) === 0) {
  326. Toast('时长不能小于1分钟')
  327. return
  328. }
  329. params.singleClssTime = Math.abs(t)
  330. }}
  331. // v-model={params.singleClssTime}
  332. v-slots={{
  333. 'right-icon': () => <div>分钟</div>
  334. }}
  335. />
  336. <Field
  337. label="开始日期"
  338. placeholder="请选择开始日期"
  339. {...fieldProps}
  340. modelValue={params.date}
  341. onClick={() => (dateShow.value = true)}
  342. />
  343. <Cell
  344. title="循环周次"
  345. v-slots={{
  346. label: () => (
  347. <RadioGroup class={styles.week} v-model={params.week}>
  348. {Object.keys(week).map((n: string) => {
  349. return (
  350. <Radio
  351. disabled={
  352. params.isSkipHolidays &&
  353. (n === '周六' || n === '周日')
  354. }
  355. name={n}
  356. >
  357. {n}
  358. </Radio>
  359. )
  360. })}
  361. </RadioGroup>
  362. )
  363. }}
  364. />
  365. <Field
  366. label="上课时间"
  367. placeholder="请选择上课时间"
  368. {...fieldProps}
  369. modelValue={timeScope.value}
  370. onClick={() => (timeShow.value = true)}
  371. />
  372. <Cell
  373. title="是否跳过节假日"
  374. v-slots={{
  375. value: () => (
  376. <RadioGroup
  377. class={styles.holdays}
  378. v-model={params.isSkipHolidays}
  379. onChange={() => {
  380. if (
  381. params.isSkipHolidays &&
  382. (params.week === '周六' || params.week === '周日')
  383. ) {
  384. params.week = ''
  385. }
  386. }}
  387. >
  388. <Radio name={true} style={{ marginRight: '10px' }}>
  389. </Radio>
  390. <Radio name={false}>否</Radio>
  391. </RadioGroup>
  392. )
  393. }}
  394. />
  395. <Cell
  396. v-slots={{
  397. title: () => (
  398. <div class={styles.tips}>
  399. <img class={styles.icon} src={iconTips} />
  400. <span>温馨提醒</span>
  401. </div>
  402. ),
  403. label: () => (
  404. <div class={styles.tipsContent}>
  405. 1、云酷琴房时长按课程人数扣减(含老师),以45分钟1对1课程师生2人为例,课程结束后将消耗时长:2人*45分钟=90分钟;
  406. <br />
  407. <br />
  408. 2、每节线上课平台赠送10分钟免费时长,分别为课前5分钟及课后5分钟,赠送时长不计算费用;
  409. <br />
  410. <br />
  411. 3、课程消耗时长按排课人数计算,无论实际到课人数是否为排课人数,都会按照排课人数扣费;
  412. <br />
  413. <br />
  414. 4、课程结束后费用立即结算;
  415. <br />
  416. <br />
  417. 5、琴房时长不足时,您将无法排课,请确保琴房剩余时长充足。
  418. </div>
  419. )
  420. }}
  421. />
  422. <Button
  423. block
  424. type="primary"
  425. round
  426. style={{ margin: '0 auto', width: '90%', marginTop: '20px' }}
  427. onClick={() => setParmas()}
  428. >
  429. 下一步
  430. </Button>
  431. </div>
  432. <Popup position="bottom" v-model:show={dateShow.value}>
  433. <DatetimePicker
  434. type="date"
  435. minDate={dayjs().year(2022).toDate()}
  436. formatter={formatterDate}
  437. onConfirm={(time: Date) => {
  438. params.date = dayjs(time).format('YYYY-MM-DD')
  439. dateShow.value = false
  440. getHolidays(dayjs(time).format('YYYY'))
  441. }}
  442. onCancel={() => (dateShow.value = false)}
  443. />
  444. </Popup>
  445. <Popup
  446. v-model:show={voiceShow.value}
  447. position="bottom"
  448. round
  449. closeable
  450. safe-area-inset-bottom
  451. >
  452. <OrganSearch
  453. subjectList={subjectList.value}
  454. v-model={params.subjectId}
  455. v-model:subjectName={params.subjectName}
  456. onSort={() => (voiceShow.value = false)}
  457. />
  458. </Popup>
  459. <Popup position="bottom" v-model:show={timeShow.value} round>
  460. <div class={styles.picker}>
  461. <DatetimePicker
  462. v-model={startTime.value}
  463. type="time"
  464. minHour={minStartHour}
  465. minMinute={minStartMimute}
  466. onConfirm={() => {
  467. timeShow.value = false
  468. }}
  469. onCancel={() => (timeShow.value = false)}
  470. />
  471. </div>
  472. </Popup>
  473. {/* 选择学员 */}
  474. <ColPopup v-model={selectStudentShow.value}>
  475. <SelectStudents
  476. ref={studentRef}
  477. subjectList={subjectList.value}
  478. onSetStudents={onSetStudents}
  479. />
  480. </ColPopup>
  481. {/* 确认排课 */}
  482. <Popup
  483. position="bottom"
  484. class={styles.coursePopup}
  485. v-model:show={confirmShow.value}
  486. closeable
  487. round
  488. >
  489. <CourseSchedule
  490. item={params}
  491. students={students.value}
  492. curriculum={curriculum.value}
  493. onClose={() => {
  494. confirmShow.value = false
  495. }}
  496. onComfirm={() => {
  497. onCourseSchedule()
  498. }}
  499. />
  500. </Popup>
  501. </>
  502. )
  503. }
  504. }
  505. })