一单四制优化及加入工作流

This commit is contained in:
王利强
2026-07-27 09:27:22 +08:00
parent 455fd4fc07
commit 7c3285ed5a
725 changed files with 19668 additions and 2921 deletions

View File

@@ -0,0 +1,228 @@
<template>
<u-popup :show="show" mode="bottom" round="20" @close="handleCancel">
<view class="user-popup">
<view class="popup-header">
<view class="popup-title text-bold">选择下一步处理人</view>
<view class="popup-close" @click="handleCancel">×</view>
</view>
<view v-if="pickerIdentityId" class="selected-summary">
<text class="summary-label">已选</text>
<text class="summary-text">{{ pickerDisplayName }}</text>
</view>
<scroll-view class="user-list-scroll" scroll-y>
<view v-if="loading" class="empty-tip">加载中...</view>
<view v-else-if="!taskId" class="empty-tip">缺少任务ID无法加载处理人</view>
<view v-else-if="deptTree.length === 0" class="empty-tip">暂无人员数据</view>
<FlowAssigneeTree
v-else
:depts="deptTree"
:selected-identity-id="pickerIdentityId"
@select="handleUserSelect"
/>
</scroll-view>
<view class="popup-footer">
<button class="btn-cancel" @click="handleCancel">取消</button>
<button class="btn-confirm bg-blue" @click="handleConfirm">确定</button>
</view>
</view>
</u-popup>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { getFlowApproverCandidates } from '@/request/api.js';
import FlowAssigneeTree from './FlowAssigneeTree.vue';
import {
findAssigneeUserInDeptTree,
formatAssigneeDisplayName,
normalizeApproverDeptTree,
resolveAssigneeIdentityId
} from './flowAssigneeUtils.js';
const props = defineProps({
show: {
type: Boolean,
default: false
},
taskId: {
type: String,
default: ''
},
pickerIdentityId: {
type: String,
default: ''
},
pickerName: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:pickerIdentityId', 'update:pickerName', 'cancel', 'confirm']);
const loading = ref(false);
const deptTree = ref([]);
const localPickerIdentityId = ref('');
const localPickerName = ref('');
const pickerDisplayName = computed(() => {
const user = findAssigneeUserInDeptTree(deptTree.value, localPickerIdentityId.value);
return user ? formatAssigneeDisplayName(user) : localPickerName.value;
});
const syncLocalPicker = () => {
localPickerIdentityId.value = props.pickerIdentityId || '';
localPickerName.value = props.pickerName || '';
};
const fetchAssigneeTree = async () => {
if (!props.taskId) {
deptTree.value = [];
return;
}
loading.value = true;
try {
const res = await getFlowApproverCandidates(props.taskId);
if (res.code === 0) {
deptTree.value = normalizeApproverDeptTree(res.data);
} else {
deptTree.value = [];
uni.showToast({ title: res.msg || '获取处理人失败', icon: 'none' });
}
} catch (error) {
console.error('获取处理人失败:', error);
deptTree.value = [];
uni.showToast({ title: '获取处理人失败', icon: 'none' });
} finally {
loading.value = false;
}
};
watch(
() => props.show,
async (visible) => {
if (!visible) return;
syncLocalPicker();
await fetchAssigneeTree();
}
);
const handleUserSelect = (user) => {
const identityId = resolveAssigneeIdentityId(user);
if (!identityId) return;
localPickerIdentityId.value = identityId;
localPickerName.value = formatAssigneeDisplayName(user);
emit('update:pickerIdentityId', identityId);
emit('update:pickerName', localPickerName.value);
};
const handleCancel = () => {
emit('cancel');
};
const handleConfirm = () => {
if (!localPickerIdentityId.value) {
uni.showToast({ title: '请选择下一步处理人', icon: 'none' });
return;
}
const user = findAssigneeUserInDeptTree(deptTree.value, localPickerIdentityId.value);
if (!user) {
uni.showToast({ title: '所选身份无效,请重新选择', icon: 'none' });
return;
}
const name = formatAssigneeDisplayName(user);
emit('update:pickerIdentityId', localPickerIdentityId.value);
emit('update:pickerName', name);
emit('confirm', {
identityId: localPickerIdentityId.value,
name,
user
});
};
</script>
<style lang="scss" scoped>
.user-popup {
background: #fff;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
.popup-title {
font-size: 32rpx;
color: #333;
}
.popup-close {
font-size: 40rpx;
color: #999;
line-height: 1;
}
}
.selected-summary {
padding: 16rpx 30rpx;
background: #f5f7fa;
border-bottom: 1rpx solid #eee;
font-size: 24rpx;
line-height: 1.5;
.summary-label {
color: #909399;
}
.summary-text {
color: #333;
}
}
.user-list-scroll {
max-height: 600rpx;
box-sizing: border-box;
}
.empty-tip {
padding: 80rpx 20rpx;
text-align: center;
color: #909399;
font-size: 26rpx;
}
.popup-footer {
display: flex;
gap: 24rpx;
padding: 24rpx 30rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
background: #fff;
button {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
margin: 0;
padding: 0;
&::after {
border: none;
}
}
.btn-cancel {
background: #fff;
color: #2667E9;
border: 2rpx solid #2667E9;
}
.btn-confirm {
color: #fff;
border: none;
}
}
}
</style>

View File

@@ -0,0 +1,88 @@
<template>
<view class="assignee-tree">
<template v-for="row in flatRows" :key="row.key">
<view
v-if="row.type === 'dept'"
class="dept-node"
:style="{ paddingLeft: `${row.level * 24 + 20}rpx` }"
>
<text class="dept-name">{{ row.deptName }}</text>
</view>
<view
v-else
class="user-item"
:class="{ active: String(selectedIdentityId) === String(resolveAssigneeIdentityId(row.user)) }"
:style="{ paddingLeft: `${row.level * 24 + 20}rpx` }"
@click="handleSelect(row.user)"
>
<text class="user-item-text">{{ formatAssigneeDisplayName(row.user) }}</text>
<text
v-if="String(selectedIdentityId) === String(resolveAssigneeIdentityId(row.user))"
class="cuIcon-check text-blue"
></text>
</view>
</template>
</view>
</template>
<script setup>
import { computed } from 'vue';
import {
flattenApproverDeptTree,
formatAssigneeDisplayName,
resolveAssigneeIdentityId
} from './flowAssigneeUtils.js';
const props = defineProps({
depts: {
type: Array,
default: () => []
},
selectedIdentityId: {
type: String,
default: ''
}
});
const emit = defineEmits(['select']);
const flatRows = computed(() => flattenApproverDeptTree(props.depts));
const handleSelect = (user) => {
emit('select', user);
};
</script>
<style lang="scss" scoped>
.dept-node {
padding: 20rpx 20rpx 12rpx;
font-size: 26rpx;
color: #909399;
line-height: 1.4;
}
.dept-name {
font-weight: 600;
}
.user-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 20rpx;
border-bottom: 1rpx solid #f5f5f5;
&.active {
.user-item-text {
color: #2667E9;
font-weight: 600;
}
}
.user-item-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
}
</style>

