<script lang="ts" setup>
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { ResearchApplicationApi } from '#/api/research/application';
|
|
import { nextTick, watch } from 'vue';
|
|
import { Button, Input, InputNumber, Select } from 'ant-design-vue';
|
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { $t } from '#/locales';
|
|
import { MEMBER_ROLE_OPTIONS, toSubmitMembers } from '../data';
|
|
const props = withDefaults(
|
defineProps<{
|
members?: ResearchApplicationApi.ApplicationMember[];
|
disabled?: boolean;
|
}>(),
|
{
|
members: () => [],
|
disabled: false,
|
},
|
);
|
|
const emit = defineEmits<{
|
'update:members': [members: ResearchApplicationApi.ApplicationMember[]];
|
}>();
|
|
const [Grid, gridApi] = useVbenVxeGrid({
|
gridOptions: {
|
columns: [
|
{ type: 'seq', title: '序号', width: 55, fixed: 'left' },
|
{
|
field: 'name',
|
title: '姓名',
|
minWidth: 110,
|
slots: { default: 'name' },
|
},
|
{
|
field: 'contactPhone',
|
title: '联系电话',
|
minWidth: 140,
|
slots: { default: 'contactPhone' },
|
},
|
{
|
field: 'workUnit',
|
title: '所属单位',
|
minWidth: 180,
|
slots: { default: 'workUnit' },
|
},
|
{
|
field: 'jobTitle',
|
title: '职称/职务',
|
minWidth: 120,
|
slots: { default: 'jobTitle' },
|
},
|
{
|
field: 'memberRole',
|
title: '成员角色',
|
minWidth: 130,
|
slots: { default: 'memberRole' },
|
},
|
{
|
field: 'sortNo',
|
title: '排序',
|
width: 90,
|
slots: { default: 'sortNo' },
|
},
|
{
|
title: '操作',
|
width: 80,
|
fixed: 'right',
|
slots: { default: 'actions' },
|
},
|
],
|
data: [],
|
minHeight: 200,
|
autoResize: true,
|
border: true,
|
rowConfig: { keyField: 'id', isHover: true },
|
pagerConfig: { enabled: false },
|
toolbarConfig: { enabled: false },
|
} as VxeTableGridOptions<ResearchApplicationApi.ApplicationMember>,
|
});
|
|
/** 读取当前全部成员行(提交形态:不含 id / applicationId) */
|
function collectMembers(): ResearchApplicationApi.ApplicationMember[] {
|
const rows =
|
(gridApi.grid.getData() as ResearchApplicationApi.ApplicationMember[]) ??
|
[];
|
const removed =
|
(gridApi.grid.getRemoveRecords() as ResearchApplicationApi.ApplicationMember[]) ??
|
[];
|
const inserted =
|
(gridApi.grid.getInsertRecords() as ResearchApplicationApi.ApplicationMember[]) ??
|
[];
|
const isRemoved = (row: ResearchApplicationApi.ApplicationMember) =>
|
removed.some(
|
(item) => item === row || (row.id != null && item.id === row.id),
|
);
|
return toSubmitMembers([
|
...rows.filter((row) => !isRemoved(row)),
|
...inserted,
|
]);
|
}
|
|
/** 同步成员数据给父组件 */
|
function recalc() {
|
emit('update:members', collectMembers());
|
}
|
|
/** 添加成员行 */
|
async function handleAdd() {
|
await gridApi.grid.insertAt(
|
{
|
memberRole: '参与人',
|
sortNo: collectMembers().length + 1,
|
} as unknown as ResearchApplicationApi.ApplicationMember,
|
-1,
|
);
|
recalc();
|
}
|
|
/** 删除成员行 */
|
async function handleDelete(row: ResearchApplicationApi.ApplicationMember) {
|
await gridApi.grid.remove(row);
|
recalc();
|
}
|
|
/** 校验成员:姓名必填;填写电话时须为 11 位手机号 */
|
function validate() {
|
const rows = collectMembers();
|
for (let i = 0; i < rows.length; i++) {
|
const row = rows[i];
|
if (!row?.name || String(row.name).trim() === '') {
|
throw new Error(`第 ${i + 1} 行成员:请填写姓名`);
|
}
|
const phone = row.contactPhone ? String(row.contactPhone).trim() : '';
|
if (phone !== '' && !/^1[3-9]\d{9}$/.test(phone)) {
|
throw new Error(
|
`第 ${i + 1} 行成员 ${row.name}:联系电话格式不正确,请输入 11 位手机号`,
|
);
|
}
|
}
|
}
|
|
defineExpose({ validate });
|
|
/**
|
* 回显成员(单向:父 → 表格)。
|
* watcher 内禁止回传 emit,避免 prop ↔ emit 死循环。
|
*/
|
watch(
|
() => props.members,
|
async (members) => {
|
if (!members) {
|
return;
|
}
|
await nextTick();
|
await gridApi.grid.loadData([...members]);
|
},
|
{ immediate: true },
|
);
|
</script>
|
|
<template>
|
<Grid class="w-full">
|
<template #name="{ row }">
|
<Input
|
v-model:value="row.name"
|
:disabled="disabled"
|
placeholder="请输入姓名"
|
@change="() => recalc()"
|
/>
|
</template>
|
<template #contactPhone="{ row }">
|
<Input
|
v-model:value="row.contactPhone"
|
:disabled="disabled"
|
:maxlength="11"
|
placeholder="11 位手机号(选填)"
|
@change="() => recalc()"
|
/>
|
</template>
|
<template #workUnit="{ row }">
|
<Input
|
v-model:value="row.workUnit"
|
:disabled="disabled"
|
placeholder="请输入所属单位"
|
@change="() => recalc()"
|
/>
|
</template>
|
<template #jobTitle="{ row }">
|
<Input
|
v-model:value="row.jobTitle"
|
:disabled="disabled"
|
placeholder="请输入职称/职务"
|
@change="() => recalc()"
|
/>
|
</template>
|
<template #memberRole="{ row }">
|
<Select
|
v-model:value="row.memberRole"
|
:disabled="disabled"
|
:options="MEMBER_ROLE_OPTIONS"
|
placeholder="请选择角色"
|
allow-clear
|
@change="() => recalc()"
|
/>
|
</template>
|
<template #sortNo="{ row }">
|
<InputNumber
|
v-model:value="row.sortNo"
|
:disabled="disabled"
|
:min="1"
|
:precision="0"
|
class="!w-full"
|
@change="() => recalc()"
|
/>
|
</template>
|
<template #actions="{ row }">
|
<TableAction
|
:actions="[
|
{
|
label: $t('common.delete'),
|
danger: true,
|
type: 'link',
|
icon: ACTION_ICON.DELETE,
|
disabled,
|
onClick: handleDelete.bind(null, row),
|
},
|
]"
|
/>
|
</template>
|
</Grid>
|
<div v-if="!disabled" class="-mt-4 flex justify-center">
|
<Button type="primary" ghost @click="handleAdd"> 添加成员 </Button>
|
</div>
|
</template>
|