瀏覽代碼

任务编辑

zhangjie 1 年之前
父節點
當前提交
3b001f6161

+ 1 - 1
src/api/order.ts

@@ -52,7 +52,7 @@ export function updateTaskNotice(
   return axios.post('/api/admin/apply/task/notice/save', datas);
 }
 export function ableTask(params: AbleParams): Promise<boolean> {
-  return axios.post('/api/admin/apply/task/enable', params);
+  return axios.post('/api/admin/apply/task/enable', {}, { params });
 }
 
 // 考生信息导入

+ 4 - 2
src/api/types/order.ts

@@ -49,11 +49,13 @@ export interface TaskRuleUpdateParams {
 }
 
 export interface TaskTimeUpdateParams {
-  times: number[][];
+  id: string;
+  times: string;
 }
 
 export interface TaskNoticeUpdateParams {
-  content: string;
+  id: string;
+  notice: string;
 }
 
 export interface OrderRecordItem {

+ 9 - 0
src/assets/style/base.less

@@ -237,3 +237,12 @@
   margin: 0 10px;
   cursor: pointer;
 }
+.align-right {
+  text-align: right;
+}
+.mr-10 {
+  margin-right: 10px;
+}
+.ml-10 {
+  margin-left: 10px;
+}

+ 12 - 0
src/assets/svgs/icon-add.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon-新增</title>
+    <g id="管理员" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="02.01-项目列表" transform="translate(-40, -171)">
+            <g id="icon-新增" transform="translate(40, 171)">
+                <rect id="add-circle-(Background)" opacity="0" x="0" y="0" width="16" height="16"></rect>
+                <path d="M4.5,8.5 L4.5,7.5 L7.5,7.5 L7.5,4.5 L8.5,4.5 L8.5,7.5 L11.5,7.5 L11.5,8.5 L8.5,8.5 L8.5,11.5 L7.5,11.5 L7.5,8.5 L4.5,8.5 Z M15,8 C15,4.13400674 11.8659935,1 8,1 C4.13400674,1 1,4.13400674 1,8 C1,11.8659935 4.13400674,15 8,15 C11.8659935,15 15,11.8659935 15,8 Z" id="add-circle" fill="#595959"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 12 - 0
src/assets/svgs/icon-delete.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg  viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon-批量删除</title>
+    <g id="管理员" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="02.01-项目列表" transform="translate(-116, -171)">
+            <g id="icon-批量删除" transform="translate(116, 171)">
+                <rect id="delete-(Background)" opacity="0" x="0" y="0" width="16" height="16"></rect>
+                <path d="M6,6 L7,6 L7,12 L6,12 L6,6 Z M9,12 L10,12 L10,6 L9,6 L9,12 Z M14,3 L14,4 L13,4 L13,14 C13,14.5522852 12.5522842,15 12,15 L4,15 C3.44771504,15 3,14.5522842 3,14 L3,4 L2,4 L2,3 L5.5,3 L5.5,1.80000073 C5.5,1.35817304 5.85817218,1 6.30000019,1 L9.69999981,1 C10.1418276,1 10.5,1.35817215 10.5,1.80000013 L10.5,3 L14,3 Z M6.5,2 L9.5,2 L9.5,3 L6.5,3 L6.5,2 Z M4,14 L12,14 L12,4 L4,4 L4,14 Z" id="delete" fill="#595959"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 4 - 0
src/components/index.ts

@@ -6,6 +6,8 @@ import SelectTask from './select-task/index.vue';
 import SelectTeaching from './select-teaching/index.vue';
 import SelectAgent from './select-agent/index.vue';
 import SelectRangeDatetime from './select-range-datetime/index.vue';
+import SelectRangeTime from './select-range-time/index.vue';
+import StatusTag from './status-tag/index.vue';
 
 export default {
   install(Vue: App) {
@@ -14,5 +16,7 @@ export default {
     Vue.component('SelectTeaching', SelectTeaching);
     Vue.component('SelectAgent', SelectAgent);
     Vue.component('SelectRangeDatetime', SelectRangeDatetime);
+    Vue.component('SelectRangeTime', SelectRangeTime);
+    Vue.component('StatusTag', StatusTag);
   },
 };

+ 11 - 11
src/components/select-range-datetime/index.vue

@@ -1,20 +1,18 @@
 <template>
   <a-range-picker
     v-model="selected"
-    show-time
-    :time-picker-props="{
-      defaultValue: ['00:00:00', '00:00:00'],
-    }"
+    :show-time="showTime"
     value-format="timestamp"
