first commit
This commit is contained in:
536
src/components/workbench/common/ApprovalAction.vue
Normal file
536
src/components/workbench/common/ApprovalAction.vue
Normal file
@@ -0,0 +1,536 @@
|
||||
<template>
|
||||
<div class="approval-action">
|
||||
<!-- 审批意见区域(待审核状态显示) -->
|
||||
<div v-if="status === 'pending' && isApprovalUser" class="approval-opinion-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title-bar"></div>
|
||||
<div class="section-title">{{ t('workbench.approval.opinion.title') }}</div>
|
||||
</div>
|
||||
<div class="opinion-actions">
|
||||
<el-button type="danger" :loading="rejecting" @click="handleReject">
|
||||
{{ t('workbench.approval.opinion.reject') }}
|
||||
</el-button>
|
||||
<el-button type="success" :loading="approving" @click="handleApprove">
|
||||
{{ t('workbench.approval.opinion.approve') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="opinion-input">
|
||||
<el-input v-model="opinionText" type="textarea" :rows="4" :placeholder="t('workbench.approval.opinion.inputPlaceholder')" resize="vertical" />
|
||||
</div>
|
||||
<!-- <div class="opinion-attachment">
|
||||
<el-icon class="attachment-icon">
|
||||
<Paperclip />
|
||||
</el-icon>
|
||||
<span class="attachment-text" @click="handleAddAttachment">
|
||||
{{ t('workbench.approval.opinion.addAttachment') }}
|
||||
</span>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 撤回区域(可撤回状态显示) -->
|
||||
<div v-if="status === 'withdrawable'" class="withdraw-section">
|
||||
<div class="withdraw-input">
|
||||
<el-input v-model="opinionText" type="textarea" :rows="4" :placeholder="t('workbench.approval.opinion.inputPlaceholder')" resize="vertical" />
|
||||
</div>
|
||||
<div class="withdraw-action">
|
||||
<el-button type="primary" @click="handleWithdraw">
|
||||
{{ t('workbench.approval.opinion.withdraw') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批记录区域(所有状态都显示) -->
|
||||
<div class="approval-record-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title-bar"></div>
|
||||
<div class="section-title">{{ t('workbench.approval.record.title') }}</div>
|
||||
</div>
|
||||
<div class="record-list">
|
||||
<div v-for="(record, index) in approvalRecords" :key="index" class="record-item">
|
||||
<div class="record-node-header">
|
||||
<span class="node-name">{{ record.name }}</span>
|
||||
</div>
|
||||
<div class="record-content" v-for="item in record.userVoList" :key="item.id">
|
||||
<div class="record-main">
|
||||
<div class="record-person">
|
||||
<!-- <span class="person-office">{{ record.office }}</span> -->
|
||||
<span class="person-name">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="record-time">{{ item.showTime || '待审核' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- <div v-if="record.action" class="record-action">{{ record.action }}</div> -->
|
||||
<template v-if="record.id !== 'root' && item.status === 2">
|
||||
<div class="record-status" :class="getStatusClass(item.approveDesc?.startsWith('拒绝原因:') ? 'rejected' : 'approved')">
|
||||
{{ getStatusLabel(item.approveDesc?.startsWith('拒绝原因:') ? 'rejected' : 'approved') }}
|
||||
</div>
|
||||
<div class="record-opinion">
|
||||
{{ item.approveDesc }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="record-opinion" v-else>
|
||||
发起审批
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="approvalRecords?.length === 0" class="record-empty">
|
||||
{{ t('workbench.approval.record.empty') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="selectUser">
|
||||
<EmployeesDialog :visible="selectUserDialog" :data="[]" type="user" @change="changeUser" @update:visible="(e:boolean) => selectUserDialog = e"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMessage, useMessageBox } from '/@/hooks/message';
|
||||
import { completeTask, formatStartNodeShow } from '/@/api/flow/task';
|
||||
import { useUserInfo } from '/@/stores/userInfo';
|
||||
import { getTaskDetail } from '/@/api/flow/flow/index'
|
||||
import EmployeesDialog from '/@/components/OrgSelector/employeesDialog.vue'
|
||||
const { t } = useI18n();
|
||||
const message = useMessage()
|
||||
const userInfo = useUserInfo()
|
||||
const selectUser = ref<boolean>(false);
|
||||
const selectUserDialog = ref<boolean>(false);
|
||||
const nextNodeId = ref<string>('');
|
||||
const isApprovalUser = computed(() => {
|
||||
const currentUserId = userInfo.userInfos.user?.userId
|
||||
const currentNode = hierarchicalLookup(nodeList.value,taskDetails.value?.nodeId)
|
||||
const currentNodeUser = currentNode ?.userVoList?.find((item:any) => item.id === currentUserId)
|
||||
selectUser.value = nextNodeIsSelectUser(nodeList.value,currentNode?.id)
|
||||
return currentNodeUser?.status === 1
|
||||
})
|
||||
function hierarchicalLookup(nodeList:any[],nodeId:string):any {
|
||||
for (const nodeItem of nodeList) {
|
||||
if (nodeItem.id === nodeId){
|
||||
return nodeItem
|
||||
}
|
||||
|
||||
if (nodeItem.children && nodeItem.children.length > 0){
|
||||
const children = hierarchicalLookup(nodeItem.children,nodeId)
|
||||
if (children){
|
||||
return children
|
||||
}
|
||||
}
|
||||
if (nodeItem.branch && nodeItem.branch.length > 0){
|
||||
const branch = hierarchicalLookup(nodeItem.branch,nodeId)
|
||||
if (branch){
|
||||
return branch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function nextNodeIsSelectUser(nodeList:any[],currentNodeId:any):boolean{
|
||||
if (!nodeList || nodeList.length === 0){
|
||||
return false
|
||||
}
|
||||
const index = nodeList.findIndex(item => item.id === currentNodeId)
|
||||
if (index === -1){
|
||||
return false
|
||||
}
|
||||
const node = nodeList[index + 1]
|
||||
nextNodeId.value = node?.id || ''
|
||||
return node?.selectUser || false
|
||||
}
|
||||
interface UserVoList {
|
||||
id: string;
|
||||
name: string;
|
||||
showTime?: string;
|
||||
avatar: string;
|
||||
approveDesc?: string;
|
||||
operType?: string;
|
||||
status: number;
|
||||
}
|
||||
interface ApprovalRecord {
|
||||
id: string;
|
||||
userVoList: UserVoList[];
|
||||
placeholder: string;
|
||||
status: number;
|
||||
name: string;
|
||||
type: number;
|
||||
selectUser: boolean;
|
||||
multiple?: null;
|
||||
children?: ApprovalRecord[];
|
||||
branch: { children: ApprovalRecord[] }[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
status: 'pending' | 'reviewed' | 'withdrawable'; // 审批状态
|
||||
processInstanceId: string;
|
||||
}>();
|
||||
const nodeList = ref<ApprovalRecord[]>([]);
|
||||
const approvalRecords = computed(() => {
|
||||
return getAllProcessedNode(nodeList.value)
|
||||
})
|
||||
// 平铺所有已处理的节点,包括分支节点内的子节点,以展示所有审批记录
|
||||
const getAllProcessedNode = (list: ApprovalRecord[]) => {
|
||||
const records: ApprovalRecord[] = []
|
||||
for (const record of list) {
|
||||
if (record.type === 0 || record.userVoList?.some(record => record.status === 2)) {
|
||||
records.push(record)
|
||||
} else if (record.branch?.length) {
|
||||
for (const branch of record.branch) {
|
||||
if (branch.children?.length) {
|
||||
const res = getAllProcessedNode(branch.children)
|
||||
if (res.length) {
|
||||
records.push(...res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'approve', opinion: string, attachments: any[]): void;
|
||||
(e: 'reject', opinion: string, attachments: any[]): void;
|
||||
(e: 'withdraw', opinion: string): void;
|
||||
(e: 'add-attachment'): void;
|
||||
}>();
|
||||
const obj = ref<any>({})
|
||||
const changeUser = (checkedList: any[]) =>{
|
||||
obj.value[`${nextNodeId.value}_assignee_select`] = checkedList;
|
||||
selectUser.value = false;
|
||||
selectUserDialog.value = false;
|
||||
handleApprove()
|
||||
}
|
||||
const opinionText = ref('');
|
||||
const attachments = ref<any[]>([]);
|
||||
|
||||
const approving = ref(false)
|
||||
const handleApprove = async () => {
|
||||
if (!opinionText.value.trim()) {
|
||||
message.warning(t('workbench.approval.opinion.inputRequired'));
|
||||
return;
|
||||
}
|
||||
approving.value = true
|
||||
try {
|
||||
if (selectUser.value && nextNodeId.value){
|
||||
selectUserDialog.value = true;
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
paramMap: {
|
||||
[`${taskDetails.value?.nodeId}_approve_condition`]: true,
|
||||
...obj.value,
|
||||
},
|
||||
taskId: taskDetails.value?.taskId,
|
||||
taskLocalParamMap: {
|
||||
approveDesc: `意见:${opinionText.value.trim()}`
|
||||
}
|
||||
}
|
||||
await completeTask(params)
|
||||
// emit('approve', opinionText.value, attachments.value);
|
||||
// 清空输入
|
||||
opinionText.value = '';
|
||||
attachments.value = [];
|
||||
message.success(t('common.success'))
|
||||
await getDataList(props.processInstanceId)
|
||||
await fetchTaskDetail(props.processInstanceId);
|
||||
} finally {
|
||||
approving.value = false
|
||||
}
|
||||
};
|
||||
|
||||
const rejecting = ref(false)
|
||||
const handleReject = () => {
|
||||
if (!opinionText.value.trim()) {
|
||||
message.warning(t('workbench.approval.opinion.inputRequired'));
|
||||
return;
|
||||
}
|
||||
useMessageBox().confirm('确定要驳回到上一个节点吗?').then(async () => {
|
||||
rejecting.value = true
|
||||
try {
|
||||
// 确认
|
||||
const params = {
|
||||
paramMap: {
|
||||
[`${taskDetails.value?.nodeId}_approve_condition`]: false
|
||||
},
|
||||
taskId: taskDetails.value?.taskId,
|
||||
taskLocalParamMap: {
|
||||
approveDesc: `拒绝原因:${opinionText.value.trim()}`
|
||||
}
|
||||
}
|
||||
await completeTask(params)
|
||||
// emit('reject', opinionText.value, attachments.value);
|
||||
// 清空输入
|
||||
opinionText.value = '';
|
||||
attachments.value = [];
|
||||
message.success(t('common.success'))
|
||||
// router.back()
|
||||
getDataList(props.processInstanceId)
|
||||
fetchTaskDetail(props.processInstanceId);
|
||||
} finally {
|
||||
rejecting.value = false
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const handleWithdraw = () => {
|
||||
emit('withdraw', opinionText.value);
|
||||
// 清空输入
|
||||
opinionText.value = '';
|
||||
};
|
||||
|
||||
const handleAddAttachment = () => {
|
||||
emit('add-attachment');
|
||||
};
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
if (status === 'approved') return 'status-approved';
|
||||
if (status === 'rejected') return 'status-rejected';
|
||||
return 'status-pending';
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
if (status === 'approved') return t('workbench.approval.record.status.approved');
|
||||
if (status === 'rejected') return t('workbench.approval.record.status.rejected');
|
||||
return t('workbench.approval.record.status.pending');
|
||||
};
|
||||
/**
|
||||
* 请求审批记录数据
|
||||
* */
|
||||
const getDataList = async (id: string) => {
|
||||
const res = await formatStartNodeShow({ processInstanceId: id })
|
||||
const data = res.data || []
|
||||
// 过滤掉未审批的记录
|
||||
nodeList.value = data
|
||||
};
|
||||
//请求当前流程节点信息
|
||||
const taskDetails = ref<any>(null)
|
||||
const fetchTaskDetail = async (processInstanceId: string) => {
|
||||
const res = await getTaskDetail(processInstanceId)
|
||||
taskDetails.value = res.data || {}
|
||||
}
|
||||
// 监听状态变化,清空输入
|
||||
watch(
|
||||
() => props.status,
|
||||
() => {
|
||||
opinionText.value = '';
|
||||
attachments.value = [];
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.processInstanceId,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
getDataList(newVal);
|
||||
fetchTaskDetail(newVal);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.processInstanceId) {
|
||||
console.log(props.processInstanceId, 123)
|
||||
getDataList(props.processInstanceId)
|
||||
fetchTaskDetail(props.processInstanceId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.approval-action {
|
||||
/* margin-top: 20px; */
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title-bar {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background: #409eff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
/* 审批意见区域 */
|
||||
.approval-opinion-section {
|
||||
/* margin-bottom: 30px; */
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
/* border-radius: 4px; */
|
||||
}
|
||||
|
||||
.opinion-actions {
|
||||
display: flex;
|
||||
/* gap: 8px; */
|
||||
margin-bottom: 16px;
|
||||
.el-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.opinion-input {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.opinion-attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.opinion-attachment:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.attachment-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.attachment-text {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 撤回区域 */
|
||||
.withdraw-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.withdraw-input {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.withdraw-action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 审批记录区域 */
|
||||
.approval-record-section {
|
||||
padding: 0 20px 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
/* padding: 16px; */
|
||||
border: 1px solid #e4e7ed;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.record-item-active {
|
||||
background: #e6f4ff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.record-node-header {
|
||||
background: #e6f4ff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.record-item-active .record-node-header {
|
||||
background: #b3d8ff;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.record-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 0 12px 12px;
|
||||
}
|
||||
|
||||
.record-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.record-person {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.person-office {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.person-name {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.record-action {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.record-status {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.record-opinion {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.record-empty {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user