<!--
|
报销审批流程(可搜索选人,点选即确认)
|
-->
|
<template>
|
<view class="flow-wrap">
|
<view v-for="(item, index) in innerList"
|
:key="item._uid"
|
class="flow-node-block">
|
<view class="flow-node-card">
|
<view class="node-header">
|
<view class="node-level-badge">{{ index + 1 }}</view>
|
<text class="node-level-text">第{{ levelLabel(index + 1) }}级审批</text>
|
<view v-if="innerList.length > 1"
|
class="node-delete"
|
@click="remove(index)">
|
<up-icon name="trash"
|
size="16"
|
color="#f56c6c" />
|
</view>
|
</view>
|
<view class="approver-row"
|
@click="openPicker(index)">
|
<view class="approver-avatar"
|
:style="{ backgroundColor: avatarColor(item.approverName) }">
|
{{ (item.approverName || '+').charAt(0) }}
|
</view>
|
<view class="approver-meta">
|
<text class="approver-name">{{ item.approverName || '点击选择审批人' }}</text>
|
<text class="approver-hint">支持搜索姓名或工号</text>
|
</view>
|
<up-icon name="arrow-right"
|
size="14"
|
color="#c0c4cc" />
|
</view>
|
</view>
|
<view v-if="index < innerList.length - 1"
|
class="flow-connector">
|
<view class="flow-connector-line" />
|
</view>
|
</view>
|
<view class="add-node-bar"
|
@click="addNode">
|
<up-icon name="plus-circle"
|
size="18"
|
color="#2979ff" />
|
<text>添加审批级次</text>
|
</view>
|
|
<OaUserSearchPicker v-model:show="pickerShow"
|
v-model="pickerUserId"
|
title="选择审批人"
|
:users="userOptions"
|
:show-self-quick="false"
|
@select="onUserSelected" />
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, watch } from "vue";
|
import OaUserSearchPicker from "../../_components/OaUserSearchPicker.vue";
|
import { userAvatarColor } from "../../_utils/userPickerUtils.js";
|
|
const props = defineProps({
|
modelValue: { type: Array, default: () => [] },
|
userOptions: { type: Array, default: () => [] },
|
});
|
const emit = defineEmits(["update:modelValue"]);
|
|
const innerList = ref([]);
|
const pickerShow = ref(false);
|
const pickerUserId = ref("");
|
const editingIndex = ref(-1);
|
|
function newUid() {
|
return `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
}
|
|
function levelLabel(n) {
|
const t = ["一", "二", "三", "四", "五", "六", "七", "八"];
|
return t[n - 1] || String(n);
|
}
|
|
function avatarColor(name) {
|
return userAvatarColor(name);
|
}
|
|
function mapIn(rows) {
|
return (rows || []).map((n, i) => ({
|
_uid: n._uid || newUid(),
|
nodeOrder: n.nodeOrder ?? i + 1,
|
signMode: n.signMode || "countersign",
|
approverId: n.approverId ?? "",
|
approverName: n.approverName || "",
|
id: n.id,
|
templateId: n.templateId,
|
}));
|
}
|
|
function mapOut() {
|
return innerList.value.map((n, i) => ({
|
nodeOrder: i + 1,
|
signMode: n.signMode || "countersign",
|
approverId: n.approverId,
|
approverName: n.approverName,
|
id: n.id,
|
templateId: n.templateId,
|
}));
|
}
|
|
function syncEmit() {
|
emit("update:modelValue", mapOut());
|
}
|
|
watch(
|
() => props.modelValue,
|
v => {
|
innerList.value = mapIn(v);
|
if (!innerList.value.length) {
|
innerList.value = [
|
{ _uid: newUid(), nodeOrder: 1, signMode: "countersign", approverId: "", approverName: "" },
|
];
|
}
|
},
|
{ immediate: true, deep: true }
|
);
|
|
function addNode() {
|
innerList.value.push({
|
_uid: newUid(),
|
nodeOrder: innerList.value.length + 1,
|
signMode: "countersign",
|
approverId: "",
|
approverName: "",
|
});
|
syncEmit();
|
}
|
|
function remove(index) {
|
if (innerList.value.length <= 1) {
|
uni.showToast({ title: "至少保留一个审批节点", icon: "none" });
|
return;
|
}
|
innerList.value.splice(index, 1);
|
syncEmit();
|
}
|
|
function openPicker(index) {
|
editingIndex.value = index;
|
pickerUserId.value = innerList.value[index]?.approverId || "";
|
pickerShow.value = true;
|
}
|
|
function onUserSelected(u) {
|
const node = innerList.value[editingIndex.value];
|
if (!node) return;
|
node.approverId = u.userId ?? u.id;
|
node.approverName = u.nickName || u.userName || "";
|
syncEmit();
|
}
|
</script>
|
|
<style scoped lang="scss">
|
.flow-node-card {
|
background: #f8f9fb;
|
border-radius: 10px;
|
padding: 12px;
|
border: 1px solid #eef0f3;
|
}
|
.node-header {
|
display: flex;
|
align-items: center;
|
margin-bottom: 10px;
|
}
|
.node-level-badge {
|
width: 22px;
|
height: 22px;
|
border-radius: 50%;
|
background: #2979ff;
|
color: #fff;
|
font-size: 12px;
|
text-align: center;
|
line-height: 22px;
|
margin-right: 8px;
|
}
|
.node-level-text {
|
flex: 1;
|
font-size: 14px;
|
color: #303133;
|
font-weight: 500;
|
}
|
.approver-row {
|
display: flex;
|
align-items: center;
|
padding: 10px 12px;
|
background: #fff;
|
border-radius: 8px;
|
}
|
.approver-avatar {
|
width: 36px;
|
height: 36px;
|
border-radius: 50%;
|
color: #fff;
|
font-size: 15px;
|
font-weight: 600;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-shrink: 0;
|
}
|
.approver-meta {
|
flex: 1;
|
margin-left: 10px;
|
min-width: 0;
|
}
|
.approver-name {
|
display: block;
|
font-size: 15px;
|
color: #303133;
|
}
|
.approver-hint {
|
display: block;
|
font-size: 12px;
|
color: #c0c4cc;
|
margin-top: 2px;
|
}
|
.flow-connector {
|
display: flex;
|
justify-content: center;
|
padding: 6px 0;
|
}
|
.flow-connector-line {
|
width: 2px;
|
height: 14px;
|
background: #dcdfe6;
|
}
|
.add-node-bar {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
gap: 6px;
|
padding: 14px 0 4px;
|
color: #2979ff;
|
font-size: 14px;
|
}
|
</style>
|