first commit

This commit is contained in:
2025-12-26 23:19:09 +08:00
commit b29d128e41
788 changed files with 100922 additions and 0 deletions

View 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>