Przeglądaj źródła

添加学员请假记录

lex 1 rok temu
rodzic
commit
25a22bace1

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

@@ -169,6 +169,14 @@ export default [
         meta: {
           title: '课程详情'
         }
+      },
+      {
+        path: '/student-leave-record',
+        name: 'student-leave-record',
+        component: () => import('@/views/student-leave-record'),
+        meta: {
+          title: '学员请假记录'
+        }
       }
     ]
   },

+ 92 - 0
src/views/student-leave-record/drop-down-modal.tsx

@@ -0,0 +1,92 @@
+import { formatterDatePicker } from '@/helpers/utils';
+import { Button, DatePicker, Picker, PickerColumn } from 'vant';
+import {
+  PropType,
+  defineComponent,
+  onMounted,
+  reactive,
+  ref,
+  watch
+} from 'vue';
+
+export default defineComponent({
+  name: 'drop-down-modal',
+  props: {
+    selectValues: {
+      type: Array,
+      default: () => []
+    },
+    columns: {
+      type: Array as PropType<PickerColumn>,
+      default: () => []
+    },
+    open: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['dropDownClose', 'dropDownConfirm'],
+  setup(props, { emit }) {
+    const forms = reactive({
+      values: [] as any
+    });
+    const pickerRef = ref();
+
+    onMounted(() => {
+      forms.values = props.selectValues;
+    });
+
+    watch(
+      () => props.selectValues,
+      () => {
+        forms.values = props.selectValues;
+      }
+    );
+    watch(
+      () => props.open,
+      () => {
+        setTimeout(() => {
+          forms.values = props.selectValues;
+        }, 100);
+      }
+    );
+    return () => (
+      <>
+        {/* <Picker
+          ref={pickerRef}
+          v-model={forms.values}
+          showToolbar={false}
+          visibleOptionNum={5}
+          columns={props.columns}
+          onConfirm={() => {
+            emit('dropDownConfirm', forms.values);
+          }}
+        /> */}
+        <DatePicker
+          ref={pickerRef}
+          v-model={forms.values}
+          formatter={formatterDatePicker}
+          columnsType={['year', 'month']}
+          visibleOptionNum={5}
+          showToolbar={false}
+          // onConfirm={() => {
+          //   emit('dropDownConfirm', forms.values);
+          // }}
+        />
+        <div class={['btnGroupPopup', 'van-hairline--top']}>
+          <Button round onClick={() => emit('dropDownClose')}>
+            取消
+          </Button>
+          <Button
+            type="primary"
+            round
+            onClick={async () => {
+              emit('dropDownConfirm', pickerRef.value.modelValue);
+            }}>
+            确定
+          </Button>
+        </div>
+      </>
+    );
+  }
+});

+ 80 - 0
src/views/student-leave-record/drop-down-more-modal.module.less

@@ -0,0 +1,80 @@
+.searchContainer {
+  max-height: 400px;
+  overflow-y: auto;
+  box-sizing: border-box;
+  padding-bottom: 16px;
+  background: var(--van-popup-background);
+  transition: var(--van-popup-transition);
+}
+
+.searchMoreGroup {
+  display: flex;
+  align-items: center;
+  padding: 18px 13px;
+  background-color: #fff;
+}
+
+.searchTitle {
+  padding: 15px 13px 2px;
+  font-size: 15px;
+  font-weight: 600;
+  color: #333333;
+  line-height: 21px;
+  text-align: left;
+}
+
+.searchTypeGroup {
+  display: flex;
+  align-items: center;
+  padding: 0 13px;
+  flex-wrap: wrap;
+  justify-content: space-between;
+}
+
+.searchTypeFlex {
+  .searchTypeItem {
+    width: 49%;
+  }
+}
+
+.searchTypeFlex1 {
+  justify-content: flex-start;
+
+  .searchTypeItem {
+    width: 31%;
+
+    &:nth-child(3n + 1) {
+      margin-right: 2.333%;
+    }
+
+    &:nth-child(3n + 3) {
+      margin-left: 2.333%;
+    }
+  }
+}
+
+.searchTypeFlex2 {
+  .searchTypeItem {
+    width: 45%;
+  }
+}
+
+.searchTypeItem {
+  box-sizing: border-box;
+  margin-top: 10px;
+  height: 34px;
+  line-height: 34px;
+  background: #F6F6F6;
+  border: 1px solid #f6f6f6;
+  border-radius: 16px;
+  font-size: 13px;
+  color: #333333;
+  padding: 0 7px;
+  text-align: center;
+
+  &.is-active {
+    background: #F2FFFC;
+    border: 1px solid #01C1B5;
+    color: #00B2A7;
+  }
+}

+ 154 - 0
src/views/student-leave-record/drop-down-more-modal.tsx

@@ -0,0 +1,154 @@
+import { Button, DatePicker, Picker, PickerColumn } from 'vant';
+import {
+  PropType,
+  defineComponent,
+  onMounted,
+  reactive,
+  ref,
+  watch
+} from 'vue';
+import styles from './drop-down-more-modal.module.less';
+import { coursesType, orchestraClassType } from '@/helpers/constant';
+
+export default defineComponent({
+  name: 'drop-down-modal',
+  props: {
+    selectValues: {
+      type: Object,
+      default: () => ({})
+    },
+    columns: {
+      type: Array,
+      default: () => []
+    },
+    open: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['dropDownClose', 'dropDownConfirm'],
+  setup(props, { emit }) {
+    const forms = reactive({
+      values: [] as any
+    });
+    const pickerRef = ref();
+
+    onMounted(() => {
+      forms.values = props.selectValues;
+    });
+
+    watch(
+      () => props.selectValues,
+      () => {
+        forms.values = props.selectValues;
+      }
+    );
+    watch(
+      () => props.open,
+      () => {
+        setTimeout(() => {
+          forms.values = props.selectValues;
+        }, 100);
+      }
+    );
+
+    // 名称
+    const formatLength = (name: string) => {
+      if (name.length > 11) {
+        const fristName = name.substring(0, 6);
+        const lastName = name.substring(name.length - 5, name.length);
+        return fristName + '...' + lastName;
+      } else {
+        return name;
+      }
+    };
+    return () => (
+      <>
+        <div class={styles.searchContainer}>
+          {props.columns.length > 0 && (
+            <>
+              <div class={styles.searchTitle}>乐团</div>
+              <div class={[styles.searchTypeGroup, styles.searchTypeFlex]}>
+                <div
+                  class={[
+                    styles.searchTypeItem,
+                    '' === forms.values.musicGroupIds && styles['is-active']
+                  ]}
+                  onClick={() => {
+                    forms.values.musicGroupIds = '';
+                  }}>
+                  全部乐团
+                </div>
+                {props.columns.map((item: any) => (
+                  <div
+                    class={[
+                      styles.searchTypeItem,
+                      item.value === forms.values.musicGroupIds &&
+                        styles['is-active']
+                    ]}
+                    onClick={() => {
+                      forms.values.musicGroupIds = item.value;
+                    }}>
+                    {formatLength(item.text)}
+                  </div>
+                ))}
+              </div>
+            </>
+          )}
+
+          <div class={styles.searchTitle}>班级类型</div>
+          <div class={[styles.searchTypeGroup, styles.searchTypeFlex]}>
+            {Object.keys(orchestraClassType).map(key => (
+              <div
+                class={[
+                  styles.searchTypeItem,
+                  key === forms.values.classGroupType && styles['is-active']
+                ]}
+                onClick={() => {
+                  forms.values.classGroupType = key;
+                }}>
+                {orchestraClassType[key]}
+              </div>
+            ))}
+          </div>
+          <div class={styles.searchTitle}>课程类型</div>
+          <div class={[styles.searchTypeGroup, styles.searchTypeFlex]}>
+            {Object.keys(coursesType).map(key => (
+              <div
+                class={[
+                  styles.searchTypeItem,
+                  key === forms.values.courseScheduleType && styles['is-active']
+                ]}
+                onClick={() => {
+                  forms.values.courseScheduleType = key;
+                }}>
+                {coursesType[key]}
+              </div>
+            ))}
+          </div>
+        </div>
+        <div class={['btnGroupPopup', 'van-hairline--top']}>
+          <Button
+            round
+            onClick={() =>
+              emit('dropDownConfirm', {
+                musicGroupIds: '',
+                classGroupType: '',
+                courseScheduleType: ''
+              })
+            }>
+            重置
+          </Button>
+          <Button
+            type="primary"
+            round
+            onClick={async () => {
+              emit('dropDownConfirm', forms.values);
+            }}>
+            查询
+          </Button>
+        </div>
+      </>
+    );
+  }
+});

+ 110 - 0
src/views/student-leave-record/index.module.less

@@ -0,0 +1,110 @@
+.cellGroup {
+  overflow: hidden;
+  padding: 0 13px;
+  margin-bottom: 12px;
+}
+
+.cellGroupIn {
+  border-radius: 10px;
+  margin-top: 12px;
+  overflow: hidden;
+}
+
+.cell {
+  padding: 12px 10px;
+
+  :global {
+    .van-cell__title {
+      flex: 0 auto;
+    }
+  }
+
+  .iconTeacher {
+    width: 48px;
+    height: 48px;
+    border-radius: 50%;
+    overflow: hidden;
+    margin-right: 8px;
+    flex-shrink: 0;
+  }
+
+  .username {
+    .name {
+      font-size: 16px;
+      font-weight: bold;
+      color: #333333;
+      max-width: 250px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .class {
+      font-size: 13px;
+      color: var(--k-gray-3);
+      line-height: 22px;
+      max-width: 90px;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+  }
+
+
+  .cellInfo {
+    flex: 1 !important;
+  }
+
+  .info {}
+
+  .times {
+    padding-left: 10px;
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+    color: #333333;
+    line-height: 19px;
+
+    .title {
+      padding-right: 20px;
+      color: #777777;
+    }
+  }
+
+  .time1 {
+    padding-bottom: 11px;
+  }
+
+  .remark {
+    margin-top: 11px;
+    background: #F8F9FC;
+    border-radius: 6px;
+    padding: 8px 11px;
+    margin-bottom: 4px;
+
+    .remarkTitle {
+      display: flex;
+      align-items: center;
+      font-size: 15px;
+      color: #333333;
+      line-height: 21px;
+
+      &::before {
+        margin-right: 7px;
+        content: '';
+        display: inline-block;
+        width: 4px;
+        height: 12px;
+        background: #01C1B5;
+        border-radius: 2px;
+      }
+    }
+
+    p {
+      padding-top: 7px;
+      font-size: 14px;
+      color: #777777;
+      line-height: 20px;
+    }
+  }
+}

+ 282 - 0
src/views/student-leave-record/index.tsx

@@ -0,0 +1,282 @@
+import { defineComponent, onMounted, reactive, ref } from 'vue';
+import styles from './index.module.less';
+import MSticky from '@/components/m-sticky';
+import MHeader from '@/components/m-header';
+import MSearch from '@/components/m-search';
+import SkeletonModal from './skeleton-modal';
+import { Cell, CellGroup, DropdownItem, DropdownMenu, Image, List } from 'vant';
+import { useRouter } from 'vue-router';
+import MFullRefresh from '@/components/m-full-refresh';
+import request from '@/helpers/request';
+import iconTeacher from '@/common/images/icon-student-default.png';
+import MEmpty from '@/components/m-empty';
+import { coursesType } from '@/helpers/constant';
+import DropDownModal from './drop-down-modal';
+import dayjs from 'dayjs';
+import DropDownMoreModal from './drop-down-more-modal';
+
+export default defineComponent({
+  name: 'student-leave-record',
+  setup() {
+    const dropDownItemRef = ref();
+    const dropDownItemRef1 = ref();
+    const router = useRouter();
+    const forms = reactive({
+      listState: {
+        loading: true,
+        dataShow: true,
+        finished: false,
+        refreshing: false
+      },
+      isClick: false,
+      params: {
+        createTime: [dayjs().format('YYYY'), dayjs().format('MM')],
+        musicGroupIds: '',
+        classGroupType: '',
+        courseScheduleType: '',
+        search: '',
+        page: 1,
+        rows: 20
+      },
+      orchestraColumns: [] as any,
+      list: []
+    });
+
+    const onDropDownClose = (item: any) => {
+      item.value && item.value.toggle();
+    };
+
+    const getList = async () => {
+      try {
+        if (forms.isClick) return;
+        forms.isClick = true;
+        const { createTime, ...res } = forms.params;
+
+        const startTime = createTime.join('-') + '-01';
+
+        const endTime = dayjs(startTime).endOf('month').format('YYYY-MM-DD');
+
+        const { data } = await request.post(
+          '/api-web/schoolStudentHomework/queryStudentLeave',
+          {
+            data: {
+              ...res,
+              startTime,
+              endTime
+            }
+          }
+        );
+        const result = data || {};
+
+        // 判断是否有数据
+        if (forms.listState.refreshing) {
+          forms.list = result.rows || [];
+        } else {
+          forms.list = forms.list.concat(result.rows || []);
+        }
+
+        forms.listState.finished = result.pageNo >= result.totalPage;
+        forms.params.page = result.pageNo + 1;
+      } catch {
+        // forms.listState.finished = true;
+      } finally {
+        forms.listState.dataShow = forms.list.length > 0;
+        forms.listState.refreshing = false;
+        forms.listState.loading = false;
+        forms.isClick = false;
+      }
+    };
+
+    const formatTime = (time: string) => {
+      if (!time) {
+        return '';
+      }
+
+      return time.substring(0, 5);
+    };
+
+    const onRefresh = () => {
+      forms.params.page = 1;
+      getList();
+    };
+
+    // 乐团列表
+    const musicGroupPage = async () => {
+      try {
+        const { data } = await request.get(
+          '/api-web/cooperationOrgan/musicGroupPage'
+        );
+        (data || []).forEach((item: any) => {
+          forms.orchestraColumns.push({
+            text: item.name,
+            value: item.id
+          });
+        });
+      } catch {
+        //
+      }
+    };
+    onMounted(() => {
+      musicGroupPage();
+      getList();
+    });
+    return () => (
+      <div class={styles.studentLeaveRecord}>
+        <MSticky position="top">
+          <MHeader />
+          <MSearch
+            placeholder="请输入学员姓名/手机号"
+            onSearch={(val: string) => {
+              forms.params.search = val;
+              forms.listState.dataShow = true;
+              forms.listState.refreshing = true;
+              forms.list = [];
+              onRefresh();
+            }}
+          />
+          <DropdownMenu>
+            <DropdownItem
+              ref={dropDownItemRef}
+              title={forms.params.createTime.join('-')}>
+              <DropDownModal
+                selectValues={forms.params.createTime}
+                open={dropDownItemRef.value.state.showPopup}
+                onDropDownClose={() => onDropDownClose(dropDownItemRef)}
+                onDropDownConfirm={(values: any) => {
+                  forms.params.createTime = values;
+                  onDropDownClose(dropDownItemRef);
+                  forms.listState.dataShow = true;
+                  forms.listState.refreshing = true;
+                  forms.list = [];
+                  onRefresh();
+                }}
+              />
+            </DropdownItem>
+            <DropdownItem ref={dropDownItemRef1} title={'筛选'}>
+              <DropDownMoreModal
+                selectValues={{
+                  musicGroupIds: forms.params.musicGroupIds,
+                  classGroupType: forms.params.classGroupType,
+                  courseScheduleType: forms.params.courseScheduleType
+                }}
+                columns={forms.orchestraColumns}
+                open={dropDownItemRef1.value.state.showPopup}
+                onDropDownClose={() => onDropDownClose(dropDownItemRef1)}
+                onDropDownConfirm={(values: any) => {
+                  forms.params.musicGroupIds = values.musicGroupIds;
+                  forms.params.classGroupType = values.classGroupType;
+                  forms.params.courseScheduleType = values.courseScheduleType;
+
+                  onDropDownClose(dropDownItemRef1);
+                  forms.listState.refreshing = true;
+                  forms.listState.dataShow = true;
+                  forms.list = [];
+                  onRefresh();
+                }}
+              />
+            </DropdownItem>
+          </DropdownMenu>
+        </MSticky>
+
+        <SkeletonModal v-model:show={forms.listState.loading}>
+          <MFullRefresh
+            v-model:modelValue={forms.listState.refreshing}
+            onRefresh={() => onRefresh()}
+            style={{
+              minHeight: `calc(100vh - var(--header-height))`
+            }}>
+            <List
+              finished={forms.listState.finished}
+              finishedText=" "
+              style={{ overflow: 'hidden' }}
+              onLoad={getList}
+              immediateCheck={false}>
+              <div class={styles.cellGroup}>
+                {forms.listState.dataShow ? (
+                  forms.list.map((item: any) => (
+                    <CellGroup class={styles.cellGroupIn}>
+                      <Cell
+                        center
+                        clickable={false}
+                        class={styles.cell}
+                        onClick={() => {
+                          router.push({
+                            path: '/teacher-attendance-detail',
+                            query: {
+                              teacherId: item.teacherId,
+                              classGroupId: item.classGroupId
+                            }
+                          });
+                        }}>
+                        {{
+                          icon: () => (
+                            <Image
+                              src={item.avatar || iconTeacher}
+                              fit="cover"
+                              class={styles.iconTeacher}
+                            />
+                          ),
+
+                          title: () => (
+                            <div class={styles.username}>
+                              <p class={styles.name}>
+                                {coursesType[item.courseScheduleType]}·
+                                {item.classGroupName}
+                              </p>
+                              <p class={styles.class}>
+                                {item.username}
+                                {item.phone && <>({item.phone})</>}
+                              </p>
+                            </div>
+                          )
+                        }}
+                      </Cell>
+                      <Cell
+                        center
+                        clickable={false}
+                        class={styles.cell}
+                        titleClass={styles.cellInfo}>
+                        {{
+                          title: () => (
+                            <div class={styles.info}>
+                              <div class={[styles.times, styles.time1]}>
+                                <span class={styles.title}>课程时间</span>
+                                <span>
+                                  {item.classDate}{' '}
+                                  {formatTime(item.endClassTime)}-
+                                  {formatTime(item.startClassTime)}
+                                </span>
+                              </div>
+                              <div class={[styles.times]}>
+                                <span class={styles.title}>提交时间</span>
+                                <span>{item.leaveTime}</span>
+                              </div>
+
+                              {item.remark && (
+                                <div class={styles.remark}>
+                                  <div class={styles.remarkTitle}>请假事由</div>
+                                  <p>{item.remark}</p>
+                                </div>
+                              )}
+                            </div>
+                          )
+                        }}
+                      </Cell>
+                    </CellGroup>
+                  ))
+                ) : (
+                  <MEmpty
+                    style={{
+                      minHeight: `calc(100vh - var(--header-height))`
+                    }}
+                    description="暂无学员请假统计"
+                  />
+                )}
+              </div>
+            </List>
+          </MFullRefresh>
+        </SkeletonModal>
+      </div>
+    );
+  }
+});

+ 108 - 0
src/views/student-leave-record/skeleton-modal.tsx

@@ -0,0 +1,108 @@
+import {
+  Cell,
+  CellGroup,
+  Skeleton,
+  SkeletonAvatar,
+  SkeletonParagraph
+} from 'vant';
+import { defineComponent, onMounted, reactive, watch } from 'vue';
+import styles from './index.module.less';
+
+export default defineComponent({
+  name: 'skeleton-modal',
+  props: {
+    show: {
+      type: Boolean,
+      default: false
+    },
+    showCount: {
+      type: Array,
+      default: () => [1, 2, 3, 4, 5]
+    }
+  },
+  setup(props, { slots }) {
+    const forms = reactive({
+      loading: false
+    });
+
+    onMounted(() => {
+      forms.loading = props.show;
+    });
+
+    watch(
+      () => props.show,
+      () => {
+        forms.loading = props.show;
+      }
+    );
+    return () => (
+      <Skeleton loading={forms.loading} style="flex-wrap: wrap">
+        {{
+          template: () => (
+            <div
+              style={{
+                height: `calc(100vh - var(--header-height))`,
+                overflow: 'hidden',
+                width: '100%'
+              }}>
+              <div class={styles.cellGroup}>
+                {props.showCount.map(() => (
+                  <CellGroup class={styles.cellGroupIn}>
+                    <Cell center clickable={false} class={styles.cell}>
+                      {{
+                        icon: () => (
+                          <SkeletonAvatar class={styles.iconTeacher} />
+                        ),
+
+                        title: () => (
+                          <div class={styles.username}>
+                            <SkeletonParagraph
+                              rowWidth={'40%'}
+                              style={{ width: '120px' }}
+                              class={styles.name}
+                            />
+                            <SkeletonParagraph
+                              rowWidth={'40%'}
+                              style={{ width: '120px' }}
+                              class={styles.class}
+                            />
+                          </div>
+                        )
+                      }}
+                    </Cell>
+                    <Cell
+                      center
+                      clickable={false}
+                      class={styles.cell}
+                      titleClass={styles.cellInfo}>
+                      {{
+                        title: () => (
+                          <div class={styles.info}>
+                            <SkeletonParagraph
+                              rowWidth={'40%'}
+                              class={[styles.times, styles.time1]}
+                            />
+                            <SkeletonParagraph
+                              class={[styles.times]}
+                              rowWidth={'40%'}
+                            />
+
+                            <SkeletonParagraph
+                              class={styles.remark}
+                              rowWidth={'100%'}
+                            />
+                          </div>
+                        )
+                      }}
+                    </Cell>
+                  </CellGroup>
+                ))}
+              </div>
+            </div>
+          ),
+          default: () => slots.default && slots.default()
+        }}
+      </Skeleton>
+    );
+  }
+});