551 lines
14 KiB
Vue
551 lines
14 KiB
Vue
<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>
|