shmily1213 1 месяц назад
Родитель
Сommit
0fc3f98a94

+ 49 - 1
back-ui/src/api/dz/cards.js

@@ -153,6 +153,30 @@ export function refundCard(cardIds) {
   });
 }
 
+// 结算
+export function settleCard(cardIds) {
+  return request({
+    url: "/dz/cards/changeCard",
+    method: "post",
+    params: {
+      action: "Settlement",
+      cardIds: Array.isArray(cardIds) ? cardIds.join(",") : cardIds,
+    },
+  });
+}
+
+// 续费
+export function renewCard(cardIds) {
+  return request({
+    url: "/dz/cards/changeCard",
+    method: "post",
+    params: {
+      action: "Renew",
+      cardIds: Array.isArray(cardIds) ? cardIds.join(",") : cardIds,
+    },
+  });
+}
+
 // 获取学校列表
 export function getSchoolList(query) {
   return request({
@@ -185,7 +209,13 @@ export function associateCampus(beginCardNo, endCardNo, campusId) {
 }
 
 // 申请开卡
-export function requestOpenCard(agentId, province, schoolId, beginCardNo, endCardNo) {
+export function requestOpenCard(
+  agentId,
+  province,
+  schoolId,
+  beginCardNo,
+  endCardNo
+) {
   return request({
     url: "/dz/cards/openCard",
     method: "post",
@@ -216,3 +246,21 @@ export function getStatisticsDetail(query) {
     params: query,
   });
 }
+
+// 根据卡号获取用户信息
+export function getUserByCardId(cardId) {
+  return request({
+    url: "/dz/cards/cardUser/" + cardId,
+    method: "get",
+    params: {},
+  });
+}
+
+// 更新卡用户信息
+export function updateCardUser(data) {
+  return request({
+    url: "/dz/cards/updateCardUser",
+    method: "post",
+    data: data,
+  });
+}

+ 17 - 0
back-ui/src/api/front/university.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request';
+
+export function getUniversityList(query) {
+  return request({
+    url: "/front/university/list",
+    method: "get",
+    params: query,
+  });
+}
+
+export function getUniversityMajorList(query) {
+  return request({
+    url: "/front/student/university/major",
+    method: "get",
+    params: query,
+  });
+}

+ 4 - 0
back-ui/src/assets/styles/element-ui.scss

@@ -115,4 +115,8 @@
   .el-dialog:not(.is-fullscreen) {
     margin-top: 0 !important;
   }
+}
+
+.el-message {
+  margin-top: 50px;
 }

+ 29 - 0
back-ui/src/components/IeUniversitySelect/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <el-select ref="selectRef" v-bind="$attrs">
+    <el-option v-for="option in dataList" :key="option.id" :label="option.name" :value="option.id" />
+  </el-select>
+</template>
+<script setup>
+import { listUniversity } from '@/api/dz/school';
+import { nextTick, onMounted } from 'vue';
+const props = defineProps({
+  deptId: {
+    type: Number,
+    default: null,
+  },
+});
+const selectRef = ref(null);
+const dataList = ref([])
+const loadData = async () => {
+  const { rows } = await listUniversity({
+    pageNum: 1,
+    pageSize: 100000
+  })
+  dataList.value = rows;
+}
+
+onMounted(() => {
+  loadData();
+});
+</script>
+<style lang="scss" scoped></style>

+ 64 - 25
back-ui/src/hooks/useSchool.js

@@ -11,8 +11,11 @@ const defaultOptions = {
   loadClass: false,
   loadExamType: false,
 };
-const useSchool = (options = defaultOptions) => {
-  const { autoLoad, loadCampus, loadClass, loadExamType } = options;
+const useSchool = (options = {}) => {
+  const { autoLoad, loadCampus, loadClass, loadExamType } = Object.assign(
+    defaultOptions,
+    options
+  );
 
   const areaList = ref([]);
   const selectedArea = ref([]);
@@ -30,35 +33,44 @@ const useSchool = (options = defaultOptions) => {
   const selectedCampusClass = ref();
 
   const examTypeList = ref([]);
-  const selectedExamType = ref('all');
-
+  const selectedExamType = ref("");
 
   const getExamTypeList = async () => {
     examTypeList.value = [];
-    const res = await getExamTypes((getProvinceName() || '').replace('省', ''));
-    console.log(res)
+    const res = await getExamTypes((getProvinceName() || "").replace("省", ""));
     examTypeList.value = res.data;
+    return res.data;
   };
 
-  const getProvincesList = async () => {
+  const getAreaList = async () => {
     const rows = await cascaderAreaList();
     areaList.value = rows;
+    return rows;
   };
 
   const getSchoolList = async () => {
+    schoolList.value = [];
     const areaId = Array.isArray(selectedArea.value)
       ? selectedArea.value?.[selectedArea.value.length - 1]
       : selectedArea.value;
     const res = await listSchool({
       pro: areaId,
+      pageNo: 1,
+      pageSize: 100000,
     });
     schoolList.value = res.rows;
-    console.log(res);
+    return res.rows;
   };
 
   const getClassList = async () => {
-    const res = await listClasses();
-    classList.value = res.data;
+    classList.value = [];
+    const res = await listClasses({
+      schoolId: selectedSchool.value,
+      pageNo: 1,
+      pageSize: 100000,
+    });
+    classList.value = res.rows;
+    return res.rows;
   };
 
   const getCampusList = async () => {
@@ -69,25 +81,34 @@ const useSchool = (options = defaultOptions) => {
       pro: areaId,
     });
     campusList.value = res.rows;
+    return res.rows;
   };
 
   const getCampusClassList = async () => {
+    campusClassList.value = [];
     const res = await listClasses({
       campusId: selectedCampus.value,
     });
     campusClassList.value = res.rows;
+    return res.rows;
   };
 
   watch(
     () => selectedArea.value,
     (newVal) => {
-      selectedSchool.value = null;
-      selectedCampus.value = null;
-      if (selectedExamType !== 'all') {
+      if (schoolList.value.length > 0) {
+        selectedSchool.value = null;
+        schoolList.value = [];
+      }
+      if (campusList.value.length > 0) {
+        selectedCampus.value = null;
+        campusList.value = [];
+      }
+      if (examTypeList.value.length > 0) {
         selectedExamType.value = null;
+        examTypeList.value = [];
       }
-
-      if (newVal || (Array.isArray(newVal) && newVal.length > 0)) {
+      if (isAreaValid(newVal)) {
         getSchoolList();
         if (loadCampus) {
           getCampusList();
@@ -102,7 +123,11 @@ const useSchool = (options = defaultOptions) => {
   watch(
     () => selectedSchool.value,
     (newVal) => {
-      selectedClass.value = null;
+      if (classList.value.length > 0) {
+        selectedClass.value = null;
+        classList.value = [];
+      }
+      console.log(newVal, 222)
       if (newVal !== null) {
         if (loadClass) {
           getClassList();
@@ -114,7 +139,10 @@ const useSchool = (options = defaultOptions) => {
   watch(
     () => selectedCampus.value,
     (newVal) => {
-      selectedCampusClass.value = null;
+      if (campusClassList.value.length > 0) {
+        selectedCampusClass.value = null;
+        campusClassList.value = [];
+      }
       if (newVal !== null) {
         getCampusClassList();
       }
@@ -130,22 +158,28 @@ const useSchool = (options = defaultOptions) => {
   };
 
   const init = () => {
-    getProvincesList();
-  }
+    getAreaList();
+  };
 
   const reset = () => {
-    selectedArea.value = [];
+    selectedArea.value = Array.isArray(selectedArea.value) ? [] : null;
     selectedSchool.value = null;
     selectedClass.value = null;
     selectedCampus.value = null;
     selectedCampusClass.value = null;
-    selectedExamType.value = 'all';
-    console.log('end', selectedArea)
-  }
+    selectedExamType.value = "";
+  };
+
+  const isAreaValid = (value) => {
+    return (
+      (!Array.isArray(value) && value !== null) ||
+      (Array.isArray(value) && value.length > 0)
+    );
+  };
 
   onMounted(() => {
     if (autoLoad) {
-      getProvincesList();
+      getAreaList();
     }
   });
 
@@ -155,21 +189,26 @@ const useSchool = (options = defaultOptions) => {
 
     areaList,
     selectedArea,
+    getAreaList,
 
     schoolList,
     selectedSchool,
 
     classList,
     selectedClass,
+    getClassList,
 
     campusList,
     selectedCampus,
+    getCampusList,
 
     campusClassList,
     selectedCampusClass,
+    getCampusClassList,
 
     examTypeList,
-    selectedExamType
+    selectedExamType,
+    getExamTypeList,
   };
 };
 export default useSchool;

+ 22 - 25
back-ui/src/views/dz/cards/components/AssignDialog.vue

@@ -1,5 +1,6 @@
 <template>
-  <IeModal title="分配卡" confirmText="确认分配" ref="modalRef" width="500px" @confirm="handleConfirm">
+  <IeModal title="分配卡" confirmText="确认分配" ref="modalRef" width="500px" @beforeClose="handleBeforeClose"
+    @confirm="handleConfirm">
     <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
       <el-form-item label="平台机构" prop="deptId" required>
         <ie-institution-select v-model="form.deptId" class="w-[350px]!" />
@@ -26,7 +27,8 @@
           class="w-[350px]!" />
       </el-form-item>
       <el-form-item label="考生类型" prop="assignExamType">
-        <ie-select v-model="selectedExamType" :options="examTypeOptions" class="w-[350px]!" />
+        <ie-select v-model="selectedExamType" :options="examTypeList" label-key="dictLabel" value-key="dictValue"
+          class="w-[350px]!" />
       </el-form-item>
       <el-form-item v-if="form.cardType === '9'" label="有效期" prop="days">
         <el-input-number v-model="form.days" :min="1" :max="10000" :step="1" :precision="0" class="w-full!">
@@ -43,7 +45,7 @@ import IeSelect from '@/components/IeSelect/index.vue';
 import IeAgentSelect from '@/components/IeAgentSelect/index.vue';
 import IeInstitutionSelect from '@/components/IeInstitutionSelect/index.vue';
 import { assignCard } from '@/api/dz/cards';
-import { getCurrentInstance } from 'vue';
+import { getCurrentInstance, watchEffect } from 'vue';
 
 
 const { proxy } = getCurrentInstance();
@@ -52,13 +54,15 @@ const {
   exam_type,
 } = proxy.useDict("card_type", "exam_type");
 const {
+  init,
+  reset,
   areaList,
   selectedArea,
   schoolList,
   selectedSchool,
   examTypeList,
   selectedExamType
-} = useSchool({ loadExamType: true });
+} = useSchool({ autoLoad: false, loadExamType: true, loadClass: false });
 
 const modalRef = ref(null);
 const formRef = ref(null);
@@ -67,18 +71,10 @@ const defaultForm = {
   agentId: null,
   cardType: '',
   count: 1,
-  assignExamType: 'all',
+  assignExamType: '',
   days: 1,
 };
-const examTypeOptions = computed(() => {
-  return [{
-    label: '全部',
-    value: 'all',
-  }, ...examTypeList.value.map(item => ({
-    label: item.dictLabel,
-    value: item.dictValue,
-  }))];
-});
+
 const form = ref({ ...defaultForm })
 // 自定义验证
 const validateCardNoRange = (rule, value, callback) => {
@@ -91,18 +87,13 @@ const validateCardNoRange = (rule, value, callback) => {
   }
 }
 
-watch([() => form.value.begin, () => form.value.end], () => {
+watchEffect(() => {
   form.value.cardNoRange = `${form.value.begin}-${form.value.end}`;
-})
-watch(() => selectedArea.value, (val) => {
-  form.value.provinceId = val;
-})
-watch(() => selectedSchool.value, (val) => {
-  form.value.schoolId = val;
-})
-watch(() => selectedExamType.value, (val) => {
-  form.value.assignExamType = val;
-})
+  form.value.provinceId = selectedArea.value;
+  form.value.schoolId = selectedSchool.value;
+  form.value.assignExamType = selectedExamType.value;
+});
+
 const rules = ref({
   deptId: [
     { required: true, message: '请选择平台机构', trigger: 'change' },
@@ -130,7 +121,13 @@ const rules = ref({
   ],
 })
 
+const handleBeforeClose = () => {
+  form.value = { ...defaultForm };
+  reset();
+}
+
 const open = () => {
+  init();
   form.value = { ...defaultForm };
   modalRef.value.open()
 }

+ 146 - 0
back-ui/src/views/dz/cards/components/DirectionDialog.vue

@@ -0,0 +1,146 @@
+<template>
+  <IeModal title="定向院校选择" confirmText="确认选择" ref="modalRef" width="800px" @beforeClose="handleBeforeClose"
+    @confirm="handleConfirm">
+    <div class="h-120 flex flex-col">
+      <div class="flex-1 min-h-1 relative">
+        <div class="absolute top-0 left-0 w-full h-full">
+          <el-table :data="tableData" height="100%" preserve-expanded-content border v-loading="univerityLoading"
+            @expand-change="handleExpandChange">
+            <el-table-column type="expand" width="50">
+              <template #default="props">
+                <div :class="{ 'is-expanded': props.expanded }">
+                  <MajorTable :key="props.row.id" :expanded="props.expanded" :universityId="props.row.id"
+                    :selection="selectedList" @selection-change="handleSelectionChange($event, props.row)" />
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="院校名称" prop="name">
+              <template #header>
+                <el-input v-model="queryParams.name" placeholder="搜索院校" prefix-icon="Search" clearable
+                  @clear="handleQuery" @keyup.enter="handleQuery" />
+              </template>
+              <template #default="scope">
+                <div class="flex items-center gap-x-2">
+                  <el-image :src="scope.row.logo" class="w-10 h-10 rounded-full shrink-0" />
+                  <div class="flex-1 min-w-1">
+                    <div class="flex items-center gap-x-2">
+                      <div class="font-bold">{{ scope.row.name }}</div>
+                      <div class="flex gap-x-2">
+                        <el-tag :type="index === 0 ? 'primary' : 'info'" size="mini" disable-transitions
+                          class="px-[4px]! py-[3px]! text-[11px]! h-auto!"
+                          v-for="(item, index) in getTags(scope.row.bxLevel)" :key="item">{{ item }}</el-tag>
+                      </div>
+                      <div v-if="scope.row.star"
+                        class="text-[10px] text-[#ff9800] border-current border-[1px] rounded-[3px] px-[4px] h-[18px] leading-[16px]">
+                        {{ scope.row.star }}</div>
+                    </div>
+                    <div class="mt-[2px] flex items-center">
+                      <el-icon color="#999">
+                        <Location />
+                      </el-icon>
+                      <div class="text-gray-400 text-[13px] ml-1 ellipsis-1">{{ scope.row.address }}</div>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+      <div class="flex justify-end">
+        <Pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
+          @pagination="getList" />
+      </div>
+    </div>
+  </IeModal>
+</template>
+<script setup>
+import IeModal from '@/components/IeModal/index.vue';
+import MajorTable from './MajorTable.vue';
+import { getUniversityList } from '@/api/front/university';
+import { getCurrentInstance } from 'vue';
+
+const { proxy } = getCurrentInstance();
+
+const tableData = ref([]);
+const total = ref(0);
+const univerityLoading = ref(false);
+const selectedList = ref([]);
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 20
+});
+
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+const getList = () => {
+  univerityLoading.value = true;
+  getUniversityList(queryParams.value).then(res => {
+    tableData.value = res.rows;
+    total.value = res.total;
+    console.log(res)
+  }).finally(() => {
+    univerityLoading.value = false;
+  })
+}
+
+const getTags = (tags) => {
+  return tags.split(',').map(item => item.trim());
+}
+
+
+const handleExpandChange = (row, expandedRows) => {
+}
+
+const handleSelectionChange = (selection, row) => {
+  const { code, name, logo } = row;
+  selectedList.value = selectedList.value.filter(item => item.universityId !== code);
+  selection.forEach(major => {
+    selectedList.value.push({
+      code,
+      universityId: code,
+      univerityLogo: logo,
+      universityName: name,
+      majorId: major.id,
+      majorName: major.name,
+      majorAncestors: major.ancestors
+    });
+  });
+  console.log(selectedList.value, 'selectedList')
+}
+
+const modalRef = ref(null);
+const handleBeforeClose = () => {
+  modalRef.value.close();
+}
+const emit = defineEmits(['confirm']);
+const handleConfirm = () => {
+  if (selectedList.value.length === 0) {
+    proxy.$modal.msgError('请选择定向院校');
+    return;
+  }
+  emit('confirm', selectedList.value);
+  close();
+}
+
+
+
+const open = (selection) => {
+  selectedList.value = selection || [];
+  console.log(selectedList.value, 111)
+  handleQuery();
+  modalRef.value.open();
+}
+const close = () => {
+  modalRef.value.close();
+}
+defineExpose({
+  open,
+  close
+})
+</script>
+<style lang="scss" scoped></style>

+ 201 - 46
back-ui/src/views/dz/cards/components/EditDialog.vue

@@ -1,81 +1,236 @@
 <template>
-  <IeModal :title="title" :confirmText="confirmButtonText" ref="modalRef" width="500px" @confirm="handleConfirm">
+  <IeModal title="修改用户" ref="modalRef" width="700px" @beforeClose="handleBeforeClose" @confirm="handleConfirm">
     <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
-      <el-form-item label="平台机构" prop="institutionId" :required="true">
-        <ie-institution-select v-model="form.institutionId" />
-      </el-form-item>
-      <el-form-item label="卡类型" prop="cardType">
-        <ie-select v-model="form.cardType" :options="card_type" />
-      </el-form-item>
-      <el-form-item label="卡数量" prop="count">
-        <el-input-number v-model="form.count" :min="1" :max="10000" :step="1" :precision="0" class="w-full!" />
-      </el-form-item>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="学生姓名" prop="nickName" :required="true">
+            <el-input v-model="form.nickName" placeholder="请输入学生姓名" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="手机号" prop="mobile" :required="true">
+            <el-input v-model="form.mobile" maxlength="11" placeholder="请输入手机号" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="省份" prop="provinceId">
+            <ie-select v-model="selectedArea" :options="areaList" label-key="areaName" value-key="areaId" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="考生类型" prop="examType">
+            <ie-select v-model="selectedExamType" :options="examTypeList" label-key="dictLabel" value-key="dictValue" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="注册学校" prop="schoolId">
+            <ie-select v-model="selectedSchool" :options="schoolList" label-key="name" value-key="id" filterable />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="注册班级" prop="classId">
+            <ie-select v-model="selectedClass" :options="classList" label-key="name" value-key="classId" filterable />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="培训学校" prop="campusSchoolId">
+            <ie-select v-model="selectedCampus" :options="campusList" label-key="name" value-key="id" filterable />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="培训班级" prop="campusClassId">
+            <ie-select v-model="selectedCampusClass" :options="campusClassList" label-key="name" value-key="classId"
+              filterable />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="语文成绩" prop="scores.chinese">
+            <el-input v-model.number="form.scores.chinese" v-number placeholder="请输入语文成绩" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="数学成绩" prop="scores.mathematics">
+            <el-input v-model.number="form.scores.mathematics" v-number placeholder="请输入数学成绩" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="外语成绩" prop="scores.foreign">
+            <el-input v-model.number="form.scores.foreign" v-number placeholder="请输入外语成绩" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="政治成绩" prop="scores.political">
+            <el-input v-model.number="form.scores.political" v-number placeholder="请输入政治成绩" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="物理成绩" prop="scores.physics">
+            <el-input v-model.number="form.scores.physics" v-number placeholder="请输入物理成绩" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="到期时间" prop="outDate">
+            <el-date-picker v-model="form.outDate" placeholder="请选择到期时间" value-format="YYYY-MM-DD" class="w-full!" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="目标院校" prop="targetCollege">
+            <div class="w-full flex items-center gap-x-2 ">
+              <div class="flex-1 flex flex-col gap-2">
+                <div
+                  class="flex items-center gap-x-2 border-1 border-solid border-[#e5e5e5] rounded-[4px] px-2 py-2 relative"
+                  v-for="(item, index) in form.directionStudy" :key="item.id">
+                  <div class="text-[13px] leading-[13px] font-bold">{{ item.universityName }}</div>
+                  <div
+                    class="text-[11px] text-[#5692fa] leading-[11px] border-1 border-solid border-[#5692fa] rounded-[4px] px-1 py-1">
+                    {{ item.majorName }}</div>
+                  <div class="text-[11px] text-[#999] leading-[11px]">{{ item.majorAncestors }}</div>
+                  <div class="absolute top-1/2 right-2 -translate-y-1/2 cursor-pointer">
+                    <el-icon @click="handleRemoveTargetCollege(index)" color="#ff4949">
+                      <Close />
+                    </el-icon>
+                  </div>
+                </div>
+              </div>
+              <el-button type="primary" :disabled="form.directionStudy.length >= 3" icon="plus" plain
+                @click="handleAddTargetCollege">添加</el-button>
+            </div>
+          </el-form-item>
+        </el-col>
+      </el-row>
     </el-form>
   </IeModal>
+  <DirectionDialog ref="directionDialogRef" @confirm="handleDirectionConfirm" />
 </template>
 <script setup>
+import useSchool from '@/hooks/useSchool';
 import IeModal from '@/components/IeModal/index.vue';
 import IeSelect from '@/components/IeSelect/index.vue';
-import IeInstitutionSelect from '@/components/IeInstitutionSelect/index.vue';
-import { issueCard } from '@/api/dz/cards';
-import { getCurrentInstance } from 'vue';
+import IeUniversitySelect from '@/components/IeUniversitySelect/index.vue';
+import DirectionDialog from './DirectionDialog.vue';
+import { updateCardUser, getUserByCardId } from '@/api/dz/cards';
+import { getCurrentInstance, nextTick, watch, watchEffect } from 'vue';
 
 const { proxy } = getCurrentInstance();
-const {
-  card_type,
-} = proxy.useDict("card_type");
 
 const modalRef = ref(null);
 const formRef = ref(null);
+const userInfo = ref({});
 const form = ref({
-  institutionId: null,
-  cardType: null,
-  count: 1,
+  scores: {},
+  directionStudy: []
 })
-const operation = ref('add')
-const rules = ref({
-  institutionId: [{ required: true, message: '请选择平台机构', trigger: 'change' }],
-  cardType: [{ required: true, message: '请选择卡类型', trigger: 'change' }],
-  count: [{ required: true, message: '请输入卡数量', trigger: 'blur' }],
-})
-const title = computed(() => {
-  return operation.value === 'add' ? '制卡' : '修改';
+
+const {
+  init,
+  reset,
+  areaList,
+  selectedArea,
+  getAreaList,
+  schoolList,
+  selectedSchool,
+  classList,
+  selectedClass,
+  examTypeList,
+  selectedExamType,
+  getExamTypeList,
+  campusList,
+  selectedCampus,
+  campusClassList,
+  selectedCampusClass,
+} = useSchool({ autoLoad: false, loadExamType: true, loadClass: true, loadCampus: true });
+
+watch(() => selectedArea.value, (val) => {
+  form.value.provinceId = selectedArea.value;
+  if (!Array.isArray(selectedArea.value) && selectedArea.value) {
+    getExamTypeList();
+  }
 })
-const confirmButtonText = computed(() => {
-  return operation.value === 'add' ? '确认制卡' : '确认修改';
+
+watchEffect(() => {
+  form.value.examType = selectedExamType.value;
+  form.value.schoolId = selectedSchool.value;
+  form.value.classId = selectedClass.value;
+  form.value.campusSchoolId = selectedCampus.value;
+  form.value.campusClassId = selectedCampusClass.value;
+});
+
+
+
+const rules = ref({
+  nickName: [
+    { required: true, message: '请输入学生姓名', trigger: ['blur', 'change'] }
+  ],
+  mobile: [
+    { required: true, message: '请输入手机号', trigger: ['blur', 'change'] },
+    {
+      pattern: /^1[3-9]\d{9}$/,
+      message: '请输入正确的手机号',
+      trigger: ['blur', 'change']
+    }
+  ],
 })
 
-// 获取卡类型枚举
-const getCardTypeEnum = (typeValue) => {
-  const cardTypeMap = {
-    6: "Platform",
-    2: "Dept",
-    7: "ECard",
-    8: "Test",
+const handleBeforeClose = () => {
+  reset();
+  form.value = {
+    scores: {},
+    directionStudy: []
   };
-  return cardTypeMap[typeValue] || "ECard";
 }
 
-const open = (type = 'add', data = { institutionId: null, cardType: null, count: 1 }) => {
-  operation.value = type;
-  form.value = data;
+const open = (cardInfo) => {
+  getUserInfo(cardInfo);
   modalRef.value.open()
 }
 const close = () => {
   modalRef.value.close()
 }
+
+const getUserInfo = (cardInfo) => {
+  const { cardId, location, nickName, outDate } = cardInfo;
+  getUserByCardId(cardId).then(async res => {
+    userInfo.value = { ...res.data };
+    form.value = {
+      ...res.data,
+      location,
+      nickName,
+      outDate
+    };
+    const areaList = await getAreaList();
+    const area = areaList.find(item => item.areaName === location + '省');
+    if (area) {
+      selectedArea.value = area.areaId;
+    }
+    selectedExamType.value = res.data.examType;
+  })
+}
+
+const directionDialogRef = ref(null);
+const handleAddTargetCollege = () => {
+  directionDialogRef.value.open(form.value.directionStudy);
+}
+const handleDirectionConfirm = (selectedList) => {
+  form.value.directionStudy = selectedList;
+  console.log(form.value.directionStudy, 'form.value.directionStudy')
+}
+
+const handleRemoveTargetCollege = (index) => {
+  form.value.directionStudy.splice(index, 1);
+}
+
 const emit = defineEmits(['refresh'])
 const handleConfirm = () => {
   formRef.value.validate((valid) => {
     if (valid) {
-      const { institutionId, cardType, count } = form.value;
       modalRef.value.showLoading()
-      issueCard(institutionId, getCardTypeEnum(cardType), count).then(res => {
-        proxy.$modal.msgSuccess('制卡成功')
+      updateCardUser(form.value).then(res => {
+        proxy.$modal.msgSuccess('修改成功')
         close();
-        setTimeout(() => {
-          emit('refresh')
-        }, 300);
+        emit('refresh')
       }).finally(() => {
         modalRef.value.hideLoading()
       })

+ 96 - 0
back-ui/src/views/dz/cards/components/MajorTable.vue

@@ -0,0 +1,96 @@
+<template>
+  <div>
+    <el-table ref="tableRef" :data="filteredData" height="100%" border v-loading="loading" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="50" align="center" :selectable="selectable"></el-table-column>
+      <el-table-column label="方向名称" prop="name">
+        <template #header>
+          <el-input v-model="tempKeyword" placeholder="搜索专业" prefix-icon="Search" clearable @clear="handleQuery" @keyup.enter="handleQuery" />
+        </template>
+        <template #default="scope">
+          <div class="flex items-center gap-x-2">
+            <span class="font-bold">{{ scope.row.name }}</span>
+            <span class="text-[#999] text-[12px]">{{ scope.row.ancestors }}</span>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+<script setup>
+import { getUniversityMajorList } from '@/api/front/university';
+import { nextTick } from 'vue';
+const props = defineProps({
+  universityId: {
+    type: Number,
+    default: null,
+  },
+  expanded: {
+    type: Boolean,
+    default: false,
+  },
+  selection: {
+    type: Array,
+    default: () => [],
+  }
+});
+
+const data = ref([]);
+const selectedRows = ref([]);
+const tableRef = ref(null);
+const filteredData = computed(() => {
+  return data.value.filter(item => item.name.includes(keyword.value));
+});
+const keyword = ref('');
+const tempKeyword = ref('');
+const loading = ref(false);
+
+const selectable = (row) => {
+  if (props.selection.length < 3) {
+    return true;
+  }
+  const hasSelected = selectedRows.value.map(item => item.id).includes(row.id);
+  return hasSelected;
+}
+
+const emit = defineEmits(['selection-change']);
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection;
+  emit('selection-change', selection);
+}
+const handleQuery = () => {
+  keyword.value = tempKeyword.value;
+}
+const getList = () => {
+  loading.value = true;
+  getUniversityMajorList({
+    universityId: props.universityId,
+  }).then(res => {
+    data.value = res.data;
+    nextTick(() => {
+      // 恢复选中状态:根据 props.selection 中的 id 找到对应的 row 并选中
+      props.selection.forEach(selectedItem => {
+        const row = data.value.find(dataItem => dataItem.id === selectedItem.majorId);
+        if (row) {
+          tableRef.value.toggleRowSelection(row, true);
+        }
+      });
+    });
+  }).finally(() => {
+    loading.value = false;
+  });
+}
+
+
+watch(() => props.expanded, (newVal) => {
+  if (newVal && data.value.length === 0) {
+    getList();
+  }
+});
+</script>
+<style lang="scss" scoped>
+:deep(.el-table__header) {
+  .el-checkbox {
+    display: none;
+  }
+}
+</style>

+ 90 - 0
back-ui/src/views/dz/cards/components/MakeDialog.vue

@@ -0,0 +1,90 @@
+<template>
+  <IeModal :title="title" :confirmText="confirmButtonText" ref="modalRef" width="500px" @confirm="handleConfirm">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+      <el-form-item label="平台机构" prop="institutionId" :required="true">
+        <ie-institution-select v-model="form.institutionId" />
+      </el-form-item>
+      <el-form-item label="卡类型" prop="cardType">
+        <ie-select v-model="form.cardType" :options="card_type" />
+      </el-form-item>
+      <el-form-item label="卡数量" prop="count">
+        <el-input-number v-model="form.count" :min="1" :max="10000" :step="1" :precision="0" class="w-full!" />
+      </el-form-item>
+    </el-form>
+  </IeModal>
+</template>
+<script setup>
+import IeModal from '@/components/IeModal/index.vue';
+import IeSelect from '@/components/IeSelect/index.vue';
+import IeInstitutionSelect from '@/components/IeInstitutionSelect/index.vue';
+import { issueCard } from '@/api/dz/cards';
+import { getCurrentInstance } from 'vue';
+
+const { proxy } = getCurrentInstance();
+const {
+  card_type,
+} = proxy.useDict("card_type");
+
+const modalRef = ref(null);
+const formRef = ref(null);
+const form = ref({
+  institutionId: null,
+  cardType: null,
+  count: 1,
+})
+const operation = ref('add')
+const rules = ref({
+  institutionId: [{ required: true, message: '请选择平台机构', trigger: 'change' }],
+  cardType: [{ required: true, message: '请选择卡类型', trigger: 'change' }],
+  count: [{ required: true, message: '请输入卡数量', trigger: 'blur' }],
+})
+const title = computed(() => {
+  return operation.value === 'add' ? '制卡' : '修改';
+})
+const confirmButtonText = computed(() => {
+  return operation.value === 'add' ? '确认制卡' : '确认修改';
+})
+
+// 获取卡类型枚举
+const getCardTypeEnum = (typeValue) => {
+  const cardTypeMap = {
+    6: "Platform",
+    2: "Dept",
+    7: "ECard",
+    8: "Test",
+  };
+  return cardTypeMap[typeValue] || "ECard";
+}
+
+const open = (type = 'add', data = { institutionId: null, cardType: null, count: 1 }) => {
+  operation.value = type;
+  form.value = data;
+  modalRef.value.open()
+}
+const close = () => {
+  modalRef.value.close()
+}
+const emit = defineEmits(['refresh'])
+const handleConfirm = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      const { institutionId, cardType, count } = form.value;
+      modalRef.value.showLoading()
+      issueCard(institutionId, getCardTypeEnum(cardType), count).then(res => {
+        proxy.$modal.msgSuccess('制卡成功')
+        close();
+        setTimeout(() => {
+          emit('refresh')
+        }, 300);
+      }).finally(() => {
+        modalRef.value.hideLoading()
+      })
+    }
+  })
+}
+defineExpose({
+  open,
+  close
+})
+</script>
+<style lang="scss" scoped></style>

+ 7 - 2
back-ui/src/views/dz/cards/components/OpenDialog.vue

@@ -1,5 +1,6 @@
 <template>
-  <IeModal title="直接开卡" confirmText="确认开卡" ref="modalRef" width="500px" @confirm="handleConfirm" @beforeClose="close">
+  <IeModal title="直接开卡" confirmText="确认开卡" ref="modalRef" width="500px" @beforeClose="handleBeforeClose"
+    @confirm="handleConfirm">
     <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
       <el-form-item label="卡号段" prop="cardNoRange" required>
         <div class="flex items-center gap-x-3">
@@ -40,7 +41,7 @@ const {
   selectedArea,
   schoolList,
   selectedSchool,
-} = useSchool({ autoLoad: false });
+} = useSchool({ autoLoad: false, loadExamType: true });
 
 const visible = ref(false);
 const modalRef = ref(null);
@@ -86,6 +87,10 @@ const rules = ref({
   ],
 })
 
+const handleBeforeClose = () => {
+  form.value = {};
+  reset();
+}
 
 
 const open = () => {

+ 52 - 18
back-ui/src/views/dz/cards/index.vue

@@ -12,7 +12,7 @@
         <ie-select v-model="queryParams.schoolId" :options="schoolList" label-key="name" value-key="id" filterable
           class="w-[180px]!" />
       </el-form-item>
-      <el-form-item label="注册学校班级" prop="classId">
+      <el-form-item label="注册班级" prop="classId">
         <ie-select v-model="selectedClass" :options="classList" label-key="name" value-key="classId" filterable
           class="w-[180px]!" />
       </el-form-item>
@@ -20,7 +20,7 @@
         <ie-select v-model="selectedCampus" :options="campusList" label-key="name" value-key="id" filterable
           class="w-[180px]!" @change="handleCampusChange" />
       </el-form-item>
-      <el-form-item label="培训学校班级" prop="campusClassId">
+      <el-form-item label="培训班级" prop="campusClassId">
         <ie-select v-model="selectedCampusClass" :options="campusClassList" label-key="name" value-key="classId"
           filterable class="w-[180px]!" />
       </el-form-item>
@@ -108,15 +108,15 @@
         @click="handleDeleteBatch">
         删除
       </CustomButton>
-      <CustomButton color="#FFC107" :disabled="batchDisabled">
+      <CustomButton color="#FFC107" :disabled="batchDisabled" @click="handleSettle">
         <svg-icon icon-class="chart" class="mr-1" style="font-size: 12px" />
         结算
       </CustomButton>
-      <CustomButton color="#009688" :disabled="batchDisabled">
+      <CustomButton color="#009688" :disabled="batchDisabled" @click="handleRenew">
         <svg-icon icon-class="time" class="mr-1" style="font-size: 14px" />
         续期
       </CustomButton>
-      <CustomButton color="#673AB7" :disabled="batchDisabled">
+      <CustomButton color="#673AB7" :disabled="editDisabled" @click="handleEdit">
         <svg-icon icon-class="edit" class="mr-1" style="font-size: 12px" />
         修改
       </CustomButton>
@@ -131,9 +131,10 @@
       <Pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
         @pagination="getList" />
     </div>
-    <EditDialog ref="editDialogRef" @refresh="getList" />
+    <MakeDialog ref="makeDialogRef" @refresh="getList" />
     <OpenDialog ref="openDialogRef" @refresh="getList" />
     <AssignDialog ref="assignDialogRef" @refresh="getList" />
+    <EditDialog ref="editDialogRef" @refresh="getList" />
   </div>
 </template>
 
@@ -145,15 +146,17 @@ import IeInstitutionSelect from '@/components/IeInstitutionSelect/index.vue';
 import CardTable from './components/CardTable.vue';
 import Pagination from '@/components/Pagination/index.vue';
 import CustomButton from './components/CustomButton.vue';
-import EditDialog from './components/EditDialog.vue';
+import MakeDialog from './components/MakeDialog.vue';
 import OpenDialog from './components/OpenDialog.vue';
 import AssignDialog from './components/AssignDialog.vue';
-import { listCards, delCards, payCard, closeCard, reopenCard, refundCard } from '@/api/dz/cards';
+import EditDialog from './components/EditDialog.vue';
+import { listCards, delCards, payCard, closeCard, reopenCard, refundCard, settleCard, renewCard } from '@/api/dz/cards';
 import { CARD_STATUS } from '@/common/enum';
-import { getCurrentInstance } from 'vue';
+import { getCurrentInstance, nextTick } from 'vue';
 
 const { proxy } = getCurrentInstance();
 const {
+  reset,
   areaList,
   selectedArea,
   schoolList,
@@ -179,7 +182,9 @@ const cascaderProps = {
   value: "areaId",
   checkStrictly: true,
 }
-const queryParams = ref({})
+const queryParams = ref({
+  pageSize: 20
+})
 const showSearch = ref(true)
 const cardList = ref([])
 const total = ref(0)
@@ -192,6 +197,9 @@ const ids = computed(() => {
 const batchDisabled = computed(() => {
   return selectedRows.value.length === 0;
 })
+const editDisabled = computed(() => {
+  return selectedRows.value.length !== 1;
+})
 
 const handleQuery = () => {
   queryParams.page = 1;
@@ -199,12 +207,10 @@ const handleQuery = () => {
 }
 
 const resetQuery = () => {
-  selectedArea.value = [];
-  selectedSchool.value = null;
-  selectedClass.value = null;
-  selectedCampus.value = null;
-  selectedCampusClass.value = null;
-  queryParams.value = {};
+  queryParams.value = {
+    pageSize: 20
+  };
+  reset();
   handleQuery();
 }
 
@@ -228,9 +234,9 @@ const handleSelectionChange = (selection) => {
   selectedRows.value = selection;
 }
 
-const editDialogRef = ref(null);
+const makeDialogRef = ref(null);
 const handleAddCard = () => {
-  editDialogRef.value.open()
+  makeDialogRef.value.open()
 }
 
 const handleDelete = (row) => {
@@ -292,6 +298,34 @@ const handleRefund = () => {
   })
 }
 
+const handleSettle = () => {
+  proxy.$modal.confirm(`是否确认结算所选数据 (${ids.value.length}项) ?`).then(() => {
+    settleCard(ids.value).then(() => {
+      proxy.$modal.msgSuccess('结算成功')
+      getList()
+    })
+  })
+}
+
+const handleRenew = () => {
+  proxy.$modal.confirm(`是否确认续费所选数据 (${ids.value.length}项) ?`).then(() => {
+    renewCard(ids.value).then(() => {
+      proxy.$modal.msgSuccess('续费成功')
+      getList()
+    })
+  })
+}
+
+const editDialogRef = ref(null);
+const handleEdit = () => {
+  const row = selectedRows.value[0];
+  if (row.status !== 30) {
+    proxy.$modal.msgError('该卡未绑定用户');
+    return;
+  }
+  editDialogRef.value.open({ ...row });
+}
+
 const assignDialogRef = ref(null);
 const handleAssign = () => {
   assignDialogRef.value.open()