delivery-partners-modal.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. <script setup lang="ts">
  2. import type { TablePaginationConfig } from 'antdv-next';
  3. import { computed, ref, watch } from 'vue';
  4. import { $t } from '@/locales';
  5. import {
  6. Button,
  7. DatePicker,
  8. Input,
  9. Menu,
  10. message,
  11. Modal,
  12. Select,
  13. Switch,
  14. Table,
  15. } from 'antdv-next';
  16. import md5 from 'crypto-js/md5';
  17. import dayjs, { Dayjs } from 'dayjs';
  18. import localeData from 'dayjs/plugin/localeData';
  19. import weekday from 'dayjs/plugin/weekday';
  20. import {
  21. addProjectToUserApi,
  22. addUserApi,
  23. deleteUserFromApplicationApi,
  24. getAssociatedProjectsApi,
  25. getAvailableProjectsApi,
  26. updateUserApi,
  27. } from '#/api';
  28. interface Props {
  29. open: boolean;
  30. mode: 'add' | 'edit';
  31. userData?: any;
  32. }
  33. const props = defineProps<Props>();
  34. const emit = defineEmits<{
  35. (e: 'save', data: any): void;
  36. (e: 'update:open', value: boolean): void;
  37. }>();
  38. dayjs.extend(weekday);
  39. dayjs.extend(localeData);
  40. const activeMenu = ref('basic');
  41. const formData = ref({
  42. id: '',
  43. account: '',
  44. chineseName: '',
  45. englishName: '',
  46. cellPhone: '',
  47. emailAddress: '',
  48. gogs_email: '',
  49. expiredTime: null as Dayjs | null,
  50. isActive: true,
  51. password: '',
  52. });
  53. const relatedProjects = ref<any[]>([]);
  54. const projectSearchKeyword = ref('');
  55. const projectModalOpen = ref(false);
  56. const selectedProjects = ref<any[]>([]);
  57. const menuItems = computed(() => [
  58. {
  59. key: 'basic',
  60. label: $t('deliveryPartners.modal.basic'),
  61. title: $t('deliveryPartners.modal.basic'),
  62. },
  63. {
  64. key: 'projects',
  65. label: $t('deliveryPartners.modal.projects'),
  66. title: $t('deliveryPartners.modal.projects'),
  67. },
  68. ]);
  69. const allProjects = ref<any[]>([]);
  70. const projectPagination = ref({
  71. currentPage: 1,
  72. pageSize: 10,
  73. total: 0,
  74. });
  75. const roleOptions = [
  76. { value: '普通人员', label: '普通人员' },
  77. { value: '兼职人员', label: '兼职人员' },
  78. { value: '内部人员', label: '内部人员' },
  79. { value: '管理员', label: '管理员' },
  80. ];
  81. async function fetchAssociatedProjects() {
  82. try {
  83. const result = await getAssociatedProjectsApi(formData.value.id);
  84. if (result?.result?.model) {
  85. relatedProjects.value = result.result.model;
  86. }
  87. } catch {}
  88. }
  89. async function fetchAvailableProjects() {
  90. try {
  91. const result = await getAvailableProjectsApi({
  92. currentPage: projectPagination.value.currentPage,
  93. pageSize: projectPagination.value.pageSize,
  94. orderByProperty: 'name',
  95. Ascending: true,
  96. filters: [
  97. {
  98. name: 'name',
  99. value: projectSearchKeyword.value,
  100. },
  101. ],
  102. });
  103. if (result?.result?.model) {
  104. result.result.model.forEach((item: any) => {
  105. item.roleTypes = '普通人员';
  106. });
  107. allProjects.value = result.result.model;
  108. projectPagination.value.total = result.result.totalCount;
  109. }
  110. } catch {}
  111. }
  112. watch(
  113. () => props.open,
  114. (val) => {
  115. if (val && props.mode === 'edit' && props.userData) {
  116. // 逐个属性赋值,避免类型错误
  117. formData.value.id = props.userData.id || '';
  118. formData.value.account = props.userData.account || '';
  119. formData.value.chineseName = props.userData.chineseName || '';
  120. formData.value.englishName = props.userData.englishName || '';
  121. formData.value.cellPhone = props.userData.cellPhone || '';
  122. formData.value.emailAddress = props.userData.emailAddress || '';
  123. formData.value.gogs_email = props.userData.gogs_email || '';
  124. formData.value.isActive = props.userData.isActive;
  125. try {
  126. formData.value.expiredTime = props.userData.expiredTime
  127. ? dayjs(props.userData.expiredTime)
  128. : null;
  129. } catch {
  130. formData.value.expiredTime = null;
  131. }
  132. fetchAssociatedProjects();
  133. fetchAvailableProjects();
  134. }
  135. if (val) {
  136. if (props.mode === 'add') {
  137. resetFormData();
  138. }
  139. activeMenu.value = 'basic';
  140. }
  141. },
  142. );
  143. const isOpen = computed({
  144. get: () => props.open,
  145. set: (val) => emit('update:open', val),
  146. });
  147. function handleMenuClick({
  148. key,
  149. }: {
  150. domEvent: Event;
  151. item: any;
  152. key: string;
  153. keyPath: string[];
  154. }) {
  155. activeMenu.value = key;
  156. }
  157. async function handleSave() {
  158. if (!formData.value.chineseName) {
  159. message.error($t('deliveryPartners.modal.enterChineseName'));
  160. return;
  161. }
  162. if (!formData.value.englishName) {
  163. message.error($t('deliveryPartners.modal.enterEnglishName'));
  164. return;
  165. }
  166. if (!formData.value.account) {
  167. message.error($t('deliveryPartners.modal.enterAccount'));
  168. return;
  169. }
  170. if (!formData.value.cellPhone) {
  171. message.error($t('deliveryPartners.modal.enterCellPhone'));
  172. return;
  173. }
  174. if (!formData.value.emailAddress) {
  175. message.error($t('deliveryPartners.modal.enterEmailAddress'));
  176. return;
  177. }
  178. if (!formData.value.gogs_email) {
  179. message.error($t('deliveryPartners.modal.enterGogsEmail'));
  180. return;
  181. }
  182. if (!formData.value.expiredTime) {
  183. message.error($t('deliveryPartners.modal.selectExpiredTime'));
  184. return;
  185. }
  186. if (props.mode === 'add' && !formData.value.password) {
  187. message.error($t('deliveryPartners.modal.enterPassword'));
  188. return;
  189. }
  190. const data = {
  191. id: formData.value.id,
  192. langNameList: [
  193. {
  194. name: 'zh-CN',
  195. value: formData.value.chineseName,
  196. },
  197. {
  198. name: 'en',
  199. value: formData.value.englishName,
  200. },
  201. ],
  202. expiredTime: formData.value.expiredTime
  203. ? formData.value.expiredTime.format('YYYY-MM-DD')
  204. : '',
  205. langName: null,
  206. account: formData.value.account,
  207. password:
  208. props.mode === 'add' ? md5(formData.value.password).toString() : '',
  209. cellPhone: formData.value.cellPhone,
  210. emailAddress: formData.value.emailAddress,
  211. gogs_email: formData.value.gogs_email,
  212. isActive: formData.value.isActive,
  213. };
  214. try {
  215. const result = formData.value.id
  216. ? await updateUserApi(data)
  217. : await addUserApi(data);
  218. if (result?.isSuccess) {
  219. emit('save', data);
  220. isOpen.value = false;
  221. message.success($t('deliveryPartners.saveSuccess'));
  222. }
  223. } catch {}
  224. }
  225. function handleCancel() {
  226. isOpen.value = false;
  227. }
  228. function handleAddProject() {
  229. projectModalOpen.value = true;
  230. projectSearchKeyword.value = '';
  231. selectedProjects.value = [];
  232. fetchAvailableProjects();
  233. }
  234. function handleProjectSearch() {
  235. projectPagination.value.currentPage = 1;
  236. fetchAvailableProjects();
  237. }
  238. function handleProjectPageChange(pagination: TablePaginationConfig) {
  239. if (pagination.current && pagination.pageSize) {
  240. projectPagination.value.currentPage = pagination.current;
  241. projectPagination.value.pageSize = pagination.pageSize;
  242. fetchAvailableProjects();
  243. }
  244. }
  245. async function handleProjectSelect(project: any) {
  246. if (!project.expiredTime) {
  247. message.error($t('deliveryPartners.enterExpiredTime'));
  248. return;
  249. }
  250. try {
  251. const result = await addProjectToUserApi(
  252. [project.id],
  253. formData.value.id,
  254. project.roleTypes,
  255. project.expiredTime ? project.expiredTime.format('YYYY-MM-DD') : '',
  256. );
  257. if (result?.isSuccess) {
  258. message.success($t('deliveryPartners.addProjectSuccess'));
  259. fetchAssociatedProjects();
  260. projectModalOpen.value = false;
  261. }
  262. } catch {}
  263. }
  264. async function handleProjectDelete(project: any) {
  265. Modal.confirm({
  266. title: $t('btn.delete'),
  267. content: $t('deliveryPartners.modal.deleteProjectConfirm'),
  268. okText: $t('btn.yes'),
  269. okType: 'danger',
  270. cancelText: $t('btn.no'),
  271. type: 'warning',
  272. onOk: async () => {
  273. try {
  274. const result = await deleteUserFromApplicationApi(formData.value.id, [
  275. project.id,
  276. ]);
  277. if (result?.isSuccess) {
  278. message.success($t('deliveryPartners.deleteProjectSuccess'));
  279. fetchAssociatedProjects();
  280. }
  281. } catch {}
  282. },
  283. onCancel: () => {},
  284. });
  285. }
  286. function resetFormData() {
  287. formData.value = {
  288. id: '',
  289. account: '',
  290. chineseName: '',
  291. englishName: '',
  292. cellPhone: '',
  293. emailAddress: '',
  294. gogs_email: '',
  295. expiredTime: null,
  296. isActive: true,
  297. password: '',
  298. };
  299. relatedProjects.value = [];
  300. }
  301. </script>
  302. <template>
  303. <Modal
  304. v-model:open="isOpen"
  305. :footer="null"
  306. :title="
  307. props.mode === 'add'
  308. ? $t('deliveryPartners.modal.addTitle')
  309. : $t('deliveryPartners.modal.editTitle')
  310. "
  311. width="1200px"
  312. >
  313. <div class="flex h-[600px]">
  314. <div
  315. v-if="props.mode !== 'add'"
  316. class="w-[200px] border-r border-gray-200"
  317. >
  318. <Menu
  319. :items="menuItems"
  320. :selected-keys="[activeMenu]"
  321. mode="vertical"
  322. @click="handleMenuClick"
  323. @update:selected-keys="(keys) => (activeMenu = keys[0] ?? 'basic')"
  324. />
  325. </div>
  326. <div class="flex-1 overflow-y-auto p-6">
  327. <div v-show="activeMenu === 'basic'" class="space-y-4">
  328. <div class="grid grid-cols-2 gap-4">
  329. <div class="flex flex-col gap-2">
  330. <label class="text-sm font-medium">
  331. {{ $t('deliveryPartners.modal.chineseName') }}
  332. <span class="text-red-500">*</span>
  333. </label>
  334. <Input
  335. v-model:value="formData.chineseName"
  336. :placeholder="$t('deliveryPartners.modal.enterChineseName')"
  337. />
  338. </div>
  339. <div class="flex flex-col gap-2">
  340. <label class="text-sm font-medium">
  341. {{ $t('deliveryPartners.modal.englishName') }}
  342. <span class="text-red-500">*</span>
  343. </label>
  344. <Input
  345. v-model:value="formData.englishName"
  346. :placeholder="$t('deliveryPartners.modal.enterEnglishName')"
  347. />
  348. </div>
  349. <div class="flex flex-col gap-2">
  350. <label class="text-sm font-medium">
  351. {{ $t('deliveryPartners.modal.account') }}
  352. <span class="text-red-500">*</span>
  353. </label>
  354. <Input
  355. v-model:value="formData.account"
  356. :disabled="props.mode === 'edit'"
  357. :placeholder="$t('deliveryPartners.modal.enterAccount')"
  358. />
  359. </div>
  360. <div v-if="props.mode === 'add'" class="flex flex-col gap-2">
  361. <label class="text-sm font-medium">
  362. {{ $t('deliveryPartners.modal.password') }}
  363. <span class="text-red-500">*</span>
  364. </label>
  365. <Input
  366. v-model:value="formData.password"
  367. :placeholder="$t('deliveryPartners.modal.enterPassword')"
  368. type="password"
  369. />
  370. </div>
  371. <div class="flex flex-col gap-2">
  372. <label class="text-sm font-medium">
  373. {{ $t('deliveryPartners.modal.cellPhone') }}
  374. <span class="text-red-500">*</span>
  375. </label>
  376. <Input
  377. v-model:value="formData.cellPhone"
  378. :placeholder="$t('deliveryPartners.modal.enterCellPhone')"
  379. />
  380. </div>
  381. <div class="flex flex-col gap-2">
  382. <label class="text-sm font-medium">
  383. {{ $t('deliveryPartners.modal.emailAddress') }}
  384. <span class="text-red-500">*</span>
  385. </label>
  386. <Input
  387. v-model:value="formData.emailAddress"
  388. :placeholder="$t('deliveryPartners.modal.enterEmailAddress')"
  389. />
  390. </div>
  391. <div class="flex flex-col gap-2">
  392. <label class="text-sm font-medium">
  393. {{ $t('deliveryPartners.modal.gogsEmail') }}
  394. <span class="text-red-500">*</span>
  395. </label>
  396. <Input
  397. v-model:value="formData.gogs_email"
  398. :placeholder="$t('deliveryPartners.modal.enterGogsEmail')"
  399. />
  400. </div>
  401. <div class="flex flex-col gap-2">
  402. <label class="text-sm font-medium">
  403. {{ $t('deliveryPartners.modal.expiredTime') }}
  404. <span class="text-red-500">*</span>
  405. </label>
  406. <DatePicker
  407. v-model:value="formData.expiredTime"
  408. :placeholder="$t('deliveryPartners.modal.selectExpiredTime')"
  409. format="YYYY-MM-DD"
  410. style="width: 100%"
  411. />
  412. </div>
  413. <div class="flex flex-col gap-2">
  414. <label class="text-sm font-medium">{{
  415. $t('deliveryPartners.modal.isActive')
  416. }}</label>
  417. <Switch v-model:checked="formData.isActive" class="w-[40px]" />
  418. </div>
  419. </div>
  420. </div>
  421. <div v-show="activeMenu === 'projects'" class="space-y-4">
  422. <div class="mb-4 flex items-center justify-between">
  423. <Button type="primary" @click="handleAddProject">
  424. {{ $t('deliveryPartners.modal.add') }}
  425. {{ $t('deliveryPartners.modal.projects') }}
  426. </Button>
  427. </div>
  428. <Table
  429. :columns="[
  430. {
  431. title: $t('deliveryPartners.modal.projectName'),
  432. dataIndex: 'name',
  433. key: 'name',
  434. },
  435. {
  436. title: $t('deliveryPartners.modal.projectCode'),
  437. dataIndex: 'code',
  438. key: 'code',
  439. },
  440. {
  441. title: $t('deliveryPartners.modal.role'),
  442. dataIndex: 'roleTypes',
  443. key: 'roleTypes',
  444. },
  445. {
  446. title: $t('deliveryPartners.modal.expiredTime'),
  447. dataIndex: 'expiredTime',
  448. key: 'expiredTime',
  449. },
  450. {
  451. title: $t('deliveryPartners.modal.action'),
  452. key: 'action',
  453. width: 100,
  454. },
  455. ]"
  456. :data-source="relatedProjects"
  457. :pagination="false"
  458. :scroll="{ y: 460 }"
  459. >
  460. <template #bodyCell="{ column, record }">
  461. <template v-if="column.key === 'action'">
  462. <Button
  463. danger
  464. size="small"
  465. @click="handleProjectDelete(record)"
  466. >
  467. {{ $t('deliveryPartners.modal.delete') }}
  468. </Button>
  469. </template>
  470. </template>
  471. </Table>
  472. </div>
  473. </div>
  474. </div>
  475. <div
  476. v-if="activeMenu === 'basic'"
  477. class="flex justify-end gap-2 border-t pt-4"
  478. >
  479. <Button @click="handleCancel">
  480. {{ $t('deliveryPartners.modal.cancel') }}
  481. </Button>
  482. <Button type="primary" @click="handleSave">
  483. {{ $t('deliveryPartners.modal.save') }}
  484. </Button>
  485. </div>
  486. <Modal
  487. v-model:open="projectModalOpen"
  488. :footer="null"
  489. :title="$t('deliveryPartners.modal.addProjectTitle')"
  490. width="760"
  491. >
  492. <div class="space-y-4">
  493. <Input
  494. v-model:value="projectSearchKeyword"
  495. :placeholder="$t('deliveryPartners.modal.searchProject')"
  496. @change="handleProjectSearch"
  497. />
  498. <Table
  499. :columns="[
  500. {
  501. title: $t('deliveryPartners.modal.projectName'),
  502. dataIndex: 'name',
  503. key: 'name',
  504. ellipsis: true,
  505. width: 120,
  506. },
  507. {
  508. title: $t('deliveryPartners.modal.projectCode'),
  509. dataIndex: 'code',
  510. key: 'code',
  511. ellipsis: true,
  512. width: 120,
  513. },
  514. {
  515. title: $t('deliveryPartners.modal.role'),
  516. key: 'roleTypes',
  517. width: 150,
  518. },
  519. {
  520. title: $t('deliveryPartners.modal.expiredTime'),
  521. key: 'expiredTime',
  522. width: 150,
  523. },
  524. {
  525. title: $t('deliveryPartners.modal.action'),
  526. key: 'action',
  527. },
  528. ]"
  529. :data-source="allProjects"
  530. :pagination="{
  531. current: projectPagination.currentPage,
  532. pageSize: projectPagination.pageSize,
  533. total: projectPagination.total,
  534. showSizeChanger: true,
  535. showTotal: (total: number) =>
  536. $t('deliveryPartners.modal.totalProjects', { total }),
  537. }"
  538. :scroll="{ y: 460 }"
  539. class="project-table"
  540. @change="handleProjectPageChange"
  541. >
  542. <template #bodyCell="{ column, record }">
  543. <template v-if="column.key === 'roleTypes'">
  544. <Select
  545. v-model:value="record.roleTypes"
  546. :options="roleOptions"
  547. style="width: 120px"
  548. />
  549. </template>
  550. <template v-if="column.key === 'expiredTime'">
  551. <DatePicker
  552. v-model:value="record.expiredTime"
  553. format="YYYY-MM-DD"
  554. style="width: 120px"
  555. />
  556. </template>
  557. <template v-if="column.key === 'action'">
  558. <Button
  559. size="small"
  560. type="primary"
  561. @click="handleProjectSelect(record)"
  562. >
  563. {{ $t('deliveryPartners.modal.add') }}
  564. </Button>
  565. </template>
  566. </template>
  567. </Table>
  568. </div>
  569. </Modal>
  570. </Modal>
  571. </template>
  572. <style lang="scss">
  573. .ant-menu-light .ant-menu-item-selected,
  574. .ant-menu-light > .ant-menu .ant-menu-item-selected {
  575. background-color: #f4f2f2;
  576. .ant-menu-title-content {
  577. color: #462424;
  578. }
  579. }
  580. .ant-table-wrapper .ant-table-tbody > tr > th,
  581. .ant-table-wrapper .ant-table-tbody > tr > td {
  582. padding: 8px 16px;
  583. }
  584. .ant-table-wrapper .ant-table-thead > tr > th,
  585. .ant-table-wrapper .ant-table-thead > tr > td {
  586. padding: 8px 16px;
  587. }
  588. </style>