index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <template>
  2. <div class="app-page">
  3. <el-form :model="queryParams" ref="queryRef" v-show="showSearch" label-width="100px" @submit.prevent="handleQuery">
  4. <!-- 其他表单项:每行4个 -->
  5. <el-row :gutter="20">
  6. <el-col :span="6">
  7. <el-form-item label="省份筛选" prop="location">
  8. <!-- <el-cascader class="w-[180px]!" :options="area.list" :props="cascaderProps" v-model="area.selected" clearable @change="handleAreaChange" /> -->
  9. <ie-select v-model="area.selected" v-model:selectedItem="area.selectedItem" :options="area.list" label-key="areaName" value-key="areaId" clearable filterable class="w-[180px]!"/>
  10. </el-form-item>
  11. </el-col>
  12. <el-col :span="6">
  13. <el-form-item label="注册学校" prop="schoolId">
  14. <ie-select v-model="selectedSchool" :options="schoolList" label-key="name" value-key="id" filterable clearable
  15. class="w-[180px]!" />
  16. </el-form-item>
  17. </el-col>
  18. <el-col :span="6">
  19. <el-form-item label="注册班级" prop="classId">
  20. <ie-select v-model="selectedClass" :options="classList" label-key="name" value-key="classId" filterable
  21. clearable class="w-[180px]!" />
  22. </el-form-item>
  23. </el-col>
  24. <el-col :span="6">
  25. <el-form-item label="分配学校" prop="assignSchoolId">
  26. <ie-select v-model="selectedAssignSchool" :options="schoolList" label-key="name" value-key="id" filterable
  27. clearable class="w-[180px]!" />
  28. </el-form-item>
  29. </el-col>
  30. </el-row>
  31. <el-row :gutter="20">
  32. <el-col :span="6">
  33. <el-form-item label="培训校区" prop="campusId">
  34. <ie-select v-model="selectedCampus" :options="campusList" label-key="name" value-key="id" filterable clearable
  35. class="w-[180px]!" @change="handleCampusChange" />
  36. </el-form-item>
  37. </el-col>
  38. <el-col :span="6">
  39. <el-form-item label="校区班级" prop="campusClassId">
  40. <ie-select v-model="selectedCampusClass" :options="campusClassList" label-key="name" value-key="classId"
  41. clearable filterable class="w-[180px]!" />
  42. </el-form-item>
  43. </el-col>
  44. <el-col :span="6">
  45. <el-form-item label="平台机构" prop="deptId">
  46. <ie-institution-select v-model="queryParams.deptId" class="w-[180px]!" clearable filterable/>
  47. </el-form-item>
  48. </el-col>
  49. <el-col :span="6">
  50. <el-form-item label="代理商" prop="agentId">
  51. <ie-agent-select v-model="queryParams.agentId" class="w-[180px]!" filterable clearable />
  52. </el-form-item>
  53. </el-col>
  54. </el-row>
  55. <!-- 第一行:卡号段、卡类型、卡分配日期 -->
  56. <el-row :gutter="20">
  57. <el-col :span="6">
  58. <el-form-item label="卡号段" prop="begin">
  59. <div class="flex items-center gap-x-1">
  60. <el-input v-model="queryParams.begin" type="text" maxlength="11" class="w-[85px]!" placeholder="开始卡号" @keyup.enter="handleQuery" />
  61. <span class="text-gray-500 text-sm">-</span>
  62. <el-input v-model="queryParams.end" type="text" maxlength="11" class="w-[85px]!" placeholder="结束卡号" @keyup.enter="handleQuery" />
  63. </div>
  64. </el-form-item>
  65. </el-col>
  66. <el-col :span="6">
  67. <el-form-item label="卡类型" prop="type">
  68. <ie-select v-model="queryParams.type" :options="card_type" class="w-[180px]!" clearable filterable/>
  69. </el-form-item>
  70. </el-col>
  71. <el-col :span="6">
  72. <el-form-item label="考生类型" prop="examType">
  73. <ie-select v-model="queryParams.examType" :options="exam_type" class="w-[180px]!" clearable filterable/>
  74. </el-form-item>
  75. </el-col>
  76. <el-col :span="6">
  77. <el-form-item label="学生姓名" prop="nickName">
  78. <el-input v-model="queryParams.nickName" type="text" class="w-[180px]!" placeholder="请输入姓名" clearable filterable @keyup.enter="handleQuery" />
  79. </el-form-item>
  80. </el-col>
  81. </el-row>
  82. <el-row :gutter="20">
  83. <el-col :span="6">
  84. <el-form-item label="手机号码" prop="phonenumber">
  85. <el-input v-model="queryParams.phonenumber" type="text" v-number maxlength="11" class="w-[180px]!"
  86. placeholder="请输入手机号码" clearable filterable @keyup.enter="handleQuery" />
  87. </el-form-item>
  88. </el-col>
  89. <el-col :span="6">
  90. <el-form-item label="分配状态" prop="distributeStatus">
  91. <ie-select v-model="queryParams.distributeStatus" :options="card_distribute_status" class="w-[180px]!"
  92. clearable />
  93. </el-form-item>
  94. </el-col>
  95. <el-col :span="6">
  96. <el-form-item label="使用状态" prop="status">
  97. <ie-select v-model="queryParams.status" :options="card_status" class="w-[180px]!" clearable filterable />
  98. </el-form-item>
  99. </el-col>
  100. <el-col :span="6">
  101. <el-form-item label="过期状态" prop="timeStatus">
  102. <ie-select v-model="queryParams.timeStatus" :options="card_time_status" class="w-[180px]!" clearable filterable/>
  103. </el-form-item>
  104. </el-col>
  105. </el-row>
  106. <el-row :gutter="20">
  107. <el-col :span="6">
  108. <el-form-item label="结算状态" prop="isSettlement">
  109. <ie-select v-model="queryParams.isSettlement" :options="card_settlement_status" class="w-[180px]!" clearable filterable/>
  110. </el-form-item>
  111. </el-col>
  112. <el-col :span="6">
  113. <el-form-item label="缴费状态" prop="payStatus">
  114. <ie-select v-model="queryParams.payStatus" :options="card_pay_status" class="w-[180px]!" clearable filterable/>
  115. </el-form-item>
  116. </el-col>
  117. <el-col :span="6">
  118. <el-form-item label="卡分配日期" prop="assignTimeRange">
  119. <el-date-picker v-model="queryParams.assignTimeRange" type="daterange" range-separator="至"
  120. start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" class="w-[180px]!" />
  121. </el-form-item>
  122. </el-col>
  123. <el-col :span="6">
  124. <el-form-item>
  125. <div class="flex gap-x-2">
  126. <el-button type="primary" icon="Search" @click="handleQuery" class="w-[80px]">搜索</el-button>
  127. <el-button icon="Refresh" @click="resetQuery" class="w-[80px]">重置</el-button>
  128. </div>
  129. </el-form-item>
  130. </el-col>
  131. </el-row>
  132. <!-- <el-row :gutter="20">
  133. <el-col :span="6">
  134. <el-form-item>
  135. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  136. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  137. </el-form-item>
  138. </el-col>
  139. </el-row> -->
  140. </el-form>
  141. <el-row class="mt-2">
  142. <CustomButton icon="plus" v-hasPermi="['dz:cards:issue']" @click="handleAddCard">制卡</CustomButton>
  143. <CustomButton color="#67c23a" v-hasPermi="['dz:cards:assign']" @click="handleAssign">
  144. <svg-icon icon-class="peoples" class="mr-1" style="font-size: 12px" />
  145. 分配卡
  146. </CustomButton>
  147. <CustomButton color="#E6A23C" v-hasPermi="['dz:cards:pay']" :disabled="batchDisabled" @click="handlePay">
  148. <svg-icon icon-class="money" class="mr-1" style="font-size: 12px" />
  149. 缴费
  150. </CustomButton>
  151. <CustomButton color="#F56C6C" v-hasPermi="['dz:cards:close']" :disabled="batchDisabled" @click="handleClose">
  152. <svg-icon icon-class="lock" class="mr-1" style="font-size: 12px" />
  153. 关卡
  154. </CustomButton>
  155. <CustomButton color="#67C23A" v-hasPermi="['dz:cards:reopen']" :disabled="batchDisabled" @click="handleReopen">
  156. <svg-icon icon-class="enter" class="mr-1" style="font-size: 12px" />
  157. 重开
  158. </CustomButton>
  159. <CustomButton color="#E6A23C" v-hasPermi="['dz:cards:refund']" :disabled="batchDisabled" @click="handleRefund">
  160. <svg-icon icon-class="money" class="mr-1" style="font-size: 12px" />
  161. 退费
  162. </CustomButton>
  163. <CustomButton color="#9C27B0" v-hasPermi="['dz:cards:associateCampus']" @click="handleRelateCampus">
  164. <svg-icon icon-class="link" class="mr-1" style="font-size: 12px" />
  165. 关联校区
  166. </CustomButton>
  167. <!-- <CustomButton color="#00BCD4" v-hasPermi="['dz:cards:openFinished']" @click="handleOpenCard">
  168. <svg-icon icon-class="enter" class="mr-1" style="font-size: 12px" />
  169. 直接开卡
  170. </CustomButton> -->
  171. <!-- <CustomButton icon="delete" color="#F56C6C" v-hasPermi="['dz:cards:remove']" :disabled="batchDisabled"
  172. @click="handleDeleteBatch" v-if="false">
  173. 删除
  174. </CustomButton> -->
  175. <CustomButton color="#FFC107" v-hasPermi="['dz:cards:settlement']" :disabled="batchDisabled" @click="handleSettle">
  176. <svg-icon icon-class="chart" class="mr-1" style="font-size: 12px" />
  177. 结算
  178. </CustomButton>
  179. <CustomButton color="#009688" :disabled="batchDisabled" @click="handleRenew">
  180. <svg-icon icon-class="time" class="mr-1" style="font-size: 14px" />
  181. 续期
  182. </CustomButton>
  183. <CustomButton color="#673AB7" v-hasPermi="['dz:cards:updateuser']" :disabled="editDisabled" @click="handleEdit">
  184. <svg-icon icon-class="edit" class="mr-1" style="font-size: 12px" />
  185. 修改
  186. </CustomButton>
  187. <CustomButton color="#606266" v-hasPermi="['dz:cards:export']" @click="handleExport">
  188. <svg-icon icon-class="download" class="mr-1" style="font-size: 14px" />
  189. 导出
  190. </CustomButton>
  191. <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
  192. </el-row>
  193. <card-table :data="cardList" :loading="loading" :hide-actions="true" @selectionChange="handleSelectionChange"
  194. @delete="handleDelete" @refresh="getList" />
  195. <div class="flex justify-end">
  196. <Pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
  197. @pagination="getList" />
  198. </div>
  199. <MakeDialog ref="makeDialogRef" @refresh="getList" />
  200. <OpenDialog ref="openDialogRef" @refresh="getList" />
  201. <AssignDialog ref="assignDialogRef" @refresh="getList" />
  202. <EditDialog ref="editDialogRef" @refresh="getList" />
  203. <RelateDialog ref="relateDialogRef" @refresh="getList" />
  204. </div>
  205. </template>
  206. <script setup name="Cards">
  207. defineOptions({ name: 'Cards' })
  208. import useSchool from '@/hooks/useSchool';
  209. import IeSelect from '@/components/IeSelect/index.vue';
  210. import IeAgentSelect from '@/components/IeAgentSelect/index.vue';
  211. import IeInstitutionSelect from '@/components/IeInstitutionSelect/index.vue';
  212. import CardTable from './components/CardTable.vue';
  213. import Pagination from '@/components/Pagination/index.vue';
  214. import CustomButton from './components/CustomButton.vue';
  215. import MakeDialog from './components/MakeDialog.vue';
  216. import OpenDialog from './components/OpenDialog.vue';
  217. import AssignDialog from './components/AssignDialog.vue';
  218. import EditDialog from './components/EditDialog.vue';
  219. import RelateDialog from './components/RelateDialog.vue';
  220. import { listCards, delCards, payCard, closeCard, reopenCard, refundCard, settleCard, renewCard } from '@/api/dz/cards';
  221. import { CARD_STATUS, EnumCardType } from '@/common/enum';
  222. import { getCurrentInstance, nextTick } from 'vue';
  223. const { proxy } = getCurrentInstance();
  224. const {
  225. reset,
  226. area,
  227. // areaList,
  228. // selectedArea,
  229. schoolList,
  230. selectedSchool,
  231. selectedAssignSchool,
  232. classList,
  233. selectedClass,
  234. campusList,
  235. selectedCampus,
  236. campusClassList,
  237. selectedCampusClass,
  238. getSchoolList,
  239. getCampusList,
  240. getAreaList,
  241. } = useSchool({ loadCampus: true, loadCampusClass: true, loadClass: true });
  242. const {
  243. exam_type,
  244. card_distribute_status,
  245. card_status,
  246. card_time_status,
  247. card_settlement_status,
  248. card_pay_status,
  249. card_type,
  250. } = proxy.useDict("exam_type", "card_distribute_status", "card_status", "card_time_status", "card_settlement_status", "card_pay_status", "card_type");
  251. const cascaderProps = {
  252. label: "areaName",
  253. value: "areaId",
  254. checkStrictly: true,
  255. }
  256. const queryParams = ref({
  257. pageNum: 1,
  258. pageSize: 20
  259. })
  260. const showSearch = ref(true)
  261. const cardList = ref([])
  262. const total = ref(0)
  263. const selectedRows = ref([])
  264. const loading = ref(false)
  265. const ids = computed(() => {
  266. return selectedRows.value.map(item => item.cardId);
  267. })
  268. const batchDisabled = computed(() => {
  269. return selectedRows.value.length === 0;
  270. })
  271. const editDisabled = computed(() => {
  272. return selectedRows.value.length !== 1;
  273. })
  274. watchEffect(() => {
  275. queryParams.value.areaIds = area.selected;
  276. // 使用 location 进行搜索
  277. queryParams.value.location = area.selectedItem?.shortName || area.selectedItem?.areaName || null;
  278. queryParams.value.schoolId = selectedSchool.value;
  279. queryParams.value.assignSchoolId = selectedAssignSchool.value;
  280. queryParams.value.classId = selectedClass.value;
  281. queryParams.value.campusId = selectedCampus.value;
  282. queryParams.value.campusClassId = selectedCampusClass.value;
  283. })
  284. const handleQuery = () => {
  285. queryParams.pageNum = 1;
  286. getList();
  287. }
  288. const resetQuery = () => {
  289. queryParams.value = {
  290. pageNum: 1,
  291. pageSize: 20
  292. };
  293. reset();
  294. handleQuery();
  295. }
  296. const getList = () => {
  297. const params = {
  298. ...queryParams.value,
  299. assignTimeBegin: queryParams.value.assignTimeRange?.[0],
  300. assignTimeEnd: queryParams.value.assignTimeRange?.[1],
  301. };
  302. delete params.assignTimeRange;
  303. listCards(params).then(res => {
  304. cardList.value = res.rows;
  305. total.value = res.total;
  306. }).finally(() => {
  307. loading.value = false;
  308. })
  309. }
  310. const handleSelectionChange = (selection) => {
  311. selectedRows.value = selection;
  312. }
  313. const makeDialogRef = ref(null);
  314. const handleAddCard = () => {
  315. makeDialogRef.value.open()
  316. }
  317. const handleDelete = (row) => {
  318. proxy.$modal.confirm(`是否确认删除学习卡号为"${row.cardNo}"的数据项?`).then(() => {
  319. delCards(row.cardId).then(() => {
  320. proxy.$modal.msgSuccess('删除成功')
  321. getList()
  322. })
  323. });
  324. }
  325. const handleDeleteBatch = () => {
  326. proxy.$modal.confirm(`是否确认删除所选数据 (${ids.value.length}项) ?`).then(() => {
  327. delCards(ids.value).then(() => {
  328. proxy.$modal.msgSuccess('删除成功')
  329. getList()
  330. })
  331. });
  332. }
  333. const relateDialogRef = ref(null);
  334. const handleRelateCampus = () => {
  335. // 如果有选中的卡片,传递cardIds;否则传递null(使用卡号段方式)
  336. const selectedCardIds = selectedRows.value.length > 0 ? ids.value : null
  337. relateDialogRef.value.open(selectedCardIds)
  338. }
  339. const openDialogRef = ref(null);
  340. const handleOpenCard = () => {
  341. openDialogRef.value.open()
  342. }
  343. const handlePay = () => {
  344. // 体验卡禁止缴费
  345. const isExperienceCard = selectedRows.value.some(item => item.type === EnumCardType.EXPERIENCE);
  346. if (isExperienceCard) {
  347. proxy.$modal.msgError('体验卡禁止缴费');
  348. return;
  349. }
  350. proxy.$modal.confirm(`是否确认缴费所选数据 (${ids.value.length}项) ?`).then(() => {
  351. payCard(ids.value).then(() => {
  352. proxy.$modal.msgSuccess('缴费成功')
  353. getList()
  354. })
  355. })
  356. }
  357. const handleClose = () => {
  358. proxy.$modal.confirm(`是否确认关卡所选数据 (${ids.value.length}项) ?`).then(() => {
  359. closeCard(ids.value).then(() => {
  360. proxy.$modal.msgSuccess('关卡成功')
  361. getList()
  362. })
  363. })
  364. }
  365. const handleReopen = () => {
  366. proxy.$modal.confirm(`是否确认重开所选数据 (${ids.value.length}项) ?`).then(() => {
  367. reopenCard(ids.value).then(() => {
  368. proxy.$modal.msgSuccess('重开成功')
  369. getList()
  370. })
  371. })
  372. }
  373. const handleRefund = () => {
  374. // 体验卡禁止退费
  375. const isExperienceCard = selectedRows.value.some(item => item.type === EnumCardType.EXPERIENCE);
  376. if (isExperienceCard) {
  377. proxy.$modal.msgError('体验卡禁止退费');
  378. return;
  379. }
  380. proxy.$modal.confirm(`是否确认退费所选数据 (${ids.value.length}项) ?`).then(() => {
  381. refundCard(ids.value).then(() => {
  382. proxy.$modal.msgSuccess('退费成功')
  383. getList()
  384. })
  385. })
  386. }
  387. const handleSettle = () => {
  388. // 体验卡禁止结算
  389. const isExperienceCard = selectedRows.value.some(item => item.type === EnumCardType.EXPERIENCE);
  390. if (isExperienceCard) {
  391. proxy.$modal.msgError('体验卡禁止结算');
  392. return;
  393. }
  394. proxy.$modal.confirm(`是否确认结算所选数据 (${ids.value.length}项) ?`).then(() => {
  395. settleCard(ids.value).then(() => {
  396. proxy.$modal.msgSuccess('结算成功')
  397. getList()
  398. })
  399. })
  400. }
  401. const handleRenew = () => {
  402. proxy.$modal.confirm(`是否确认续费所选数据 (${ids.value.length}项) ?`).then(() => {
  403. renewCard(ids.value).then(() => {
  404. proxy.$modal.msgSuccess('续费成功')
  405. getList()
  406. })
  407. })
  408. }
  409. const editDialogRef = ref(null);
  410. const handleEdit = () => {
  411. const row = selectedRows.value[0];
  412. if (row.status !== 30) {
  413. proxy.$modal.msgError('该卡未绑定用户');
  414. return;
  415. }
  416. editDialogRef.value.open({ ...row });
  417. }
  418. const assignDialogRef = ref(null);
  419. const handleAssign = () => {
  420. // 如果有选中的卡片,传递cardIds;否则传递null(使用卡号段方式)
  421. const selectedCardIds = selectedRows.value.length > 0 ? ids.value : null
  422. assignDialogRef.value.open(selectedCardIds)
  423. }
  424. const handleExport = () => {
  425. proxy.download('dz/cards/export', {
  426. ...queryParams.value
  427. }, `cards_${new Date().getTime()}.xlsx`)
  428. }
  429. // 处理省份变化
  430. const handleAreaChange = () => {
  431. // 当省份变化时,useSchool hook 中的 watch 会自动调用 getSchoolList 和 getCampusList
  432. // 这里不需要手动调用,因为 watch 已经处理了
  433. // 但为了确保数据更新,我们可以显式调用(虽然 watch 已经处理了)
  434. // 实际上 watch 会自动处理,所以这里可以留空或者添加其他逻辑
  435. }
  436. // 初始化数据加载(只在首次挂载时执行,keep-alive 缓存后不会再次执行)
  437. onMounted(() => {
  438. // 如果列表数据已存在(keep-alive 缓存),则不重新调用接口
  439. // if (cardList.value.length > 0 || total.value > 0) {
  440. // // 只检查省份数据,如果丢失则重新加载
  441. // if (!area.list || area.list.length === 0) {
  442. // getAreaList()
  443. // }
  444. // return
  445. // }
  446. // 初始进入页面时,加载分配学校、注册学校、培训校区的数据
  447. // 即使没有选择省份,也调用接口获取所有数据(areaCode 为 undefined 时接口会返回所有数据)
  448. getSchoolList()
  449. getCampusList()
  450. // 加载列表数据
  451. handleQuery()
  452. })
  453. </script>
  454. <style lang="scss" scoped></style>