Browse Source

云酷琴房排课

skyblued 2 years ago
parent
commit
0b8736cb6b

+ 9 - 0
src/business-components/calendar/index.tsx

@@ -56,6 +56,11 @@ export default defineComponent({
     selectDay: {
       type: Function,
       default: (obj: any) => {}
+    },
+    isSkipHolidays: {
+      // 是否跳过节假日
+      type: Boolean,
+      default: false
     }
   },
   data() {
@@ -119,6 +124,10 @@ export default defineComponent({
       } else {
         date.type = 'disabled'
       }
+      if (dateObj && this.isSkipHolidays && dateObj.holiday) {
+        // date.bottomInfo = '节假日'
+        date.type = 'disabled'
+      }
 
       date.type = date.type === 'selected' ? '' : date.type
       return date

+ 8 - 0
src/router/routes-teacher.ts

@@ -166,6 +166,14 @@ export default [
         }
       },
       {
+        path: '/createClass',
+        name: 'createClass',
+        component: () => import('@/teacher/piano-room/class-arrangement/create-class'),
+        meta: {
+          title: '创建直播课'
+        }
+      },
+      {
         path: '/rechargeRecord',
         name: 'rechargeRecord',
         component: () => import('@/teacher/piano-room/recharge-record'),

+ 137 - 0
src/teacher/piano-room/class-arrangement/class-info/index.module.less

@@ -0,0 +1,137 @@
+.container {
+  padding: 13px 14px;
+  :global {
+    .van-cell {
+      border-radius: 4px;
+      margin-bottom: 10px;
+    }
+    .van-field__label {
+      border-right: 1px solid #dfdfdf;
+      font-size: 16px;
+      color: #333333;
+    }
+  }
+}
+.tips {
+  display: flex;
+  align-items: center;
+  font-size: 16px;
+  color: #1a1a1a;
+  font-weight: 500;
+  .icon {
+    width: 19px;
+    height: 19px;
+    margin-right: 7px;
+  }
+}
+.tipsContent {
+  font-size: 14px;
+  color: #696969;
+  line-height: 21px;
+}
+.week {
+  display: flex;
+  justify-content: space-between;
+  :global {
+    .van-radio__icon {
+      display: none;
+    }
+    .van-radio__label {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 39px;
+      height: 39px;
+      background: #f5f8fb;
+      border-radius: 6px;
+      font-size: 12px;
+      font-weight: 500;
+      color: var(--van-primary);
+      margin: 0;
+    }
+    .van-radio[aria-checked='true'] {
+      .van-radio__label {
+        background-color: var(--van-primary);
+        color: #fff;
+      }
+    }
+    .van-radio.van-radio--disabled {
+      .van-radio__label {
+        color: #cdced0;
+      }
+    }
+  }
+}
+.holdays {
+  display: flex;
+  justify-content: flex-end;
+  :global {
+    .van-radio__icon {
+      display: none;
+    }
+    .van-radio__label {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 38px;
+      height: 22px;
+      background: #f5f8fb;
+      border-radius: 3px;
+      border: 1px solid #999999;
+      font-size: 12px;
+      font-weight: 500;
+      color: #999999;
+      margin: 0;
+    }
+    .van-radio[aria-checked='true'] {
+      .van-radio__label {
+        background-color: var(--van-primary);
+        border-color: var(--van-primary);
+        color: #fff;
+      }
+    }
+  }
+}
+
+.coursePopup {
+  min-height: 411px;
+  border-radius: 8px;
+  padding: 18px 14px;
+  box-sizing: border-box;
+}
+
+.tags {
+  display: flex;
+  flex-wrap: wrap;
+  padding: 12px;
+  :global {
+    .van-tag {
+      padding: 2px 5px;
+      border-radius: 3px;
+      border: 1px solid var(--van-primary);
+      background-color: #e0f7f3;
+      color: var(--van-primary);
+      margin-right: 8px;
+      margin-bottom: 8px;
+      font-size: 12px;
+      line-height: 17px;
+      font-weight: 500;
+    }
+  }
+}
+.pickerTitle {
+  justify-content: center;
+  font-size: 18px;
+  color: #000000;
+  font-weight: 500;
+  text-align: center;
+}
+
+.student{
+  margin: 0 !important;
+  :global{
+    .van-field__control{
+      opacity: 0 !important;
+    }
+  }
+}

+ 322 - 0
src/teacher/piano-room/class-arrangement/class-info/index.tsx

@@ -0,0 +1,322 @@
+import {
+  ActionSheet,
+  ActionSheetAction,
+  Button,
+  Cell,
+  Field,
+  Form,
+  Popup,
+  Radio,
+  RadioGroup,
+  Stepper,
+  Tag,
+  Toast
+} from 'vant'
+import { computed, defineComponent, onMounted, reactive, ref } from 'vue'
+import styles from './index.module.less'
+import iconTips from '../../images/icon_tips.png'
+import request from '@/helpers/request'
+import ColPopup from '@/components/col-popup'
+import SelectStudents, { IStudent } from '../select-students'
+import Voice from '../../model/voice'
+const fieldProps = {
+  'is-link': true,
+  readonly: true,
+  'arrow-direction': 'down'
+}
+
+export const params = reactive({
+  courseName: '', // 课程名称
+  classNum: 1, // 课时数
+  singleClassTime: '', //单课时长
+  freeCourseMinutes: '', // 休息时间
+  studentIds: [] as IStudent[], //学员id集合
+  subjectId: 0,
+  subjectName: '',
+  week: '', // 周几
+  isSkipHolidays: true,
+  startTime: '',
+  endTime: ''
+})
+export default defineComponent({
+  name: 'ClassArrangement',
+  props: ['onSubmit'],
+  setup(props) {
+    const selectStudentShow = ref(false)
+
+    // 设置学员
+    const studentRef = ref('') as any
+    const students = ref<IStudent[]>([])
+    const onSetStudents = (result: IStudent[]) => {
+      students.value = result
+      params.studentIds = students.value
+      selectStudentShow.value = false
+    }
+    const onDeleteStudent = index => {
+      const n = students.value.splice(index, 1)[0]
+      studentRef?.value.onDelete(n)
+    }
+
+    // 训练声部
+    const voiceShow = ref(false)
+    const subjectList = ref<[]>([]) // 声部分类
+    const getSubjectSelect = async () => {
+      try {
+        const teachRes = await request.post('/api-teacher/teacher/querySubject')
+        subjectList.value = teachRes.data || []
+      } catch {}
+    }
+    onMounted(() => {
+      getSubjectSelect()
+    })
+
+    //上课时间
+    type IClassTimeItem = {
+      courseMinutes: string
+      freeMinutes: string
+      id: number
+    }
+    const classTime = ref<IClassTimeItem[]>([])
+    const classTimeShow = ref(false)
+    const getClassTime = async () => {
+      try {
+        const res = await request.get(
+          '/api-teacher/sysConfig/queryByParamNameList',
+          {
+            params: {
+              paramNames:
+                'course_start_setting,course_end_setting,piano_time_setting'
+            }
+          }
+        )
+        if (res.code === 200) {
+          for (let i = 0, len = res.data.length; i < len; i++) {
+            if (res.data[i].paramName === 'course_start_setting') {
+              params.startTime = res.data[i].paramValue
+            }
+            if (res.data[i].paramName === 'course_end_setting') {
+              params.endTime = res.data[i].paramValue
+            }
+            if (res.data[i].paramName === 'course_start_setting') {
+              params.startTime = res.data[i].paramValue
+            }
+            if (res.data[i].paramName === 'piano_time_setting') {
+              let paramValue: IClassTimeItem[] = []
+              try {
+                paramValue = JSON.parse(res.data[i].paramValue)
+              } catch (error) {}
+              classTime.value = paramValue
+            }
+          }
+        }
+      } catch (error) {}
+    }
+    const actions = computed(() => {
+      let list: ActionSheetAction[] = []
+      classTime.value.forEach((n: IClassTimeItem) => {
+        list.push({
+          name: n.courseMinutes + '分钟'
+        })
+      })
+      return list
+    })
+    onMounted(() => {
+      getClassTime()
+    })
+
+    // 节假日
+    const holidays = ref('')
+    const getHolidays = async (year: string) => {
+      try {
+        let result = await request.get(
+          '/api-teacher/courseSchedule/selectHoliday',
+          { params: { year } }
+        )
+        holidays.value = result?.data?.holidaysFestivalsJson || ''
+      } catch (error) {}
+    }
+    return () => {
+      return (
+        <>
+          <Form
+            scrollToError
+            onSubmit={() => {
+              if (props.onSubmit) {
+                props.onSubmit(params)
+              }
+            }}
+          >
+            <div class={styles.container}>
+              <Field
+                label="课程名称"
+                placeholder="请输入课程名称"
+                maxlength={50}
+                name="courseName"
+                v-model={params.courseName}
+                rules={[{ required: true, message: '请输入您的课程名称' }]}
+              />
+              <Field
+                label="训练声部"
+                placeholder="请选择训练声部"
+                {...fieldProps}
+                name="subjectName"
+                modelValue={params.subjectName}
+                onClick={() => (voiceShow.value = true)}
+                rules={[{ required: true, message: '请选择训练声部' }]}
+              />
+              <Cell style={{ padding: 0 }}>
+                <Field
+                  name="students"
+                  class={styles.student}
+                  border={false}
+                  label="上课学员"
+                  placeholder="请选择上课学员"
+                  modelValue={students.value.length}
+                  {...fieldProps}
+                  onClick={() => (selectStudentShow.value = true)}
+                  rules={[
+                    {
+                      required: true,
+                      validator: () => {
+                        return students.value.length ? true : false
+                      },
+                      message: '请选择上课学员'
+                    }
+                  ]}
+                />
+                {students.value.length ? (
+                  <div class={styles.tags}>
+                    {students.value.map((n: IStudent, index: number) => (
+                      <Tag closeable onClose={() => onDeleteStudent(index)}>
+                        {n.userName}
+                      </Tag>
+                    ))}
+                  </div>
+                ) : null}
+              </Cell>
+              <Field
+                label="课时数"
+                placeholder="请输入课时数"
+                v-slots={{
+                  input: () => <Stepper v-model={params.classNum}></Stepper>
+                }}
+              />
+              <Field
+                class={styles.singleClassTime}
+                label="单课时时长"
+                {...fieldProps}
+                modelValue={params.singleClassTime}
+                onClick={() => (classTimeShow.value = true)}
+                rules={[{ required: true, message: '请选择单课时时长' }]}
+              />
+              <Cell
+                title="是否跳过节假日"
+                v-slots={{
+                  value: () => (
+                    <RadioGroup
+                      class={styles.holdays}
+                      v-model={params.isSkipHolidays}
+                    >
+                      <Radio name={true} style={{ marginRight: '10px' }}>
+                        是
+                      </Radio>
+                      <Radio name={false}>否</Radio>
+                    </RadioGroup>
+                  )
+                }}
+              />
+              <Cell
+                v-slots={{
+                  title: () => (
+                    <div class={styles.tips}>
+                      <img class={styles.icon} src={iconTips} />
+                      <span>温馨提醒</span>
+                    </div>
+                  ),
+                  label: () => (
+                    <div class={styles.tipsContent}>
+                      1、云酷琴房时长按课程人数扣减(含老师),以45分钟1对1课程师生2人为例,课程结束后将消耗时长:2人*(2人-1)*45分钟=90分钟;
+                      <br />
+                      <br />
+                      2、每节线上课平台赠送10分钟免费时长,分别为课前5分钟及课后5分钟,赠送时长不计算费用;
+                      <br />
+                      <br />
+                      3、课程消耗时长按排课人数计算,无论实际到课人数是否为排课人数,都会按照排课人数扣费;
+                      <br />
+                      <br />
+                      4、课程结束后费用立即结算;
+                      <br />
+                      <br />
+                      5、琴房时长不足时,您将无法排课,请确保琴房剩余时长充足。
+                    </div>
+                  )
+                }}
+              />
+
+              <Button
+                block
+                type="primary"
+                round
+                nativeType="submit"
+                style={{ margin: '0 auto', width: '90%', marginTop: '20px' }}
+                // onClick={() => setParmas()}
+              >
+                下一步
+              </Button>
+            </div>
+          </Form>
+
+          <Popup
+            v-model:show={voiceShow.value}
+            position="bottom"
+            round
+            closeable
+            safe-area-inset-bottom
+            class={styles.voicePopup}
+          >
+            <Voice
+              class={styles.voicePopupContent}
+              single
+              selectType={'Radio'}
+              subjectList={subjectList.value}
+              onChoice={val => {
+                const voice: any = subjectList.value.filter(
+                  (n: any) => n.id === val
+                )[0]
+                if (voice) {
+                  params.subjectId = voice.id
+                  params.subjectName = voice.name
+                  voiceShow.value = false
+                } else {
+                  params.subjectId = 0
+                  params.subjectName = ''
+                }
+              }}
+            />
+          </Popup>
+
+          {/* 选择学员 */}
+          <ColPopup v-model={selectStudentShow.value}>
+            <SelectStudents
+              ref={studentRef}
+              subjectList={subjectList.value}
+              onSetStudents={onSetStudents}
+            />
+          </ColPopup>
+
+          {/* 上课时间 */}
+          <ActionSheet
+            v-model:show={classTimeShow.value}
+            actions={actions.value}
+            cancelText="取消"
+            closeOnClickAction
+            onSelect={(value, index) => {
+              params.singleClassTime = classTime.value[index].courseMinutes
+              params.freeCourseMinutes = classTime.value[index].freeMinutes
+            }}
+          />
+        </>
+      )
+    }
+  }
+})

+ 13 - 15
src/teacher/piano-room/class-arrangement/course-schedule/index.tsx

@@ -1,21 +1,15 @@
+import dayjs from 'dayjs'
 import { Button } from 'vant'
 import { computed, defineComponent, PropType } from 'vue'
 import { IStudent } from '../select-students'
 import styles from './index.module.less'
+import { params as item } from '../class-info'
 
 export default defineComponent({
   name: 'CourseSchedule',
   props: {
-    item: {
-      type: Object,
-      default: {}
-    },
-    students: {
-      type: Array as PropType<IStudent[]>,
-      default: []
-    },
     curriculum: {
-      type: Array as PropType<string[]>,
+      type: Array as PropType<any[]>,
       default: []
     },
     onClose: {
@@ -29,11 +23,11 @@ export default defineComponent({
   },
   setup(props: any) {
     const students = computed(() => {
-      let list = props.students.map((n: IStudent) => n.userName)
+      let list = item.studentIds.map((n: IStudent) => n.userName)
       return list.join('、')
     })
     return () => {
-      const n = props.students.length + 1
+      const n = item.studentIds.length + 1
       return (
         <div class={styles.box}>
           <div class={styles.title}>
@@ -51,22 +45,26 @@ export default defineComponent({
               <div>
                 <span style={{ color: '#FF4E19', margin: '0 10px' }}>
-                  {props.item.classNum}节 {props.item.singleClssTime}分钟
+                  {item.classNum}节 {item.singleClassTime}分钟
                 </span>
                 课程
               </div>
             </div>
             <div class={styles.timeBox}>
               <div class={styles.timeTitle}>上课时间:</div>
-              {props.curriculum.map((item: string) => (
-                <div class={styles.timeItem}>{item}</div>
+              {props.curriculum.map((item: any) => (
+                <div class={styles.timeItem}>
+                  {dayjs(item.startTime || new Date()).format('YYYY-MM-DD')}{' '}
+                  {dayjs(item.startTime || new Date()).format('HH:mm')}~
+                  {dayjs(item.endTime || new Date()).format('HH:mm')}
+                </div>
               ))}
             </div>
 
             <div style={{ color: '#999999' }}>
               以上课程预计将消耗琴房时长{' '}
               {Math.ceil(
-                n * (n - 1) * props.item.classNum * props.item.singleClssTime
+                n * (n - 1) * item.classNum * parseInt(item.singleClassTime)
               )}{' '}
               分钟 <br />
               确认排课后时长冻结 <br />

+ 74 - 0
src/teacher/piano-room/class-arrangement/create-class/index.module.less

@@ -0,0 +1,74 @@
+.createClass {
+  padding: 13px 14px;
+}
+.arrangeCell {
+  margin: 10px 0 0;
+  width: auto;
+  border-radius: 10px;
+  overflow: hidden;
+}
+.rTitle {
+  display: flex;
+  align-items: center;
+  font-size: 16px;
+  color: #333;
+  font-weight: 500;
+  &::before {
+    margin-right: 8px;
+    content: ' ';
+    display: inline-block;
+    width: 4px;
+    height: 17px;
+    background: linear-gradient(180deg, #59e5d5 0%, #2dc7aa 100%);
+    border-radius: 3px;
+  }
+}
+.rTag {
+  padding: 10px 0;
+  .tag {
+    background: #e9fff8;
+    margin-bottom: 8px;
+  }
+}
+.selectPopup {
+  width: 312px;
+  background: #ffffff;
+  border-radius: 8px;
+  .selectContainer {
+    padding: 18px 14px;
+  }
+  .rTitle {
+    font-size: 18px;
+  }
+  .selectPopupContent {
+    padding: 20px 0;
+  }
+  .desc,
+  .times {
+    font-size: 14px;
+    color: #666666;
+    line-height: 20px;
+  }
+
+  .times {
+    padding-top: 15px;
+    span {
+      display: block;
+    }
+  }
+
+  .selectBtn {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .btn {
+      width: 48%;
+    }
+  }
+}
+.coursePopup {
+  min-height: 411px;
+  border-radius: 8px;
+  padding: 18px 14px;
+  box-sizing: border-box;
+}

+ 335 - 0
src/teacher/piano-room/class-arrangement/create-class/index.tsx

@@ -0,0 +1,335 @@
+import Calendar from '@/business-components/calendar'
+import request from '@/helpers/request'
+import { state } from '@/state'
+import Arrange from '@/teacher/live-class/create-components/arrange'
+import dayjs from 'dayjs'
+import {
+  computed,
+  defineComponent,
+  nextTick,
+  onMounted,
+  reactive,
+  ref,
+  watch
+} from 'vue'
+import styles from './index.module.less'
+import { params as classInfo } from '../class-info'
+import { Button, Cell, Dialog, Popup, Sticky, Tag, Toast } from 'vant'
+import { getWeekCh } from '@/helpers/utils'
+import { active } from '../index'
+import { useRouter } from 'vue-router'
+import CourseSchedule from '../course-schedule'
+
+export default defineComponent({
+  name: 'createClass',
+  setup() {
+    watch(active, () => {
+      if (active.value === 2) {
+        getList()
+      }
+    })
+    //日期设置
+    const data = reactive({
+      calendarList: {},
+      selectList: []
+    })
+    const getList = async (date?: Date) => {
+      let params = {
+        day: dayjs(date || new Date()).format('DD'),
+        month: dayjs(date || new Date()).format('MM'),
+        year: dayjs(date || new Date()).format('YYYY')
+      }
+      try {
+        let res = await request.post(
+          '/api-teacher/courseSchedule/createLiveCourseCalendar',
+          {
+            data: {
+              ...params,
+              singleCourseMinutes: classInfo.singleClassTime,
+              freeCourseMinutes: classInfo.freeCourseMinutes,
+              teacherId: state.user.data?.userId
+            }
+          }
+        )
+        const result = res.data || []
+        let tempObj = {}
+        result.forEach((item: any) => {
+          tempObj[item.date] = item
+        })
+        data.calendarList = tempObj
+      } catch {}
+    }
+
+    const onSelectDay = (res: any) => {
+      // 对数组进行排序
+      res.sort((first: any, second: any) => {
+        if (first.startTime > second.startTime) return 1
+        if (first.startTime < second.startTime) return -1
+        return 0
+      })
+      data.selectList = res
+    }
+    const showSelectList = computed(() => {
+      // 显示时间
+      let list = [...data.selectList]
+      list.forEach((item: any) => {
+        item.title =
+          dayjs(item.startTime).format('YYYY-MM-DD') +
+          ' ' +
+          getWeekCh(dayjs(item.startTime).day()) +
+          ' ' +
+          item.start +
+          '~' +
+          item.end
+      })
+      return list
+    })
+    const selectType = computed(() => {
+      // 循环次数是否足够
+      return data.selectList.length < classInfo.classNum ? 'noEnough' : 'enough'
+    })
+    const onCloseTag = (item: any) => {
+      // 删除课程
+      Dialog.confirm({
+        title: '提示',
+        message: '您是否要删除该选择的课程?',
+        confirmButtonColor: 'var(--van-primary)'
+      }).then(() => {
+        const index = data.selectList.findIndex(
+          (course: any) => course.startTime === item.startTime
+        )
+        data.selectList.splice(index, 1)
+      })
+    }
+
+    const selectStatus = ref(false)
+    const onSubmit = async () => {
+      if (data.selectList.length <= 0) {
+        Toast('请选择课程时间')
+        return
+      }
+
+      if (data.selectList.length < classInfo.classNum) {
+        selectStatus.value = true
+        return
+      }
+
+      // await this._lookCourse()
+      confirmShow.value = true
+    }
+    const onComfirm = async () => {
+      if (selectType.value === 'noEnough') {
+        let times = [] as any
+        data.selectList.forEach((item: any) => {
+          times.push({
+            startTime: item.startTime,
+            endTime: item.endTime
+          })
+        })
+        console.log(data.selectList)
+        const res = await request.post(
+          '/api-teacher/courseGroup/lockCourseToCache',
+          {
+            data: {
+              courseNum: classInfo.classNum,
+              courseType: 'LIVE',
+              loop: 1,
+              teacherId: state.user.data?.userId,
+              timeList: [...times]
+            }
+          }
+        )
+        if (res.code === 200) {
+          res.data.forEach((n: any) => {
+            n.start = dayjs(n.startTime).format('HH:mm')
+            n.end = dayjs(n.endTime).format('HH:mm')
+          })
+          data.selectList = res.data
+        }
+      } else {
+        selectStatus.value = false
+        nextTick(() => {
+          confirmShow.value = true
+        })
+      }
+    }
+
+    const router = useRouter()
+    const confirmShow = ref(false)
+    // 排课
+    const onCourseSchedule = async () => {
+      const timeList = data.selectList
+      const n = classInfo.studentIds.length + 1
+      try {
+        let { code, data } = await request.post(
+          '/api-teacher/courseSchedule/arrangeCourse',
+          {
+            data: {
+              classNum: classInfo.classNum, //课时数
+              consumeTime: Math.ceil(
+                n *
+                  (n - 1) *
+                  classInfo.classNum *
+                  parseInt(classInfo.singleClassTime)
+              ), //消耗时长
+              courseName: classInfo.courseName, //课程名称
+              singleClssTime: classInfo.singleClassTime, //单课时长
+              studentIds: classInfo.studentIds.map(n => n.userId), //学员id集合
+              subjectId: classInfo.subjectId, //声部id
+              timeList
+            }
+          }
+        )
+        if (code === 200) {
+          confirmShow.value = false
+
+          setTimeout(() => {
+            Toast({
+              icon: 'success',
+              message: '排课成功',
+              duration: 1500,
+              onClose: () => {
+                router.back()
+              }
+            })
+          }, 100)
+        }
+      } catch (error) {}
+    }
+    return () => {
+      return (
+        <div class={styles.createClass}>
+          <Calendar
+            maxDays={classInfo.classNum}
+            list={data.calendarList}
+            prevMonth={(date: Date) => getList(date)}
+            nextMonth={(date: Date) => getList(date)}
+            selectDay={onSelectDay}
+            selectList={data.selectList}
+            isSkipHolidays={classInfo.isSkipHolidays}
+          />
+          <Cell
+            class={[styles.arrangeCell, 'mb12']}
+            v-slots={{
+              title: () => (
+                <div class={styles.rTitle}>
+                  <span>已选择课程时间</span>
+                </div>
+              ),
+              label: () => (
+                <div class={styles.rTag}>
+                  {showSelectList.value.map((item: any) => (
+                    <>
+                      <Tag
+                        plain
+                        round
+                        closeable
+                        size="large"
+                        type="primary"
+                        class={styles.tag}
+                        onClose={() => onCloseTag(item)}
+                      >
+                        {item.title}
+                      </Tag>
+                      <br />
+                    </>
+                  ))}
+                </div>
+              )
+            }}
+          ></Cell>
+
+          <Sticky offsetBottom={0} position="bottom">
+            <div class={['btnGroup', 'btnMore']}>
+              <Button
+                block
+                round
+                type="primary"
+                plain
+                onClick={() => {
+                  active.value = 1
+                  // 重置选择的课次
+                  data.selectList = []
+                }}
+              >
+                上一步
+              </Button>
+              <Button block round type="primary" onClick={() => onSubmit()}>
+                下一步
+              </Button>
+            </div>
+          </Sticky>
+          {/* <Arrange /> */}
+          <Popup show={selectStatus.value} class={styles.selectPopup}>
+            <div class={styles.selectContainer}>
+              <div class={styles.rTitle}>
+                <span>提示</span>
+              </div>
+              <div class={styles.selectPopupContent}>
+                <p class={styles.desc}>
+                  {selectType.value === 'noEnough'
+                    ? '您所选择的上课时间未达到您输入的课时数,系统根据已选时间将自动按周顺延排课。'
+                    : '您已选择以下上课时间段,时间段会暂时锁定,锁定期间学员不可购买该时间段课程。'}
+                </p>
+                {selectType.value === 'enough' && (
+                  <p class={styles.times}>
+                    {data.selectList.map((item: any) => (
+                      <span>
+                        {dayjs(item.startTime || new Date()).format(
+                          'YYYY-MM-DD'
+                        )}{' '}
+                        {dayjs(item.startTime || new Date()).format('HH:mm')}~
+                        {dayjs(item.endTime || new Date()).format('HH:mm')}
+                      </span>
+                    ))}
+                  </p>
+                )}
+              </div>
+
+              <div class={styles.selectBtn}>
+                <Button
+                  class={styles.btn}
+                  type="primary"
+                  round
+                  block
+                  plain
+                  onClick={() => (selectStatus.value = false)}
+                >
+                  {selectType.value === 'noEnough' ? '继续选择' : '重新选择'}
+                </Button>
+                <Button
+                  class={styles.btn}
+                  type="primary"
+                  round
+                  block
+                  onClick={() => onComfirm()}
+                >
+                  确认
+                </Button>
+              </div>
+            </div>
+          </Popup>
+
+          {/* 确认排课 */}
+          <Popup
+            position="bottom"
+            class={styles.coursePopup}
+            v-model:show={confirmShow.value}
+            closeable
+            round
+          >
+            <CourseSchedule
+              curriculum={data.selectList}
+              onClose={() => {
+                confirmShow.value = false
+              }}
+              onComfirm={() => {
+                onCourseSchedule()
+              }}
+            />
+          </Popup>
+        </div>
+      )
+    }
+  }
+})

+ 23 - 135
src/teacher/piano-room/class-arrangement/index.module.less

@@ -1,147 +1,35 @@
-.container {
-  padding: 13px 14px;
+.classWrap {
   :global {
-    .van-cell {
-      border-radius: 4px;
-      margin-bottom: 10px;
-    }
-    .van-field__label {
-      border-right: 1px solid #dfdfdf;
-      font-size: 16px;
-      color: #333333;
-    }
-  }
-}
-.tips {
-  display: flex;
-  align-items: center;
-  font-size: 16px;
-  color: #1a1a1a;
-  font-weight: 500;
-  .icon {
-    width: 19px;
-    height: 19px;
-    margin-right: 7px;
-  }
-}
-.tipsContent {
-  font-size: 14px;
-  color: #696969;
-  line-height: 21px;
-}
-.week {
-  display: flex;
-  justify-content: space-between;
-  :global {
-    .van-radio__icon {
+    .van-tabs .van-tabs__line {
       display: none;
     }
-    .van-radio__label {
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      width: 39px;
-      height: 39px;
-      background: #f5f8fb;
-      border-radius: 6px;
-      font-size: 12px;
-      font-weight: 500;
-      color: var(--van-primary);
-      margin: 0;
-    }
-    .van-radio[aria-checked='true'] {
-      .van-radio__label {
-        background-color: var(--van-primary);
-        color: #fff;
-      }
-    }
-    .van-radio.van-radio--disabled {
-      .van-radio__label {
-        color: #cdced0;
-      }
-    }
   }
 }
-.holdays {
+.tabs {
   display: flex;
-  justify-content: flex-end;
-  :global {
-    .van-radio__icon {
-      display: none;
-    }
-    .van-radio__label {
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      width: 38px;
-      height: 22px;
-      background: #f5f8fb;
-      border-radius: 3px;
-      border: 1px solid #999999;
-      font-size: 12px;
-      font-weight: 500;
-      color: #999999;
-      margin: 0;
-    }
-    .van-radio[aria-checked='true'] {
-      .van-radio__label {
-        background-color: var(--van-primary);
-        border-color: var(--van-primary);
-        color: #fff;
-      }
-    }
-  }
-}
-
-.coursePopup {
-  min-height: 411px;
-  border-radius: 8px;
-  padding: 18px 14px;
+  justify-content: center;
+  align-items: center;
+  padding: 12px 14px 0 14px;
   box-sizing: border-box;
 }
-
-.tags {
+.tabItem {
   display: flex;
-  flex-wrap: wrap;
-  padding: 12px;
-  :global {
-    .van-tag {
-      padding: 2px 5px;
-      border-radius: 3px;
-      border: 1px solid var(--van-primary);
-      background-color: #e0f7f3;
-      color: var(--van-primary);
-      margin-right: 8px;
-      margin-bottom: 8px;
-      font-size: 12px;
-      line-height: 17px;
-      font-weight: 500;
-    }
-  }
-}
-// .picker {
-//   display: flex;
-//   align-items: center;
-//   :global {
-//     .van-picker__toolbar {
-//       display: none;
-//     }
-//     .van-picker {
-//       flex: 1;
-//     }
-//   }
-// }
-.pickerTitle {
+  align-items: center;
   justify-content: center;
-  font-size: 18px;
-  color: #000000;
+  width: 168px;
+  height: 62px;
+  border-radius: 10px;
+  background-color: #fff;
+  font-size: 14px;
   font-weight: 500;
-  text-align: center;
+  color: #b4b4b4;
+  .title{
+    margin-left: 8px;
+  }
+}
+.tabItemRight {
+  margin-left: 11px;
+}
+.tabItemActive{
+  color: var(--van-primary-color);
 }
-// .singleClssTime {
-//   :global {
-//     .van-field__control {
-//       text-align: center;
-//     }
-//   }
-// }

+ 34 - 530
src/teacher/piano-room/class-arrangement/index.tsx

@@ -1,546 +1,50 @@
-import {
-  Button,
-  Cell,
-  DatetimePicker,
-  Field,
-  Popup,
-  Radio,
-  RadioGroup,
-  Stepper,
-  Tag,
-  Toast
-} from 'vant'
-import { computed, defineComponent, onMounted, reactive, ref } from 'vue'
+import { computed, defineComponent, onMounted, reactive, ref, watch } from 'vue'
 import styles from './index.module.less'
-import iconTips from '../images/icon_tips.png'
-import { formatterDate } from '@/helpers/utils'
-import request from '@/helpers/request'
-import OrganSearch from '@/student/practice-class/model/organ-search'
-import dayjs from 'dayjs'
 import ColHeader from '@/components/col-header'
-import ColPopup from '@/components/col-popup'
-import SelectStudents, { IStudent } from './select-students'
-import CourseSchedule from './course-schedule'
-import { checkNumberInteger } from '@/helpers/toolsValidate'
-import Voice from '../model/voice'
-import { useRouter } from 'vue-router'
-const fieldProps = {
-  'is-link': true,
-  readonly: true,
-  'arrow-direction': 'down'
-}
+import { Icon } from 'vant'
+import nameActive from '../images/icon_name_active.png'
+import education from '../images/icon_education.png'
+import educationActive from '../images/icon_education_active.png'
+import ClassInfo from './class-info'
+import CreateClass from './create-class'
+export const active = ref(1)
 export default defineComponent({
   name: 'ClassArrangement',
   setup() {
-    const router = useRouter()
-    const dateShow = ref(false)
-    const timeShow = ref(false)
-    const voiceShow = ref(false)
-    const selectStudentShow = ref(false)
-    const confirmShow = ref(false)
-
-    // 参数
-    const params = reactive({
-      courseName: '', // 课程名称
-      classNum: 1, // 课时数
-      singleClssTime: 45, //单课时长
-      studentIds: [], //学员id集合
-      timeList: [] as any, // 上课时间
-      date: '',
-      time: '',
-      subjectId: 0,
-      subjectName: '',
-      week: '', // 周几
-      isSkipHolidays: true
-    })
-    const startTime = ref('')
-    const endTime = ref('')
-
-    // 上课时间
-    const timeScope = computed(() => {
-      //   if (startTime.value && endTime.value) {
-      //     let start = startTime.value.split(':')
-      //     let end = endTime.value.split(':')
-      //     let min = parseInt(start[0]) * 60 + parseInt(start[1])
-      //     let max = parseInt(end[0]) * 60 + parseInt(end[1])
-      //     // params.singleClssTime = max - min
-      //     return startTime.value + ' ~ ' + endTime.value
-      //   }
-      //   params.singleClssTime = 0
-      return startTime.value
-    })
-
-    // 学员
-    const students = ref<IStudent[]>([])
-    // 设置学员
-    const onSetStudents = (result: IStudent[]) => {
-      students.value = result
-      selectStudentShow.value = false
-    }
-    const onDeleteStudent = index => {
-      const n = students.value.splice(index, 1)[0]
-      studentRef?.value.onDelete(n)
-    }
-    const studentRef = ref('') as any
-
-    // 训练声部
-    const choiceSubjectIds = ref<[]>([])
-    const subjectList2 = ref<[]>([])
-    const subjectList = ref<[]>([]) // 声部分类
-    const getSubjectSelect = async () => {
-      try {
-        const res = await request.get('/api-teacher/subject/subjectSelect')
-        subjectList2.value = res.data
-        const teachRes = await request.post('/api-teacher/teacher/querySubject')
-        subjectList.value = teachRes.data || []
-      } catch {}
-    }
-
-    //上课时间
-    const startClassTime = ref<string>('')
-    const endClassTime = ref<string>('')
-    const getClassTime = async () => {
-      try {
-        const res = await request.get(
-          '/api-teacher/sysConfig/queryByParamNameList',
-          {
-            params: {
-              paramNames: 'course_start_setting,course_end_setting'
-            }
-          }
-        )
-        if (res.code === 200) {
-          for (let i = 0, len = res.data.length; i < len; i++) {
-            if (res.data[i].paramName === 'course_start_setting') {
-              startClassTime.value = res.data[i].paramValue
-            }
-            if (res.data[i].paramName === 'course_end_setting') {
-              endClassTime.value = res.data[i].paramValue
-            }
-          }
-        }
-      } catch (error) {}
-    }
-    onMounted(() => {
-      getSubjectSelect()
-      getClassTime()
-    })
-
-    //检查上课时间是否满足后台设置的最晚时间
-    const checkClassTimeIsSatisfyLastTime = () => {
-      const baseTime = dayjs()
-      const _endTime = baseTime
-        .set('hour', Number(startTime.value.split(':')[0]))
-        .set('minute', Number(startTime.value.split(':')[1]))
-        .add(params.singleClssTime, 'minute')
-      const _endClassTime = baseTime
-        .set('hour', Number(endClassTime.value.split(':')[0]))
-        .set('minute', Number(endClassTime.value.split(':')[1]))
-      // console.log(_endTime.format('HH:mm'),_endClassTime.format('HH:mm'))
-      return {
-        isOk: _endTime.isBefore(_endClassTime),
-        _endClassTime: _endClassTime.format('HH:mm')
-      }
-    }
-
-    //开始时间
-    const onSetTime = async (time: string) => {
-      console.log(time)
-      params.time = time
-      timeShow.value = false
-    }
-
-    const holidays = ref('') // 节假日
-    const getHolidays = async (year: string) => {
-      try {
-        let result = await request.get(
-          '/api-teacher/courseSchedule/selectHoliday',
-          { params: { year } }
-        )
-        holidays.value = result?.data?.holidaysFestivalsJson || ''
-      } catch (error) {}
-    }
-
-    // 计算日期
-    const calcDate = () => {
-      const timeList = [] as any
-      const curriculumList = [] as any
-      let total = 0
-      let index = 0
-      const selectWeekIndex = week[params.week]
-      const endTime = dayjs()
-        .set('hour', Number(startTime.value.split(':')[0]))
-        .set('minute', Number(startTime.value.split(':')[1]))
-        .add(params.singleClssTime, 'minute')
-        .format('HH:mm')
-      while (total < params.classNum) {
-        const time = dayjs(params.date).add(index, 'day')
-        index++
-        const weekIndex = time.get('day')
-        if (weekIndex !== selectWeekIndex) {
-          continue
-        }
-        const year_month_date = time.format('YYYY-MM-DD')
-        if (params.isSkipHolidays) {
-          // 只需要后台配置的
-          if (
-            // ![6, 0].includes(time.get('day')) &&
-            !holidays.value.includes(year_month_date)
-          ) {
-            total++
-            timeList.push({
-              startTime: year_month_date + ` ${startTime.value}`,
-              endTime: year_month_date + ` ${endTime}`
-            })
-            curriculumList.push(
-              year_month_date + ` ${startTime.value} ~ ${endTime}`
-            )
-          }
-        } else {
-          total++
-          timeList.push({
-            startTime: year_month_date + ` ${startTime.value}`,
-            endTime: year_month_date + ` ${endTime}`
-          })
-          curriculumList.push(
-            year_month_date + ` ${startTime.value} ~ ${endTime}`
-          )
-        }
-      }
-      return { timeList, curriculumList }
-    }
-
-    const curriculum = ref<string[]>([])
-    //  设置排课数据
-    const setParmas = () => {
-      if (!params.courseName) {
-        Toast('请填写课程名称')
-        return
-      }
-      if (!params.subjectId) {
-        Toast('请选择训练声部')
-        return
-      }
-
-      if (!params.singleClssTime) {
-        Toast('请填写单课时时长')
-        return
-      }
-      if (
-        checkNumberInteger(String(params.singleClssTime)) ||
-        params.singleClssTime < 0
-      ) {
-        Toast('课时时长为正整数')
-        return
-      }
-      if (!params.date) {
-        Toast('请选择开始日期')
-        return
-      }
-
-      if (!params.week) {
-        Toast('请选择循环周次')
-        return
-      }
-
-      if (!startTime.value) {
-        Toast('请选择上课时间')
-        return
-      }
-
-      let checkData = checkClassTimeIsSatisfyLastTime()
-      if (!checkData.isOk) {
-        Toast(`上课结束时间不能晚于${checkData._endClassTime}`)
-        return
-      }
-
-      if (!students.value.length) {
-        Toast('请选择上课学员')
-        return
-      }
-      const { timeList, curriculumList } = calcDate()
-      params.timeList = timeList
-      curriculum.value = curriculumList
-      // console.log(curriculumList)
-      confirmShow.value = true
-    }
-
-    // 排课
-    const onCourseSchedule = async () => {
-      const n = students.value.length + 1
-      try {
-        let { code, data } = await request.post(
-          '/api-teacher/courseSchedule/arrangeCourse',
-          {
-            data: {
-              classNum: params.classNum, //课时数
-              consumeTime: Math.ceil(
-                n * (n - 1) * params.classNum * params.singleClssTime
-              ), //消耗时长
-              courseName: params.courseName, //课程名称
-              singleClssTime: params.singleClssTime, //单课时长
-              studentIds: students.value.map(n => n.userId), //学员id集合
-              subjectId: params.subjectId, //声部id
-              timeList: params.timeList
-            }
-          }
-        )
-        if (code === 200) {
-          confirmShow.value = false
-
-          setTimeout(() => {
-            Toast({
-              icon: 'success',
-              message: '排课成功',
-              duration: 1500,
-              onClose: () => {
-                router.back()
-              }
-            })
-          }, 100)
-        }
-      } catch (error) {}
-    }
-    const week = {
-      周一: 1,
-      周二: 2,
-      周三: 3,
-      周四: 4,
-      周五: 5,
-      周六: 6,
-      周日: 0
-    }
-    // console.log(Object.values(week))
     return () => {
-      const minStartHour = startClassTime.value.split(':')[0] || ''
-      const minStartMimute = startClassTime.value.split(':')[1] || ''
       return (
-        <>
+        <div class={styles.classWrap}>
           <ColHeader />
-          <div class={styles.container}>
-            <Field
-              label="课程名称"
-              placeholder="请输入课程名称"
-              v-model={params.courseName}
-            />
-            <Field
-              label="训练声部"
-              placeholder="请选择训练声部"
-              {...fieldProps}
-              modelValue={params.subjectName}
-              onClick={() => (voiceShow.value = true)}
-            />
-            <Cell style={{ padding: 0 }}>
-              <Field
-                style={{ margin: 0 }}
-                border={false}
-                label="上课学员"
-                placeholder="请选择上课学员"
-                {...fieldProps}
-                onClick={() => (selectStudentShow.value = true)}
-              />
-              {students.value.length ? (
-                <div class={styles.tags}>
-                  {students.value.map((n: IStudent, index: number) => (
-                    <Tag closeable onClose={() => onDeleteStudent(index)}>
-                      {n.userName}
-                    </Tag>
-                  ))}
-                </div>
-              ) : null}
-            </Cell>
-            <Field
-              label="课时数"
-              placeholder="请输入课时数"
-              v-slots={{
-                input: () => <Stepper v-model={params.classNum}></Stepper>
-              }}
-            />
-            <Field
-              class={styles.singleClssTime}
-              type="number"
-              label="单课时时长"
-              placeholder="请输入课程时长"
-              v-model={params.singleClssTime}
-              v-slots={{
-                'right-icon': () => <div>分钟</div>
-              }}
-            />
-            <Field
-              label="开始日期"
-              placeholder="请选择开始日期"
-              {...fieldProps}
-              modelValue={params.date}
-              onClick={() => (dateShow.value = true)}
-            />
-            <Cell
-              title="循环周次"
-              v-slots={{
-                label: () => (
-                  <RadioGroup class={styles.week} v-model={params.week}>
-                    {Object.keys(week).map((n: string) => {
-                      return <Radio name={n}>{n}</Radio>
-                    })}
-                  </RadioGroup>
-                )
-              }}
-            />
-            <Field
-              label="上课时间"
-              placeholder="请选择上课时间"
-              {...fieldProps}
-              modelValue={timeScope.value}
-              onClick={() => (timeShow.value = true)}
-            />
-            <Cell
-              title="是否跳过节假日"
-              v-slots={{
-                value: () => (
-                  <RadioGroup
-                    class={styles.holdays}
-                    v-model={params.isSkipHolidays}
-                  >
-                    <Radio name={true} style={{ marginRight: '10px' }}>
-                      是
-                    </Radio>
-                    <Radio name={false}>否</Radio>
-                  </RadioGroup>
-                )
-              }}
-            />
-            <Cell
-              v-slots={{
-                title: () => (
-                  <div class={styles.tips}>
-                    <img class={styles.icon} src={iconTips} />
-                    <span>温馨提醒</span>
-                  </div>
-                ),
-                label: () => (
-                  <div class={styles.tipsContent}>
-                    1、云酷琴房时长按课程人数扣减(含老师),以45分钟1对1课程师生2人为例,课程结束后将消耗时长:2人*45分钟=90分钟;
-                    <br />
-                    <br />
-                    2、每节线上课平台赠送10分钟免费时长,分别为课前5分钟及课后5分钟,赠送时长不计算费用;
-                    <br />
-                    <br />
-                    3、课程消耗时长按排课人数计算,无论实际到课人数是否为排课人数,都会按照排课人数扣费;
-                    <br />
-                    <br />
-                    4、课程结束后费用立即结算;
-                    <br />
-                    <br />
-                    5、琴房时长不足时,您将无法排课,请确保琴房剩余时长充足。
-                  </div>
-                )
-              }}
-            />
-
-            <Button
-              block
-              type="primary"
-              round
-              style={{ margin: '0 auto', width: '90%', marginTop: '20px' }}
-              onClick={() => setParmas()}
+          <div class={styles.tabs}>
+            <div class={[styles.tabItem, styles.tabItemActive]}>
+              <Icon name={nameActive} size={38} />
+              <span class={styles.title}>课程信息</span>
+            </div>
+            <div
+              class={[
+                styles.tabItem,
+                styles.tabItemRight,
+                active.value > 1 ? styles.tabItemActive : ''
+              ]}
             >
-              下一步
-            </Button>
-          </div>
-
-          <Popup position="bottom" v-model:show={dateShow.value}>
-            <DatetimePicker
-              type="date"
-              minDate={dayjs().year(2022).toDate()}
-              formatter={formatterDate}
-              onConfirm={(time: Date) => {
-                params.date = dayjs(time).format('YYYY-MM-DD')
-                dateShow.value = false
-                getHolidays(dayjs(time).format('YYYY'))
-              }}
-              onCancel={() => (dateShow.value = false)}
-            />
-          </Popup>
-
-          <Popup
-            v-model:show={voiceShow.value}
-            position="bottom"
-            round
-            closeable
-            safe-area-inset-bottom
-            class={styles.voicePopup}
-          >
-            <Voice
-              class={styles.voicePopupContent}
-              single
-              selectType={'Radio'}
-              subjectList={subjectList.value}
-              onChoice={val => {
-                const voice: any = subjectList.value.filter(
-                  (n: any) => n.id === val
-                )[0]
-                if (voice) {
-                  params.subjectId = voice.id
-                  params.subjectName = voice.name
-                  voiceShow.value = false
-                } else {
-                  params.subjectId = 0
-                  params.subjectName = ''
-                }
-              }}
-            />
-          </Popup>
-
-          <Popup position="bottom" v-model:show={timeShow.value} round>
-            <div class={styles.picker}>
-              <DatetimePicker
-                v-model={startTime.value}
-                type="time"
-                minHour={minStartHour}
-                minMinute={minStartMimute}
-                onConfirm={() => {
-                  if (!startTime.value) {
-                    startTime.value = dayjs()
-                      .set('hour', Number(minStartHour))
-                      .set('minute', Number(minStartMimute))
-                      .format('HH:mm')
-                  }
-                  timeShow.value = false
-                }}
-                onCancel={() => (timeShow.value = false)}
+              <Icon
+                name={active.value > 1 ? educationActive : education}
+                size={38}
               />
+              <span class={styles.title}>课程信息</span>
             </div>
-          </Popup>
-
-          {/* 选择学员 */}
-          <ColPopup v-model={selectStudentShow.value}>
-            <SelectStudents
-              ref={studentRef}
-              subjectList={subjectList.value}
-              onSetStudents={onSetStudents}
-            />
-          </ColPopup>
-
-          {/* 确认排课 */}
-          <Popup
-            position="bottom"
-            class={styles.coursePopup}
-            v-model:show={confirmShow.value}
-            closeable
-            round
-          >
-            <CourseSchedule
-              item={params}
-              students={students.value}
-              curriculum={curriculum.value}
-              onClose={() => {
-                confirmShow.value = false
-              }}
-              onComfirm={() => {
-                onCourseSchedule()
+          </div>
+          <div style={{ display: active.value === 1 ? 'block' : 'none' }}>
+            <ClassInfo
+              onSubmit={val => {
+                active.value = 2
               }}
             />
-          </Popup>
-        </>
+          </div>
+          <div style={{ display: active.value === 2 ? 'block' : 'none' }}>
+            <CreateClass />
+          </div>
+        </div>
       )
     }
   }

+ 1 - 0
src/teacher/piano-room/components/student/index.module.less

@@ -8,6 +8,7 @@
     height: 48px;
     border-radius: 50%;
     overflow: hidden;
+    object-fit: cover;
   }
 
   .subject {

BIN
src/teacher/piano-room/images/icon_education.png


BIN
src/teacher/piano-room/images/icon_education_active.png


BIN
src/teacher/piano-room/images/icon_name_active.png