-    :disabled="disabled"
-    :allow-clear="clearable"
+    :format="format"
+    :style="{ width: '340px' }"
+    v-bind="attrs"
     @change="onChange"
   />
 </template>
 
 <script setup lang="ts">
   import { objModifyAssign } from '@/utils/utils';
-  import { ref, watch } from 'vue';
+  import { ref, watch, useAttrs } from 'vue';
 
   defineOptions({
     name: 'SelectRangeDatetime',
@@ -29,8 +27,8 @@
     defineProps<{
       modelValue: object;
       keys?: KeysType;
-      disabled?: boolean;
-      clearable?: boolean;
+      showTime?: boolean;
+      format?: string;
     }>(),
     {
       modelValue: () => {
@@ -39,11 +37,12 @@
       keys: () => {
         return { startTime: 'startTime', endTime: 'endTime' };
       },
-      disabled: false,
-      clearable: true,
+      showTime: true,
+      format: 'YYYY-MM-DD HH:mm',
     }
   );
   const emit = defineEmits(['update:modelValue', 'change']);
+  const attrs = useAttrs();
 
   const selected = ref([null, null]);
 
@@ -70,6 +69,7 @@
     },
     {
       immediate: true,
+      deep: true,
     }
   );
 </script>

+ 73 - 0
src/components/select-range-time/index.vue

@@ -0,0 +1,73 @@
+<template>
+  <a-time-picker
+    v-model="selected"
+    type="time-range"
+    :format="format"
+    :style="{ width: '300px' }"
+    v-bind="attrs"
+    @change="onChange"
+  />
+</template>
+
+<script setup lang="ts">
+  import { objModifyAssign } from '@/utils/utils';
+  import { ref, watch, useAttrs } from 'vue';
+
+  defineOptions({
+    name: 'SelectRangeDatetime',
+  });
+
+  interface KeysType {
+    startTime: string;
+    endTime: string;
+  }
+
+  const props = withDefaults(
+    defineProps<{
+      modelValue: object;
+      keys?: KeysType;
+      format?: string;
+    }>(),
+    {
+      modelValue: () => {
+        return {};
+      },
+      keys: () => {
+        return { startTime: 'startTime', endTime: 'endTime' };
+      },
+      showTime: true,
+      format: 'HH:mm',
+    }
+  );
+  const emit = defineEmits(['update:modelValue', 'change']);
+  const attrs = useAttrs();
+
+  const selected = ref([null, null]);
+
+  function onChange(value) {
+    const { startTime, endTime } = props.keys;
+    const dt = {};
+    if (value) {
+      dt[startTime] = value[0];
+      dt[endTime] = value[1];
+    } else {
+      dt[startTime] = null;
+      dt[endTime] = null;
+    }
+    objModifyAssign(props.modelValue, dt);
+    emit('update:modelValue', props.modelValue);
+    emit('change', props.modelValue);
+  }
+
+  watch(
+    () => props.modelValue,
+    (val) => {
+      const { startTime, endTime } = props.keys;
+      selected.value = [val[startTime], val[endTime]];
+    },
+    {
+      immediate: true,
+      deep: true,
+    }
+  );
+</script>

+ 41 - 0
src/components/status-tag/index.vue

@@ -0,0 +1,41 @@
+<template>
+  <a-tag :color="theme">{{ label }}</a-tag>
+</template>
+
+<script setup lang="ts">
+  import { computed } from 'vue';
+  import useDictOption from '@/hooks/dict-option';
+
+  defineOptions({
+    name: 'StatusTag',
+  });
+
+  const configs = {
+    enable: {
+      themeDict: { true: 'green', false: 'red' },
+      valFilter: useDictOption('ABLE_TYPE').getLabel,
+    },
+  };
+  type ConfigKeyType = keyof typeof configs;
+
+  const props = defineProps({
+    value: { type: [Boolean, String, Number], default: '' },
+    type: { type: String },
+  });
+
+  function getConfig(type: ConfigKeyType) {
+    // @ts-ignore
+    return configs[type] || { themeDict: {}, valFilter: (val) => val };
+  }
+
+  const { themeDict, valFilter } = getConfig(props.type as ConfigKeyType);
+
+  const theme = computed(() => {
+    // @ts-ignore
+    return themeDict[props.value];
+  });
+  const label = computed(() => {
+    // @ts-ignore
+    return valFilter(props.value);
+  });
+</script>

