feat: 支持审批步骤绑定部门与多对象
- 新增审批对象关联表和存量数据迁移 - 支持用户、角色、部门多选及办理权限匹配 - 完善部门状态与批量管理交互
This commit is contained in:
@@ -24,7 +24,8 @@
|
||||
"revoke": "Revoke",
|
||||
"submit": "Submit Approval",
|
||||
"addScope": "Add Scope",
|
||||
"addStep": "Add Step"
|
||||
"addStep": "Add Step",
|
||||
"removeTarget": "Remove"
|
||||
},
|
||||
"scope": {
|
||||
"category": "Category",
|
||||
@@ -32,7 +33,8 @@
|
||||
},
|
||||
"assignee": {
|
||||
"role": "Role",
|
||||
"user": "User"
|
||||
"user": "User",
|
||||
"dept": "Department"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
@@ -53,7 +55,8 @@
|
||||
},
|
||||
"helper": {
|
||||
"scope": "Limit the flow by resource category or applicant department. Leave empty to apply globally.",
|
||||
"scopeEmpty": "No scope configured. This flow applies globally."
|
||||
"scopeEmpty": "No scope configured. This flow applies globally.",
|
||||
"assigneeAny": "Any selected assignee can process the same step."
|
||||
},
|
||||
"fields": {
|
||||
"flowName": "Flow Name",
|
||||
@@ -77,7 +80,9 @@
|
||||
"applicantId": "Applicant ID",
|
||||
"submittedAt": "Submitted At",
|
||||
"finishedAt": "Finished At",
|
||||
"assigneeType": "Assignee Type",
|
||||
"assigneeTarget": "Assignee",
|
||||
"selectedDeptCount": "Selected departments: {value}",
|
||||
"actedBy": "Acted By",
|
||||
"actedAt": "Acted At",
|
||||
"comment": "Comment",
|
||||
@@ -112,6 +117,7 @@
|
||||
"message": {
|
||||
"needStep": "At least one step is required",
|
||||
"needStepAssignee": "Each approval step requires an assignee",
|
||||
"confirmAssigneeTypeChange": "Changing the assignee type clears the current selections. Continue?",
|
||||
"saveSuccess": "Flow saved",
|
||||
"statusUpdated": "Flow status updated",
|
||||
"deleteSuccess": "Flow deleted",
|
||||
|
||||
@@ -13,5 +13,18 @@
|
||||
"modified": "Modified",
|
||||
"modifiedBy": "ModifiedBy",
|
||||
"remark": "Remark",
|
||||
"isDeleted": "IsDeleted"
|
||||
"isDeleted": "IsDeleted",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"batchEnable": "Enable Selected",
|
||||
"batchDisable": "Disable Selected",
|
||||
"batchDelete": "Delete Selected",
|
||||
"selectedCount": "{count} selected",
|
||||
"selectRequired": "Select at least one department",
|
||||
"statusConfirm": "{action} department “{name}”?",
|
||||
"batchStatusConfirm": "{action} the {count} selected departments?",
|
||||
"deleteConfirm": "Delete department “{name}”? This action cannot be undone.",
|
||||
"batchDeleteConfirm": "Delete the {count} selected departments? This action cannot be undone.",
|
||||
"operationSuccess": "Operation completed",
|
||||
"operationFailed": "Operation failed. Please try again."
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"revoke": "撤回",
|
||||
"submit": "提交审批",
|
||||
"addScope": "新增范围",
|
||||
"addStep": "新增步骤"
|
||||
"addStep": "新增步骤",
|
||||
"removeTarget": "移除"
|
||||
},
|
||||
"scope": {
|
||||
"category": "分类",
|
||||
@@ -32,7 +33,8 @@
|
||||
},
|
||||
"assignee": {
|
||||
"role": "角色",
|
||||
"user": "用户"
|
||||
"user": "用户",
|
||||
"dept": "部门"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "启用",
|
||||
@@ -53,7 +55,8 @@
|
||||
},
|
||||
"helper": {
|
||||
"scope": "按资源分类或申请人部门限定流程命中范围;留空时默认对全部资源生效。",
|
||||
"scopeEmpty": "未配置范围,当前流程默认全局生效。"
|
||||
"scopeEmpty": "未配置范围,当前流程默认全局生效。",
|
||||
"assigneeAny": "同一步骤中,任一选中的审批对象均可处理。"
|
||||
},
|
||||
"fields": {
|
||||
"flowName": "流程名称",
|
||||
@@ -77,7 +80,9 @@
|
||||
"applicantId": "申请人ID",
|
||||
"submittedAt": "提交时间",
|
||||
"finishedAt": "完成时间",
|
||||
"assigneeType": "对象类型",
|
||||
"assigneeTarget": "审批对象",
|
||||
"selectedDeptCount": "已选 {value} 个部门",
|
||||
"actedBy": "处理人",
|
||||
"actedAt": "处理时间",
|
||||
"comment": "处理意见",
|
||||
@@ -112,6 +117,7 @@
|
||||
"message": {
|
||||
"needStep": "至少需要一个审批步骤",
|
||||
"needStepAssignee": "每个审批步骤都需要配置审批对象",
|
||||
"confirmAssigneeTypeChange": "切换审批方式会清空当前已选对象,确认继续吗?",
|
||||
"saveSuccess": "审批流程已保存",
|
||||
"statusUpdated": "流程状态已更新",
|
||||
"deleteSuccess": "流程已删除",
|
||||
|
||||
@@ -13,5 +13,18 @@
|
||||
"modified": "修改时间",
|
||||
"modifiedBy": "修改者",
|
||||
"remark": "备注",
|
||||
"isDeleted": "删除标识"
|
||||
"isDeleted": "删除标识",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"batchEnable": "批量启用",
|
||||
"batchDisable": "批量禁用",
|
||||
"batchDelete": "批量删除",
|
||||
"selectedCount": "已选 {count} 项",
|
||||
"selectRequired": "请先选择要操作的部门",
|
||||
"statusConfirm": "确认{action}部门“{name}”吗?",
|
||||
"batchStatusConfirm": "确认{action}已选中的 {count} 个部门吗?",
|
||||
"deleteConfirm": "确认删除部门“{name}”吗?删除后不可恢复。",
|
||||
"batchDeleteConfirm": "确认删除已选中的 {count} 个部门吗?删除后不可恢复。",
|
||||
"operationSuccess": "操作成功",
|
||||
"operationFailed": "操作失败,请稍后重试"
|
||||
}
|
||||
|
||||
@@ -206,13 +206,44 @@ function formatApplicationReason(value?: null | string) {
|
||||
}
|
||||
|
||||
function formatAssigneeDisplay(row: Record<string, any>) {
|
||||
if (!row?.assigneeType || !row?.assigneeTargetName) {
|
||||
if (!row?.assigneeType) {
|
||||
return '-';
|
||||
}
|
||||
if (row.assigneeType === 'ROLE') {
|
||||
return `${$t('approval.assignee.role')}:${row.assigneeTargetName}`;
|
||||
const labelMap: Record<string, string> = {
|
||||
DEPT: $t('approval.assignee.dept'),
|
||||
ROLE: $t('approval.assignee.role'),
|
||||
USER: $t('approval.assignee.user'),
|
||||
};
|
||||
let targets: Array<Record<string, any>> = [];
|
||||
if (Array.isArray(row.assigneeTargets) && row.assigneeTargets.length > 0) {
|
||||
targets = row.assigneeTargets;
|
||||
} else if (row.assigneeTargetName) {
|
||||
targets = [
|
||||
{
|
||||
includeChildren: 0,
|
||||
targetName: row.assigneeTargetName,
|
||||
},
|
||||
];
|
||||
}
|
||||
return `${$t('approval.assignee.user')}:${row.assigneeTargetName}`;
|
||||
const targetNames = targets
|
||||
.map((target: Record<string, any>) => {
|
||||
const name = String(target?.targetName || '').trim();
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
row.assigneeType === 'DEPT' &&
|
||||
(target.includeChildren === 1 || target.includeChildren === true)
|
||||
) {
|
||||
return `${name}(${$t('approval.fields.includeChildren')})`;
|
||||
}
|
||||
return name;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (targetNames.length === 0) {
|
||||
return '-';
|
||||
}
|
||||
return `${labelMap[row.assigneeType] || row.assigneeType}:${targetNames.join('、')}`;
|
||||
}
|
||||
|
||||
function getEventTypeLabel(eventType?: string) {
|
||||
@@ -384,7 +415,7 @@ function formatEventInfo(row: Record<string, any>) {
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.assigneeTarget')"
|
||||
width="220"
|
||||
min-width="260"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatAssigneeDisplay(row) }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
@@ -32,7 +33,7 @@ defineExpose({
|
||||
|
||||
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
|
||||
type ActionType = '' | 'DELETE' | 'OFFLINE' | 'PUBLISH';
|
||||
type AssigneeType = 'ROLE' | 'USER';
|
||||
type AssigneeType = 'DEPT' | 'ROLE' | 'USER';
|
||||
type ScopeType = 'CATEGORY' | 'DEPT';
|
||||
type FlowStatus = 'DISABLED' | 'ENABLED';
|
||||
|
||||
@@ -49,14 +50,21 @@ interface ScopeItem {
|
||||
}
|
||||
|
||||
interface StepItem {
|
||||
assigneeTargetCode?: string;
|
||||
assigneeTargetId?: number | string;
|
||||
assigneeTargetName?: string;
|
||||
assigneeTargetIds: Array<number | string>;
|
||||
assigneeTargets: AssigneeTarget[];
|
||||
assigneeType: AssigneeType;
|
||||
includeChildrenByTarget: Record<string, boolean>;
|
||||
id?: number | string;
|
||||
stepName: string;
|
||||
}
|
||||
|
||||
interface AssigneeTarget {
|
||||
includeChildren: boolean;
|
||||
targetCode?: string;
|
||||
targetId: number | string;
|
||||
targetName?: string;
|
||||
}
|
||||
|
||||
interface FlowFormModel {
|
||||
actionType: ActionType;
|
||||
id?: number | string;
|
||||
@@ -94,14 +102,16 @@ const SCOPE_TYPE_OPTIONS = [
|
||||
const ASSIGNEE_TYPE_OPTIONS: Array<{ label: string; value: AssigneeType }> = [
|
||||
{ label: $t('approval.assignee.role'), value: 'ROLE' },
|
||||
{ label: $t('approval.assignee.user'), value: 'USER' },
|
||||
{ label: $t('approval.assignee.dept'), value: 'DEPT' },
|
||||
];
|
||||
const ENABLED_DATA_STATUS = 1;
|
||||
const ASSIGNEE_VISIBLE_TAG_COUNT = 2;
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const btnLoading = ref(false);
|
||||
const categoryLoaded = ref(false);
|
||||
const deptLoaded = ref(false);
|
||||
const roleLoaded = ref(false);
|
||||
const accountLoading = ref(false);
|
||||
const deptTreeOptions = ref<any[]>([]);
|
||||
@@ -112,6 +122,19 @@ const categoryOptions = ref<Record<Exclude<ResourceType, ''>, SelectOption[]>>({
|
||||
KNOWLEDGE: [],
|
||||
WORKFLOW: [],
|
||||
});
|
||||
const deptOptionMap = computed(() => {
|
||||
const result = new Map<string, string>();
|
||||
const visit = (items: any[]) => {
|
||||
for (const item of items) {
|
||||
result.set(String(item.id), item.deptName || String(item.id));
|
||||
if (Array.isArray(item.children)) {
|
||||
visit(item.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(deptTreeOptions.value);
|
||||
return result;
|
||||
});
|
||||
|
||||
const formModel = ref<FlowFormModel>(buildDefaultForm());
|
||||
|
||||
@@ -158,7 +181,17 @@ function buildDefaultForm(): FlowFormModel {
|
||||
resourceType: 'BOT',
|
||||
scopes: [],
|
||||
status: 'ENABLED',
|
||||
steps: [{ assigneeType: 'ROLE', stepName: '' }],
|
||||
steps: [buildDefaultStep()],
|
||||
};
|
||||
}
|
||||
|
||||
function buildDefaultStep(): StepItem {
|
||||
return {
|
||||
assigneeTargetIds: [],
|
||||
assigneeTargets: [],
|
||||
assigneeType: 'ROLE',
|
||||
includeChildrenByTarget: {},
|
||||
stepName: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -200,17 +233,20 @@ async function openDialog(row: any = {}) {
|
||||
scopeValue: item.scopeValue,
|
||||
})),
|
||||
status: res.data?.status || 'ENABLED',
|
||||
steps: (res.data?.steps || []).map((item: any) => ({
|
||||
assigneeTargetCode: item.assigneeTargetCode,
|
||||
assigneeTargetId: item.assigneeTargetId,
|
||||
assigneeTargetName: item.assigneeTargetName,
|
||||
assigneeType: item.assigneeType || 'ROLE',
|
||||
id: item.id,
|
||||
stepName: item.stepName,
|
||||
})),
|
||||
steps: (res.data?.steps || []).map((item: any) => {
|
||||
const targets = normalizeAssigneeTargets(item);
|
||||
return {
|
||||
assigneeTargetIds: targets.map((target) => target.targetId),
|
||||
assigneeTargets: targets,
|
||||
assigneeType: item.assigneeType || 'ROLE',
|
||||
id: item.id,
|
||||
includeChildrenByTarget: buildIncludeChildrenByTarget(targets),
|
||||
stepName: item.stepName,
|
||||
};
|
||||
}),
|
||||
};
|
||||
if (formModel.value.steps.length === 0) {
|
||||
formModel.value.steps = [{ assigneeType: 'ROLE', stepName: '' }];
|
||||
formModel.value.steps = [buildDefaultStep()];
|
||||
}
|
||||
for (const step of formModel.value.steps) {
|
||||
registerAccountOption(step);
|
||||
@@ -249,18 +285,15 @@ async function ensureCategoryOptions() {
|
||||
}
|
||||
|
||||
async function ensureDeptOptions() {
|
||||
if (deptLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const res = await api.get('/api/v1/sysDept/list', {
|
||||
params: {
|
||||
asTree: true,
|
||||
sortKey: 'sortNo',
|
||||
sortType: 'asc',
|
||||
status: ENABLED_DATA_STATUS,
|
||||
},
|
||||
});
|
||||
deptTreeOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
deptLoaded.value = true;
|
||||
}
|
||||
|
||||
async function ensureRoleOptions() {
|
||||
@@ -310,6 +343,41 @@ function normalizeSelectOptions(data: any[] = []) {
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeAssigneeTargets(item: any): AssigneeTarget[] {
|
||||
const targets = Array.isArray(item?.assigneeTargets)
|
||||
? item.assigneeTargets
|
||||
: [];
|
||||
if (targets.length > 0) {
|
||||
return targets
|
||||
.filter((target: any) => target?.targetId)
|
||||
.map((target: any) => ({
|
||||
includeChildren: target.includeChildren === 1,
|
||||
targetCode: target.targetCode,
|
||||
targetId: target.targetId,
|
||||
targetName: target.targetName,
|
||||
}));
|
||||
}
|
||||
if (!item?.assigneeTargetId) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
includeChildren: false,
|
||||
targetCode: item.assigneeTargetCode,
|
||||
targetId: item.assigneeTargetId,
|
||||
targetName: item.assigneeTargetName,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildIncludeChildrenByTarget(targets: AssigneeTarget[]) {
|
||||
const result: Record<string, boolean> = {};
|
||||
for (const target of targets) {
|
||||
result[String(target.targetId)] = target.includeChildren;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function addScope() {
|
||||
formModel.value.scopes.push({
|
||||
includeChildren: false,
|
||||
@@ -323,7 +391,7 @@ function removeScope(index: number) {
|
||||
}
|
||||
|
||||
function addStep() {
|
||||
formModel.value.steps.push({ assigneeType: 'ROLE', stepName: '' });
|
||||
formModel.value.steps.push(buildDefaultStep());
|
||||
}
|
||||
|
||||
function removeStep(index: number) {
|
||||
@@ -341,104 +409,163 @@ function getCategoryScopeOptions() {
|
||||
return categoryOptions.value[resourceType];
|
||||
}
|
||||
|
||||
function handleAssigneeTypeChange(step: StepItem) {
|
||||
step.assigneeTargetId = undefined;
|
||||
step.assigneeTargetCode = '';
|
||||
step.assigneeTargetName = '';
|
||||
if (step.assigneeType === 'USER') {
|
||||
async function handleAssigneeTypeChange(
|
||||
step: StepItem,
|
||||
assigneeType: AssigneeType,
|
||||
) {
|
||||
if (step.assigneeType === assigneeType) {
|
||||
return;
|
||||
}
|
||||
if (step.assigneeTargetIds.length > 0) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('approval.message.confirmAssigneeTypeChange'),
|
||||
$t('approval.fields.assigneeTarget'),
|
||||
{
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
step.assigneeType = assigneeType;
|
||||
step.assigneeTargetIds = [];
|
||||
step.assigneeTargets = [];
|
||||
step.includeChildrenByTarget = {};
|
||||
if (assigneeType === 'USER') {
|
||||
void searchAccountOptions('');
|
||||
}
|
||||
}
|
||||
|
||||
function handleAssigneeTargetChange(step: StepItem) {
|
||||
const options =
|
||||
step.assigneeType === 'ROLE' ? roleOptions.value : accountOptions.value;
|
||||
const selected = options.find(
|
||||
(item) => String(item.id) === String(step.assigneeTargetId),
|
||||
const selectedIds = new Set(step.assigneeTargetIds.map(String));
|
||||
step.assigneeTargets = step.assigneeTargets.filter((target) =>
|
||||
selectedIds.has(String(target.targetId)),
|
||||
);
|
||||
step.assigneeTargetName = selected?.label || '';
|
||||
for (const targetId of Object.keys(step.includeChildrenByTarget)) {
|
||||
if (!selectedIds.has(targetId)) {
|
||||
delete step.includeChildrenByTarget[targetId];
|
||||
}
|
||||
}
|
||||
for (const targetId of step.assigneeTargetIds) {
|
||||
const targetKey = String(targetId);
|
||||
if (!(targetKey in step.includeChildrenByTarget)) {
|
||||
step.includeChildrenByTarget[targetKey] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeAssigneeTarget(step: StepItem, targetId: number | string) {
|
||||
const targetKey = String(targetId);
|
||||
step.assigneeTargetIds = step.assigneeTargetIds.filter(
|
||||
(item) => String(item) !== targetKey,
|
||||
);
|
||||
handleAssigneeTargetChange(step);
|
||||
}
|
||||
|
||||
function getDeptTargetName(targetId: number | string) {
|
||||
return deptOptionMap.value.get(String(targetId)) || String(targetId);
|
||||
}
|
||||
|
||||
function registerAccountOption(step?: StepItem) {
|
||||
if (!step?.assigneeTargetId || !step.assigneeTargetName) {
|
||||
if (!step || step.assigneeType !== 'USER') {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
accountOptions.value.some(
|
||||
(item) => String(item.id) === String(step.assigneeTargetId),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
for (const target of step.assigneeTargets) {
|
||||
if (
|
||||
!target.targetName ||
|
||||
accountOptions.value.some(
|
||||
(item) => String(item.id) === String(target.targetId),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
accountOptions.value.push({
|
||||
id: target.targetId,
|
||||
label: target.targetName,
|
||||
});
|
||||
}
|
||||
accountOptions.value.push({
|
||||
id: step.assigneeTargetId,
|
||||
label: step.assigneeTargetName,
|
||||
});
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saveForm.value?.validate(async (valid) => {
|
||||
if (btnLoading.value) {
|
||||
return;
|
||||
}
|
||||
btnLoading.value = true;
|
||||
try {
|
||||
const valid = await saveForm.value?.validate().catch(() => false);
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
btnLoading.value = true;
|
||||
try {
|
||||
const steps = formModel.value.steps
|
||||
.map((step, index) => ({
|
||||
assigneeTargetId: step.assigneeTargetId,
|
||||
assigneeType: step.assigneeType,
|
||||
id: step.id,
|
||||
stepName: step.stepName?.trim(),
|
||||
stepNo: index + 1,
|
||||
}))
|
||||
.filter((step) => step.stepName);
|
||||
if (steps.length === 0) {
|
||||
ElMessage.warning($t('approval.message.needStep'));
|
||||
btnLoading.value = false;
|
||||
return;
|
||||
}
|
||||
if (steps.some((step) => !step.assigneeType || !step.assigneeTargetId)) {
|
||||
ElMessage.warning($t('approval.message.needStepAssignee'));
|
||||
btnLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const scopes = formModel.value.scopes
|
||||
.filter((scope) => scope.scopeValue && scope.scopeType)
|
||||
.map((scope) => ({
|
||||
id: scope.id,
|
||||
includeChildren: scope.includeChildren ? 1 : 0,
|
||||
scopeType: scope.scopeType,
|
||||
scopeValue: scope.scopeValue,
|
||||
}));
|
||||
|
||||
const payload = {
|
||||
actionType: formModel.value.actionType,
|
||||
id: formModel.value.id,
|
||||
name: formModel.value.name.trim(),
|
||||
priority: formModel.value.priority,
|
||||
remark: formModel.value.remark.trim(),
|
||||
resourceType: formModel.value.resourceType,
|
||||
scopes,
|
||||
status: formModel.value.status,
|
||||
steps,
|
||||
};
|
||||
|
||||
const url = isAdd.value
|
||||
? '/api/v1/approvalFlow/save'
|
||||
: '/api/v1/approvalFlow/update';
|
||||
const res = await api.post(url, payload);
|
||||
if (res.errorCode !== 0) {
|
||||
btnLoading.value = false;
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('approval.message.saveSuccess'));
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
const steps = formModel.value.steps
|
||||
.map((step, index) => ({
|
||||
assigneeTargets: step.assigneeTargetIds.map((targetId) => ({
|
||||
includeChildren:
|
||||
step.assigneeType === 'DEPT' &&
|
||||
step.includeChildrenByTarget[String(targetId)]
|
||||
? 1
|
||||
: 0,
|
||||
targetId,
|
||||
})),
|
||||
assigneeType: step.assigneeType,
|
||||
id: step.id,
|
||||
stepName: step.stepName?.trim(),
|
||||
stepNo: index + 1,
|
||||
}))
|
||||
.filter((step) => step.stepName);
|
||||
if (steps.length === 0) {
|
||||
ElMessage.warning($t('approval.message.needStep'));
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (
|
||||
steps.some(
|
||||
(step) => !step.assigneeType || step.assigneeTargets.length === 0,
|
||||
)
|
||||
) {
|
||||
ElMessage.warning($t('approval.message.needStepAssignee'));
|
||||
return;
|
||||
}
|
||||
|
||||
const scopes = formModel.value.scopes
|
||||
.filter((scope) => scope.scopeValue && scope.scopeType)
|
||||
.map((scope) => ({
|
||||
id: scope.id,
|
||||
includeChildren: scope.includeChildren ? 1 : 0,
|
||||
scopeType: scope.scopeType,
|
||||
scopeValue: scope.scopeValue,
|
||||
}));
|
||||
|
||||
const payload = {
|
||||
actionType: formModel.value.actionType,
|
||||
id: formModel.value.id,
|
||||
name: formModel.value.name.trim(),
|
||||
priority: formModel.value.priority,
|
||||
remark: formModel.value.remark.trim(),
|
||||
resourceType: formModel.value.resourceType,
|
||||
scopes,
|
||||
status: formModel.value.status,
|
||||
steps,
|
||||
};
|
||||
|
||||
const url = isAdd.value
|
||||
? '/api/v1/approvalFlow/save'
|
||||
: '/api/v1/approvalFlow/update';
|
||||
const res = await api.post(url, payload);
|
||||
if (res.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('approval.message.saveSuccess'));
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
@@ -600,84 +727,185 @@ function closeDialog() {
|
||||
<div
|
||||
v-for="(step, index) in formModel.steps"
|
||||
:key="`step-${index}`"
|
||||
class="grid grid-cols-1 gap-3 rounded-2xl border border-[hsl(var(--divider-faint)/0.66)] bg-[hsl(var(--surface-panel)/0.72)] p-4 md:grid-cols-[72px,minmax(0,1.1fr),148px,minmax(0,1fr),88px]"
|
||||
class="approval-flow-modal__step-card"
|
||||
>
|
||||
<div
|
||||
class="flex items-center text-sm font-medium text-[hsl(var(--text-muted))]"
|
||||
>
|
||||
{{ $t('approval.fields.stepNo', { value: index + 1 }) }}
|
||||
</div>
|
||||
<ElInput
|
||||
v-model.trim="step.stepName"
|
||||
:placeholder="$t('approval.placeholder.stepName')"
|
||||
/>
|
||||
<div class="approval-flow-modal__assignee-type">
|
||||
<button
|
||||
v-for="item in ASSIGNEE_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="approval-flow-modal__assignee-type-item"
|
||||
:class="{
|
||||
'is-active': step.assigneeType === item.value,
|
||||
}"
|
||||
:aria-pressed="step.assigneeType === item.value"
|
||||
@click="
|
||||
step.assigneeType !== item.value &&
|
||||
((step.assigneeType = item.value),
|
||||
handleAssigneeTypeChange(step))
|
||||
"
|
||||
<div class="approval-flow-modal__step-header">
|
||||
<span class="approval-flow-modal__step-index">
|
||||
{{ $t('approval.fields.stepNo', { value: index + 1 }) }}
|
||||
</span>
|
||||
<ElButton
|
||||
text
|
||||
type="danger"
|
||||
:disabled="formModel.steps.length <= 1"
|
||||
@click="removeStep(index)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="approval-flow-modal__step-grid">
|
||||
<div class="approval-flow-modal__step-field">
|
||||
<span class="approval-flow-modal__step-field-label">
|
||||
{{ $t('approval.fields.stepName') }}
|
||||
</span>
|
||||
<ElInput
|
||||
v-model.trim="step.stepName"
|
||||
:placeholder="$t('approval.placeholder.stepName')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="approval-flow-modal__step-field">
|
||||
<span class="approval-flow-modal__step-field-label">
|
||||
{{ $t('approval.fields.assigneeType') }}
|
||||
</span>
|
||||
<div class="approval-flow-modal__assignee-type">
|
||||
<button
|
||||
v-for="item in ASSIGNEE_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="approval-flow-modal__assignee-type-item"
|
||||
:class="{
|
||||
'is-active': step.assigneeType === item.value,
|
||||
}"
|
||||
:aria-pressed="step.assigneeType === item.value"
|
||||
@click="handleAssigneeTypeChange(step, item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="approval-flow-modal__step-field approval-flow-modal__target-field"
|
||||
>
|
||||
<div class="approval-flow-modal__target-heading">
|
||||
<span class="approval-flow-modal__step-field-label">
|
||||
{{ $t('approval.fields.assigneeTarget') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
step.assigneeType === 'DEPT' &&
|
||||
step.assigneeTargetIds.length > 0
|
||||
"
|
||||
class="approval-flow-modal__target-count"
|
||||
>
|
||||
{{
|
||||
$t('approval.fields.selectedDeptCount', {
|
||||
value: step.assigneeTargetIds.length,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ElSelect
|
||||
v-if="step.assigneeType === 'ROLE'"
|
||||
v-model="step.assigneeTargetIds"
|
||||
filterable
|
||||
clearable
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="ASSIGNEE_VISIBLE_TAG_COUNT"
|
||||
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||
@change="handleAssigneeTargetChange(step)"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in roleOptions"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
|
||||
<ElSelect
|
||||
v-else-if="step.assigneeType === 'USER'"
|
||||
v-model="step.assigneeTargetIds"
|
||||
filterable
|
||||
clearable
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="ASSIGNEE_VISIBLE_TAG_COUNT"
|
||||
remote
|
||||
reserve-keyword
|
||||
:loading="accountLoading"
|
||||
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||
:remote-method="searchAccountOptions"
|
||||
@change="handleAssigneeTargetChange(step)"
|
||||
@visible-change="
|
||||
(visible) => visible && searchAccountOptions('')
|
||||
"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in accountOptions"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
|
||||
<div v-else class="approval-flow-modal__dept-target">
|
||||
<ElTreeSelect
|
||||
v-model="step.assigneeTargetIds"
|
||||
clearable
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="ASSIGNEE_VISIBLE_TAG_COUNT"
|
||||
check-strictly
|
||||
:data="deptTreeOptions"
|
||||
:props="{
|
||||
label: 'deptName',
|
||||
children: 'children',
|
||||
value: 'id',
|
||||
}"
|
||||
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||
@change="handleAssigneeTargetChange(step)"
|
||||
/>
|
||||
<div
|
||||
v-if="step.assigneeTargetIds.length > 0"
|
||||
class="approval-flow-modal__dept-target-list"
|
||||
>
|
||||
<div
|
||||
v-for="targetId in step.assigneeTargetIds"
|
||||
:key="String(targetId)"
|
||||
class="approval-flow-modal__dept-target-option"
|
||||
>
|
||||
<span
|
||||
class="approval-flow-modal__dept-target-name"
|
||||
:title="getDeptTargetName(targetId)"
|
||||
>
|
||||
{{ getDeptTargetName(targetId) }}
|
||||
</span>
|
||||
<div class="approval-flow-modal__dept-target-actions">
|
||||
<label>
|
||||
{{ $t('approval.fields.includeChildren') }}
|
||||
<ElSwitch
|
||||
v-model="
|
||||
step.includeChildrenByTarget[String(targetId)]
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
@click="removeAssigneeTarget(step, targetId)"
|
||||
>
|
||||
{{ $t('approval.action.removeTarget') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElSelect
|
||||
v-if="step.assigneeType === 'ROLE'"
|
||||
v-model="step.assigneeTargetId"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||
@change="handleAssigneeTargetChange(step)"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in roleOptions"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElSelect
|
||||
v-else
|
||||
v-model="step.assigneeTargetId"
|
||||
filterable
|
||||
clearable
|
||||
remote
|
||||
reserve-keyword
|
||||
:loading="accountLoading"
|
||||
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||
:remote-method="searchAccountOptions"
|
||||
@change="handleAssigneeTargetChange(step)"
|
||||
@visible-change="(visible) => visible && searchAccountOptions('')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in accountOptions"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElButton
|
||||
text
|
||||
type="danger"
|
||||
:disabled="formModel.steps.length <= 1"
|
||||
@click="removeStep(index)"
|
||||
>
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElButton text type="primary" class="w-fit" @click="addStep">
|
||||
{{ $t('approval.action.addStep') }}
|
||||
</ElButton>
|
||||
<p class="approval-flow-modal__assignee-helper">
|
||||
{{ $t('approval.helper.assigneeAny') }}
|
||||
</p>
|
||||
</div>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
@@ -692,16 +920,16 @@ function closeDialog() {
|
||||
|
||||
.approval-flow-modal__section-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.approval-flow-modal__section-meta p {
|
||||
margin: 0;
|
||||
color: hsl(var(--text-muted));
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-list {
|
||||
@@ -712,8 +940,8 @@ function closeDialog() {
|
||||
|
||||
.approval-flow-modal__scope-empty {
|
||||
padding: 4px 0 2px;
|
||||
color: hsl(var(--text-muted));
|
||||
font-size: 13px;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-row {
|
||||
@@ -724,7 +952,7 @@ function closeDialog() {
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-row + .approval-flow-modal__scope-row {
|
||||
border-top: 1px solid hsl(var(--divider-faint) / 0.72);
|
||||
border-top: 1px solid hsl(var(--divider-faint) / 72%);
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-control {
|
||||
@@ -733,40 +961,90 @@ function closeDialog() {
|
||||
|
||||
.approval-flow-modal__scope-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: hsl(var(--text-muted));
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.approval-flow-modal__step-card {
|
||||
padding: var(--space-4);
|
||||
background: hsl(var(--surface-panel) / 78%);
|
||||
border: 1px solid hsl(var(--divider-faint) / 72%);
|
||||
border-radius: var(--radius-panel);
|
||||
}
|
||||
|
||||
.approval-flow-modal__step-header {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 32px;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.approval-flow-modal__step-index {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 var(--space-3);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 9%);
|
||||
border-radius: var(--radius-pill);
|
||||
}
|
||||
|
||||
.approval-flow-modal__step-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--space-4);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.approval-flow-modal__step-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.approval-flow-modal__step-field-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 4px;
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.72);
|
||||
border-radius: 14px;
|
||||
background: hsl(var(--surface-contrast-soft) / 0.82);
|
||||
background: hsl(var(--surface-contrast-soft) / 82%);
|
||||
border: 1px solid hsl(var(--divider-faint) / 72%);
|
||||
border-radius: var(--radius-toolbar);
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type-item {
|
||||
min-width: 64px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: hsl(var(--text-muted));
|
||||
padding: 0 var(--space-3);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
color: hsl(var(--text-muted));
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var(--radius-control);
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type-item:hover {
|
||||
@@ -774,14 +1052,86 @@ function closeDialog() {
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type-item:focus-visible {
|
||||
outline: 2px solid hsl(var(--primary) / 0.28);
|
||||
outline: 2px solid hsl(var(--primary) / 28%);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type-item.is-active {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.12);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 0.14);
|
||||
background: hsl(var(--primary) / 12%);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 14%);
|
||||
}
|
||||
|
||||
.approval-flow-modal__target-heading {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.approval-flow-modal__target-count {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.approval-flow-modal__dept-target {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.approval-flow-modal__dept-target-list {
|
||||
max-height: 176px;
|
||||
padding: 0 var(--space-2);
|
||||
overflow-y: auto;
|
||||
background: hsl(var(--surface-subtle));
|
||||
border: 1px solid hsl(var(--divider-faint) / 72%);
|
||||
border-radius: var(--radius-toolbar);
|
||||
}
|
||||
|
||||
.approval-flow-modal__dept-target-option {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
padding: var(--space-2) 0;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.approval-flow-modal__dept-target-option
|
||||
+ .approval-flow-modal__dept-target-option {
|
||||
border-top: 1px solid hsl(var(--divider-faint) / 72%);
|
||||
}
|
||||
|
||||
.approval-flow-modal__dept-target-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: hsl(var(--foreground));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.approval-flow-modal__dept-target-actions {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.approval-flow-modal__dept-target-actions label {
|
||||
display: inline-flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-helper {
|
||||
margin: -4px 0 0;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -789,5 +1139,35 @@ function closeDialog() {
|
||||
grid-template-columns: 148px minmax(0, 1fr) 128px 72px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.approval-flow-modal__step-grid {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(216px, 0.8fr);
|
||||
}
|
||||
|
||||
.approval-flow-modal__target-field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.approval-flow-modal__step-grid {
|
||||
grid-template-columns:
|
||||
minmax(180px, 0.85fr) minmax(216px, 0.72fr)
|
||||
minmax(280px, 1.35fr);
|
||||
}
|
||||
|
||||
.approval-flow-modal__target-field {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.approval-flow-modal__dept-target-option {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.approval-flow-modal__dept-target-actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Delete, MoreFilled, Plus } from '@element-plus/icons-vue';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
@@ -20,89 +17,243 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
import SysDeptModal from './SysDeptModal.vue';
|
||||
|
||||
onMounted(() => {
|
||||
getTree();
|
||||
void getTree();
|
||||
initDict();
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const tableRef = ref<InstanceType<typeof ElTable>>();
|
||||
const treeData = ref([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const batchActionType = ref<'' | 'delete' | 'disable' | 'enable'>('');
|
||||
const rowDeleteLoadingId = ref('');
|
||||
const rowStatusLoadingId = ref('');
|
||||
const saveDialog = ref();
|
||||
const formInline = ref({
|
||||
deptName: '',
|
||||
status: '',
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
const selectedCount = computed(() => selectedRows.value.length);
|
||||
const batchActionLoading = computed(() => batchActionType.value !== '');
|
||||
const rowActionLoading = computed(
|
||||
() => rowDeleteLoadingId.value !== '' || rowStatusLoadingId.value !== '',
|
||||
);
|
||||
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
function search(formEl: FormInstance | undefined) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
getTree();
|
||||
void getTree();
|
||||
}
|
||||
});
|
||||
}
|
||||
function reset(formEl: FormInstance | undefined) {
|
||||
formEl?.resetFields();
|
||||
getTree();
|
||||
void getTree();
|
||||
}
|
||||
function showDialog(row: any) {
|
||||
saveDialog.value.openDialog({ ...row });
|
||||
}
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/sysDept/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset(formRef.value);
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
|
||||
function isRootDept(row: any) {
|
||||
return row?.deptCode === 'root_dept';
|
||||
}
|
||||
function getTree() {
|
||||
|
||||
function isSelectable(row: any) {
|
||||
return !isRootDept(row);
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: any[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
return selectedRows.value
|
||||
.map((row) => row?.id)
|
||||
.filter((id) => id !== null && id !== undefined);
|
||||
}
|
||||
|
||||
function getErrorMessage(error: any) {
|
||||
return error?.message || $t('sysDept.operationFailed');
|
||||
}
|
||||
|
||||
async function confirmAction(message: string) {
|
||||
try {
|
||||
await ElMessageBox.confirm(message, $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function changeStatus(row: any) {
|
||||
const status = row.status === 1 ? 0 : 1;
|
||||
const action = status === 1 ? $t('sysDept.enable') : $t('sysDept.disable');
|
||||
const confirmed = await confirmAction(
|
||||
$t('sysDept.statusConfirm', {
|
||||
action,
|
||||
name: row.deptName,
|
||||
}),
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
rowStatusLoadingId.value = String(row.id);
|
||||
try {
|
||||
const res = await api.post('/api/v1/sysDept/changeStatusBatch', {
|
||||
ids: [row.id],
|
||||
status,
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
ElMessage.error(res.message || $t('sysDept.operationFailed'));
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('sysDept.operationSuccess'));
|
||||
await getTree();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(getErrorMessage(error));
|
||||
} finally {
|
||||
rowStatusLoadingId.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function changeSelectedStatus(status: 0 | 1) {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) {
|
||||
ElMessage.warning($t('sysDept.selectRequired'));
|
||||
return;
|
||||
}
|
||||
const action = status === 1 ? $t('sysDept.enable') : $t('sysDept.disable');
|
||||
const confirmed = await confirmAction(
|
||||
$t('sysDept.batchStatusConfirm', {
|
||||
action,
|
||||
count: ids.length,
|
||||
}),
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
batchActionType.value = status === 1 ? 'enable' : 'disable';
|
||||
try {
|
||||
const res = await api.post('/api/v1/sysDept/changeStatusBatch', {
|
||||
ids,
|
||||
status,
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
ElMessage.error(res.message || $t('sysDept.operationFailed'));
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('sysDept.operationSuccess'));
|
||||
await getTree();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(getErrorMessage(error));
|
||||
} finally {
|
||||
batchActionType.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDelete(
|
||||
url: string,
|
||||
payload: Record<string, any>,
|
||||
confirmMessage: string,
|
||||
isBatch: boolean,
|
||||
) {
|
||||
const confirmed = await confirmAction(confirmMessage);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBatch) {
|
||||
batchActionType.value = 'delete';
|
||||
} else {
|
||||
rowDeleteLoadingId.value = String(payload.id);
|
||||
}
|
||||
try {
|
||||
const res = await api.post(url, payload);
|
||||
if (res.errorCode !== 0) {
|
||||
ElMessage.error(res.message || $t('sysDept.operationFailed'));
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('sysDept.operationSuccess'));
|
||||
await getTree();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(getErrorMessage(error));
|
||||
} finally {
|
||||
if (isBatch) {
|
||||
batchActionType.value = '';
|
||||
} else {
|
||||
rowDeleteLoadingId.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function remove(row: any) {
|
||||
void submitDelete(
|
||||
'/api/v1/sysDept/remove',
|
||||
{ id: row.id },
|
||||
$t('sysDept.deleteConfirm', { name: row.deptName }),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
function removeSelected() {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) {
|
||||
ElMessage.warning($t('sysDept.selectRequired'));
|
||||
return;
|
||||
}
|
||||
void submitDelete(
|
||||
'/api/v1/sysDept/removeBatch',
|
||||
{ ids },
|
||||
$t('sysDept.batchDeleteConfirm', { count: ids.length }),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async function getTree() {
|
||||
loading.value = true;
|
||||
api
|
||||
.get('/api/v1/sysDept/list', {
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysDept/list', {
|
||||
params: {
|
||||
asTree: true,
|
||||
...formInline.value,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
treeData.value = res.data;
|
||||
});
|
||||
treeData.value = res.data || [];
|
||||
clearSelection();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-1.5 p-6">
|
||||
<SysDeptModal ref="saveDialog" @reload="reset" />
|
||||
<div class="flex items-center justify-between">
|
||||
<ElForm ref="formRef" :inline="true" :model="formInline">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<ElForm ref="formRef" :inline="true" :model="formInline" class="min-w-0">
|
||||
<ElFormItem prop="deptName" class="!mr-3">
|
||||
<ElInput
|
||||
class="search-input"
|
||||
@@ -110,6 +261,14 @@ function getTree() {
|
||||
:placeholder="$t('sysDept.deptName')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="status" class="!mr-3">
|
||||
<DictSelect
|
||||
class="status-select"
|
||||
v-model="formInline.status"
|
||||
dict-code="dataStatus"
|
||||
:placeholder="$t('sysDept.status')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton @click="search(formRef)" type="primary">
|
||||
{{ $t('button.query') }}
|
||||
@@ -119,9 +278,42 @@ function getTree() {
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="handle-div">
|
||||
<div class="department-actions">
|
||||
<div v-if="selectedCount > 0" class="department-batch-actions">
|
||||
<ElButton
|
||||
v-access:code="'/api/v1/sysDept/save'"
|
||||
class="department-batch-action is-enable"
|
||||
:disabled="batchActionLoading || rowActionLoading"
|
||||
:loading="batchActionType === 'enable'"
|
||||
text
|
||||
@click="changeSelectedStatus(1)"
|
||||
>
|
||||
{{ $t('sysDept.batchEnable') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-access:code="'/api/v1/sysDept/save'"
|
||||
class="department-batch-action is-disable"
|
||||
:disabled="batchActionLoading || rowActionLoading"
|
||||
:loading="batchActionType === 'disable'"
|
||||
text
|
||||
@click="changeSelectedStatus(0)"
|
||||
>
|
||||
{{ $t('sysDept.batchDisable') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-access:code="'/api/v1/sysDept/remove'"
|
||||
class="department-batch-action is-danger"
|
||||
:disabled="batchActionLoading || rowActionLoading"
|
||||
:loading="batchActionType === 'delete'"
|
||||
text
|
||||
@click="removeSelected"
|
||||
>
|
||||
{{ $t('sysDept.batchDelete') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElButton
|
||||
v-access:code="'/api/v1/sysDept/save'"
|
||||
class="department-add-action"
|
||||
@click="showDialog({})"
|
||||
type="primary"
|
||||
>
|
||||
@@ -134,7 +326,16 @@ function getTree() {
|
||||
</div>
|
||||
|
||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
||||
<ElTable :data="treeData" row-key="id" v-loading="loading" border>
|
||||
<ElTable
|
||||
ref="tableRef"
|
||||
:data="treeData"
|
||||
:tree-props="{ children: 'children', checkStrictly: true }"
|
||||
row-key="id"
|
||||
v-loading="loading"
|
||||
border
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<ElTableColumn type="selection" width="48" :selectable="isSelectable" />
|
||||
<ElTableColumn prop="deptName" :label="$t('sysDept.deptName')">
|
||||
<template #default="{ row }">
|
||||
{{ row.deptName }}
|
||||
@@ -150,6 +351,11 @@ function getTree() {
|
||||
{{ row.sortNo }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" :label="$t('sysDept.status')" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ dictStore.getDictLabel('dataStatus', row.status) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created" :label="$t('sysDept.created')">
|
||||
<template #default="{ row }">
|
||||
{{ row.created }}
|
||||
@@ -160,26 +366,61 @@ function getTree() {
|
||||
{{ row.remark }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('common.handle')" width="90" align="right">
|
||||
<ElTableColumn
|
||||
:label="$t('common.handle')"
|
||||
width="224"
|
||||
align="right"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton link type="primary" @click="showDialog(row)">
|
||||
<div class="department-row-actions">
|
||||
<ElButton
|
||||
v-access:code="'/api/v1/sysDept/save'"
|
||||
:disabled="
|
||||
batchActionLoading ||
|
||||
rowDeleteLoadingId !== '' ||
|
||||
rowStatusLoadingId !== '' ||
|
||||
(isRootDept(row) && row.status === 1)
|
||||
"
|
||||
:loading="rowStatusLoadingId === String(row.id)"
|
||||
link
|
||||
type="primary"
|
||||
@click="changeStatus(row)"
|
||||
>
|
||||
{{
|
||||
row.status === 1
|
||||
? $t('sysDept.disable')
|
||||
: $t('sysDept.enable')
|
||||
}}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-access:code="'/api/v1/sysDept/save'"
|
||||
:disabled="
|
||||
batchActionLoading ||
|
||||
rowDeleteLoadingId !== '' ||
|
||||
rowStatusLoadingId !== ''
|
||||
"
|
||||
link
|
||||
type="primary"
|
||||
@click="showDialog(row)"
|
||||
>
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="remove(row)">
|
||||
<ElButton link :icon="Delete" type="danger">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
<ElButton
|
||||
v-access:code="'/api/v1/sysDept/remove'"
|
||||
:disabled="
|
||||
batchActionLoading ||
|
||||
rowStatusLoadingId !== '' ||
|
||||
rowDeleteLoadingId !== '' ||
|
||||
isRootDept(row)
|
||||
"
|
||||
:loading="rowDeleteLoadingId === String(row.id)"
|
||||
link
|
||||
type="danger"
|
||||
@click="remove(row)"
|
||||
>
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
@@ -193,4 +434,121 @@ function getTree() {
|
||||
width: 300px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.department-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.department-batch-actions {
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 0 3px;
|
||||
background: hsl(var(--toolbar-bg));
|
||||
border: 1px solid hsl(var(--toolbar-border));
|
||||
border-radius: var(--radius-toolbar);
|
||||
}
|
||||
|
||||
.department-actions :deep(.department-add-action) {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.department-batch-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.department-batch-actions :deep(.department-batch-action) {
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
font-weight: 500;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-control);
|
||||
transition:
|
||||
color var(--motion-duration-base) var(--motion-ease-standard),
|
||||
background-color var(--motion-duration-base) var(--motion-ease-standard),
|
||||
box-shadow var(--motion-duration-base) var(--motion-ease-standard);
|
||||
}
|
||||
|
||||
.department-batch-actions :deep(.department-batch-action.is-enable) {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.department-batch-actions :deep(.department-batch-action.is-disable) {
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.department-batch-actions :deep(.department-batch-action.is-danger) {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.department-batch-actions
|
||||
:deep(.department-batch-action.is-enable:not(.is-disabled):hover),
|
||||
.department-batch-actions
|
||||
:deep(.department-batch-action.is-enable:not(.is-disabled):focus-visible) {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 8%);
|
||||
}
|
||||
|
||||
.department-batch-actions
|
||||
:deep(.department-batch-action.is-disable:not(.is-disabled):hover),
|
||||
.department-batch-actions
|
||||
:deep(.department-batch-action.is-disable:not(.is-disabled):focus-visible) {
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--surface-contrast-soft));
|
||||
}
|
||||
|
||||
.department-batch-actions
|
||||
:deep(.department-batch-action.is-danger:not(.is-disabled):hover),
|
||||
.department-batch-actions
|
||||
:deep(.department-batch-action.is-danger:not(.is-disabled):focus-visible) {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 8%);
|
||||
}
|
||||
|
||||
.department-batch-actions
|
||||
:deep(.department-batch-action:not(.is-disabled):focus-visible) {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 20%);
|
||||
}
|
||||
|
||||
.department-row-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.department-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.department-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.department-batch-actions {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.department-batch-actions :deep(.department-batch-action) {
|
||||
flex: 1;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
@@ -12,24 +12,15 @@ import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
// vue
|
||||
onMounted(() => {});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const ENABLED_STATUS = 1;
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>({
|
||||
parentId: '',
|
||||
ancestors: '',
|
||||
deptName: '',
|
||||
deptCode: '',
|
||||
sortNo: '',
|
||||
status: '',
|
||||
remark: '',
|
||||
});
|
||||
const entity = ref<any>(buildDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
parentId: [
|
||||
@@ -43,40 +34,52 @@ const rules = ref({
|
||||
],
|
||||
});
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
function buildDefaultEntity() {
|
||||
return {
|
||||
ancestors: '',
|
||||
deptCode: '',
|
||||
deptName: '',
|
||||
parentId: '',
|
||||
remark: '',
|
||||
sortNo: '',
|
||||
status: ENABLED_STATUS,
|
||||
};
|
||||
}
|
||||
function openDialog(row: any = {}) {
|
||||
isAdd.value = !row.id;
|
||||
entity.value = {
|
||||
...buildDefaultEntity(),
|
||||
...row,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post(
|
||||
isAdd.value ? 'api/v1/sysDept/save' : 'api/v1/sysDept/update',
|
||||
entity.value,
|
||||
)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
async function save() {
|
||||
if (btnLoading.value) {
|
||||
return;
|
||||
}
|
||||
btnLoading.value = true;
|
||||
try {
|
||||
const valid = await saveForm.value?.validate().catch(() => false);
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
const res = await api.post(
|
||||
isAdd.value ? 'api/v1/sysDept/save' : 'api/v1/sysDept/update',
|
||||
entity.value,
|
||||
);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
}
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = {};
|
||||
entity.value = buildDefaultEntity();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -120,6 +123,13 @@ function closeDialog() {
|
||||
<ElFormItem prop="sortNo" :label="$t('sysDept.sortNo')">
|
||||
<ElInput v-model.trim="entity.sortNo" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="status" :label="$t('sysDept.status')">
|
||||
<DictSelect
|
||||
v-model="entity.status"
|
||||
:clearable="false"
|
||||
dict-code="dataStatus"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="remark" :label="$t('sysDept.remark')">
|
||||
<ElInput v-model.trim="entity.remark" />
|
||||
</ElFormItem>
|
||||
|
||||
Reference in New Issue
Block a user