You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1354 lines
40 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="process-query-container">
<!-- 页面头部 -->
<div class="page-header">
<h1 class="page-title">
<el-icon :size="24"><Search /></el-icon>
流程查询
</h1>
</div>
<!-- 子流程卡片网格选择器 -->
<el-card class="subprocess-selector-section" shadow="never" v-loading="subprocessLoading">
<div class="subprocess-grid" v-if="availableSubprocesses.length > 0">
<div
v-for="subprocess in availableSubprocesses"
:key="subprocess.id"
class="subprocess-card"
:class="{ active: selectedSubprocess?.id === subprocess.id }"
@click="handleSubprocessSelect(subprocess)"
>
<div class="subprocess-icon">
<el-icon :size="32">
<component :is="getIcon(subprocess.icon || subprocess.custom_model?.icon)" />
</el-icon>
</div>
<div class="subprocess-name">{{ subprocess.name || subprocess.custom_model?.name || '未命名流程' }}</div>
<div class="subprocess-description" v-if="subprocess.description">
{{ subprocess.description }}
</div>
</div>
</div>
<el-empty v-else description="暂无可用流程" />
</el-card>
<!-- 查询类型和筛选区域 -->
<el-card class="filter-section" shadow="never">
<!-- 查询类型和年份选择合并到同一行 -->
<div class="filter-bar" v-if="selectedSubprocess">
<div class="filter-group">
<span class="filter-label">关联状态</span>
<el-radio-group v-model="queryType" @change="handleQueryTypeChange">
<el-radio-button label="not-linked">未关联支付</el-radio-button>
<el-radio-button label="linked">已关联支付</el-radio-button>
</el-radio-group>
</div>
<div class="filter-group" v-loading="loadingYears">
<span class="filter-label">发起年份:</span>
<el-radio-group v-model="selectedYear" @change="handleYearChange">
<el-radio-button
v-for="year in availableYears"
:key="year"
:label="year"
>
{{ year }}年
</el-radio-button>
</el-radio-group>
</div>
</div>
<!-- 如果未选择子流程,只显示关联状态选择 -->
<div class="filter-bar" v-else>
<div class="filter-group">
<span class="filter-label">关联状态:</span>
<el-radio-group v-model="queryType" @change="handleQueryTypeChange">
<el-radio-button label="not-linked">未关联支付</el-radio-button>
<el-radio-button label="linked">已关联支付</el-radio-button>
</el-radio-group>
</div>
</div>
<!-- 筛选表单 -->
<el-form :model="filterForm" inline style="margin-top: 16px">
<el-form-item label="流程编号">
<el-input
v-model="filterForm.keyword"
placeholder="请输入流程编号或关键词"
style="width: 200px"
clearable
/>
</el-form-item>
<!-- 高级查询(仅在选择了子流程时显示) -->
<template v-if="selectedSubprocess && allModelFields.length > 0">
<el-form-item label="字段">
<el-select
v-model="filterForm.field_id"
placeholder="请选择字段"
style="width: 180px"
filterable
clearable
>
<el-option
v-for="field in allModelFields"
:key="field.id"
:value="field.id"
:label="field.label"
/>
</el-select>
</el-form-item>
<el-form-item label="类型">
<el-select
v-model="filterForm.operator"
placeholder="请选择类型"
style="width: 150px"
clearable
>
<el-option
v-for="op in operatorOptions"
:key="op.id"
:value="op.id"
:label="op.label"
/>
</el-select>
</el-form-item>
<el-form-item label="关键词">
<el-input
v-model="filterForm.field_keyword"
placeholder="请输入关键词"
style="width: 200px"
clearable
/>
</el-form-item>
</template>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
查询
</el-button>
</el-form-item>
<el-form-item>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never">
<el-table :data="tableData" style="width: 100%" v-loading="loading" border>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="no" label="流程编号" width="180" />
<!-- 动态字段列(放在流程编号后面) -->
<template v-if="Array.isArray(dynamicFields) && dynamicFields.length > 0">
<el-table-column
v-for="field in dynamicFields"
:key="field.id"
:prop="`data.${field.name}`"
:label="field.label"
:min-width="field.type === 'relation' ? 300 : getFieldWidth(field.type)"
>
<template #default="scope">
<div v-if="field.type === 'relation'" class="relation-field-renderer">
<el-table
:data="getRelationFieldData(scope.row.data, field)"
border
size="small"
style="width: 100%"
:show-header="true"
>
<el-table-column
v-for="subField in getSubFormFields(field.name)"
:key="subField.id"
:prop="subField.name"
:label="subField.label"
:min-width="100"
>
<template #default="subScope">
{{ formatSubFieldValue(subScope.row, subField) }}
</template>
</el-table-column>
</el-table>
</div>
<span v-else>{{ formatFieldValue(scope.row.data, field) }}</span>
</template>
</el-table-column>
</template>
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
<template #default="scope">
<div class="title-cell">
<span>{{ scope.row.title || '-' }}</span>
<el-tooltip
:content="scope.row.is_created_by_me ? '我创建的' : '我办理过的'"
placement="top"
>
<el-icon
:size="14"
:color="scope.row.is_created_by_me ? '#409eff' : '#67c23a'"
style="margin-left: 6px; cursor: pointer;"
>
<component :is="scope.row.is_created_by_me ? User : Check" />
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column prop="creator" label="申请人" width="100">
<template #default="scope">
{{ scope.row.creator?.name || scope.row.creator_name || '-' }}
</template>
</el-table-column>
<el-table-column prop="creator_department" label="部门" width="120">
<template #default="scope">
{{ scope.row.creator_department?.name || scope.row.creator_department_name || '-' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="current_node" label="当前步骤" width="120">
<template #default="scope">
{{ scope.row.current_node?.name || scope.row.currentNode?.name || '-' }}
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="scope">
{{ formatDateTime(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="相关支付情况" width="200" fixed="right">
<template #default="scope">
<div v-if="scope.row.related_payments && scope.row.related_payments.length > 0" class="related-payments">
<el-tag
v-for="(payment, idx) in scope.row.related_payments"
:key="payment.id"
type="primary"
size="small"
class="payment-tag"
@click.stop="handleViewPayment(payment)"
>
{{ payment.serial_number }}
</el-tag>
</div>
<span v-else class="no-payment">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleView(scope.row)">
查看
</el-button>
<el-button
v-if="hasGlobalFlowSupervisionRole"
type="success"
link
size="small"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
Search,
Refresh,
Document,
User,
Check
} from '@element-plus/icons-vue'
// 动态导入所有图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { preApprovalProcessConfigAPI, oaFlowAPI } from '@/utils/api'
import { getToken } from '@/utils/auth'
import config from '@/config'
import { useUserStore } from '@/store/user'
const router = useRouter()
const userStore = useUserStore()
// 加载状态
const loading = ref(false)
const subprocessLoading = ref(false)
const loadingYears = ref(false)
// 请求序列号,用于防止竞态条件
let requestSequence = 0
// 可用子流程列表
const availableSubprocesses = ref([])
// 选中的子流程
const selectedSubprocess = ref(null)
// 查询类型not-linked未关联支付或 linked已关联支付
const queryType = ref('not-linked')
// 动态字段列表show_in_list=1的字段
const dynamicFields = ref([])
// 子表单字段映射(用于 relation 类型字段)
const subFormFieldsMap = ref({})
// 选项数据映射(用于 select 类型字段key 为 selection_modelvalue 为选项数组)
const selectionOptionsMap = ref({})
// 表格数据
const tableData = ref([])
// 年份相关
const availableYears = ref([]) // 可用年份列表
const selectedYear = ref(null) // 选中的年份
// 筛选表单
const filterForm = ref({
keyword: '',
field_id: '',
operator: '',
field_keyword: ''
})
// 操作符选项
const operatorOptions = [
{ id: 'eq', label: '等于' },
{ id: 'neq', label: '不等于' },
{ id: 'like', label: '包含' }
]
// 所有模型字段用于高级查询不仅仅是show_in_list=1的字段
const allModelFields = ref([])
// 分页
const pagination = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
// 图标映射
const iconMap = {
Document
}
// 获取图标组件
const getIcon = (iconName) => {
if (!iconName) {
return Document
}
// 首先尝试从静态映射表获取
if (iconMap[iconName]) {
return iconMap[iconName]
}
// 然后尝试从ElementPlusIconsVue动态获取
if (ElementPlusIconsVue[iconName]) {
return ElementPlusIconsVue[iconName]
}
// 如果找不到,返回默认图标
return Document
}
// 获取状态类型
const getStatusType = (status) => {
const statusMap = {
0: 'warning', // 待审批
1: 'success', // 已批准
2: 'danger', // 已拒绝
3: 'info' // 已撤回
}
return statusMap[status] || ''
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
0: '待审批',
1: '已批准',
2: '已拒绝',
3: '已撤回'
}
return statusMap[status] || '未知'
}
// 格式化日期时间
const formatDateTime = (dateTime) => {
if (!dateTime) return '-'
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 获取字段宽度
const getFieldWidth = (fieldType) => {
const widthMap = {
'text': 120,
'textarea': 200,
'number': 100,
'money': 120,
'date': 120,
'datetime': 150,
'select': 120,
'file': 150
}
return widthMap[fieldType] || 120
}
// 获取子表单字段
const getSubFormFields = (fieldName) => {
return subFormFieldsMap.value[fieldName] || []
}
// 获取 relation 字段的数据(转换为数组格式)
const getRelationFieldData = (data, field) => {
if (!data || !field || !field.name) return []
let value = data[field.name]
// 如果 value 是字符串,尝试解析为 JSON
if (typeof value === 'string') {
try {
value = JSON.parse(value)
} catch (e) {
console.warn('解析 relation 字段 JSON 失败:', value, e)
return []
}
}
// 确保是数组
if (!Array.isArray(value)) {
return []
}
return value
}
// 格式化子表单字段值
const formatSubFieldValue = (rowData, subField) => {
if (!rowData || !subField || !subField.name) return '-'
let value = rowData[subField.name]
if (value === null || value === undefined || value === '') return '-'
switch (subField.type) {
case 'number':
case 'money':
return typeof value === 'number' ? value.toLocaleString('zh-CN') : value
case 'date':
if (value) {
try {
const date = new Date(value)
if (!isNaN(date.getTime())) {
return date.toLocaleDateString('zh-CN')
}
} catch (e) {
console.warn('日期格式化失败:', value, e)
}
}
return '-'
case 'datetime':
if (value) {
try {
return formatDateTime(value)
} catch (e) {
console.warn('日期时间格式化失败:', value, e)
}
}
return '-'
case 'select':
case 'radio':
// 如果有 selection_model从 selectionOptionsMap 中查找
if (subField.selection_model && selectionOptionsMap.value[subField.selection_model]) {
const options = selectionOptionsMap.value[subField.selection_model]
const option = options.find(opt =>
opt.id == value ||
opt.value == value ||
String(opt.id) === String(value) ||
String(opt.value) === String(value)
)
if (option) {
return option.label || option.name || value
}
}
// 如果没有 selection_model 或找不到,尝试使用 subField.options
if (subField.options && Array.isArray(subField.options)) {
const option = subField.options.find(opt => opt.value === value || opt.id === value)
return option ? (option.label || option.name || value) : value
}
return value
default:
return String(value)
}
}
// 格式化字段值
const formatFieldValue = (data, field) => {
if (!data || !field || !field.name) return '-'
// relation 类型已经在模板中特殊处理,这里不需要处理
if (field.type === 'relation') {
return ''
}
// 尝试多种方式获取值
let value = data[field.name]
// 如果 data 是对象但没有该字段,尝试其他路径
if (value === undefined && typeof data === 'object') {
// 尝试使用字段的 label 或其他可能的键
value = data[field.label] || data[`${field.name}_text`] || null
}
if (value === null || value === undefined || value === '') return '-'
switch (field.type) {
case 'number':
case 'money':
return typeof value === 'number' ? value.toLocaleString('zh-CN') : value
case 'date':
if (value) {
try {
const date = new Date(value)
if (!isNaN(date.getTime())) {
return date.toLocaleDateString('zh-CN')
}
} catch (e) {
console.warn('日期格式化失败:', value, e)
}
}
return '-'
case 'datetime':
if (value) {
try {
return formatDateTime(value)
} catch (e) {
console.warn('日期时间格式化失败:', value, e)
}
}
return '-'
case 'select':
case 'radio':
// 如果有 selection_model从 selectionOptionsMap 中查找
if (field.selection_model && selectionOptionsMap.value[field.selection_model]) {
const options = selectionOptionsMap.value[field.selection_model]
const option = options.find(opt =>
opt.id == value ||
opt.value == value ||
String(opt.id) === String(value) ||
String(opt.value) === String(value)
)
if (option) {
return option.label || option.name || value
}
}
// 如果没有 selection_model 或找不到,尝试使用 field.options
if (field.options && Array.isArray(field.options)) {
const option = field.options.find(opt => opt.value === value || opt.id === value)
return option ? (option.label || option.name || value) : value
}
return value
case 'file':
case 'files':
// 文件类型,显示文件数量或文件名
if (Array.isArray(value)) {
return `${value.length} 个文件`
}
return value
default:
return String(value)
}
}
// 加载年份列表
const loadAvailableYears = async (expectedCustomModelId = null) => {
if (!selectedSubprocess.value || !selectedSubprocess.value.custom_model_id) {
availableYears.value = []
return
}
const currentCustomModelId = selectedSubprocess.value.custom_model_id
// 如果传入了期望的 custom_model_id检查是否仍然匹配
if (expectedCustomModelId && currentCustomModelId !== expectedCustomModelId) {
return
}
loadingYears.value = true
try {
const response = await oaFlowAPI.getFlowYears({
custom_model_id: currentCustomModelId
})
// 再次检查 custom_model_id 是否仍然匹配
if (selectedSubprocess.value?.custom_model_id !== currentCustomModelId) {
return
}
if (response.code === 0 && Array.isArray(response.data)) {
availableYears.value = response.data
// 自动选定当年
const currentYear = new Date().getFullYear()
if (availableYears.value.includes(currentYear)) {
selectedYear.value = currentYear
} else if (availableYears.value.length > 0) {
// 如果没有当年,选择最新的年份
selectedYear.value = availableYears.value[0]
} else {
selectedYear.value = null
}
} else {
availableYears.value = []
// 如果没有年份数据,默认使用当年
selectedYear.value = new Date().getFullYear()
}
} catch (error) {
// 只有在 custom_model_id 仍然匹配时才处理错误
if (selectedSubprocess.value?.custom_model_id === currentCustomModelId) {
console.error('加载年份列表失败:', error)
availableYears.value = []
// 出错时默认使用当年
selectedYear.value = new Date().getFullYear()
}
} finally {
// 只有在 custom_model_id 仍然匹配时才更新加载状态
if (selectedSubprocess.value?.custom_model_id === currentCustomModelId) {
loadingYears.value = false
}
}
}
// 年份切换
const handleYearChange = () => {
pagination.value.currentPage = 1
// 年份切换时只清空关键词,保留高级查询条件
filterForm.value.keyword = ''
// 增加请求序列号
requestSequence++
loadFlowList(requestSequence)
}
// 选择子流程
const handleSubprocessSelect = async (subprocess) => {
if (!subprocess.custom_model_id) {
ElMessage.warning('该流程项未关联OA模型')
return
}
// 增加请求序列号,取消之前的请求
requestSequence++
const currentSequence = requestSequence
// 重置分页和筛选
pagination.value.currentPage = 1
filterForm.value = {
keyword: '',
field_id: '',
operator: '',
field_keyword: ''
}
selectedSubprocess.value = subprocess
// 清空子表单字段映射、选项数据映射和所有字段
subFormFieldsMap.value = {}
selectionOptionsMap.value = {}
allModelFields.value = []
// 清空表格数据,避免显示旧数据
tableData.value = []
pagination.value.total = 0
try {
// 加载模型字段(传入期望的 custom_model_id 用于验证)
await loadModelFields(subprocess.custom_model_id, subprocess.custom_model_id)
// 检查是否仍然是最新的请求
if (currentSequence !== requestSequence) {
return
}
// 再次检查 selectedSubprocess 是否仍然匹配
if (selectedSubprocess.value?.custom_model_id !== subprocess.custom_model_id) {
return
}
// 先加载年份列表(传入期望的 custom_model_id 用于验证)
await loadAvailableYears(subprocess.custom_model_id)
// 检查是否仍然是最新的请求
if (currentSequence !== requestSequence) {
return
}
// 再次检查 selectedSubprocess 是否仍然匹配
if (selectedSubprocess.value?.custom_model_id !== subprocess.custom_model_id) {
return
}
// 年份加载完成后,再加载流程列表
if (selectedYear.value && selectedSubprocess.value?.custom_model_id === subprocess.custom_model_id) {
await loadFlowList(currentSequence)
}
} catch (error) {
// 如果请求被取消或出错,不显示错误(可能是用户快速切换导致的)
if (currentSequence === requestSequence) {
console.error('加载子流程数据失败:', error)
}
}
}
// 查询类型改变
const handleQueryTypeChange = () => {
if (selectedSubprocess.value) {
// 重置分页
pagination.value.currentPage = 1
// 增加请求序列号
requestSequence++
loadFlowList(requestSequence)
}
}
// 加载可用子流程
const loadAvailableSubprocesses = async () => {
subprocessLoading.value = true
try {
const res = await preApprovalProcessConfigAPI.getForStartProcess()
if (res.code === 0) {
// 提取所有流程项(第二层)
const subprocesses = []
// 确保 res.data 是数组
const data = Array.isArray(res.data) ? res.data : []
data.forEach(group => {
// 确保 children 是数组
const children = Array.isArray(group.active_children)
? group.active_children
: Array.isArray(group.activeChildren)
? group.activeChildren
: Array.isArray(group.children)
? group.children
: []
children.forEach(child => {
if (child && child.custom_model_id) {
subprocesses.push({
id: child.id,
name: child.name,
description: child.description,
icon: child.icon || child.custom_model?.icon,
custom_model_id: child.custom_model_id,
custom_model: child.custom_model
})
}
})
})
availableSubprocesses.value = subprocesses
// 默认选中第一个
if (subprocesses.length > 0) {
await handleSubprocessSelect(subprocesses[0])
}
} else {
ElMessage.error(res.msg || res.message || '获取流程配置失败')
availableSubprocesses.value = []
}
} catch (error) {
ElMessage.error('获取流程配置失败:' + error.message)
availableSubprocesses.value = []
} finally {
subprocessLoading.value = false
}
}
// 加载模型字段
const loadModelFields = async (customModelId, expectedCustomModelId = null) => {
// 如果传入了期望的 custom_model_id检查是否仍然匹配
if (expectedCustomModelId && customModelId !== expectedCustomModelId) {
return
}
try {
const res = await oaFlowAPI.getCustomModelFields(customModelId)
// 再次检查 custom_model_id 是否仍然匹配
if (expectedCustomModelId && customModelId !== expectedCustomModelId) {
return
}
// 检查当前选中的子流程是否仍然匹配
if (selectedSubprocess.value?.custom_model_id !== customModelId) {
return
}
if (res.code === 0 && res.data?.customModel?.fields) {
// 确保 fields 是数组
const fieldsArray = Array.isArray(res.data.customModel.fields)
? res.data.customModel.fields
: []
// 筛选 show_in_list = 1 的字段,并按 myindex 排序(用于列表显示)
const fields = fieldsArray
.filter(field => field && field.show_in_list === 1)
.sort((a, b) => (a.myindex || 0) - (b.myindex || 0))
// 所有字段用于高级查询过滤掉没有name的字段
const allFields = fieldsArray
.filter(field => field && field.name)
.sort((a, b) => (a.myindex || 0) - (b.myindex || 0))
// 再次检查是否仍然匹配
if (selectedSubprocess.value?.custom_model_id !== customModelId) {
return
}
dynamicFields.value = fields
allModelFields.value = allFields
// 加载 select 类型字段的选项数据(如果有 selection_model
const selectFields = fields.filter(field =>
(field.type === 'select' || field.type === 'radio') &&
field.selection_model
)
for (const selectField of selectFields) {
// 检查是否仍然匹配
if (selectedSubprocess.value?.custom_model_id !== customModelId) {
break
}
// 如果还没有加载过这个 selection_model 的选项,则加载
if (!selectionOptionsMap.value[selectField.selection_model]) {
try {
const optionsRes = await oaFlowAPI.getSelectionOptions(selectField.selection_model)
// 再次检查是否仍然匹配
if (selectedSubprocess.value?.custom_model_id !== customModelId) {
break
}
if (optionsRes.code === 0 && Array.isArray(optionsRes.data)) {
selectionOptionsMap.value[selectField.selection_model] = optionsRes.data
} else {
selectionOptionsMap.value[selectField.selection_model] = []
}
} catch (error) {
// 只有在仍然匹配时才记录错误
if (selectedSubprocess.value?.custom_model_id === customModelId) {
console.error(`加载选项数据失败 (${selectField.selection_model}):`, error)
selectionOptionsMap.value[selectField.selection_model] = []
}
}
}
}
// 加载 relation 类型字段的子表单字段
const relationFields = fields.filter(field => field.type === 'relation' && field.sub_custom_model_id)
for (const relationField of relationFields) {
// 检查是否仍然匹配
if (selectedSubprocess.value?.custom_model_id !== customModelId) {
break
}
if (relationField.sub_custom_model_id && !subFormFieldsMap.value[relationField.name]) {
try {
const subRes = await oaFlowAPI.getCustomModelFields(relationField.sub_custom_model_id)
// 再次检查是否仍然匹配
if (selectedSubprocess.value?.custom_model_id !== customModelId) {
break
}
if (subRes.code === 0 && subRes.data?.customModel?.fields) {
const subFields = Array.isArray(subRes.data.customModel.fields)
? subRes.data.customModel.fields
: []
// 按 myindex 排序
subFormFieldsMap.value[relationField.name] = subFields.sort((a, b) => (a.myindex || 0) - (b.myindex || 0))
// 加载子表单中 select 类型字段的选项数据(如果有 selection_model
const subSelectFields = subFields.filter(field =>
(field.type === 'select' || field.type === 'radio') &&
field.selection_model
)
for (const subSelectField of subSelectFields) {
// 如果还没有加载过这个 selection_model 的选项,则加载
if (!selectionOptionsMap.value[subSelectField.selection_model]) {
try {
const optionsRes = await oaFlowAPI.getSelectionOptions(subSelectField.selection_model)
if (optionsRes.code === 0 && Array.isArray(optionsRes.data)) {
selectionOptionsMap.value[subSelectField.selection_model] = optionsRes.data
} else {
selectionOptionsMap.value[subSelectField.selection_model] = []
}
} catch (error) {
console.error(`加载子表单选项数据失败 (${subSelectField.selection_model}):`, error)
selectionOptionsMap.value[subSelectField.selection_model] = []
}
}
}
}
} catch (error) {
// 只有在仍然匹配时才记录错误
if (selectedSubprocess.value?.custom_model_id === customModelId) {
console.error(`加载子表单字段失败 (${relationField.name}):`, error)
subFormFieldsMap.value[relationField.name] = []
}
}
}
}
} else {
// 只有在仍然匹配时才更新
if (selectedSubprocess.value?.custom_model_id === customModelId) {
dynamicFields.value = []
}
}
} catch (error) {
// 只有在仍然匹配时才记录错误
if (selectedSubprocess.value?.custom_model_id === customModelId) {
console.error('获取模型字段失败:', error)
dynamicFields.value = []
}
}
}
// 加载流程列表
const loadFlowList = async (sequence = null) => {
// 如果没有传入序列号,使用当前序列号
if (sequence === null) {
requestSequence++
sequence = requestSequence
}
if (!selectedSubprocess.value || !selectedSubprocess.value.custom_model_id) {
return
}
// 如果没有选中年份,不加载数据
if (!selectedYear.value) {
tableData.value = []
pagination.value.total = 0
return
}
// 保存当前请求的关键参数,用于验证返回结果是否仍然有效
const currentCustomModelId = selectedSubprocess.value.custom_model_id
const currentYear = selectedYear.value
const currentQueryType = queryType.value
const currentPage = pagination.value.currentPage
loading.value = true
try {
const params = {
custom_model_id: currentCustomModelId,
year: currentYear, // 必填:年份参数
page: currentPage,
page_size: pagination.value.pageSize,
is_simple: 0, // 完整版本包含data字段
payment_link_status: currentQueryType, // 关联支付状态not-linked 或 linked
keyword: filterForm.value.keyword, // 流程编号关键词
// 高级查询参数
field_id: filterForm.value.field_id || undefined,
operator: filterForm.value.operator || undefined,
field_keyword: filterForm.value.field_keyword || undefined
}
// 移除空值
Object.keys(params).forEach(key => {
if (params[key] === '' || params[key] === null || params[key] === undefined) {
delete params[key]
}
})
// 使用 "all" 类型,因为我们现在通过 payment_link_status 参数来过滤
const res = await oaFlowAPI.getFlowList('all', params)
// 检查请求是否仍然有效(序列号是否匹配,关键参数是否改变)
if (sequence !== requestSequence) {
// 请求已被新的请求取代,忽略此结果
return
}
// 验证关键参数是否仍然匹配
if (
selectedSubprocess.value?.custom_model_id !== currentCustomModelId ||
selectedYear.value !== currentYear ||
queryType.value !== currentQueryType ||
pagination.value.currentPage !== currentPage
) {
// 关键参数已改变,忽略此结果
return
}
if (res.code === 0) {
// 后端返回结构res.data.data 是分页对象Laravel Paginator
// 分页对象包含:{ data: [...], total: 100, current_page: 1, per_page: 10, ... }
const paginationData = res.data?.data
if (paginationData) {
// 如果 paginationData 有 data 属性,说明是分页对象
if (paginationData.data && Array.isArray(paginationData.data)) {
tableData.value = paginationData.data
pagination.value.total = paginationData.total || 0
}
// 如果 paginationData 本身就是数组,直接使用
else if (Array.isArray(paginationData)) {
tableData.value = paginationData
// 尝试从其他地方获取总数
pagination.value.total = res.data?.total || paginationData.length || 0
}
// 其他情况,尝试作为数组处理
else {
tableData.value = []
pagination.value.total = 0
console.warn('未识别的数据格式,完整响应:', res)
}
} else {
// 如果没有 paginationData尝试直接使用 res.data.data
const data = res.data?.data
if (Array.isArray(data)) {
tableData.value = data
pagination.value.total = res.data?.total || data.length || 0
} else {
tableData.value = []
pagination.value.total = 0
console.warn('数据格式异常,完整响应:', res)
}
}
// 调试信息:检查数据是否正确加载
if (tableData.value.length === 0 && pagination.value.total > 0) {
console.warn('数据为空但总数不为0可能数据格式不匹配')
}
} else {
// 只有在请求仍然有效时才显示错误
if (sequence === requestSequence) {
ElMessage.error(res.msg || res.message || '获取流程列表失败')
}
tableData.value = []
pagination.value.total = 0
}
} catch (error) {
// 只有在请求仍然有效时才显示错误
if (sequence === requestSequence) {
ElMessage.error('获取流程列表失败:' + error.message)
}
tableData.value = []
pagination.value.total = 0
} finally {
// 只有在请求仍然有效时才更新加载状态
if (sequence === requestSequence) {
loading.value = false
}
}
}
// 查询
const handleSearch = () => {
pagination.value.currentPage = 1
requestSequence++
loadFlowList(requestSequence)
}
// 重置
const handleReset = () => {
filterForm.value = {
keyword: '',
field_id: '',
operator: '',
field_keyword: ''
}
pagination.value.currentPage = 1
// 年份不重置,保持当前选择
requestSequence++
loadFlowList(requestSequence)
}
// 查看支付详情
const handleViewPayment = (payment) => {
// 打开新窗口显示打印预览页
const url = router.resolve({
name: 'PaymentDetailPrint',
params: { id: payment.id }
}).href
window.open(url, '_blank')
}
// 查看
const handleView = (row) => {
// 跳转到OA流程详情页面
const token = getToken()
const baseUrl = '/oa/#/flow/detail'
const params = new URLSearchParams({
module_id: (row.custom_model_id || row.custom_model?.id || row.customModel?.id || '').toString(),
flow_id: row.id.toString(),
isSinglePage: '1',
module_name: 'oa',
form_canal: 'budget'
})
if (token) {
params.set('auth_token', token)
}
const fullUrl = `${baseUrl}?${params.toString()}`
// 在新窗口打开
window.open(fullUrl, '_blank')
}
// 检查用户是否有"全局流程监管"角色
const hasGlobalFlowSupervisionRole = computed(() => {
return userStore.roles && userStore.roles.includes('全局流程监管')
})
// 编辑
const handleEdit = (row) => {
// 跳转到OA流程编辑页面
const token = getToken()
const baseUrl = '/oa/#/flow/edit'
const params = new URLSearchParams({
flow_id: row.id.toString(),
isSinglePage: '1'
})
if (token) {
params.set('auth_token', token)
}
const fullUrl = `${baseUrl}?${params.toString()}`
// 在新窗口打开
window.open(fullUrl, '_blank')
}
// 分页大小改变
const handleSizeChange = (val) => {
pagination.value.pageSize = val
pagination.value.currentPage = 1
requestSequence++
loadFlowList(requestSequence)
}
// 当前页改变
const handleCurrentChange = (val) => {
pagination.value.currentPage = val
requestSequence++
loadFlowList(requestSequence)
}
// 页面加载
onMounted(() => {
loadAvailableSubprocesses()
})
</script>
<style scoped>
.process-query-container {
padding: 20px;
background: #f5f7fa;
min-height: 100%;
}
.page-header {
background: white;
padding: 25px 30px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #333;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
.subprocess-selector-section {
margin-bottom: 20px;
}
.subprocess-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.subprocess-card {
padding: 20px;
border: 2px solid #e4e7ed;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.subprocess-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.subprocess-card.active {
border-color: #409eff;
background: #409eff;
color: white;
}
.subprocess-card.active .subprocess-icon {
color: white;
}
.subprocess-icon {
margin-bottom: 12px;
color: #409eff;
}
.subprocess-card.active .subprocess-icon {
color: white;
}
.subprocess-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
}
.subprocess-description {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
.subprocess-card.active .subprocess-description {
color: rgba(255, 255, 255, 0.8);
}
.filter-section {
margin-bottom: 20px;
}
.filter-bar {
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 16px;
padding: 12px;
background: #f5f7fa;
border-radius: 4px;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 14px;
color: #606266;
font-weight: 500;
white-space: nowrap;
}
.type-cell {
display: flex;
align-items: center;
gap: 8px;
}
.title-cell {
display: flex;
align-items: center;
gap: 0;
}
.related-payments {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.payment-tag {
cursor: pointer;
transition: all 0.2s;
}
.payment-tag:hover {
opacity: 0.8;
transform: scale(1.05);
}
.no-payment {
color: #909399;
font-style: italic;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.relation-field-renderer {
max-width: 100%;
overflow-x: auto;
}
.relation-field-renderer :deep(.el-table) {
font-size: 12px;
}
.relation-field-renderer :deep(.el-table th) {
padding: 8px 4px;
background-color: #f5f7fa;
font-weight: 500;
}
.relation-field-renderer :deep(.el-table td) {
padding: 6px 4px;
}
@media (max-width: 768px) {
.subprocess-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.subprocess-card {
padding: 16px;
}
}
</style>