+ 4 - 0
src/components/svg-icon/index.vue

@@ -11,6 +11,8 @@
   import IconSystem from '../../assets/svgs/icon-system.svg?component';
   import IconUser from '../../assets/svgs/icon-user.svg?component';
   import IconImport from '../../assets/svgs/icon-import.svg?component';
+  import IconAdd from '../../assets/svgs/icon-add.svg?component';
+  import IconDelete from '../../assets/svgs/icon-delete.svg?component';
 
   defineOptions({
     name: 'SvgIcon',
@@ -33,6 +35,8 @@
     IconSystem,
     IconUser,
     IconImport,
+    IconAdd,
+    IconDelete,
   };
 
   const iconComponent = computed(() => icons[snakeToHump(props.name)]);

+ 156 - 0
src/views/order/task-manage/addTimes.vue

@@ -0,0 +1,156 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    title-align="start"
+    title="添加时间"
+    :width="800"
+    top="10vh"
+    :align-center="false"
+    @before-open="modalBeforeOpen"
+  >
+    <a-form ref="formRef" :model="formData" auto-label-width>
+      <a-form-item label="添加方式">
+        <a-radio-group v-model="formData.type" @change="typeChange">
+          <a-radio value="simple">简单创建</a-radio>
+          <a-radio value="loop">循环创建</a-radio>
+        </a-radio-group>
+      </a-form-item>
+
+      <a-form-item
+        v-if="IS_LOOP"
+        label="日期范围"
+        field="date.startTime"
+        :rules="[
+          {
+            required: true,
+            message: '请选择时间',
+          },
+        ]"
+      >
+        <select-range-datetime v-model="formData.date" :show-time="false">
+        </select-range-datetime>
+      </a-form-item>
+
+      <a-form-item
+        v-for="(time, index) in formData.times"
+        :key="index"
+        :field="`times[${index}].startTime`"
+        :rules="[
+          {
+            required: true,
+            message: '请选择时间',
+          },
+        ]"
+      >
+        <select-range-time v-if="IS_LOOP" v-model="formData.times[index]">
+        </select-range-time>
+        <select-range-datetime v-else v-model="formData.times[index]">
+        </select-range-datetime>
+        <a-button class="ml-10" type="primary" @click="toAdd(index)">
+          <template #icon>
+            <svg-icon name="icon-add"></svg-icon>
+          </template>
+        </a-button>
+        <a-button status="danger" @click="toDelete(index)">
+          <template #icon>
+            <svg-icon name="icon-delete"></svg-icon>
+          </template>
+        </a-button>
+      </a-form-item>
+    </a-form>
+
+    <template #footer>
+      <a-button @click="close">取消</a-button>
+      <a-button type="primary" @click="confirm">确认</a-button>
+    </template>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { computed, reactive, ref } from 'vue';
+  import type { FormInstance } from '@arco-design/web-vue/es/form';
+  import useModal from '@/hooks/modal';
+  import { formatDate, objModifyAssign } from '@/utils/utils';
+
+  defineOptions({
+    name: 'AddTimes',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  const emit = defineEmits(['confirm']);
+
+  function getItemItem() {
+    return {
+      id: null,
+      startTime: null,
+      endTime: null,
+    };
+  }
+  function getDefaultFormData() {
+    return {
+      type: 'simple',
+      date: {
+        startTime: null,
+        endTime: null,
+      },
+      times: [getItemItem()],
+    };
+  }
+  const formRef = ref<FormInstance>();
+  const formData = reactive(getDefaultFormData());
+  const IS_LOOP = computed(() => {
+    return formData.type === 'loop';
+  });
+
+  function typeChange() {
+    formData.times = [getItemItem()];
+  }
+
+  function toAdd(index: number) {
+    formData.times.splice(index + 1, 0, getItemItem());
+  }
+
+  function toDelete(index: number) {
+    formData.times.splice(index, 1);
+  }
+
+  /* confirm */
+  async function confirm() {
+    const err = await formRef.value?.validate();
+    if (err) return;
+
+    if (!IS_LOOP.value) {
+      emit('confirm', formData.times);
+      close();
+      return;
+    }
+
+    const times = [];
+    const oneDay = 24 * 60 * 60 * 1000;
+    for (
+      let i = formData.date.startTime;
+      i <= formData.date.endTime;
+      i += oneDay
+    ) {
+      const curDate = formatDate('YYYY/MM/DD', new Date(i));
+      formData.times.forEach((item) => {
+        times.push({
+          id: null,
+          startTime: new Date(`${curDate} ${item.startTime}`).getTime(),
+          endTime: new Date(`${curDate} ${item.endTime}`).getTime(),
+        });
+      });
+    }
+
+    emit('confirm', times);
+    close();
+  }
+
+  /* init modal */
+  function modalBeforeOpen() {
+    objModifyAssign(formData, getDefaultFormData());
+  }
+</script>

+ 28 - 28
src/views/order/task-manage/index.vue

@@ -1,27 +1,21 @@
 <template>
   <div class="part-box is-filter">
-    <a-form
-      layout="inline"
-      :model="searchModel"
-      :label-col-props="{ span: 0, offset: 0 }"
-      :wrapper-col-props="{ span: 24, offset: 0 }"
-    >
-      <a-form-item label="任务名称">
-        <a-input
-          v-model.trim="searchModel.name"
-          placeholder="任务名称"
-          allow-clear
-        ></a-input>
-      </a-form-item>
-      <a-form-item>
-        <a-button type="primary" @click="toPage(1)">查询</a-button>
-      </a-form-item>
-    </a-form>
-    <div>
-      <a-button type="primary" @click="toAdd">新增</a-button>
-    </div>
+    <a-space class="filter-line" :size="12" wrap>
+      <a-input v-model.trim="searchModel.name" placeholder="请输入" allow-clear>
+        <template #prefix>任务名称</template>
+      </a-input>
+      <a-button type="primary" @click="toPage(1)">查询</a-button>
+    </a-space>
   </div>
   <div class="part-box">
+    <a-space class="part-action" :size="6">
+      <a-button type="text" @click="toAdd">
+        <template #icon>
+          <svg-icon name="icon-add"></svg-icon>
+        </template>
+        新增
+      </a-button>
+    </a-space>
     <a-table
       :columns="columns"
       :data="dataList"
@@ -30,7 +24,7 @@
       :bordered="false"
     >
       <template #enable="{ record }">
-        {{ getAbleLabel(record.enable) }}
+        <status-tag type="enable" :value="record.enable" />
       </template>
       <template #selfApplyStartTime="{ record }">
         {{ timestampFilter(record.selfApplyStartTime) }}
@@ -54,6 +48,13 @@
       </template>
     </a-table>
   </div>
+
+  <!-- modifyTask -->
+  <modify-task
+    ref="modifyTaskRef"
+    :row-data="curRow"
+    @modified="getList"
+  ></modify-task>
 </template>
 
 <script setup lang="ts">
@@ -62,19 +63,18 @@
   import { ableTask, taskListPage } from '@/api/order';
   import { TaskItem } from '@/api/types/order';
   import useTable from '@/hooks/table';
-  import useDictOption from '@/hooks/dict-option';
   import { timestampFilter } from '@/utils/filter';
   import { modalConfirm } from '@/utils/arco';
   import { useAppStore } from '@/store';
 
+  import modifyTask from './modifyTask.vue';
+
   defineOptions({
     name: 'TaskManage',
   });
   const appStore = useAppStore();
   appStore.setInfo({ breadcrumbs: ['考试预约管理', '预约任务列表'] });
 
-  const { getLabel: getAbleLabel } = useDictOption('ABLE_TYPE');
-
   const searchModel = reactive({
     name: '',
   });
@@ -83,6 +83,7 @@
     {
       title: '任务ID',
       dataIndex: 'id',
+      width: 80,
     },
     {
       title: '任务名称',
@@ -106,7 +107,6 @@
     {
       title: '更新时间',
       slotName: 'updateTime',
-      width: 170,
     },
     {
       title: '操作',
@@ -123,15 +123,15 @@
   );
 
   // table action
-  const modifyUserRef = ref(null);
+  const modifyTaskRef = ref(null);
   const curRow = ref({});
   function toAdd() {
     curRow.value = {};
-    modifyUserRef.value?.open();
+    modifyTaskRef.value?.open();
   }
   function toEdit(row: TaskItem) {
     curRow.value = row;
-    modifyUserRef.value?.open();
+    modifyTaskRef.value?.open();
   }
 
   async function toEnable(row: TaskItem) {

+ 77 - 0
src/views/order/task-manage/modifyTask.vue

@@ -0,0 +1,77 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    title-align="start"
+    :width="800"
+    :footer="false"
+    top="10vh"
+    :align-center="false"
+    @before-open="modalBeforeOpen"
+  >
+    <template #title> {{ title }} </template>
+
+    <a-tabs
+      v-if="isEdit"
+      v-model:activeKey="compType"
+      type="card-gutter"
+      hide-content
+    >
+      <a-tab-pane key="rule" title="基础规则"> </a-tab-pane>
+      <a-tab-pane key="time" title="预约时段"> </a-tab-pane>
+      <a-tab-pane key="notice" title="考试说明"> </a-tab-pane>
+    </a-tabs>
+
+    <component
+      :is="formComp"
+      v-if="visible"
+      :row-data="rowData"
+      @cancel="close"
+      @modified="modifiedHandle"
+    ></component>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { computed, ref } from 'vue';
+  import useModal from '@/hooks/modal';
+  import { TaskItem } from '@/api/types/order';
+
+  import ruleForm from './ruleForm.vue';
+  import timeForm from './timeForm.vue';
+  import noticeForm from './noticeForm.vue';
+
+  defineOptions({
+    name: 'ModifyTask',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  const props = defineProps<{
+    rowData: TaskItem;
+  }>();
+  const emit = defineEmits(['modified']);
+
+  const isEdit = computed(() => !!props.rowData.id);
+  const title = computed(() => `${isEdit.value ? '编辑' : '新增'}任务`);
+
+  const comps = {
+    rule: ruleForm,
+    time: timeForm,
+    noitce: noticeForm,
+  };
+
+  const compType = ref('rule');
+  const formComp = computed(() => comps[compType.value]);
+
+  function modifiedHandle() {
+    emit('modified');
+    if (isEdit.value) return;
+    close();
+  }
+
+  function modalBeforeOpen() {
+    compType.value = 'rule';
+  }
+</script>

+ 88 - 0
src/views/order/task-manage/noticeForm.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="part-box" :style="{ minHeight: '300px' }">
+    <a-form ref="formRef" :model="formData" :rules="rules" auto-label-width>
+      <a-form-item field="notice" label="预约任务名称">
+        <a-input
+          v-model.trim="formData.notice"
+          placeholder="请输入"
+          allow-clear
+        ></a-input>
+      </a-form-item>
+    </a-form>
+  </div>
+
+  <div class="align-right">
+    <a-button @click="close">取消</a-button>
+    <a-button type="primary" :disabled="loading" @click="confirm"
+      >确认</a-button
+    >
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, reactive, ref } from 'vue';
+  import { Message } from '@arco-design/web-vue';
+  import { updateTaskNotice } from '@/api/order';
+  import useLoading from '@/hooks/loading';
+  import type { FormInstance, FieldRule } from '@arco-design/web-vue/es/form';
+  import { objAssign, objModifyAssign } from '@/utils/utils';
+  import { TaskItem } from '@/api/types/order';
+
+  defineOptions({
+    name: 'NoticeForm',
+  });
+  const props = defineProps<{
+    rowData: TaskItem;
+  }>();
+  const emit = defineEmits(['cancel', 'modified']);
+
+  const defaultFormData = {
+    id: '',
+    notice: '',
+  };
+  type FormDataType = typeof defaultFormData;
+
+  const formRef = ref<FormInstance>();
+  const formData = reactive<FormDataType>({ ...defaultFormData });
+  const rules: Partial<Record<keyof FormDataType, FieldRule[]>> = {
+    notice: [
+      {
+        required: true,
+        message: '请输入内容',
+      },
+    ],
+  };
+
+  function close() {
+    emit('cancel');
+  }
+
+  /* confirm */
+  const { loading, setLoading } = useLoading();
+  async function confirm() {
+    const err = await formRef.value?.validate();
+    if (err) return;
+
+    setLoading(true);
+    const datas = objAssign(formData, {});
+    let res = true;
+    await updateTaskNotice(datas).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+    Message.success('修改成功!');
+    emit('modified');
+  }
+  /* init modal */
+  function modalBeforeOpen() {
+    if (props.rowData.id) {
+      objModifyAssign(formData, props.rowData);
+    } else {
+      objModifyAssign(formData, defaultFormData);
+    }
+  }
+  onMounted(() => {
+    modalBeforeOpen();
+  });
+</script>

+ 187 - 0
src/views/order/task-manage/ruleForm.vue

@@ -0,0 +1,187 @@
+<template>
+  <div class="part-box" :style="{ minHeight: '300px' }">
+    <a-form ref="formRef" :model="formData" :rules="rules" auto-label-width>
+      <a-form-item field="name" label="预约任务名称">
+        <a-input
+          v-model.trim="formData.name"
+          placeholder="请输入"
+          allow-clear
+        ></a-input>
+      </a-form-item>
+      <a-form-item field="allowApplyDays" label="开放预约限制">
+        <span>考前</span>
+        <a-input-number
+          v-model="formData.allowApplyDays"
+          :style="{ width: '120px', margin: '0 10px' }"
+          placeholder="请输入"
+          mode="button"
+          :min="1"
+          :max="999"
+          :step="1"
+        />
+        <span>天,禁止考生自主预约</span>
+      </a-form-item>
+      <a-form-item field="allowApplyCancelDays" label="取消预约限制">
+        <span>考前</span>
+        <a-input-number
+          v-model="formData.allowApplyCancelDays"
+          :style="{ width: '120px', margin: '0 10px' }"
+          placeholder="请输入"
+          mode="button"
+          :min="1"
+          :max="999"
+          :step="1"
+        />
+        <span>天,禁止考生自主预约</span>
+      </a-form-item>
+      <a-form-item field="selfApplyStartTime" label="第一阶段开启时间">
+        <select-range-datetime
+          v-model="formData"
+          :keys="{
+            startTime: 'selfApplyStartTime',
+            endTime: 'selfApplyEndTime',
+          }"
+        >
+        </select-range-datetime>
+        <template #extra>
+          <div>本阶段考生只能预约所属教学点下的考点</div>
+        </template>
+      </a-form-item>
+      <a-form-item field="openApplyStartTime" label="第三阶段开启时间">
+        <select-range-datetime
+          v-model="formData"
+          :keys="{
+            startTime: 'openApplyStartTime',
+            endTime: 'openApplyEndTime',
+          }"
+        >
+        </select-range-datetime>
+        <template #extra>
+          <div>本阶段考生可以预约其他教学点下的考点</div>
+        </template>
+      </a-form-item>
+    </a-form>
+  </div>
+
+  <div class="align-right">
+    <a-button @click="close">取消</a-button>
+    <a-button type="primary" :disabled="loading" @click="confirm"
+      >确认</a-button
+    >
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, reactive, ref } from 'vue';
+  import { Message } from '@arco-design/web-vue';
+  import { updateTaskRule } from '@/api/order';
+  import useLoading from '@/hooks/loading';
+  import type { FormInstance, FieldRule } from '@arco-design/web-vue/es/form';
+  import { objAssign, objModifyAssign } from '@/utils/utils';
+  import { TaskItem } from '@/api/types/order';
+
+  defineOptions({
+    name: 'RuleForm',
+  });
+  const props = defineProps<{
+    rowData: TaskItem;
+  }>();
+  const emit = defineEmits(['cancel', 'modified']);
+
+  const defaultFormData = {
+    id: '',
+    name: '',
+    allowApplyDays: 1,
+    allowApplyCancelDays: 1,
+    selfApplyStartTime: null,
+    selfApplyEndTime: null,
+    openApplyStartTime: null,
+    openApplyEndTime: null,
+  };
+  type FormDataType = typeof defaultFormData;
+
+  const formRef = ref<FormInstance>();
+  const formData = reactive<FormDataType>({ ...defaultFormData });
+  const rules: Partial<Record<keyof FormDataType, FieldRule[]>> = {
+    name: [
+      {
+        required: true,
+        message: '请输入名称',
+      },
+      {
+        max: 50,
+        message: '名称不能超过50',
+      },
+    ],
+    allowApplyDays: [
+      {
+        required: true,
+        message: '请输入开放预约限制时间',
+      },
+    ],
+    allowApplyCancelDays: [
+      {
+        required: true,
+        message: '请输入取消预约限制时间',
+      },
+    ],
+    selfApplyStartTime: [
+      {
+        required: true,
+        message: '请选择自主预约时间',
+      },
+    ],
+    openApplyStartTime: [
+      {
+        required: true,
+        message: '请选择开放预约时间',
+      },
+      {
+        validator: (value, callback) => {
+          if (
+            formData.openApplyStartTime &&
+            formData.selfApplyEndTime &&
+            formData.openApplyStartTime < formData.selfApplyEndTime
+          ) {
+            return callback('第三阶段开启时间不得早于第一阶段开启时间');
+          }
+          return callback();
+        },
+      },
+    ],
+  };
+
+  function close() {
+    emit('cancel');
+  }
+
+  /* confirm */
+  const { loading, setLoading } = useLoading();
+  async function confirm() {
+    const err = await formRef.value?.validate();
+    if (err) return;
+
+    setLoading(true);
+    const datas = objAssign(formData, {});
+    let res = true;
+    await updateTaskRule(datas).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+    Message.success('修改成功!');
+    objModifyAssign(props.rowData, formData);
+    emit('modified');
+  }
+  /* init modal */
+  function modalBeforeOpen() {
+    if (props.rowData.id) {
+      objModifyAssign(formData, props.rowData);
+    } else {
+      objModifyAssign(formData, defaultFormData);
+    }
+  }
+  onMounted(() => {
+    modalBeforeOpen();
+  });
+</script>

+ 116 - 0
src/views/order/task-manage/timeForm.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="part-box" :style="{ minHeight: '300px' }">
+    <a-form ref="formRef" :model="formData" auto-label-width>
+      <a-form-item label="可选考试时段">
+        <a-button type="primary" @click="toAdd">添加</a-button>
+      </a-form-item>
+      <a-form-item
+        v-for="(time, index) in formData.times"
+        :key="index"
+        :field="`times[${index}].startTime`"
+        :rules="rules"
+      >
+        <select-range-datetime v-model="formData.times[index]">
+        </select-range-datetime>
+        <a-button class="ml-10" status="danger" @click="toDelete(index)">
+          <template #icon>
+            <svg-icon name="icon-delete"></svg-icon>
+          </template>
+        </a-button>
+      </a-form-item>
+    </a-form>
+  </div>
+
+  <div class="align-right">
+    <a-button @click="close">取消</a-button>
+    <a-button type="primary" :disabled="loading" @click="confirm"
+      >确认</a-button
+    >
+  </div>
+
+  <!-- AddTimes -->
+  <AddTimes ref="addTimesRef" @confirm="timeSelected" />
+</template>
+
+<script setup lang="ts">
+  import { onMounted, reactive, ref } from 'vue';
+  import { Message } from '@arco-design/web-vue';
+  import { updateTaskTime } from '@/api/order';
+  import useLoading from '@/hooks/loading';
+  import type { FormInstance, FieldRule } from '@arco-design/web-vue/es/form';
+  import { objAssign, objModifyAssign } from '@/utils/utils';
+  import { TaskItem } from '@/api/types/order';
+  import AddTimes from './addTimes.vue';
+
+  defineOptions({
+    name: 'TimeForm',
+  });
+  const props = defineProps<{
+    rowData: TaskItem;
+  }>();
+  const emit = defineEmits(['cancel', 'modified']);
+
+  const defaultFormData = {
+    id: null,
+    times: [],
+  };
+
+  const formRef = ref<FormInstance>();
+  const formData = reactive(defaultFormData);
+
+  const rules: FieldRule[] = [
+    {
+      required: true,
+      message: '请选择时间',
+    },
+  ];
+
+  function close() {
+    emit('cancel');
+  }
+
+  const addTimesRef = ref(null);
+  function toAdd() {
+    addTimesRef.value?.open();
+  }
+
+  function timeSelected(times) {
+    formData.times.push(...times);
+  }
+
+  function toDelete(index: number) {
+    formData.times.splice(index, 1);
+  }
+
+  /* confirm */
+  const { loading, setLoading } = useLoading();
+  async function confirm() {
+    const err = await formRef.value?.validate();
+    if (err) return;
+
+    setLoading(true);
+    const datas = {
+      id: formData.id,
+      timeJson: JSON.parse(formData.times),
+    };
+    let res = true;
+    await updateTaskTime(datas).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+    Message.success('修改成功!');
+    emit('modified');
+  }
+  /* init */
+  function modalBeforeOpen() {
+    if (props.rowData.id) {
+      objModifyAssign(formData, props.rowData);
+    } else {
+      objModifyAssign(formData, defaultFormData);
+    }
+  }
+  onMounted(() => {
+    modalBeforeOpen();
+  });
+</script>