View File

@@ -0,0 +1,71 @@
export const resolveAssigneeIdentityId = (user) => {
if (!user) return '';
const id = user.identityId ?? user.userIdentityId ?? user.userId ?? '';
return id === '' || id == null ? '' : String(id);
};
export const getAssigneeItemKey = (user) => {
const identityId = resolveAssigneeIdentityId(user);
if (identityId) return `identity-${identityId}`;
return `user-${user.userId || user.nickName || ''}`;
};
export const formatAssigneeDisplayName = (user) => {
if (!user) return '';
if (user.identityName) {
return `${user.nickName || user.userName || ''}_${user.identityName}`;
}
if (user.postName) {
return `${user.nickName || user.userName || ''}_${user.postName}`;
}
return user.nickName || user.userName || user.name || '未知人员';
};
export const normalizeApproverDeptTree = (data) => {
if (!data) return [];
if (Array.isArray(data)) return data;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
return [];
};
export const findAssigneeUserInDeptTree = (depts, identityId) => {
if (!identityId || !Array.isArray(depts)) return null;
for (const dept of depts) {
const user = (dept.users || []).find(
(item) => String(resolveAssigneeIdentityId(item)) === String(identityId)
);
if (user) return user;
if (dept.children?.length) {
const found = findAssigneeUserInDeptTree(dept.children, identityId);
if (found) return found;
}
}
return null;
};
/** 将部门树拍平为可渲染行(部门标题 + 人员),避免小程序递归组件不展示子部门 */
export const flattenApproverDeptTree = (depts, level = 0) => {
const rows = [];
if (!Array.isArray(depts)) return rows;
for (const dept of depts) {
rows.push({
type: 'dept',
key: `dept-${dept.deptId ?? dept.deptName ?? level}`,
deptName: dept.deptName || '',
level
});
for (const user of dept.users || []) {
rows.push({
type: 'user',
key: getAssigneeItemKey(user),
user,
level: level + 1
});
}
if (dept.children?.length) {
rows.push(...flattenApproverDeptTree(dept.children, level + 1));
}
}
return rows;
};