<!--
|
协同审批 / 审批流程配置
|
按审批类型(1~7)维护审批人节点,接口 /approveProcessConfigNode
|
-->
|
<template>
|
<view class="approve-config-page">
|
<PageHeader title="审批流程配置"
|
@back="goBack" />
|
|
<view class="tabs-wrap">
|
<up-tabs :list="tabList"
|
:current="activeTab"
|
line-color="#2979ff"
|
@click="onTabClick" />
|
</view>
|
|
<scroll-view class="config-scroll"
|
scroll-y
|
:show-scrollbar="false">
|
<view class="config-tip">
|
<text class="config-tip-title">{{ currentTypeLabel }}</text>
|
<text class="config-tip-desc">
|
{{ approverList.length ? `已配置 ${approverList.length} 个审批人` : "未配置审批人" }}
|
</text>
|
</view>
|
|
<view v-if="loading"
|
class="loading-wrap">
|
<up-loading-icon mode="circle" />
|
<text class="loading-text">加载中...</text>
|
</view>
|
|
<view v-else-if="approverList.length"
|
class="node-list">
|
<view v-for="(item, index) in approverList"
|
:key="index"
|
class="node-card">
|
<view class="node-head">
|
<view class="node-badge">{{ index + 1 }}</view>
|
<text class="node-level">{{ levelText(index) }}</text>
|
<view class="node-actions">
|
<view class="icon-btn"
|
:class="{ 'is-disabled': index === 0 }"
|
@click="moveUp(index)">
|
<up-icon name="arrow-up"
|
size="16"
|
color="#2979ff" />
|
</view>
|
<view class="icon-btn"
|
:class="{ 'is-disabled': index === approverList.length - 1 }"
|
@click="moveDown(index)">
|
<up-icon name="arrow-down"
|
size="16"
|
color="#2979ff" />
|
</view>
|
<view class="icon-btn"
|
@click="removeNode(index)">
|
<up-icon name="trash"
|
size="16"
|
color="#f56c6c" />
|
</view>
|
</view>
|
</view>
|
<view class="node-body"
|
@click="openUserPicker(index)">
|
<text :class="['node-approver', item.approverName ? '' : 'is-placeholder']">
|
{{ item.approverName || "请选择审批人" }}
|
</text>
|
<up-icon name="arrow-right"
|
size="16"
|
color="#c0c4cc" />
|
</view>
|
</view>
|
</view>
|
|
<view v-else
|
class="empty-wrap">
|
<up-empty mode="data"
|
text="暂无审批人配置" />
|
</view>
|
|
<view class="add-node"
|
@click="addNode">
|
<up-icon name="plus"
|
size="16"
|
color="#2979ff" />
|
<text class="add-node-text">新增审批人</text>
|
</view>
|
|
<view class="bottom-space" />
|
</scroll-view>
|
|
<FooterButtons cancel-text="返回"
|
confirm-text="保存配置"
|
:loading="saving"
|
@cancel="goBack"
|
@confirm="handleSave" />
|
|
<OaUserSearchPicker v-model:show="showUserPicker"
|
v-model="pickingUserId"
|
title="选择审批人"
|
:users="userList"
|
@select="onUserPicked" />
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, ref } from "vue";
|
import { onLoad } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import FooterButtons from "@/components/FooterButtons.vue";
|
import OaUserSearchPicker from "@/pages/oa/_components/OaUserSearchPicker.vue";
|
import { userListNoPage } from "@/api/system/user";
|
import {
|
addApproveProcessConfigNode,
|
getApproveProcessConfigNodeList,
|
} from "@/api/collaborativeApproval/approvalManagement";
|
|
/** 与 Web approvalManagement 一致 */
|
const APPROVE_TYPES = [
|
{ value: "1", label: "公出管理" },
|
{ value: "2", label: "请假管理" },
|
{ value: "3", label: "出差管理" },
|
{ value: "4", label: "报销管理" },
|
{ value: "5", label: "采购审批" },
|
{ value: "6", label: "报价审批" },
|
{ value: "7", label: "发货审批" },
|
];
|
|
const activeTab = ref(0);
|
const approverList = ref([]);
|
const loading = ref(false);
|
const saving = ref(false);
|
const userList = ref([]);
|
|
const showUserPicker = ref(false);
|
const pickingUserId = ref("");
|
const pickingIndex = ref(-1);
|
|
const tabList = computed(() => APPROVE_TYPES.map(t => ({ name: t.label })));
|
const currentTypeLabel = computed(
|
() => APPROVE_TYPES[activeTab.value]?.label || ""
|
);
|
const currentApproveType = computed(
|
() => Number(APPROVE_TYPES[activeTab.value]?.value ?? 1)
|
);
|
|
const levelText = index => `第${index + 1}级`;
|
|
const unwrapNodeList = res => {
|
if (Array.isArray(res?.data)) return res.data;
|
if (Array.isArray(res?.rows)) return res.rows;
|
if (Array.isArray(res?.data?.records)) return res.data.records;
|
return [];
|
};
|
|
const unwrapUserList = res => {
|
if (Array.isArray(res?.data)) return res.data;
|
if (Array.isArray(res?.rows)) return res.rows;
|
return [];
|
};
|
|
const loadData = async () => {
|
loading.value = true;
|
try {
|
const res = await getApproveProcessConfigNodeList(currentApproveType.value);
|
approverList.value = unwrapNodeList(res)
|
.map((item, index) => ({
|
...item,
|
sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1,
|
}))
|
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
} catch {
|
approverList.value = [];
|
uni.showToast({ title: "加载审批配置失败", icon: "none" });
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
const loadUserList = async () => {
|
try {
|
userList.value = unwrapUserList(await userListNoPage());
|
} catch {
|
userList.value = [];
|
uni.showToast({ title: "加载人员列表失败", icon: "none" });
|
}
|
};
|
|
const onTabClick = item => {
|
activeTab.value = item?.index ?? 0;
|
loadData();
|
};
|
|
const addNode = () => {
|
approverList.value.push({
|
id: null,
|
approveType: currentApproveType.value,
|
approverId: null,
|
approverName: "",
|
sortOrder: approverList.value.length + 1,
|
});
|
};
|
|
const removeNode = index => {
|
uni.showModal({
|
title: "删除确认",
|
content: "确定删除该审批人吗?",
|
confirmText: "确定删除",
|
confirmColor: "#f56c6c",
|
success: res => {
|
if (!res.confirm) return;
|
approverList.value.splice(index, 1);
|
approverList.value.forEach((item, idx) => {
|
item.sortOrder = idx + 1;
|
});
|
},
|
});
|
};
|
|
const swapNode = (from, to) => {
|
const list = approverList.value;
|
const temp = list[from];
|
list[from] = list[to];
|
list[to] = temp;
|
list.forEach((item, idx) => {
|
item.sortOrder = idx + 1;
|
});
|
};
|
|
const moveUp = index => {
|
if (index <= 0) return;
|
swapNode(index, index - 1);
|
};
|
|
const moveDown = index => {
|
if (index >= approverList.value.length - 1) return;
|
swapNode(index, index + 1);
|
};
|
|
const openUserPicker = index => {
|
pickingIndex.value = index;
|
pickingUserId.value = approverList.value[index]?.approverId ?? "";
|
showUserPicker.value = true;
|
};
|
|
const onUserPicked = user => {
|
const idx = pickingIndex.value;
|
if (idx < 0 || !user) return;
|
approverList.value[idx].approverId = user.userId ?? user.id;
|
approverList.value[idx].approverName =
|
user.nickName || user.userName || "";
|
};
|
|
const handleSave = async () => {
|
if (saving.value) return;
|
if (!approverList.value.length) {
|
uni.showToast({ title: "请至少配置一个审批人", icon: "none" });
|
return;
|
}
|
if (approverList.value.some(item => !item.approverId)) {
|
uni.showToast({ title: "请选择所有审批人", icon: "none" });
|
return;
|
}
|
const ids = approverList.value.map(item => item.approverId);
|
if (new Set(ids).size !== ids.length) {
|
uni.showToast({ title: "审批人不能重复", icon: "none" });
|
return;
|
}
|
|
saving.value = true;
|
try {
|
await addApproveProcessConfigNode(
|
approverList.value.map((item, index) => ({
|
approveType: currentApproveType.value,
|
nodeOrder: index + 1,
|
approverId: item.approverId,
|
approverName: item.approverName,
|
}))
|
);
|
uni.showToast({ title: "保存成功", icon: "success" });
|
await loadData();
|
} catch {
|
uni.showToast({ title: "保存失败", icon: "none" });
|
} finally {
|
saving.value = false;
|
}
|
};
|
|
const goBack = () => uni.navigateBack();
|
|
onLoad(async () => {
|
await loadUserList();
|
await loadData();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/styles/sales-common.scss";
|
|
.approve-config-page {
|
display: flex;
|
flex-direction: column;
|
min-height: 100vh;
|
background: #f0f3f8;
|
}
|
|
.tabs-wrap {
|
background: #fff;
|
border-bottom: 1px solid #f0f0f0;
|
padding: 0 8px;
|
}
|
|
.config-scroll {
|
flex: 1;
|
height: 0;
|
padding: 10px 12px 0;
|
}
|
|
.config-tip {
|
display: flex;
|
align-items: baseline;
|
justify-content: space-between;
|
padding: 2px 4px 10px;
|
}
|
|
.config-tip-title {
|
font-size: 15px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.config-tip-desc {
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.loading-wrap {
|
padding: 48px 0;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
gap: 12px;
|
}
|
|
.loading-text {
|
font-size: 14px;
|
color: #909399;
|
}
|
|
.node-card {
|
background: #fff;
|
border-radius: 10px;
|
padding: 12px;
|
margin-bottom: 10px;
|
box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
|
}
|
|
.node-head {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
margin-bottom: 10px;
|
}
|
|
.node-badge {
|
width: 26px;
|
height: 26px;
|
border-radius: 8px;
|
background: #2979ff;
|
color: #fff;
|
font-size: 14px;
|
font-weight: 600;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-shrink: 0;
|
}
|
|
.node-level {
|
flex: 1;
|
font-size: 15px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.node-actions {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
}
|
|
.icon-btn {
|
width: 30px;
|
height: 30px;
|
border-radius: 8px;
|
background: #f5f7fa;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
&.is-disabled {
|
opacity: 0.4;
|
pointer-events: none;
|
}
|
}
|
|
.node-body {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 12px;
|
background: #f7f9fc;
|
border: 1px solid #eef1f6;
|
border-radius: 8px;
|
}
|
|
.node-approver {
|
font-size: 15px;
|
color: #303133;
|
|
&.is-placeholder {
|
color: #c0c4cc;
|
}
|
}
|
|
.empty-wrap {
|
padding: 40px 20px 20px;
|
}
|
|
.add-node {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
gap: 6px;
|
height: 44px;
|
background: #fff;
|
border: 1px dashed #c0c4cc;
|
border-radius: 10px;
|
|
&:active {
|
opacity: 0.85;
|
}
|
}
|
|
.add-node-text {
|
font-size: 14px;
|
color: #2979ff;
|
}
|
|
.bottom-space {
|
height: calc(80px + env(safe-area-inset-bottom));
|
}
|
</style>
|