<template>
|
<view class="account-detail">
|
<PageHeader title="审批流程" @back="goBack" />
|
|
<!-- 表单区域 -->
|
<view class="form-section">
|
<van-form ref="formRef" @submit="submitForm" :rules="rules" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center">
|
<van-cell-group style="margin-bottom: 16px;">
|
<van-field
|
v-model="form.approveReason"
|
name="approveReason"
|
rows="2"
|
autosize
|
label="申请事由"
|
type="textarea"
|
maxlength="200"
|
:rules="[{ required: true, message: '申请事由不能为空' }]"
|
placeholder="请输入申请事由"
|
show-word-limit
|
required
|
/>
|
</van-cell-group>
|
<van-cell-group>
|
<van-field
|
v-model="form.approveDeptName"
|
readonly
|
name="picker"
|
label="申请部门"
|
placeholder="请选择申请部门"
|
:rules="[{ required: true, message: '请选择申请部门' }]"
|
@click="showPicker = true"
|
required
|
/>
|
<van-field
|
v-model="form.approveUserName"
|
name="taxPrice"
|
label="申请人"
|
placeholder="请输入申请人"
|
:rules="[{ required: true, message: '申请人不能为空' }]"
|
required
|
readonly
|
/>
|
<van-popup
|
v-model:show="showPicker"
|
position="bottom"
|
>
|
<van-picker
|
:columns="productOptions"
|
:model-value="pickerValue"
|
@confirm="onConfirm"
|
@cancel="showPicker = false"
|
/>
|
</van-popup>
|
<van-field
|
v-model="form.approveTime"
|
label="申请日期"
|
placeholder="请选择"
|
readonly
|
required
|
@click="showDatePicker"
|
:rules="[{ required: true, message: '请选择来款日期' }]"
|
/>
|
<!-- 日期选择器 -->
|
<van-popup v-model:show="showDate" position="bottom">
|
<van-date-picker
|
v-model="currentDate"
|
title="选择日期"
|
@confirm="onDateConfirm"
|
@cancel="showDate = false"
|
/>
|
</van-popup>
|
</van-cell-group>
|
</van-form>
|
</view>
|
<!-- 审核流程区域 -->
|
<view class="approval-process">
|
<view class="approval-header">
|
<text class="approval-title">审核流程</text>
|
<text class="approval-desc">每个步骤只能选择一个审批人</text>
|
</view>
|
|
<view class="approval-steps">
|
<view v-for="(step, stepIndex) in approverNodes" :key="stepIndex" class="approval-step">
|
<view class="step-dot"></view>
|
<view class="step-title">
|
<text>审批人</text>
|
</view>
|
<view class="approver-container">
|
<view v-if="step.nickName" class="approver-item">
|
<view class="approver-avatar">
|
<text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
|
<view class="status-dot"></view>
|
</view>
|
<view class="approver-info">
|
<text class="approver-name">{{ step.nickName }}</text>
|
</view>
|
<view class="delete-approver-btn" @click="removeApprover(stepIndex)">×</view>
|
</view>
|
<view v-else class="add-approver-btn" @click="addApprover(stepIndex)">
|
<view class="add-circle">+</view>
|
<text class="add-label">选择审批人</text>
|
</view>
|
</view>
|
<view class="step-line" v-if="stepIndex < approverNodes.length - 1"></view>
|
<view class="delete-step-btn" v-if="approverNodes.length > 1" @click="removeApprovalStep(stepIndex)">删除节点</view>
|
</view>
|
</view>
|
|
<view class="add-step-btn">
|
<van-button icon="plus" plain type="primary" style="width: 100%" @click="addApprovalStep">新增节点</van-button>
|
</view>
|
</view>
|
|
<!-- 底部按钮 -->
|
<view class="footer-btns">
|
<van-button class="cancel-btn" @click="goBack">取消</van-button>
|
<van-button class="save-btn" @click="submitForm">保存</van-button>
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
import PageHeader from "@/components/PageHeader.vue";
|
import useUserStore from "@/store/modules/user";
|
import {getDept, approveProcessGetInfo, approveProcessAdd, approveProcessUpdate} from "@/api/collaborativeApproval/approvalProcess";
|
const showToast = (message) => {
|
uni.showToast({
|
title: message,
|
icon: 'none'
|
})
|
}
|
import {userListNoPageByTenantId} from "@/api/system/user";
|
|
const data = reactive({
|
form: {
|
approveTime: "",
|
approveId: "",
|
approveUser: "",
|
approveUserName: "",
|
approveDeptName: "",
|
approveDeptId: "",
|
approveReason: "",
|
checkResult: "",
|
tempFileIds: [],
|
approverList: [] // 新增字段,存储所有节点的审批人id
|
},
|
rules: {
|
approveTime: [{ required: false, message: "请输入", trigger: "change" },],
|
approveId: [{ required: false, message: "请输入", trigger: "blur" }],
|
approveUser: [{ required: false, message: "请输入", trigger: "blur" }],
|
approveDeptId: [{ required: true, message: "请输入", trigger: "blur" }],
|
approveReason: [{ required: true, message: "请输入", trigger: "blur" }],
|
checkResult: [{ required: false, message: "请输入", trigger: "blur" }],
|
},
|
});
|
const { form, rules } = toRefs(data);
|
const result = ref("");
|
const pickerValue = ref([]);
|
const showPicker = ref(false);
|
const productOptions = ref([]);
|
const operationType = ref("");
|
const currentApproveStatus = ref("");
|
const approverNodes = ref([]);
|
const userList = ref([]);
|
const formRef = ref(null);
|
const message = ref("");
|
const showDate = ref(false)
|
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()])
|
const userStore = useUserStore()
|
|
const getProductOptions = () => {
|
getDept().then((res) => {
|
productOptions.value = res.data.map(item => ({
|
value: item.deptId,
|
text: item.deptName
|
}))
|
});
|
};
|
const fileList = ref([]);
|
let nextApproverId = 2;
|
|
onMounted(async () => {
|
try {
|
getProductOptions()
|
userListNoPageByTenantId().then((res) => {
|
userList.value = res.data
|
})
|
form.value.approveUser = userStore.id
|
form.value.approveUserName = userStore.nickName
|
form.value.approveTime = getCurrentDate();
|
|
// 获取URL参数
|
const pages = getCurrentPages();
|
const currentPage = pages[pages.length - 1];
|
const options = currentPage && currentPage.options ? currentPage.options : {};
|
operationType.value = options.operationType || 'add';
|
|
// 如果是编辑模式,从本地存储获取数据
|
if (operationType.value === 'edit') {
|
const storedData = uni.getStorageSync('invoiceLedgerEditRow');
|
if (storedData) {
|
const row = JSON.parse(storedData);
|
fileList.value = row.commonFileList || [];
|
form.value.tempFileIds = fileList.value.map(file => file.id);
|
currentApproveStatus.value = row.approveStatus;
|
|
approveProcessGetInfo({id: row.approveId, approveReason: '1'}).then(res => {
|
form.value = {...res.data};
|
// 反显审批人
|
if (res.data && res.data.approveUserIds) {
|
const userIds = res.data.approveUserIds.split(',');
|
approverNodes.value = userIds.map((userId, idx) => {
|
const userIdNum = parseInt(userId.trim());
|
// 从userList中找到对应的用户信息
|
const userInfo = userList.value.find(user => user.userId === userIdNum);
|
return {
|
id: idx + 1,
|
userId: userIdNum,
|
nickName: userInfo ? userInfo.nickName : null
|
};
|
});
|
nextApproverId = userIds.length + 1;
|
} else {
|
// 新增模式,初始化一个空的审批节点
|
approverNodes.value = [{ id: 1, userId: null, nickName: null }];
|
nextApproverId = 2;
|
}
|
});
|
}
|
} else {
|
// 新增模式,初始化一个空的审批节点
|
approverNodes.value = [{ id: 1, userId: null }];
|
}
|
|
// 监听联系人选择事件
|
uni.$on('selectContact', handleSelectContact);
|
} catch (error) {
|
console.error("获取部门数据失败:", error);
|
}
|
});
|
|
onUnmounted(() => {
|
// 移除事件监听
|
uni.$off('selectContact', handleSelectContact);
|
});
|
|
const onConfirm = ({ selectedValues, selectedOptions }) => {
|
form.value.approveDeptName = selectedOptions[0]?.text;
|
form.value.approveDeptId = selectedOptions[0]?.value;
|
pickerValue.value = selectedValues;
|
showPicker.value = false;
|
};
|
|
const goBack = () => {
|
// 清除本地存储的数据
|
uni.removeStorageSync('invoiceLedgerEditRow');
|
uni.navigateBack();
|
};
|
|
const submitForm = () => {
|
// 检查每个审批步骤是否都有审批人
|
const hasEmptyStep = approverNodes.value.some(step => !step.nickName);
|
if (hasEmptyStep) {
|
showToast('请为每个审批步骤选择审批人');
|
return;
|
}
|
|
formRef.value.validate().then(() => {
|
// 表单校验通过,可以提交数据
|
// 收集所有节点的审批人id
|
console.log('approverNodes---', approverNodes.value)
|
form.value.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
|
form.value.approveType = 0
|
if (operationType.value === "add" || currentApproveStatus.value == 3) {
|
approveProcessAdd(form.value).then(res => {
|
showToast("提交成功");
|
goBack()
|
})
|
} else {
|
approveProcessUpdate(form.value).then(res => {
|
showToast("提交成功");
|
goBack()
|
})
|
}
|
}).catch((error) => {
|
console.error("表单校验失败:", error);
|
// 显示具体的错误信息
|
if (error.length > 0) {
|
const firstError = error[0];
|
uni.showToast({
|
title: firstError.message || '表单校验失败',
|
icon: 'none'
|
});
|
} else {
|
uni.showToast({
|
title: '表单校验失败,请检查必填项',
|
icon: 'none'
|
});
|
}
|
});
|
};
|
|
// 处理联系人选择结果
|
const handleSelectContact = (data) => {
|
const { stepIndex, contact } = data;
|
// 将选中的联系人设置为对应审批步骤的审批人
|
approverNodes.value[stepIndex].userId = contact.userId;
|
approverNodes.value[stepIndex].nickName = contact.nickName;
|
};
|
|
const addApprover = (stepIndex) => {
|
// 跳转到联系人选择页面
|
uni.navigateTo({
|
url: `/pages/cooperativeOffice/collaborativeApproval/contactSelect?stepIndex=${stepIndex}`
|
});
|
};
|
|
const addApprovalStep = () => {
|
// 添加新的审批步骤
|
approverNodes.value.push({ userId: null, nickName: null });
|
};
|
|
const removeApprover = (stepIndex) => {
|
// 移除审批人
|
approverNodes.value[stepIndex].userId = null;
|
approverNodes.value[stepIndex].nickName = null;
|
};
|
|
const removeApprovalStep = (stepIndex) => {
|
// 确保至少保留一个审批步骤
|
if (approverNodes.value.length > 1) {
|
approverNodes.value.splice(stepIndex, 1);
|
} else {
|
uni.showToast({
|
title: '至少需要一个审批步骤',
|
icon: 'none'
|
});
|
}
|
};
|
// 显示日期选择器
|
const showDatePicker = () => {
|
showDate.value = true
|
}
|
|
// 确认日期选择
|
const onDateConfirm = ({ selectedValues }) => {
|
form.value.approveTime = selectedValues.join('-')
|
currentDate.value = selectedValues
|
showDate.value = false
|
}
|
// 获取当前日期并格式化为 YYYY-MM-DD
|
function getCurrentDate() {
|
const today = new Date();
|
const year = today.getFullYear();
|
const month = String(today.getMonth() + 1).padStart(2, "0"); // 月份从0开始
|
const day = String(today.getDate()).padStart(2, "0");
|
return `${year}-${month}-${day}`;
|
}
|
</script>
|
|
<style scoped lang="scss">
|
.account-detail {
|
min-height: 100vh;
|
background: #f8f9fa;
|
padding-bottom: 80px;
|
}
|
|
.header {
|
display: flex;
|
align-items: center;
|
background: #fff;
|
padding: 16px 20px;
|
border-bottom: 1px solid #f0f0f0;
|
position: sticky;
|
top: 0;
|
z-index: 100;
|
}
|
|
.title {
|
flex: 1;
|
text-align: center;
|
font-size: 18px;
|
font-weight: 600;
|
color: #333;
|
}
|
|
.form-section {
|
margin-top: 16px;
|
}
|
|
.approval-process {
|
background: #fff;
|
margin: 16px;
|
border-radius: 16px;
|
padding: 16px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
}
|
|
.approval-header {
|
margin-bottom: 16px;
|
}
|
|
.approval-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #333;
|
display: block;
|
margin-bottom: 4px;
|
}
|
|
.approval-desc {
|
font-size: 12px;
|
color: #999;
|
}
|
|
/* 样式增强为“简洁小圆圈风格” */
|
.approval-steps {
|
padding-left: 22px;
|
position: relative;
|
|
&::before {
|
content: '';
|
position: absolute;
|
left: 11px;
|
top: 40px;
|
bottom: 40px;
|
width: 2px;
|
background: linear-gradient(to bottom, #e6f7ff 0%, #bae7ff 50%, #91d5ff 100%);
|
border-radius: 1px;
|
}
|
}
|
|
.approval-step {
|
position: relative;
|
margin-bottom: 24px;
|
|
&::before {
|
content: '';
|
position: absolute;
|
left: -18px;
|
top: 14px; // 从 8px 调整为 14px,与文字中心对齐
|
width: 12px;
|
height: 12px;
|
background: #fff;
|
border: 3px solid #006cfb;
|
border-radius: 50%;
|
z-index: 2;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
}
|
}
|
|
.step-title {
|
top: 12px;
|
margin-bottom: 12px;
|
position: relative;
|
margin-left: 6px;
|
}
|
|
.step-title text {
|
font-size: 14px;
|
color: #666;
|
background: #f0f0f0;
|
padding: 4px 12px;
|
border-radius: 12px;
|
position: relative;
|
line-height: 1.4; // 确保文字行高一致
|
}
|
|
.approver-item {
|
display: flex;
|
align-items: center;
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
border-radius: 16px;
|
padding: 16px;
|
gap: 12px;
|
position: relative;
|
border: 1px solid #e6f7ff;
|
box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08);
|
transition: all 0.3s ease;
|
}
|
|
.approver-avatar {
|
width: 48px;
|
height: 48px;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
position: relative;
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
}
|
|
.avatar-text {
|
color: #fff;
|
font-size: 18px;
|
font-weight: 600;
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
}
|
|
.approver-info {
|
flex: 1;
|
position: relative;
|
}
|
|
.approver-name {
|
display: block;
|
font-size: 16px;
|
color: #333;
|
font-weight: 500;
|
position: relative;
|
}
|
|
.approver-dept {
|
font-size: 12px;
|
color: #999;
|
background: rgba(0, 108, 251, 0.05);
|
padding: 2px 8px;
|
border-radius: 8px;
|
display: inline-block;
|
position: relative;
|
|
&::before {
|
content: '';
|
position: absolute;
|
left: 4px;
|
top: 50%;
|
transform: translateY(-50%);
|
width: 2px;
|
height: 2px;
|
background: #006cfb;
|
border-radius: 50%;
|
}
|
}
|
|
.delete-approver-btn {
|
font-size: 16px;
|
color: #ff4d4f;
|
background: linear-gradient(135deg, rgba(255, 77, 79, 0.1) 0%, rgba(255, 77, 79, 0.05) 100%);
|
width: 28px;
|
height: 28px;
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
transition: all 0.3s ease;
|
position: relative;
|
}
|
|
.add-approver-btn {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
|
border: 2px dashed #006cfb;
|
border-radius: 16px;
|
padding: 20px;
|
color: #006cfb;
|
font-size: 14px;
|
position: relative;
|
transition: all 0.3s ease;
|
|
&::before {
|
content: '';
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
transform: translate(-50%, -50%);
|
width: 32px;
|
height: 32px;
|
border: 2px solid #006cfb;
|
border-radius: 50%;
|
opacity: 0;
|
transition: all 0.3s ease;
|
}
|
}
|
|
.delete-step-btn {
|
color: #ff4d4f;
|
font-size: 12px;
|
background: linear-gradient(135deg, rgba(255, 77, 79, 0.1) 0%, rgba(255, 77, 79, 0.05) 100%);
|
padding: 6px 12px;
|
border-radius: 12px;
|
display: inline-block;
|
position: relative;
|
transition: all 0.3s ease;
|
|
&::before {
|
content: '';
|
position: absolute;
|
left: 6px;
|
top: 50%;
|
transform: translateY(-50%);
|
width: 4px;
|
height: 4px;
|
background: #ff4d4f;
|
border-radius: 50%;
|
}
|
}
|
|
.step-line {
|
display: none; // 隐藏原来的线条,使用伪元素代替
|
}
|
|
.add-step-btn {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
.footer-btns {
|
position: fixed;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background: #fff;
|
display: flex;
|
justify-content: space-around;
|
align-items: center;
|
padding: 0.75rem 0;
|
box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
|
z-index: 1000;
|
}
|
|
.cancel-btn {
|
font-weight: 400;
|
font-size: 1rem;
|
color: #FFFFFF;
|
width: 6.375rem;
|
background: #C7C9CC;
|
box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
|
border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
|
}
|
|
.save-btn {
|
font-weight: 400;
|
font-size: 1rem;
|
color: #FFFFFF;
|
width: 14rem;
|
background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
|
box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
|
border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
|
}
|
|
// 动画定义
|
@keyframes pulse {
|
0% {
|
transform: scale(1);
|
opacity: 1;
|
}
|
50% {
|
transform: scale(1.2);
|
opacity: 0.7;
|
}
|
100% {
|
transform: scale(1);
|
opacity: 1;
|
}
|
}
|
|
@keyframes rotate {
|
0% {
|
transform: rotate(0deg);
|
}
|
100% {
|
transform: rotate(360deg);
|
}
|
}
|
|
@keyframes ripple {
|
0% {
|
transform: translate(-50%, -50%) scale(0.8);
|
opacity: 1;
|
}
|
100% {
|
transform: translate(-50%, -50%) scale(1.6);
|
opacity: 0;
|
}
|
}
|
|
/* 如果已有 .step-line,这里更精准定位到左侧与小圆点对齐 */
|
.step-line {
|
position: absolute;
|
left: 4px;
|
top: 48px;
|
width: 2px;
|
height: calc(100% - 48px);
|
background: #E5E7EB;
|
}
|
|
.approver-container {
|
display: flex;
|
align-items: center;
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
border-radius: 16px;
|
gap: 12px;
|
padding: 10px 0;
|
background: transparent;
|
border: none;
|
box-shadow: none;
|
}
|
|
.approver-item {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
padding: 8px 10px;
|
background: transparent;
|
border: none;
|
box-shadow: none;
|
border-radius: 0;
|
}
|
|
.approver-avatar {
|
position: relative;
|
width: 40px;
|
height: 40px;
|
border-radius: 50%;
|
background: #F3F4F6;
|
border: 2px solid #E5E7EB;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
animation: none; /* 禁用旋转等动画,回归简洁 */
|
}
|
|
.avatar-text {
|
font-size: 14px;
|
color: #374151;
|
font-weight: 600;
|
}
|
|
.add-approver-btn {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
background: transparent;
|
border: none;
|
box-shadow: none;
|
padding: 0;
|
}
|
|
.add-approver-btn .add-circle {
|
width: 40px;
|
height: 40px;
|
border: 2px dashed #A0AEC0;
|
border-radius: 50%;
|
color: #6B7280;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
font-size: 22px;
|
line-height: 1;
|
}
|
|
.add-approver-btn .add-label {
|
color: #3B82F6;
|
font-size: 14px;
|
}
|
</style>
|