| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633 |
- <script setup lang="ts">
- import type { TablePaginationConfig } from 'antdv-next';
- import { computed, ref, watch } from 'vue';
- import { $t } from '@/locales';
- import {
- Button,
- DatePicker,
- Input,
- Menu,
- message,
- Modal,
- Select,
- Switch,
- Table,
- } from 'antdv-next';
- import md5 from 'crypto-js/md5';
- import dayjs, { Dayjs } from 'dayjs';
- import localeData from 'dayjs/plugin/localeData';
- import weekday from 'dayjs/plugin/weekday';
- import {
- addProjectToUserApi,
- addUserApi,
- deleteUserFromApplicationApi,
- getAssociatedProjectsApi,
- getAvailableProjectsApi,
- updateUserApi,
- } from '#/api';
- interface Props {
- open: boolean;
- mode: 'add' | 'edit';
- userData?: any;
- }
- const props = defineProps<Props>();
- const emit = defineEmits<{
- (e: 'save', data: any): void;
- (e: 'update:open', value: boolean): void;
- }>();
- dayjs.extend(weekday);
- dayjs.extend(localeData);
- const activeMenu = ref('basic');
- const formData = ref({
- id: '',
- account: '',
- chineseName: '',
- englishName: '',
- cellPhone: '',
- emailAddress: '',
- gogs_email: '',
- expiredTime: null as Dayjs | null,
- isActive: true,
- password: '',
- });
- const relatedProjects = ref<any[]>([]);
- const projectSearchKeyword = ref('');
- const projectModalOpen = ref(false);
- const selectedProjects = ref<any[]>([]);
- const menuItems = computed(() => [
- {
- key: 'basic',
- label: $t('deliveryPartners.modal.basic'),
- title: $t('deliveryPartners.modal.basic'),
- },
- {
- key: 'projects',
- label: $t('deliveryPartners.modal.projects'),
- title: $t('deliveryPartners.modal.projects'),
- },
- ]);
- const allProjects = ref<any[]>([]);
- const projectPagination = ref({
- currentPage: 1,
- pageSize: 10,
- total: 0,
- });
- const roleOptions = [
- { value: '普通人员', label: '普通人员' },
- { value: '兼职人员', label: '兼职人员' },
- { value: '内部人员', label: '内部人员' },
- { value: '管理员', label: '管理员' },
- ];
- async function fetchAssociatedProjects() {
- try {
- const result = await getAssociatedProjectsApi(formData.value.id);
- if (result?.result?.model) {
- relatedProjects.value = result.result.model;
- }
- } catch {}
- }
- async function fetchAvailableProjects() {
- try {
- const result = await getAvailableProjectsApi({
- currentPage: projectPagination.value.currentPage,
- pageSize: projectPagination.value.pageSize,
- orderByProperty: 'name',
- Ascending: true,
- filters: [
- {
- name: 'name',
- value: projectSearchKeyword.value,
- },
- ],
- });
- if (result?.result?.model) {
- result.result.model.forEach((item: any) => {
- item.roleTypes = '普通人员';
- });
- allProjects.value = result.result.model;
- projectPagination.value.total = result.result.totalCount;
- }
- } catch {}
- }
- watch(
- () => props.open,
- (val) => {
- if (val && props.mode === 'edit' && props.userData) {
- // 逐个属性赋值,避免类型错误
- formData.value.id = props.userData.id || '';
- formData.value.account = props.userData.account || '';
- formData.value.chineseName = props.userData.chineseName || '';
- formData.value.englishName = props.userData.englishName || '';
- formData.value.cellPhone = props.userData.cellPhone || '';
- formData.value.emailAddress = props.userData.emailAddress || '';
- formData.value.gogs_email = props.userData.gogs_email || '';
- formData.value.isActive = props.userData.isActive;
- try {
- formData.value.expiredTime = props.userData.expiredTime
- ? dayjs(props.userData.expiredTime)
- : null;
- } catch {
- formData.value.expiredTime = null;
- }
- fetchAssociatedProjects();
- fetchAvailableProjects();
- }
- if (val) {
- if (props.mode === 'add') {
- resetFormData();
- }
- activeMenu.value = 'basic';
- }
- },
- );
- const isOpen = computed({
- get: () => props.open,
- set: (val) => emit('update:open', val),
- });
- function handleMenuClick({
- key,
- }: {
- domEvent: Event;
- item: any;
- key: string;
- keyPath: string[];
- }) {
- activeMenu.value = key;
- }
- async function handleSave() {
- if (!formData.value.chineseName) {
- message.error($t('deliveryPartners.modal.enterChineseName'));
- return;
- }
- if (!formData.value.englishName) {
- message.error($t('deliveryPartners.modal.enterEnglishName'));
- return;
- }
- if (!formData.value.account) {
- message.error($t('deliveryPartners.modal.enterAccount'));
- return;
- }
- if (!formData.value.cellPhone) {
- message.error($t('deliveryPartners.modal.enterCellPhone'));
- return;
- }
- if (!formData.value.emailAddress) {
- message.error($t('deliveryPartners.modal.enterEmailAddress'));
- return;
- }
- if (!formData.value.gogs_email) {
- message.error($t('deliveryPartners.modal.enterGogsEmail'));
- return;
- }
- if (!formData.value.expiredTime) {
- message.error($t('deliveryPartners.modal.selectExpiredTime'));
- return;
- }
- if (props.mode === 'add' && !formData.value.password) {
- message.error($t('deliveryPartners.modal.enterPassword'));
- return;
- }
- const data = {
- id: formData.value.id,
- langNameList: [
- {
- name: 'zh-CN',
- value: formData.value.chineseName,
- },
- {
- name: 'en',
- value: formData.value.englishName,
- },
- ],
- expiredTime: formData.value.expiredTime
- ? formData.value.expiredTime.format('YYYY-MM-DD')
- : '',
- langName: null,
- account: formData.value.account,
- password:
- props.mode === 'add' ? md5(formData.value.password).toString() : '',
- cellPhone: formData.value.cellPhone,
- emailAddress: formData.value.emailAddress,
- gogs_email: formData.value.gogs_email,
- isActive: formData.value.isActive,
- };
- try {
- const result = formData.value.id
- ? await updateUserApi(data)
- : await addUserApi(data);
- if (result?.isSuccess) {
- emit('save', data);
- isOpen.value = false;
- message.success($t('deliveryPartners.saveSuccess'));
- }
- } catch {}
- }
- function handleCancel() {
- isOpen.value = false;
- }
- function handleAddProject() {
- projectModalOpen.value = true;
- projectSearchKeyword.value = '';
- selectedProjects.value = [];
- fetchAvailableProjects();
- }
- function handleProjectSearch() {
- projectPagination.value.currentPage = 1;
- fetchAvailableProjects();
- }
- function handleProjectPageChange(pagination: TablePaginationConfig) {
- if (pagination.current && pagination.pageSize) {
- projectPagination.value.currentPage = pagination.current;
- projectPagination.value.pageSize = pagination.pageSize;
- fetchAvailableProjects();
- }
- }
- async function handleProjectSelect(project: any) {
- if (!project.expiredTime) {
- message.error($t('deliveryPartners.enterExpiredTime'));
- return;
- }
- try {
- const result = await addProjectToUserApi(
- [project.id],
- formData.value.id,
- project.roleTypes,
- project.expiredTime ? project.expiredTime.format('YYYY-MM-DD') : '',
- );
- if (result?.isSuccess) {
- message.success($t('deliveryPartners.addProjectSuccess'));
- fetchAssociatedProjects();
- projectModalOpen.value = false;
- }
- } catch {}
- }
- async function handleProjectDelete(project: any) {
- Modal.confirm({
- title: $t('btn.delete'),
- content: $t('deliveryPartners.modal.deleteProjectConfirm'),
- okText: $t('btn.yes'),
- okType: 'danger',
- cancelText: $t('btn.no'),
- type: 'warning',
- onOk: async () => {
- try {
- const result = await deleteUserFromApplicationApi(formData.value.id, [
- project.id,
- ]);
- if (result?.isSuccess) {
- message.success($t('deliveryPartners.deleteProjectSuccess'));
- fetchAssociatedProjects();
- }
- } catch {}
- },
- onCancel: () => {},
- });
- }
- function resetFormData() {
- formData.value = {
- id: '',
- account: '',
- chineseName: '',
- englishName: '',
- cellPhone: '',
- emailAddress: '',
- gogs_email: '',
- expiredTime: null,
- isActive: true,
- password: '',
- };
- relatedProjects.value = [];
- }
- </script>
- <template>
- <Modal
- v-model:open="isOpen"
- :footer="null"
- :title="
- props.mode === 'add'
- ? $t('deliveryPartners.modal.addTitle')
- : $t('deliveryPartners.modal.editTitle')
- "
- width="1200px"
- >
- <div class="flex h-[600px]">
- <div
- v-if="props.mode !== 'add'"
- class="w-[200px] border-r border-gray-200"
- >
- <Menu
- :items="menuItems"
- :selected-keys="[activeMenu]"
- mode="vertical"
- @click="handleMenuClick"
- @update:selected-keys="(keys) => (activeMenu = keys[0] ?? 'basic')"
- />
- </div>
- <div class="flex-1 overflow-y-auto p-6">
- <div v-show="activeMenu === 'basic'" class="space-y-4">
- <div class="grid grid-cols-2 gap-4">
- <div class="flex flex-col gap-2">
- <label class="text-sm font-medium">
- {{ $t('deliveryPartners.modal.chineseName') }}
- <span class="text-red-500">*</span>
- </label>
- <Input
- v-model:value="formData.chineseName"
- :placeholder="$t('deliveryPartners.modal.enterChineseName')"
- />
- </div>
- <div class="flex flex-col gap-2">
- <label class="text-sm font-medium">
- {{ $t('deliveryPartners.modal.englishName') }}
- <span class="text-red-500">*</span>
- </label>
- <Input
- v-model:value="formData.englishName"
- :placeholder="$t('deliveryPartners.modal.enterEnglishName')"
- />
- </div>
- <div class="flex flex-col gap-2">
- <label class="text-sm font-medium">
- {{ $t('deliveryPartners.modal.account') }}
- <span class="text-red-500">*</span>
- </label>
- <Input
- v-model:value="formData.account"
- :disabled="props.mode === 'edit'"
- :placeholder="$t('deliveryPartners.modal.enterAccount')"
- />
- </div>
- <div v-if="props.mode === 'add'" class="flex flex-col gap-2">
- <label class="text-sm font-medium">
- {{ $t('deliveryPartners.modal.password') }}
- <span class="text-red-500">*</span>
- </label>
- <Input
- v-model:value="formData.password"
- :placeholder="$t('deliveryPartners.modal.enterPassword')"
- type="password"
- />
- </div>
- <div class="flex flex-col gap-2">
- <label class="text-sm font-medium">
- {{ $t('deliveryPartners.modal.cellPhone') }}
- <span class="text-red-500">*</span>
- </label>
- <Input
- v-model:value="formData.cellPhone"
- :placeholder="$t('deliveryPartners.modal.enterCellPhone')"
- />
- </div>
- <div class="flex flex-col gap-2">
- <label class="text-sm font-medium">
- {{ $t('deliveryPartners.modal.emailAddress') }}
- <span class="text-red-500">*</span>
- </label>
- <Input
- v-model:value="formData.emailAddress"
- :placeholder="$t('deliveryPartners.modal.enterEmailAddress')"
- />
- </div>
- <div class="flex flex-col gap-2">
- <label class="text-sm font-medium">
- {{ $t('deliveryPartners.modal.gogsEmail') }}
- <span class="text-red-500">*</span>
- </label>
- <Input
- v-model:value="formData.gogs_email"
- :placeholder="$t('deliveryPartners.modal.enterGogsEmail')"
- />
- </div>
- <div class="flex flex-col gap-2">
- <label class="text-sm font-medium">
- {{ $t('deliveryPartners.modal.expiredTime') }}
- <span class="text-red-500">*</span>
- </label>
- <DatePicker
- v-model:value="formData.expiredTime"
- :placeholder="$t('deliveryPartners.modal.selectExpiredTime')"
- format="YYYY-MM-DD"
- style="width: 100%"
- />
- </div>
- <div class="flex flex-col gap-2">
- <label class="text-sm font-medium">{{
- $t('deliveryPartners.modal.isActive')
- }}</label>
- <Switch v-model:checked="formData.isActive" class="w-[40px]" />
- </div>
- </div>
- </div>
- <div v-show="activeMenu === 'projects'" class="space-y-4">
- <div class="mb-4 flex items-center justify-between">
- <Button type="primary" @click="handleAddProject">
- {{ $t('deliveryPartners.modal.add') }}
- {{ $t('deliveryPartners.modal.projects') }}
- </Button>
- </div>
- <Table
- :columns="[
- {
- title: $t('deliveryPartners.modal.projectName'),
- dataIndex: 'name',
- key: 'name',
- },
- {
- title: $t('deliveryPartners.modal.projectCode'),
- dataIndex: 'code',
- key: 'code',
- },
- {
- title: $t('deliveryPartners.modal.role'),
- dataIndex: 'roleTypes',
- key: 'roleTypes',
- },
- {
- title: $t('deliveryPartners.modal.expiredTime'),
- dataIndex: 'expiredTime',
- key: 'expiredTime',
- },
- {
- title: $t('deliveryPartners.modal.action'),
- key: 'action',
- width: 100,
- },
- ]"
- :data-source="relatedProjects"
- :pagination="false"
- :scroll="{ y: 460 }"
- >
- <template #bodyCell="{ column, record }">
- <template v-if="column.key === 'action'">
- <Button
- danger
- size="small"
- @click="handleProjectDelete(record)"
- >
- {{ $t('deliveryPartners.modal.delete') }}
- </Button>
- </template>
- </template>
- </Table>
- </div>
- </div>
- </div>
- <div
- v-if="activeMenu === 'basic'"
- class="flex justify-end gap-2 border-t pt-4"
- >
- <Button @click="handleCancel">
- {{ $t('deliveryPartners.modal.cancel') }}
- </Button>
- <Button type="primary" @click="handleSave">
- {{ $t('deliveryPartners.modal.save') }}
- </Button>
- </div>
- <Modal
- v-model:open="projectModalOpen"
- :footer="null"
- :title="$t('deliveryPartners.modal.addProjectTitle')"
- width="760"
- >
- <div class="space-y-4">
- <Input
- v-model:value="projectSearchKeyword"
- :placeholder="$t('deliveryPartners.modal.searchProject')"
- @change="handleProjectSearch"
- />
- <Table
- :columns="[
- {
- title: $t('deliveryPartners.modal.projectName'),
- dataIndex: 'name',
- key: 'name',
- ellipsis: true,
- width: 120,
- },
- {
- title: $t('deliveryPartners.modal.projectCode'),
- dataIndex: 'code',
- key: 'code',
- ellipsis: true,
- width: 120,
- },
- {
- title: $t('deliveryPartners.modal.role'),
- key: 'roleTypes',
- width: 150,
- },
- {
- title: $t('deliveryPartners.modal.expiredTime'),
- key: 'expiredTime',
- width: 150,
- },
- {
- title: $t('deliveryPartners.modal.action'),
- key: 'action',
- },
- ]"
- :data-source="allProjects"
- :pagination="{
- current: projectPagination.currentPage,
- pageSize: projectPagination.pageSize,
- total: projectPagination.total,
- showSizeChanger: true,
- showTotal: (total: number) =>
- $t('deliveryPartners.modal.totalProjects', { total }),
- }"
- :scroll="{ y: 460 }"
- class="project-table"
- @change="handleProjectPageChange"
- >
- <template #bodyCell="{ column, record }">
- <template v-if="column.key === 'roleTypes'">
- <Select
- v-model:value="record.roleTypes"
- :options="roleOptions"
- style="width: 120px"
- />
- </template>
- <template v-if="column.key === 'expiredTime'">
- <DatePicker
- v-model:value="record.expiredTime"
- format="YYYY-MM-DD"
- style="width: 120px"
- />
- </template>
- <template v-if="column.key === 'action'">
- <Button
- size="small"
- type="primary"
- @click="handleProjectSelect(record)"
- >
- {{ $t('deliveryPartners.modal.add') }}
- </Button>
- </template>
- </template>
- </Table>
- </div>
- </Modal>
- </Modal>
- </template>
- <style lang="scss">
- .ant-menu-light .ant-menu-item-selected,
- .ant-menu-light > .ant-menu .ant-menu-item-selected {
- background-color: #f4f2f2;
- .ant-menu-title-content {
- color: #462424;
- }
- }
- .ant-table-wrapper .ant-table-tbody > tr > th,
- .ant-table-wrapper .ant-table-tbody > tr > td {
- padding: 8px 16px;
- }
- .ant-table-wrapper .ant-table-thead > tr > th,
- .ant-table-wrapper .ant-table-thead > tr > td {
- padding: 8px 16px;
- }
- </style>
|