260720
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '编辑审核流' : '添加审核流'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="名称" name="title">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入名称"
|
||||
v-model:value="form.title"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="机构" name="organizationIds">
|
||||
<a-tree-select
|
||||
multiple
|
||||
allow-clear
|
||||
tree-default-expand-all
|
||||
:tree-data="organizationTreeData"
|
||||
:value="form.organizationIds"
|
||||
:loading="organizationLoading"
|
||||
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
|
||||
placeholder="请选择审核机构"
|
||||
@update:value="updateOrganizationIds"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核顺序">
|
||||
<div class="flow-step-list">
|
||||
<div
|
||||
v-for="(item, index) in form.flowItems"
|
||||
:key="item.organizationId"
|
||||
class="flow-step-item"
|
||||
>
|
||||
<div class="flow-step-info">
|
||||
<a-tag color="blue">第{{ index + 1 }}级</a-tag>
|
||||
<span>{{ getOrganizationName(item.organizationId) }}</span>
|
||||
</div>
|
||||
<div class="flow-step-reviewer">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:value="item.reviewUserId"
|
||||
:loading="reviewUserLoadingMap[item.organizationId || 0]"
|
||||
:options="getReviewUserOptions(item.organizationId)"
|
||||
:filter-option="filterReviewUserOption"
|
||||
placeholder="请选择审核人"
|
||||
@focus="loadReviewUsers(item.organizationId)"
|
||||
@update:value="updateReviewUser(index, $event)"
|
||||
/>
|
||||
</div>
|
||||
<a-space :size="4">
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="index === 0"
|
||||
@click="moveOrganization(index, 'up')"
|
||||
>
|
||||
上移
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="index === form.flowItems.length - 1"
|
||||
@click="moveOrganization(index, 'down')"
|
||||
>
|
||||
下移
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="removeOrganization(index)">
|
||||
移除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-empty v-if="!form.flowItems.length" description="请选择审核机构" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { toTreeData } from 'ele-admin-pro/es';
|
||||
import { addReviewFlow, removeReviewFlow } from '@/api/gxmu/reviewFlow';
|
||||
import { ReviewFlow } from '@/api/gxmu/reviewFlow/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { listOrganizations } from '@/api/system/organization';
|
||||
import type { Organization } from '@/api/system/organization/model';
|
||||
import { listUsers } from '@/api/system/user';
|
||||
import type { User } from '@/api/system/user/model';
|
||||
|
||||
interface ReviewFlowItem {
|
||||
organizationId?: number;
|
||||
reviewUserId?: number;
|
||||
}
|
||||
|
||||
interface ReviewFlowForm extends Omit<ReviewFlow, 'reviewUserId'> {
|
||||
organizationIds: number[];
|
||||
flowItems: ReviewFlowItem[];
|
||||
}
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ReviewFlow | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
// 机构树数据
|
||||
const organizationTreeData = ref<Organization[]>([]);
|
||||
const organizationLoading = ref(false);
|
||||
const organizationNameMap = ref<Record<number, string>>({});
|
||||
const reviewUserOptionsMap = ref<Record<number, User[]>>({});
|
||||
const reviewUserLoadingMap = ref<Record<number, boolean>>({});
|
||||
const reviewUserNameMap = ref<Record<number, string>>({});
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ReviewFlowForm>({
|
||||
id: undefined,
|
||||
title: undefined,
|
||||
organizationId: undefined,
|
||||
organizationIds: [],
|
||||
flowItems: [],
|
||||
level: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
userId: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
whitespace: true,
|
||||
message: '请输入名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
organizationIds: [
|
||||
{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: '请选择至少一个机构',
|
||||
trigger: 'change'
|
||||
},
|
||||
{
|
||||
validator: async () => {
|
||||
const invalid = form.flowItems.some((item) => !item.reviewUserId);
|
||||
if (invalid) {
|
||||
return Promise.reject(new Error('请为每一级选择审核人'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
const syncOrganizationIds = () => {
|
||||
form.organizationIds = form.flowItems
|
||||
.map((item) => item.organizationId)
|
||||
.filter((item): item is number => typeof item === 'number');
|
||||
};
|
||||
|
||||
const loadOrganizations = async () => {
|
||||
organizationLoading.value = true;
|
||||
try {
|
||||
const result = await listOrganizations();
|
||||
organizationNameMap.value = result.reduce<Record<number, string>>(
|
||||
(map, item) => {
|
||||
if (item.organizationId) {
|
||||
map[item.organizationId] =
|
||||
item.organizationName || `机构${item.organizationId}`;
|
||||
}
|
||||
return map;
|
||||
},
|
||||
{}
|
||||
);
|
||||
organizationTreeData.value = toTreeData({
|
||||
data: result.map((item) => ({
|
||||
...item,
|
||||
key: item.organizationId,
|
||||
value: item.organizationId,
|
||||
title: item.organizationName
|
||||
})),
|
||||
idField: 'organizationId',
|
||||
parentIdField: 'parentId'
|
||||
}) as Organization[];
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '机构列表加载失败');
|
||||
} finally {
|
||||
organizationLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateOrganizationIds = (values?: number[]) => {
|
||||
const selectedIds = values || [];
|
||||
const selectedSet = new Set(selectedIds);
|
||||
const flowItemMap = new Map(
|
||||
form.flowItems.map((item) => [item.organizationId, item] as const)
|
||||
);
|
||||
const keptItems = form.flowItems.filter((item) =>
|
||||
selectedSet.has(item.organizationId || 0)
|
||||
);
|
||||
const appendItems = selectedIds
|
||||
.filter((id) => !flowItemMap.has(id))
|
||||
.map((organizationId) => ({
|
||||
organizationId,
|
||||
reviewUserId: undefined
|
||||
}));
|
||||
form.flowItems = [...keptItems, ...appendItems];
|
||||
syncOrganizationIds();
|
||||
};
|
||||
|
||||
const moveOrganization = (index: number, direction: 'up' | 'down') => {
|
||||
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (targetIndex < 0 || targetIndex >= form.flowItems.length) {
|
||||
return;
|
||||
}
|
||||
const items = [...form.flowItems];
|
||||
[items[index], items[targetIndex]] = [items[targetIndex], items[index]];
|
||||
form.flowItems = items;
|
||||
syncOrganizationIds();
|
||||
};
|
||||
|
||||
const removeOrganization = (index: number) => {
|
||||
form.flowItems = form.flowItems.filter((_, i) => i !== index);
|
||||
syncOrganizationIds();
|
||||
};
|
||||
|
||||
const getOrganizationName = (organizationId?: number) => {
|
||||
if (!organizationId) {
|
||||
return '-';
|
||||
}
|
||||
return organizationNameMap.value[organizationId] || `机构${organizationId}`;
|
||||
};
|
||||
|
||||
const loadReviewUsers = async (organizationId?: number) => {
|
||||
if (!organizationId || reviewUserOptionsMap.value[organizationId]) {
|
||||
return;
|
||||
}
|
||||
reviewUserLoadingMap.value = {
|
||||
...reviewUserLoadingMap.value,
|
||||
[organizationId]: true
|
||||
};
|
||||
try {
|
||||
const result = await listUsers({ organizationId });
|
||||
reviewUserOptionsMap.value = {
|
||||
...reviewUserOptionsMap.value,
|
||||
[organizationId]: result
|
||||
};
|
||||
reviewUserNameMap.value = result.reduce<Record<number, string>>(
|
||||
(map, item) => {
|
||||
if (item.userId) {
|
||||
map[item.userId] =
|
||||
item.realName || item.nickname || `用户${item.userId}`;
|
||||
}
|
||||
return map;
|
||||
},
|
||||
reviewUserNameMap.value
|
||||
);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '审核人列表加载失败');
|
||||
} finally {
|
||||
reviewUserLoadingMap.value = {
|
||||
...reviewUserLoadingMap.value,
|
||||
[organizationId]: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getReviewUserOptions = (organizationId?: number) => {
|
||||
if (!organizationId) {
|
||||
return [];
|
||||
}
|
||||
return (reviewUserOptionsMap.value[organizationId] || []).map((item) => ({
|
||||
value: item.userId,
|
||||
label: item.realName || item.nickname || `用户${item.userId}`
|
||||
}));
|
||||
};
|
||||
|
||||
const filterReviewUserOption = (input: string, option: any) => {
|
||||
return String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase());
|
||||
};
|
||||
|
||||
const updateReviewUser = (index: number, reviewUserId?: number) => {
|
||||
const items = [...form.flowItems];
|
||||
const organizationId = items[index]?.organizationId;
|
||||
const currentUser = organizationId
|
||||
? reviewUserOptionsMap.value[organizationId]?.find(
|
||||
(item) => item.userId === reviewUserId
|
||||
)
|
||||
: undefined;
|
||||
if (reviewUserId && currentUser?.userId) {
|
||||
reviewUserNameMap.value = {
|
||||
...reviewUserNameMap.value,
|
||||
[reviewUserId]:
|
||||
currentUser.realName ||
|
||||
currentUser.nickname ||
|
||||
`用户${currentUser.userId}`
|
||||
};
|
||||
}
|
||||
items[index] = {
|
||||
...items[index],
|
||||
reviewUserId
|
||||
};
|
||||
form.flowItems = items;
|
||||
syncOrganizationIds();
|
||||
};
|
||||
|
||||
const getSortedFlows = () => {
|
||||
if (props.data?.flows?.length) {
|
||||
return [...props.data.flows].sort(
|
||||
(a, b) => (a.level || 0) - (b.level || 0)
|
||||
);
|
||||
}
|
||||
if (props.data?.id) {
|
||||
return [props.data];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const preloadReviewUsers = async () => {
|
||||
for (const item of form.flowItems) {
|
||||
await loadReviewUsers(item.organizationId);
|
||||
}
|
||||
};
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = async () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
let successMessage = '';
|
||||
|
||||
if (isUpdate.value) {
|
||||
const currentFlows = getSortedFlows();
|
||||
for (const currentFlow of currentFlows) {
|
||||
if (currentFlow.id) {
|
||||
await removeReviewFlow(currentFlow.id);
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < form.flowItems.length; index++) {
|
||||
const item = form.flowItems[index];
|
||||
const formData: ReviewFlow = {
|
||||
...form,
|
||||
title: form.title,
|
||||
organizationId: item.organizationId,
|
||||
reviewUserId: item.reviewUserId,
|
||||
level: index + 1
|
||||
};
|
||||
delete (formData as ReviewFlow & { organizationIds?: number[] })
|
||||
.organizationIds;
|
||||
delete (formData as ReviewFlow & { flowItems?: ReviewFlowItem[] })
|
||||
.flowItems;
|
||||
delete formData.id;
|
||||
successMessage = await addReviewFlow(formData);
|
||||
}
|
||||
} else {
|
||||
for (let index = 0; index < form.flowItems.length; index++) {
|
||||
const item = form.flowItems[index];
|
||||
const formData: ReviewFlow = {
|
||||
...form,
|
||||
organizationId: item.organizationId,
|
||||
reviewUserId: item.reviewUserId,
|
||||
level: index + 1
|
||||
};
|
||||
delete (formData as ReviewFlow & { organizationIds?: number[] })
|
||||
.organizationIds;
|
||||
delete (formData as ReviewFlow & { flowItems?: ReviewFlowItem[] })
|
||||
.flowItems;
|
||||
successMessage = await addReviewFlow(formData);
|
||||
}
|
||||
}
|
||||
|
||||
message.success(successMessage || '保存成功');
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
if (e?.errorFields) {
|
||||
return;
|
||||
}
|
||||
message.error(e.message || '保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
if (!organizationTreeData.value.length) {
|
||||
await loadOrganizations();
|
||||
}
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
const currentFlows = getSortedFlows();
|
||||
form.flowItems = currentFlows
|
||||
.filter((item) => typeof item.organizationId === 'number')
|
||||
.map((item) => ({
|
||||
organizationId: item.organizationId,
|
||||
reviewUserId: item.reviewUserId
|
||||
}));
|
||||
syncOrganizationIds();
|
||||
await preloadReviewUsers();
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
resetFields();
|
||||
form.organizationIds = [];
|
||||
form.flowItems = [];
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
form.organizationIds = [];
|
||||
form.flowItems = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.flow-step-list {
|
||||
width: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.flow-step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.flow-step-item + .flow-step-item {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.flow-step-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.flow-step-reviewer {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user