Files
pj_mini_app_cloud/src/pages/integrated/todo/index.vue
T
dzc 653b4688cd fix:图片访问
fix:权限修改
2026-10-29 14:25:46 +08:00

1813 lines
44 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<crud-layout title="工作代办" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="todo-page">
<view class="todo-tabs">
<view :class="['tab-item', activeTab === 0 ? 'active' : '']" @click="switchTab(0)">我的待办</view>
<view :class="['tab-item', activeTab === 1 ? 'active' : '']" @click="switchTab(1)">我的已办</view>
</view>
<view class="todo-list">
<view v-if="todoList.length === 0 && !loading" class="todo-empty">
<image src="/static/icon/empty-todo.png" class="empty-img" />
<view class="empty-text">暂无数据</view>
</view>
<view v-else>
<view v-for="item in todoList" :key="item.taskId || item.id" class="todo-card">
<view class="card-bar"></view>
<view class="todo-card-content">
<view class="todo-card-title">
<text class="group">{{ item.groupName }}</text>
<text class="process">{{ item.processName }}</text>
</view>
<view class="todo-card-info">
<view class="info-row">
<text class="label">发起人:</text>
<text class="value">{{ item.rootUserName }}</text>
</view>
<view class="info-row">
<text class="label">发起时间:</text>
<text class="value">{{ item.startTime }}</text>
</view>
</view>
<view class="todo-card-actions">
<button v-if="activeTab === 0" class="todo-btn process" @click="startProcess(item)">开始处理</button>
<button v-if="activeTab === 1" class="todo-btn view" @click="viewProcess(item)">查看</button>
</view>
</view>
</view>
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-else-if="finished" class="no-more-tip">没有更多了</view>
</view>
</view>
<!-- 审核意见弹窗 -->
<view v-if="showCommentModal" class="modal-overlay comment-modal" @tap="hideCommentModal">
<view class="modal-content" @tap.stop>
<view class="modal-header">
<text class="modal-title">提交审核</text>
<text class="modal-close" @tap="hideCommentModal">×</text>
</view>
<view class="modal-body">
<view class="comment-section">
<text class="comment-label">审核意见</text>
<textarea v-model="approvalComment" class="comment-textarea" placeholder="请输入审核意见..."
maxlength="100"></textarea>
<view class="char-count">{{ approvalComment.length }}/100</view>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel-btn" @tap="hideCommentModal">取消</view>
<view class="modal-btn confirm-btn" @tap="confirmApproval">确定</view>
</view>
</view>
</view>
<!-- 转办人员选择弹窗 -->
<view v-if="showDelegateModal" class="modal-overlay delegate-modal" @tap="hideDelegateModal">
<view class="modal-content delegate-modal" @tap.stop>
<view class="modal-header">
<text class="modal-title">选择转办人员</text>
<text class="modal-close" @tap="hideDelegateModal">×</text>
</view>
<view class="modal-body">
<!-- 搜索框 -->
<view class="search-section">
<input v-model="searchKeyword" class="search-input" placeholder="搜索" @input="filterUsers" />
</view>
<!-- 面包屑导航 -->
<view v-if="breadcrumbPath.length > 1" class="breadcrumb-section">
<view class="breadcrumb">
<view v-for="(item, index) in breadcrumbPath" :key="item.id" class="breadcrumb-item"
@tap="navigateToBreadcrumb(index)">
<text class="breadcrumb-text">{{ item.name }}</text>
<text v-if="index < breadcrumbPath.length - 1" class="breadcrumb-separator">></text>
</view>
</view>
</view>
<!-- 用户列表 -->
<view class="user-list">
<!-- 部门列表 -->
<view class="section-title">部门</view>
<view v-for="dept in filteredDepartments" :key="dept.id" class="user-item" @tap="selectUser(dept)">
<view class="user-checkbox" :class="{ checked: selectedUser?.id === dept.id }">
<text v-if="selectedUser?.id === dept.id" class="check-icon">✓</text>
</view>
<view class="user-icon dept-icon">🏢</view>
<view class="user-info">
<text class="user-name">{{ dept.name }}</text>
<text class="user-type">部门</text>
</view>
<text class="subordinate-link">下级</text>
</view>
<!-- 员工列表 -->
<view class="section-title">员工</view>
<view v-for="employee in filteredEmployees" :key="employee.id" class="user-item"
@tap="selectUser(employee)">
<view class="user-checkbox" :class="{ checked: selectedUser?.id === employee.id }">
<text v-if="selectedUser?.id === employee.id" class="check-icon">✓</text>
</view>
<view class="user-icon employee-icon">
<image v-if="employee.avatar" :src="$fileUrl(employee.avatar)" class="avatar-img" @error="$fileRetry(employee.avatar)" />
<text v-else class="avatar-text">{{ employee.name.charAt(0) }}</text>
</view>
<view class="user-info">
<text class="user-name">{{ employee.name }}</text>
<text class="user-type">员工</text>
</view>
</view>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel-btn" @tap="hideDelegateModal">取消</view>
<view class="modal-btn confirm-btn" @tap="confirmDelegateUser">确定</view>
</view>
</view>
</view>
<!-- 转办确认弹窗 -->
<view v-if="showDelegateConfirmModal" class="modal-overlay delegate-confirm-modal"
@tap="hideDelegateConfirmModal">
<view class="modal-content" @tap.stop>
<view class="modal-header">
<text class="modal-title">转办确认</text>
<text class="modal-close" @tap="hideDelegateConfirmModal">×</text>
</view>
<view class="modal-body">
<view class="delegate-info">
<view class="info-item">
<text class="info-label">转办给:</text>
<text class="info-value">{{ selectedUser?.name }}</text>
</view>
</view>
<view class="comment-section">
<text class="comment-label">审核意见 *</text>
<textarea v-model="delegateComment" class="comment-textarea" placeholder="请输入审核意见 (2-100字)"
maxlength="100"></textarea>
<view class="char-count">{{ delegateComment.length }}/100</view>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel-btn" @tap="hideDelegateConfirmModal">取消</view>
<view class="modal-btn confirm-btn" @tap="confirmDelegate">确定</view>
</view>
</view>
</view>
<!-- 审批弹窗 -->
<view v-if="showApprovalModal" class="modal-overlay" @tap="hideApprovalModal">
<view class="modal-content" @tap.stop>
<view class="modal-header">
<text class="modal-title">审批处理</text>
<text class="modal-close" @tap="hideApprovalModal">×</text>
</view>
<view class="modal-body">
<!-- 动态字段渲染 - 根据 formItems 渲染 -->
<view v-if="currentTask.formItems && currentTask.formItems.length > 0">
<view v-for="field in currentTask.formItems" :key="field.id" class="form-item">
<text class="form-label">{{ field.name }}{{ field.required ? ' *' : '' }}</text>
<view v-if="field.type === 'Textarea'" class="form-textarea">
{{ field.props?.value || '' }}
</view>
<view v-else-if="field.type === 'Sequence'" class="form-sequence">
{{ field.props?.value || field.placeholder || '未知' }}
</view>
<view v-else class="form-input">{{ field.props?.value || '未知' }}</view>
</view>
</view>
<!-- 如果没有动态字段,显示默认字段 -->
<view v-else>
<!-- 流水号 -->
<view class="form-item">
<text class="form-label">流水号</text>
<view class="form-value">{{ currentTask.serialNumber || '未知流水号' }}</view>
</view>
<!-- 会议室 -->
<view class="form-item">
<text class="form-label">* 会议室</text>
<view class="form-input">{{ currentTask.roomName || '未知会议室' }}</view>
</view>
<!-- 预约人 -->
<view class="form-item">
<text class="form-label">* 预约人</text>
<view class="form-input">{{ currentTask.applicantName || '未知申请人' }}</view>
</view>
<!-- 预约时间 -->
<view class="form-item">
<text class="form-label">* 预约时间</text>
<view class="form-input">{{ currentTask.meetingTime || '未知时间' }}</view>
</view>
</view>
<!-- 动态审批流程 - 根据 approvalFlow 渲染 -->
<view class="approval-flow">
<view v-if="currentTask.approvalFlow && currentTask.approvalFlow.length > 0">
<view v-for="(flow, index) in currentTask.approvalFlow" :key="flow.id" class="flow-item">
<view class="flow-icon" :class="flow.status === 2 ? 'completed' : 'pending'">
{{ flow.status === 2 ? '✓' : '⟲' }}
</view>
<view class="flow-content">
<text class="flow-label">{{ flow.name }}{{ flow.placeholder ? ` [${flow.placeholder}]` : ''
}}</text>
<view v-if="flow.userVoList && flow.userVoList.length > 0" class="flow-users">
<view v-for="user in flow.userVoList" :key="user.id" class="flow-user">
<view class="user-avatar">
<image v-if="user.avatar" :src="$fileUrl(user.avatar)" class="avatar-img" @error="$fileRetry(user.avatar)" />
<text v-else class="avatar-text">{{ user.name?.charAt(0) || '?' }}</text>
</view>
<text class="user-name">{{ user.name || '未知用户' }}</text>
</view>
</view>
<view v-else class="flow-user">
<view class="user-avatar"></view>
<text class="user-name">待分配</text>
</view>
</view>
<view v-if="index < currentTask.approvalFlow.length - 1" class="flow-line"></view>
</view>
</view>
<!-- 如果没有动态审批流程,显示默认流程 -->
<view v-else>
<view class="flow-item">
<view class="flow-icon completed">✓</view>
<view class="flow-content">
<text class="flow-label">发起人</text>
<view class="flow-user">
<view class="user-avatar"></view>
<text class="user-name">{{ currentTask.applicantName || '未知申请人' }}</text>
</view>
</view>
</view>
<view class="flow-line"></view>
<view class="flow-item">
<view class="flow-icon pending">⟲</view>
<view class="flow-content">
<text class="flow-label">审批人</text>
<view class="flow-user">
<view class="user-avatar"></view>
<text class="user-name">当前用户</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="modal-footer">
<!-- 查看模式下只显示关闭按钮 -->
<view v-if="currentTask.isViewMode" class="modal-btn close-btn" @tap="hideApprovalModal">
<text class="btn-icon">×</text>
<text class="btn-text">关闭</text>
</view>
<!-- 审批模式下显示操作按钮 -->
<template v-else>
<view class="modal-btn reject-btn" @tap="handleReject">
<text class="btn-icon">✕</text>
<text class="btn-text">拒绝</text>
</view>
<view class="modal-btn submit-btn" @tap="handleSubmit">
<text class="btn-icon">✓</text>
<text class="btn-text">同意</text>
</view>
<view class="modal-btn transfer-btn" @tap="handleTransfer">
<text class="btn-icon">↗</text>
<text class="btn-text">转办</text>
</view>
</template>
</view>
</view>
</view>
<!-- 转办表单弹窗 -->
<view v-if="showDelegateFormModal" class="modal-overlay delegate-form-modal" @tap="hideDelegateFormModal">
<view class="modal-content delegate-form-content" @tap.stop>
<view class="modal-header">
<text class="modal-title">转办</text>
<view class="close-btn" @tap="hideDelegateFormModal">
<text class="iconfont icon-guanbi"></text>
</view>
</view>
<view class="delegate-form">
<!-- 选择转办人员 -->
<view class="form-section">
<view class="section-title">
<text class="title-text">选择转办人员</text>
<text class="required">*</text>
</view>
<view class="user-selector" @tap="showUserSelection">
<view v-if="selectedUser" class="selected-user-tag">
<text class="user-tag-name">{{ selectedUser.name }}</text>
<text class="user-tag-remove" @tap.stop="removeSelectedUser">×</text>
</view>
<view class="add-user-btn">
<text class="add-icon">+</text>
</view>
</view>
<view v-if="userSelectError" class="error-message">{{ userSelectError }}</view>
</view>
<!-- 审核意见 -->
<view class="form-section">
<view class="section-title">
<text class="title-text">审核意见</text>
<text class="required">*</text>
</view>
<view class="comment-input-wrapper">
<textarea v-model="delegateFormComment" class="comment-textarea" placeholder="请输入审核意见 (2-100字)"
:maxlength="100" auto-height />
<view class="char-count">{{ delegateFormComment.length }}/100</view>
</view>
<view v-if="commentError" class="error-message">{{ commentError }}</view>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel-btn" @tap="hideDelegateFormModal">
<text class="btn-text">取消</text>
</view>
<view class="modal-btn confirm-btn" @tap="confirmDelegateForm">
<text class="btn-text">确定</text>
</view>
</view>
</view>
</view>
</view>
</crud-layout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { filePath } from "@@/utils/file"
import { SERVICE_IMAGE } from "@@/constant/index"
import { taskApi, integratedApi } from "@/sheep/api/index"
const todoList = ref<any[]>([])
const current = ref(1)
const size = 10
const total = ref(0)
const loading = ref(false)
const finished = ref(false)
const activeTab = ref(0)
// 审批弹窗相关
const showApprovalModal = ref(false)
const currentTask = ref<any>({})
const isProcessing = ref(false)
// 审核意见弹窗相关
const showCommentModal = ref(false)
const approvalComment = ref('')
const pendingApproval = ref<boolean | null>(null) // true: 同意, false: 拒绝
// 转办相关
const showDelegateModal = ref(false)
const showDelegateConfirmModal = ref(false)
const selectedUser = ref<any>(null)
const delegateComment = ref('')
const searchKeyword = ref('')
// 用户选择数据
const userSelectionData = ref<any>({})
const filteredDepartments = ref<any[]>([])
const filteredEmployees = ref<any[]>([])
// 转办表单弹窗
const showDelegateFormModal = ref(false)
const delegateFormComment = ref('')
// 面包屑导航
const breadcrumbPath = ref<any[]>([])
// 表单验证错误提示
const userSelectError = ref('')
const commentError = ref('')
function fetchTodoList(isRefresh = false) {
if (loading.value) return
loading.value = true
const apiFn = activeTab.value === 0 ? integratedApi.getTodoList : integratedApi.getDoneList
apiFn({ current: current.value, size }).then((res: any) => {
if (res.code === 0 && res.data) {
total.value = res.data.total || 0
const records = res.data.records || []
if (isRefresh) {
todoList.value = records
} else {
todoList.value = [...todoList.value, ...records]
}
finished.value = todoList.value.length >= total.value
}
loading.value = false
if (isRefresh) uni.stopPullDownRefresh()
}).catch(() => {
loading.value = false
if (isRefresh) uni.stopPullDownRefresh()
})
}
function switchTab(tabIdx: number) {
if (activeTab.value === tabIdx) return
activeTab.value = tabIdx
current.value = 1
finished.value = false
fetchTodoList(true)
}
onMounted(() => {
fetchTodoList(true)
})
onPullDownRefresh(() => {
current.value = 1
finished.value = false
fetchTodoList(true)
})
onReachBottom(() => {
if (finished.value || loading.value) return
current.value += 1
fetchTodoList()
})
// 查看已办任务
async function viewProcess(item: any) {
if (isProcessing.value) return
isProcessing.value = true
uni.showLoading({ title: '加载中...', mask: true })
try {
// 调用第一个接口:查询任务详情 (view=true)
const taskRes = await taskApi.queryTask(item.taskId, true)
// 调用第二个接口:格式化开始节点显示
const formatData = {
flowId: item.flowId,
processInstanceId: item.processInstanceId,
paramMap: {
mcmxfktv1b0zg: null,
roomName: item.roomName,
applicantName: item.applicantName,
meetingTime: item.meetingTime
},
taskId: item.taskId
}
const formatRes = await taskApi.formatStartNodeShow(formatData)
// 从接口返回的数据中提取动态字段和审批流程
const taskData = taskRes as any
const formatData_result = formatRes as any
let formItems = []
let approvalFlow = []
if (taskData.code === 0 && taskData.data) {
// 从 queryTask 接口获取表单字段
formItems = taskData.data.formItems || []
}
if (formatData_result.code === 0 && formatData_result.data) {
// 从 formatStartNodeShow 接口获取审批流程
approvalFlow = formatData_result.data || []
}
// 设置当前任务数据 - 使用实际的任务数据和接口返回的动态数据
currentTask.value = {
...item,
serialNumber: item.serialNumber || item.taskId?.substring(0, 13) || '未知流水号',
roomName: item.roomName || '未知会议室',
applicantName: item.applicantName || item.rootUserName || '未知申请人',
meetingTime: item.meetingTime || item.startTime || '未知时间',
flowId: item.flowId,
processInstanceId: item.processInstanceId,
taskId: item.taskId,
// 添加动态数据
formItems: formItems,
approvalFlow: approvalFlow,
// 标记为查看模式
isViewMode: true
}
// 显示审批弹窗
showApprovalModal.value = true
} catch (error) {
console.error('加载查看信息失败:', error)
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
uni.hideLoading()
isProcessing.value = false
}
}
// 开始处理审批
async function startProcess(item: any) {
if (isProcessing.value) return
isProcessing.value = true
uni.showLoading({ title: '加载中...', mask: true })
try {
// 调用第一个接口:查询任务详情
const taskRes = await taskApi.queryTask(item.taskId, false)
// 调用第二个接口:格式化开始节点显示
const formatData = {
flowId: item.flowId,
processInstanceId: item.processInstanceId,
paramMap: {
mcmxfktv1b0zg: null,
roomName: item.roomName,
applicantName: item.applicantName,
meetingTime: item.meetingTime
},
taskId: item.taskId
}
const formatRes = await taskApi.formatStartNodeShow(formatData)
// 从接口返回的数据中提取动态字段和审批流程
const taskData = taskRes as any
const formatData_result = formatRes as any
let formItems = []
let approvalFlow = []
if (taskData.code === 0 && taskData.data) {
// 从 queryTask 接口获取表单字段
formItems = taskData.data.formItems || []
}
if (formatData_result.code === 0 && formatData_result.data) {
// 从 formatStartNodeShow 接口获取审批流程
approvalFlow = formatData_result.data || []
}
// 设置当前任务数据 - 使用实际的任务数据和接口返回的动态数据
currentTask.value = {
...item,
serialNumber: item.serialNumber || item.taskId?.substring(0, 13) || '未知流水号',
roomName: item.roomName || '未知会议室',
applicantName: item.applicantName || item.rootUserName || '未知申请人',
meetingTime: item.meetingTime || item.startTime || '未知时间',
flowId: item.flowId,
processInstanceId: item.processInstanceId,
taskId: item.taskId,
// 添加动态数据
formItems: formItems,
approvalFlow: approvalFlow
}
// 显示审批弹窗
showApprovalModal.value = true
} catch (error) {
console.error('加载审批信息失败:', error)
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
uni.hideLoading()
isProcessing.value = false
}
}
// 隐藏审批弹窗
function hideApprovalModal() {
showApprovalModal.value = false
currentTask.value = {}
}
// 处理拒绝
function handleReject() {
pendingApproval.value = false
approvalComment.value = ''
showCommentModal.value = true
}
// 处理提交(同意)
function handleSubmit() {
pendingApproval.value = true
approvalComment.value = ''
showCommentModal.value = true
}
// 处理转办
async function handleTransfer() {
// 先显示转办表单弹窗
delegateFormComment.value = ''
selectedUser.value = null
showDelegateFormModal.value = true
}
// 提交审批结果
async function submitApproval(approved: boolean) {
if (isProcessing.value) return
isProcessing.value = true
uni.showLoading({
title: approved ? '提交中...' : '拒绝中...',
mask: true
})
try {
// 检查任务数据
if (!currentTask.value.taskId) {
throw new Error('任务ID不存在')
}
// 构建审批参数
const paramMap = {
mcmxfktv1b0zg: null,
roomName: currentTask.value.roomName,
applicantName: currentTask.value.applicantName,
meetingTime: currentTask.value.meetingTime,
mcmxoxqvzau0u_approve_condition: approved
}
const taskLocalParamMap = {
approveDesc: approvalComment.value || (approved ? '同意申请' : '拒绝申请')
}
const requestData = {
taskId: currentTask.value.taskId,
paramMap: paramMap,
taskLocalParamMap: taskLocalParamMap
}
const res = await taskApi.completeTask(requestData)
uni.showToast({
title: approved ? '提交成功' : '拒绝成功',
icon: 'success'
})
// 关闭弹窗并刷新列表
hideApprovalModal()
setTimeout(() => {
fetchTodoList(true)
}, 1000)
} catch (error) {
console.error('审批失败:', error)
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
} finally {
uni.hideLoading()
isProcessing.value = false
}
}
// 审核意见弹窗相关方法
function hideCommentModal() {
showCommentModal.value = false
approvalComment.value = ''
pendingApproval.value = null
}
function confirmApproval() {
if (!approvalComment.value.trim()) {
uni.showToast({
title: '请输入审核意见',
icon: 'none'
})
return
}
// 先保存 pendingApproval 的值,因为 hideCommentModal 会重置它
const approvalValue = pendingApproval.value
hideCommentModal()
submitApproval(approvalValue!)
}
// 转办表单弹窗相关方法
function hideDelegateFormModal() {
showDelegateFormModal.value = false
delegateFormComment.value = ''
selectedUser.value = null
}
async function showUserSelection() {
try {
// 获取用户选择列表 - 使用抽取的API
const res = await taskApi.getDeptOrgUsers(0)
const responseData = res as any
if (responseData.code === 0 && responseData.data) {
// 处理接口返回的数据结构
const apiData = responseData.data
userSelectionData.value = {
titleDepartments: apiData.titleDepartments || [],
employees: apiData.employees || [],
childDepartments: apiData.childDepartments || []
}
filteredDepartments.value = apiData.childDepartments || []
filteredEmployees.value = apiData.employees || []
searchKeyword.value = ''
// 初始化面包屑导航
breadcrumbPath.value = [{ id: 0, name: '全部', type: 'root' }]
showDelegateModal.value = true
} else {
uni.showToast({
title: '获取用户列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取用户列表失败:', error)
uni.showToast({
title: '获取用户列表失败',
icon: 'none'
})
}
}
function removeSelectedUser() {
selectedUser.value = null
}
function confirmDelegateForm() {
// 清除之前的错误提示
userSelectError.value = ''
commentError.value = ''
let hasError = false
// 验证表单
if (!selectedUser.value) {
userSelectError.value = '请选择转办人员'
hasError = true
}
if (!delegateFormComment.value || delegateFormComment.value.length < 2) {
commentError.value = '请输入审核意见(至少2字)'
hasError = true
} else if (delegateFormComment.value.length > 100) {
commentError.value = '审核意见不能超过100字'
hasError = true
}
if (hasError) {
return
}
// 设置转办数据并提交
delegateComment.value = delegateFormComment.value
// 先提交转办,再关闭弹窗
submitDelegate()
}
// 转办相关方法
function hideDelegateModal() {
showDelegateModal.value = false
// 不要清空 selectedUser,因为用户可能已经选择了
searchKeyword.value = ''
}
function hideDelegateConfirmModal() {
showDelegateConfirmModal.value = false
delegateComment.value = ''
}
async function selectUser(user: any) {
// 如果是部门类型,请求下级数据
if (user.type === 'dept') {
try {
uni.showLoading({ title: '加载中...', mask: true })
const res = await taskApi.getDeptOrgUsers(user.id)
const responseData = res as any
if (responseData.code === 0 && responseData.data) {
const apiData = responseData.data
// 更新数据,显示下级部门和员工
userSelectionData.value = {
titleDepartments: [...(userSelectionData.value.titleDepartments || []), user],
employees: apiData.employees || [],
childDepartments: apiData.childDepartments || []
}
filteredDepartments.value = apiData.childDepartments || []
filteredEmployees.value = apiData.employees || []
searchKeyword.value = ''
// 更新面包屑导航
breadcrumbPath.value.push(user)
} else {
uni.showToast({
title: '获取下级数据失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取下级数据失败:', error)
uni.showToast({
title: '获取下级数据失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
} else {
// 如果是员工,直接选择并关闭弹窗
selectedUser.value = user
hideDelegateModal()
// 返回到转办表单弹窗
showDelegateFormModal.value = true
}
}
function confirmDelegateUser() {
if (!selectedUser.value) {
uni.showToast({
title: '请选择转办人员',
icon: 'none'
})
return
}
hideDelegateModal()
// 返回到转办表单弹窗
showDelegateFormModal.value = true
}
// 面包屑导航点击
async function navigateToBreadcrumb(index: number) {
if (index === breadcrumbPath.value.length - 1) {
// 点击当前层级,不执行任何操作
return
}
try {
uni.showLoading({ title: '加载中...', mask: true })
// 截取面包屑路径到指定层级
breadcrumbPath.value = breadcrumbPath.value.slice(0, index + 1)
// 获取目标层级的数据
const targetItem = breadcrumbPath.value[index]
const res = await taskApi.getDeptOrgUsers(targetItem.id)
const responseData = res as any
if (responseData.code === 0 && responseData.data) {
const apiData = responseData.data
// 更新数据
filteredDepartments.value = apiData.childDepartments || []
filteredEmployees.value = apiData.employees || []
searchKeyword.value = ''
} else {
uni.showToast({
title: '获取数据失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
function confirmDelegate() {
if (!delegateComment.value.trim()) {
uni.showToast({
title: '请输入审核意见',
icon: 'none'
})
return
}
if (delegateComment.value.length < 2) {
uni.showToast({
title: '审核意见至少2个字符',
icon: 'none'
})
return
}
submitDelegate()
}
async function submitDelegate() {
if (isProcessing.value) {
return
}
isProcessing.value = true
uni.showLoading({ title: '转办中...', mask: true })
try {
// 使用抽取的转办接口
const res = await taskApi.setAssignee({
paramMap: {
mcmxfktv1b0zg: null,
roomName: currentTask.value.roomName,
applicantName: currentTask.value.applicantName,
meetingTime: currentTask.value.meetingTime,
mcmxoxqvzau0u_approve_condition: true
},
taskId: currentTask.value.taskId,
targetUserIdList: [selectedUser.value.id],
taskLocalParamMap: {
approveDesc: delegateComment.value
}
})
const responseData = res as any
if (responseData.code === 0) {
uni.showToast({
title: '转办成功',
icon: 'success'
})
// 关闭弹窗并刷新列表
hideDelegateFormModal()
hideApprovalModal()
setTimeout(() => {
fetchTodoList(true)
}, 1000)
} else {
uni.showToast({
title: responseData.msg || '转办失败',
icon: 'none'
})
}
} catch (error) {
console.error('转办失败:', error)
uni.showToast({
title: '转办失败,请重试',
icon: 'none'
})
} finally {
uni.hideLoading()
isProcessing.value = false
}
}
// 搜索过滤
function filterUsers() {
const keyword = searchKeyword.value.toLowerCase()
if (!keyword) {
filteredDepartments.value = userSelectionData.value.childDepartments || []
filteredEmployees.value = userSelectionData.value.employees || []
return
}
filteredDepartments.value = (userSelectionData.value.childDepartments || []).filter((dept: any) =>
dept.name.toLowerCase().includes(keyword)
)
filteredEmployees.value = (userSelectionData.value.employees || []).filter((emp: any) =>
emp.name.toLowerCase().includes(keyword)
)
}
</script>
<style scoped lang="scss">
.todo-page {
background: linear-gradient(180deg, #f5f7fa 60%, #e3eafc 100%);
min-height: 100vh;
}
.todo-tabs {
display: flex;
justify-content: center;
align-items: center;
background: #fff;
border-radius: 0 0 20rpx 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
margin-bottom: 12rpx;
position: sticky;
top: 0;
z-index: 9;
}
.tab-item {
flex: 1;
text-align: center;
font-size: 30rpx;
color: #888;
padding: 24rpx 0 18rpx 0;
font-weight: 500;
cursor: pointer;
transition: color 0.2s, border 0.2s;
border-bottom: 4rpx solid transparent;
}
.tab-item.active {
color: #1976d2;
border-bottom: 4rpx solid #1976d2;
background: linear-gradient(90deg, #e3eafc 0%, #fff 100%);
}
.todo-list {
padding: 32rpx 24rpx 0 24rpx;
}
.todo-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #bbb;
font-size: 32rpx;
padding: 120rpx 0 60rpx 0;
}
.empty-img {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.8;
}
.empty-text {
font-size: 32rpx;
color: #bbb;
margin-top: 8rpx;
}
.todo-card {
display: flex;
background: #fff;
border-radius: 16rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(25, 118, 210, 0.08);
overflow: hidden;
position: relative;
min-height: 120rpx;
}
.card-bar {
width: 12rpx;
background: linear-gradient(180deg, #1976d2 0%, #42a5f5 100%);
border-radius: 0 12rpx 12rpx 0;
}
.todo-card-content {
flex: 1;
padding: 16rpx 20rpx 36rpx 20rpx;
display: flex;
flex-direction: column;
position: relative;
}
.todo-card-title {
display: flex;
align-items: baseline;
gap: 12rpx;
margin-bottom: 4rpx;
}
.group {
font-size: 24rpx;
color: #1976d2;
font-weight: 500;
}
.process {
font-size: 26rpx;
color: #222;
font-weight: bold;
margin-top: 0;
}
.todo-card-info {
margin-bottom: 4rpx;
}
.info-row {
display: flex;
margin-bottom: 2rpx;
}
.label {
color: #888;
font-size: 24rpx;
min-width: 80rpx;
}
.value {
color: #333;
font-size: 24rpx;
}
.todo-card-actions {
position: absolute;
right: 20rpx;
bottom: 16rpx;
display: flex;
gap: 16rpx;
}
.todo-btn {
font-size: 24rpx;
padding: 6rpx 28rpx;
border-radius: 20rpx;
border: none;
cursor: pointer;
transition: background 0.2s, box-shadow 0.2s;
box-shadow: 0 2rpx 8rpx rgba(25, 118, 210, 0.08);
}
.todo-btn.process {
background: linear-gradient(90deg, #1976d2 60%, #42a5f5 100%);
color: #fff;
}
.todo-btn.view {
background: linear-gradient(90deg, #4caf50 60%, #66bb6a 100%);
color: #fff;
}
.loading-tip,
.no-more-tip {
text-align: center;
color: #bbb;
font-size: 28rpx;
padding: 40rpx 0 0 0;
}
/* 审批弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
/* 审核意见弹窗 - 更高层级 */
.modal-overlay.comment-modal {
z-index: 1100;
}
/* 转办人员选择弹窗 - 最高层级 */
.modal-overlay.delegate-modal {
z-index: 1500;
}
/* 转办确认弹窗 - 更高层级 */
.modal-overlay.delegate-confirm-modal {
z-index: 1300;
}
.modal-content {
width: 90%;
max-width: 600rpx;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
max-height: 80vh;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1px solid #f0f0f0;
background: linear-gradient(90deg, #1976d2 0%, #42a5f5 100%);
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #fff;
}
.modal-close {
font-size: 40rpx;
color: #fff;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 32rpx;
max-height: 60vh;
overflow-y: auto;
}
.form-item {
margin-bottom: 24rpx;
}
.form-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
font-weight: 500;
}
.form-value {
font-size: 26rpx;
color: #666;
padding: 12rpx 0;
}
.form-input {
font-size: 26rpx;
color: #333;
padding: 16rpx;
background: #f5f5f5;
border-radius: 8rpx;
border: 1px solid #e0e0e0;
}
.form-textarea {
font-size: 26rpx;
color: #333;
padding: 16rpx;
background: #f5f5f5;
border-radius: 8rpx;
border: 1px solid #e0e0e0;
min-height: 120rpx;
line-height: 1.5;
}
.form-sequence {
font-size: 26rpx;
color: #1976d2;
padding: 16rpx;
background: #f0f8ff;
border-radius: 8rpx;
border: 1px solid #d0e7ff;
font-weight: 500;
font-family: monospace;
}
.approval-flow {
margin-top: 32rpx;
padding: 24rpx;
background: #f8f9fa;
border-radius: 12rpx;
}
.flow-item {
display: flex;
align-items: flex-start;
margin-bottom: 16rpx;
}
.flow-item:last-child {
margin-bottom: 0;
}
.flow-icon {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: bold;
margin-right: 16rpx;
flex-shrink: 0;
}
.flow-icon.completed {
background: #4caf50;
color: #fff;
}
.flow-icon.pending {
background: #ff9800;
color: #fff;
}
.flow-line {
width: 2rpx;
height: 32rpx;
background: #e0e0e0;
margin-left: 23rpx;
margin-bottom: 16rpx;
}
.flow-content {
flex: 1;
}
.flow-label {
display: block;
font-size: 26rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.flow-user {
display: flex;
align-items: center;
}
.flow-users {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
}
.user-avatar {
width: 32rpx;
height: 32rpx;
background: #e0e0e0;
border-radius: 50%;
margin-right: 8rpx;
}
.user-name {
font-size: 24rpx;
color: #666;
}
.modal-footer {
display: flex;
border-top: 1px solid #f0f0f0;
}
.modal-btn {
flex: 1;
padding: 24rpx;
text-align: center;
font-size: 26rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.reject-btn {
color: #f44336;
background: #fff;
}
.submit-btn {
color: #4caf50;
background: #fff;
}
.transfer-btn {
color: #666;
background: #fff;
}
.close-btn {
color: #666;
background: #fff;
flex: none;
width: 200rpx;
margin: 0 auto;
}
.btn-icon {
font-size: 24rpx;
font-weight: bold;
}
.btn-text {
font-size: 26rpx;
}
/* 审核意见弹窗样式 */
.comment-section {
margin-bottom: 24rpx;
}
.comment-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
font-weight: 500;
}
.comment-textarea {
width: 100%;
min-height: 200rpx;
padding: 16rpx;
border: 1px solid #e0e0e0;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
background: #fff;
resize: none;
box-sizing: border-box;
}
.char-count {
text-align: right;
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
/* 转办人员选择弹窗样式 */
.delegate-modal {
max-height: 80vh;
}
.search-section {
margin-bottom: 24rpx;
}
.breadcrumb-section {
margin-bottom: 24rpx;
padding: 16rpx 0;
border-bottom: 1px solid #f0f0f0;
}
.breadcrumb {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.breadcrumb-item {
display: flex;
align-items: center;
cursor: pointer;
}
.breadcrumb-text {
font-size: 26rpx;
color: #1976d2;
padding: 4rpx 8rpx;
border-radius: 4rpx;
transition: background-color 0.2s;
}
.breadcrumb-text:active {
background-color: #f0f8ff;
}
.breadcrumb-separator {
font-size: 24rpx;
color: #999;
margin: 0 8rpx;
}
.search-input {
width: 100%;
padding: 16rpx;
border: 1px solid #e0e0e0;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
background: #fff;
}
.user-list {
max-height: 400rpx;
overflow-y: auto;
}
.section-title {
font-size: 24rpx;
color: #666;
margin-bottom: 12rpx;
padding: 8rpx 0;
border-bottom: 1px solid #f0f0f0;
}
.user-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1px solid #f5f5f5;
}
.user-item:last-child {
border-bottom: none;
}
.user-checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #e0e0e0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
}
.user-checkbox.checked {
background: #1976d2;
border-color: #1976d2;
}
.check-icon {
color: #fff;
font-size: 20rpx;
font-weight: bold;
}
.user-icon {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
}
.dept-icon {
background: #e3f2fd;
font-size: 24rpx;
}
.employee-icon {
background: #f3e5f5;
position: relative;
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.avatar-text {
font-size: 20rpx;
color: #1976d2;
font-weight: bold;
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
}
.user-name {
font-size: 26rpx;
color: #333;
font-weight: 500;
margin-bottom: 4rpx;
}
.user-type {
font-size: 22rpx;
color: #666;
}
.subordinate-link {
font-size: 22rpx;
color: #1976d2;
padding: 8rpx 16rpx;
border: 1px solid #1976d2;
border-radius: 16rpx;
}
/* 转办确认弹窗样式 */
.delegate-info {
margin-bottom: 24rpx;
padding: 16rpx;
background: #f8f9fa;
border-radius: 8rpx;
}
.info-item {
display: flex;
align-items: center;
}
.info-label {
font-size: 26rpx;
color: #666;
margin-right: 8rpx;
}
.info-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
/* 转办表单弹窗样式 */
.delegate-form-modal {
z-index: 1400;
}
.delegate-form-content {
width: 600rpx;
max-height: 80vh;
padding: 0;
}
.delegate-form {
padding: 32rpx;
}
.form-section {
margin-bottom: 32rpx;
}
.form-section:last-child {
margin-bottom: 0;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.title-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.required {
color: #f44336;
font-size: 28rpx;
margin-left: 4rpx;
}
.user-selector {
border: 1px solid #ddd;
border-radius: 8rpx;
padding: 16rpx;
background: #fff;
cursor: pointer;
transition: border-color 0.2s;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.user-selector:active {
border-color: #1976d2;
}
.selected-user-tag {
display: flex;
align-items: center;
background: #ff9800;
color: #fff;
padding: 8rpx 12rpx;
border-radius: 16rpx;
font-size: 26rpx;
}
.user-tag-name {
margin-right: 8rpx;
}
.user-tag-remove {
font-size: 24rpx;
font-weight: bold;
cursor: pointer;
padding: 2rpx;
border-radius: 50%;
width: 24rpx;
height: 24rpx;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.user-tag-remove:active {
background-color: rgba(255, 255, 255, 0.2);
}
.add-user-btn {
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
border: 1px solid #ddd;
border-radius: 50%;
background: #f5f5f5;
cursor: pointer;
transition: all 0.2s;
}
.add-user-btn:active {
background: #e0e0e0;
border-color: #1976d2;
}
.add-icon {
font-size: 24rpx;
color: #666;
font-weight: bold;
}
.comment-input-wrapper {
position: relative;
}
.comment-textarea {
width: 100%;
min-height: 120rpx;
padding: 16rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
background: #fff;
resize: none;
box-sizing: border-box;
}
.comment-textarea:focus {
border-color: #1976d2;
outline: none;
}
.char-count {
position: absolute;
bottom: 8rpx;
right: 16rpx;
font-size: 24rpx;
color: #999;
}
.cancel-btn {
color: #666;
background: #fff;
border: 1px solid #ddd;
}
.confirm-btn {
color: #fff;
background: #1976d2;
}
.error-message {
font-size: 24rpx;
color: #f44336;
margin-top: 8rpx;
padding-left: 4rpx;
}
</style>