qiangdanxitong/frontend/src/components/TaskFlowQueryDialog.vue

406 lines
11 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>
<el-dialog
v-model="dialogVisible"
title="任务节点查询"
width="720px"
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 搜索框 -->
<div class="search-box">
<el-input
v-model="searchInput"
placeholder="请输入任务编号或任务标题关键字"
clearable
size="large"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
<template #append>
<el-button type="primary" :loading="loading" @click="handleSearch">
查询
</el-button>
</template>
</el-input>
<div class="search-tip">
支持输入完整任务编号(如 TASK202601221029343146)或任务标题关键字搜索
</div>
</div>
<!-- 查询结果 -->
<div v-if="result" class="result-container">
<!-- 基本信息卡片 -->
<el-card class="info-card" shadow="never">
<template #header>
<div class="card-header">
<span>任务信息</span>
<el-tag v-if="result.task.isPrimaryTask" type="warning" size="small">一级需求任务</el-tag>
<el-tag v-else size="small">普通任务</el-tag>
<el-tag v-if="result.task.isPipeline" type="info" size="small" style="margin-left: 8px;">流水线</el-tag>
</div>
</template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="任务编号">
<el-text type="primary" copyable>{{ result.task.taskNo }}</el-text>
</el-descriptions-item>
<el-descriptions-item label="任务标题">{{ result.task.title }}</el-descriptions-item>
<el-descriptions-item label="任务积分">
<el-text type="success">{{ result.task.score }} 分</el-text>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatTime(result.task.createTime) }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 当前节点卡片(重点展示) -->
<div class="current-node-card">
<div class="current-node">
<div class="node-icon">📍</div>
<div class="node-info">
<div class="node-status">当前节点:{{ result.currentNode.statusName }}</div>
<div class="node-waiting" v-if="result.currentNode.waitingFor">
<span v-if="result.currentNode.waitingFor.userName">
等待操作人:<strong>{{ result.currentNode.waitingFor.userName }}</strong>
<span v-if="result.currentNode.waitingFor.deptName">({{ result.currentNode.waitingFor.deptName }})</span>
</span>
<span v-else-if="result.currentNode.waitingFor.roleName">
{{ result.currentNode.waitingFor.roleName }}
</span>
</div>
<div class="node-desc">{{ result.currentNode.description }}</div>
</div>
</div>
</div>
<!-- 流程时间线 -->
<el-card class="timeline-card" shadow="never">
<template #header>
<span>流程进度</span>
</template>
<el-timeline>
<el-timeline-item
v-for="(node, index) in result.flowTimeline"
:key="index"
:type="getTimelineType(node.status)"
:hollow="node.status !== 'completed'"
:timestamp="node.time ? formatTime(node.time) : (node.status === 'pending' ? '进行中...' : '等待中')"
placement="top"
>
<div class="timeline-node">
<div class="node-name">
{{ node.nodeName }}
<el-tag v-if="node.status === 'pending'" type="warning" size="small" style="margin-left: 8px;">当前</el-tag>
</div>
<div class="node-operator" v-if="node.operatorName">
<el-icon><User /></el-icon>
{{ node.operatorName }}
<span v-if="node.operatorDept">({{ node.operatorDept }})</span>
</div>
<div class="node-comment" v-if="node.comment">
<el-icon><ChatDotRound /></el-icon>
{{ node.comment }}
</div>
</div>
</el-timeline-item>
</el-timeline>
</el-card>
<!-- 相关人员 -->
<el-card class="users-card" shadow="never">
<template #header>
<span>相关人员</span>
</template>
<div class="users-grid">
<div class="user-item" v-if="result.relatedUsers.publisher">
<div class="user-role">发起人</div>
<div class="user-name">{{ result.relatedUsers.publisher.name }}</div>
<div class="user-dept">{{ result.relatedUsers.publisher.dept }}</div>
</div>
<div class="user-item" v-if="result.relatedUsers.detailCompleter">
<div class="user-role">明细完善人</div>
<div class="user-name">{{ result.relatedUsers.detailCompleter.name }}</div>
<div class="user-dept">{{ result.relatedUsers.detailCompleter.dept }}</div>
</div>
<div class="user-item" v-if="result.relatedUsers.receiver">
<div class="user-role">接单人</div>
<div class="user-name">{{ result.relatedUsers.receiver.name }}</div>
<div class="user-dept">{{ result.relatedUsers.receiver.dept }}</div>
</div>
<div class="user-item" v-if="result.relatedUsers.currentApprover">
<div class="user-role">当前审批人</div>
<div class="user-name">{{ result.relatedUsers.currentApprover.name }}</div>
<div class="user-dept">{{ result.relatedUsers.currentApprover.dept }}</div>
</div>
</div>
</el-card>
</div>
<!-- 空状态 -->
<el-empty v-else-if="searched && !result && !loading" description="未找到相关任务" />
<!-- 初始状态 -->
<div v-else-if="!searched" class="initial-state">
<el-icon class="search-icon"><Search /></el-icon>
<p>输入任务编号或标题关键字查询任务流程状态</p>
</div>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Search, User, ChatDotRound } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getTaskFlowStatus, type TaskFlowStatusVO } from '@/api/task'
const props = defineProps<{
modelValue: boolean
initialTaskNo?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const dialogVisible = ref(props.modelValue)
const searchInput = ref('')
const loading = ref(false)
const searched = ref(false)
const result = ref<TaskFlowStatusVO | null>(null)
// 同步外部 v-model
watch(() => props.modelValue, (val) => {
dialogVisible.value = val
if (val && props.initialTaskNo) {
searchInput.value = props.initialTaskNo
handleSearch()
}
})
watch(dialogVisible, (val) => {
emit('update:modelValue', val)
})
// 处理搜索
const handleSearch = async () => {
if (!searchInput.value.trim()) {
ElMessage.warning('请输入任务编号或关键字')
return
}
loading.value = true
searched.value = true
result.value = null
try {
const input = searchInput.value.trim()
// 判断是否为任务编号格式
const isTaskNo = /^TASK\d+$/.test(input)
const params = isTaskNo
? { taskNo: input }
: { keyword: input }
const res = await getTaskFlowStatus(params)
if (res.data) {
result.value = res.data
}
} catch (error: any) {
ElMessage.error(error.message || '查询失败')
} finally {
loading.value = false
}
}
// 处理关闭
const handleClose = () => {
searchInput.value = ''
result.value = null
searched.value = false
}
// 获取时间线类型
const getTimelineType = (status: string) => {
switch (status) {
case 'completed':
return 'success'
case 'pending':
return 'warning'
default:
return 'info'
}
}
// 格式化时间
const formatTime = (time: string | undefined) => {
if (!time) return ''
const date = new Date(time)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
}
// 暴露方法给父组件
defineExpose({
search: (taskNo: string) => {
searchInput.value = taskNo
handleSearch()
}
})
</script>
<style scoped lang="scss">
.search-box {
margin-bottom: 20px;
.search-tip {
margin-top: 8px;
font-size: 12px;
color: #909399;
}
}
.result-container {
.info-card {
margin-bottom: 16px;
.card-header {
display: flex;
align-items: center;
gap: 8px;
}
}
.current-node-card {
margin-bottom: 16px;
padding: 20px;
border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.current-node {
display: flex;
align-items: flex-start;
color: #fff;
.node-icon {
font-size: 40px;
margin-right: 16px;
line-height: 1;
}
.node-info {
flex: 1;
.node-status {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.node-waiting {
font-size: 14px;
margin-bottom: 6px;
opacity: 0.95;
strong {
font-weight: 600;
}
}
.node-desc {
font-size: 13px;
opacity: 0.85;
}
}
}
}
.timeline-card {
margin-bottom: 16px;
.timeline-node {
.node-name {
font-weight: 600;
font-size: 14px;
color: #303133;
display: flex;
align-items: center;
}
.node-operator {
display: flex;
align-items: center;
gap: 4px;
color: #409eff;
font-size: 13px;
margin-top: 6px;
}
.node-comment {
display: flex;
align-items: center;
gap: 4px;
color: #67c23a;
font-size: 12px;
margin-top: 4px;
}
}
}
.users-card {
.users-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
.user-item {
text-align: center;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
.user-role {
font-size: 12px;
color: #909399;
margin-bottom: 6px;
}
.user-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.user-dept {
font-size: 12px;
color: #606266;
}
}
}
}
}
.initial-state {
text-align: center;
padding: 60px 0;
color: #909399;
.search-icon {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
}
}
// 暗色模式适配
:deep(.el-card) {
--el-card-bg-color: var(--el-bg-color);
}
</style>