Files
IRS-ui-develop/src/components/workbench/common/ApprovalAction.vue
2025-12-27 12:52:38 +08:00

551 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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">
<span>{{ item.approveDesc }}</span>
</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-item .record-content .record-status {
max-width: 100%;
text-wrap: wrap;
word-wrap: break-word; /* 允许长单词换行 */
word-break: break-all; /* 打断所有单词 */
white-space: pre-wrap; /* 保留空白符和换行符 */
overflow-wrap: break-word; /* 确保超长内容能换行 */
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-item .record-content .record-opinion {
max-width: 100%;
text-wrap: wrap;
word-wrap: break-word; /* 允许长单词换行 */
word-break: break-all; /* 打断所有单词 */
white-space: pre-wrap; /* 保留空白符和换行符 */
overflow-wrap: break-word; /* 确保超长内容能换行 */
overflow-x: hidden;
height: auto;
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>