优化:全面重构任务节点查询弹窗UI设计,统一使用网站设计风格

This commit is contained in:
Your Name 2026-01-24 11:02:06 +08:00
parent 0bb9975f31
commit d987fb6016

View File

@ -2,25 +2,34 @@
<a-modal
v-model:open="dialogVisible"
title="任务节点查询"
width="720px"
width="680px"
:footer="null"
:maskClosable="false"
@cancel="handleClose"
class="task-flow-query-modal"
centered
>
<!-- 搜索框 -->
<div class="search-box">
<a-input-search
v-model:value="searchInput"
placeholder="请输入任务编号或任务标题关键字"
size="large"
enter-button="查询"
:loading="loading"
@search="handleSearch"
>
<template #prefix>
<Icon name="search" size="sm" />
</template>
</a-input-search>
<div class="search-section">
<div class="search-input-wrapper">
<Icon name="search" size="sm" class="search-prefix-icon" />
<input
v-model="searchInput"
type="text"
class="search-input"
placeholder="请输入任务编号或任务标题关键字"
@keyup.enter="handleSearch"
/>
<button
class="search-btn"
:class="{ loading: loading }"
:disabled="loading"
@click="handleSearch"
>
<span v-if="!loading">查询</span>
<span v-else class="loading-spinner"></span>
</button>
</div>
<div class="search-tip">
支持输入完整任务编号(如 TASK202601221029343146)或任务标题关键字搜索
</div>
@ -29,30 +38,38 @@
<!-- 查询结果 -->
<div v-if="result" class="result-container">
<!-- 基本信息卡片 -->
<a-card class="info-card" size="small">
<template #title>
<div class="card-header">
<span>任务信息</span>
<a-tag v-if="result.task.isPrimaryTask" color="warning">一级需求任务</a-tag>
<a-tag v-else>普通任务</a-tag>
<a-tag v-if="result.task.isPipeline" color="processing">流水线</a-tag>
<div class="info-section">
<div class="section-header">
<span class="section-title">任务信息</span>
<div class="task-tags">
<span v-if="result.task.isPrimaryTask" class="tag tag-warning">一级需求任务</span>
<span v-else class="tag tag-default">普通任务</span>
<span v-if="result.task.isPipeline" class="tag tag-info">流水线</span>
</div>
</template>
<a-descriptions :column="2" bordered size="small">
<a-descriptions-item label="任务编号">
<a-typography-text copyable>{{ result.task.taskNo }}</a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="任务标题">{{ result.task.title }}</a-descriptions-item>
<a-descriptions-item label="任务积分">
<span class="score-text">{{ result.task.score }} 分</span>
</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ formatTime(result.task.createTime) }}</a-descriptions-item>
</a-descriptions>
</a-card>
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">任务编号</span>
<span class="info-value task-no">{{ result.task.taskNo }}</span>
</div>
<div class="info-item">
<span class="info-label">任务标题</span>
<span class="info-value">{{ result.task.title }}</span>
</div>
<div class="info-item">
<span class="info-label">任务积分</span>
<span class="info-value score">{{ result.task.score }} 分</span>
</div>
<div class="info-item">
<span class="info-label">创建时间</span>
<span class="info-value">{{ formatTime(result.task.createTime) }}</span>
</div>
</div>
</div>
<!-- 当前节点卡片(重点展示) -->
<div class="current-node-card">
<div class="current-node">
<div class="current-node-section">
<div class="current-node-content">
<div class="node-icon">📍</div>
<div class="node-info">
<div class="node-status">当前节点:{{ result.currentNode.statusName }}</div>
@ -71,70 +88,88 @@
</div>
<!-- 流程时间线 -->
<a-card class="timeline-card" size="small" title="流程进度">
<a-timeline>
<a-timeline-item
v-for="(node, index) in result.flowTimeline"
<div class="timeline-section">
<div class="section-header">
<span class="section-title">流程进度</span>
</div>
<div class="timeline">
<div
v-for="(node, index) in result.flowTimeline"
:key="index"
:color="getTimelineColor(node.status)"
class="timeline-item"
:class="[`status-${node.status}`]"
>
<div class="timeline-node">
<div class="node-name">
{{ node.nodeName }}
<a-tag v-if="node.status === 'pending'" color="warning" size="small">当前</a-tag>
<div class="timeline-dot"></div>
<div class="timeline-line" v-if="index < result.flowTimeline.length - 1"></div>
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-title">{{ node.nodeName }}</span>
<span v-if="node.status === 'pending'" class="tag tag-current">当前</span>
</div>
<div class="node-time">
<div class="timeline-time">
{{ node.time ? formatTime(node.time) : (node.status === 'pending' ? '进行中...' : '等待中') }}
</div>
<div class="node-operator" v-if="node.operatorName">
<div class="timeline-operator" v-if="node.operatorName">
<Icon name="user" size="xs" />
{{ node.operatorName }}
<span v-if="node.operatorDept">({{ node.operatorDept }})</span>
<span>{{ node.operatorName }}</span>
<span v-if="node.operatorDept" class="operator-dept">({{ node.operatorDept }})</span>
</div>
<div class="node-comment" v-if="node.comment">
<div class="timeline-comment" v-if="node.comment">
<Icon name="info" size="xs" />
{{ node.comment }}
<span>{{ node.comment }}</span>
</div>
</div>
</a-timeline-item>
</a-timeline>
</a-card>
</div>
</div>
</div>
<!-- 相关人员 -->
<a-card class="users-card" size="small" title="相关人员">
<div class="users-section">
<div class="section-header">
<span class="section-title">相关人员</span>
</div>
<div class="users-grid">
<div class="user-item" v-if="result.relatedUsers.publisher">
<div class="user-card" 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-card" 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-card" 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-card" 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>
</a-card>
</div>
</div>
<!-- 空状态 -->
<a-empty v-else-if="searched && !result && !loading" description="未找到相关任务" />
<div v-else-if="searched && !result && !loading" class="empty-state">
<Icon name="inbox" size="xl" class="empty-icon" />
<p>未找到相关任务</p>
</div>
<!-- 初始状态 -->
<div v-else-if="!searched" class="initial-state">
<Icon name="search" size="xl" class="search-icon" />
<div v-else-if="!searched && !loading" class="initial-state">
<Icon name="search" size="xl" class="initial-icon" />
<p>输入任务编号或标题关键字查询任务流程状态</p>
</div>
<!-- 加载状态 -->
<div v-if="loading && !result" class="loading-state">
<div class="loading-spinner large"></div>
<p>正在查询...</p>
</div>
</a-modal>
</template>
@ -210,18 +245,6 @@ const handleClose = () => {
searched.value = false
}
// 获取时间线颜色
const getTimelineColor = (status: string) => {
switch (status) {
case 'completed':
return 'green'
case 'pending':
return 'orange'
default:
return 'gray'
}
}
// 格式化时间
const formatTime = (time: string | undefined) => {
if (!time) return ''
@ -241,228 +264,37 @@ defineExpose({
<style scoped lang="scss">
@import "@/styles/variables.scss";
.search-box {
margin-bottom: 20px;
// 搜索区域
.search-section {
margin-bottom: $space-lg;
}
.search-tip {
margin-top: 8px;
font-size: 12px;
.search-input-wrapper {
display: flex;
align-items: center;
background: $bg-tertiary;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: 0 $space-md;
transition: all $transition-base $ease-out;
&:focus-within {
border-color: $brand-primary;
box-shadow: 0 0 0 2px rgba($brand-primary, 0.1);
}
.search-prefix-icon {
color: $text-tertiary;
}
}
.result-container {
max-height: 60vh;
overflow-y: auto;
.info-card {
margin-bottom: 16px;
.card-header {
display: flex;
align-items: center;
gap: 8px;
}
.score-text {
color: $brand-primary;
font-weight: 600;
}
}
.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: $text-primary;
display: flex;
align-items: center;
gap: 8px;
}
.node-time {
font-size: 12px;
color: $text-tertiary;
margin-top: 4px;
}
.node-operator {
display: flex;
align-items: center;
gap: 4px;
color: $brand-primary;
font-size: 13px;
margin-top: 6px;
}
.node-comment {
display: flex;
align-items: center;
gap: 4px;
color: $color-success;
font-size: 12px;
margin-top: 4px;
}
}
}
.users-card {
.users-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
@media (max-width: 600px) {
grid-template-columns: repeat(2, 1fr);
}
.user-item {
text-align: center;
padding: 12px;
background: $bg-tertiary;
border-radius: 8px;
.user-role {
font-size: 12px;
color: $text-tertiary;
margin-bottom: 6px;
}
.user-name {
font-size: 14px;
font-weight: 600;
color: $text-primary;
margin-bottom: 4px;
}
.user-dept {
font-size: 12px;
color: $text-secondary;
}
}
}
}
}
.initial-state {
text-align: center;
padding: 60px 0;
color: $text-tertiary;
.search-icon {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
p {
font-size: 14px;
}
}
// 暗色模式下的 Modal 样式
:deep(.ant-modal-content) {
background: $bg-secondary;
.ant-modal-header {
background: $bg-secondary;
border-bottom-color: $border-primary;
.ant-modal-title {
color: $text-primary;
}
flex-shrink: 0;
}
.ant-modal-close-x {
color: $text-secondary;
}
}
:deep(.ant-card) {
background: $bg-card;
border-color: $border-primary;
.ant-card-head {
border-bottom-color: $border-primary;
color: $text-primary;
}
.ant-card-body {
color: $text-primary;
}
}
:deep(.ant-descriptions) {
.ant-descriptions-item-label {
background: $bg-tertiary;
color: $text-secondary;
}
.ant-descriptions-item-content {
background: $bg-card;
color: $text-primary;
}
}
:deep(.ant-timeline-item-content) {
color: $text-primary;
}
:deep(.ant-empty-description) {
color: $text-tertiary;
}
:deep(.ant-input-search) {
.ant-input {
background: $bg-card;
border-color: $border-primary;
.search-input {
flex: 1;
background: transparent;
border: none;
outline: none;
padding: $space-md $space-sm;
font-size: $font-size-base;
color: $text-primary;
&::placeholder {
@ -470,9 +302,445 @@ defineExpose({
}
}
.ant-input-group-addon {
.search-btn {
flex-shrink: 0;
background: $brand-primary;
color: $bg-primary;
border: none;
border-radius: $radius-md;
padding: $space-sm $space-lg;
font-size: $font-size-base;
font-weight: $font-weight-medium;
cursor: pointer;
transition: all $transition-base $ease-out;
display: flex;
align-items: center;
justify-content: center;
min-width: 72px;
height: 36px;
&:hover:not(:disabled) {
background: $brand-hover;
}
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
}
}
.search-tip {
margin-top: $space-sm;
font-size: $font-size-sm;
color: $text-tertiary;
}
// 加载动画
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
&.large {
width: 32px;
height: 32px;
border-width: 3px;
border-top-color: $brand-primary;
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
// 结果容器
.result-container {
max-height: 55vh;
overflow-y: auto;
padding-right: $space-xs;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
border-color: $border-primary;
}
&::-webkit-scrollbar-thumb {
background: $border-primary;
border-radius: 3px;
&:hover {
background: $text-tertiary;
}
}
}
// 通用区块样式
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $space-md;
}
.section-title {
font-size: $font-size-base;
font-weight: $font-weight-semibold;
color: $text-primary;
}
// 标签样式
.tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
font-size: $font-size-xs;
font-weight: $font-weight-medium;
border-radius: $radius-small;
margin-left: $space-xs;
&.tag-default {
background: rgba($text-secondary, 0.15);
color: $text-secondary;
}
&.tag-warning {
background: rgba($color-warning, 0.15);
color: $color-warning;
}
&.tag-info {
background: rgba($brand-primary, 0.15);
color: $brand-primary;
}
&.tag-current {
background: $color-warning;
color: $bg-primary;
}
}
.task-tags {
display: flex;
align-items: center;
}
// 任务信息区域
.info-section {
background: $bg-tertiary;
border-radius: $radius-lg;
padding: $space-md;
margin-bottom: $space-md;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $space-md $space-xl;
}
.info-item {
display: flex;
flex-direction: column;
gap: $space-xs;
}
.info-label {
font-size: $font-size-sm;
color: $text-tertiary;
}
.info-value {
font-size: $font-size-base;
color: $text-primary;
&.task-no {
font-family: $font-family-mono;
font-size: $font-size-sm;
}
&.score {
color: $brand-primary;
font-weight: $font-weight-semibold;
}
}
// 当前节点区域
.current-node-section {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, rgba(#764ba2, 0.9) 100%);
border-radius: $radius-lg;
padding: $space-lg;
margin-bottom: $space-md;
}
.current-node-content {
display: flex;
align-items: flex-start;
gap: $space-md;
.node-icon {
font-size: 32px;
line-height: 1;
}
.node-info {
flex: 1;
color: $text-white;
}
.node-status {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
margin-bottom: $space-sm;
}
.node-waiting {
font-size: $font-size-base;
margin-bottom: $space-xs;
opacity: 0.95;
strong {
font-weight: $font-weight-semibold;
}
}
.node-desc {
font-size: $font-size-sm;
opacity: 0.85;
}
}
// 时间线区域
.timeline-section {
background: $bg-tertiary;
border-radius: $radius-lg;
padding: $space-md;
margin-bottom: $space-md;
}
.timeline {
position: relative;
padding-left: $space-md;
}
.timeline-item {
position: relative;
padding-left: $space-lg;
padding-bottom: $space-md;
&:last-child {
padding-bottom: 0;
}
.timeline-dot {
position: absolute;
left: 0;
top: 4px;
width: 10px;
height: 10px;
border-radius: 50%;
background: $text-tertiary;
border: 2px solid $bg-tertiary;
z-index: 1;
}
.timeline-line {
position: absolute;
left: 4px;
top: 14px;
width: 2px;
height: calc(100% - 10px);
background: $border-primary;
}
&.status-completed {
.timeline-dot {
background: $brand-primary;
}
}
&.status-pending {
.timeline-dot {
background: $color-warning;
box-shadow: 0 0 0 4px rgba($color-warning, 0.2);
}
}
&.status-waiting {
.timeline-dot {
background: $text-disabled;
}
.timeline-content {
opacity: 0.6;
}
}
}
.timeline-content {
.timeline-header {
display: flex;
align-items: center;
gap: $space-sm;
margin-bottom: $space-xs;
}
.timeline-title {
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $text-primary;
}
.timeline-time {
font-size: $font-size-sm;
color: $text-tertiary;
margin-bottom: $space-xs;
}
.timeline-operator {
display: flex;
align-items: center;
gap: $space-xs;
font-size: $font-size-sm;
color: $brand-primary;
margin-top: $space-xs;
.operator-dept {
color: $text-secondary;
}
}
.timeline-comment {
display: flex;
align-items: center;
gap: $space-xs;
font-size: $font-size-sm;
color: $color-success;
margin-top: $space-xs;
}
}
// 相关人员区域
.users-section {
background: $bg-tertiary;
border-radius: $radius-lg;
padding: $space-md;
}
.users-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: $space-sm;
@media (max-width: 600px) {
grid-template-columns: repeat(2, 1fr);
}
}
.user-card {
background: $bg-card;
border-radius: $radius-md;
padding: $space-md;
text-align: center;
.user-role {
font-size: $font-size-xs;
color: $text-tertiary;
margin-bottom: $space-xs;
}
.user-name {
font-size: $font-size-base;
font-weight: $font-weight-semibold;
color: $text-primary;
margin-bottom: $space-xxs;
}
.user-dept {
font-size: $font-size-xs;
color: $text-secondary;
}
}
// 空状态和初始状态
.empty-state,
.initial-state,
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $space-xxl 0;
color: $text-tertiary;
.empty-icon,
.initial-icon {
font-size: 48px;
margin-bottom: $space-md;
opacity: 0.5;
}
p {
font-size: $font-size-base;
margin: 0;
}
}
.loading-state {
p {
margin-top: $space-md;
}
}
// Modal 样式覆盖
:deep(.ant-modal) {
.ant-modal-content {
background: $bg-secondary;
border: 1px solid $border-primary;
border-radius: $radius-xl;
box-shadow: $shadow-strong;
padding: 0;
}
.ant-modal-header {
background: transparent;
border-bottom: 1px solid $border-primary;
padding: $space-lg $space-xl;
margin: 0;
.ant-modal-title {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $text-primary;
}
}
.ant-modal-body {
padding: $space-xl;
}
.ant-modal-close {
top: $space-lg;
right: $space-lg;
.ant-modal-close-x {
width: 32px;
height: 32px;
line-height: 32px;
font-size: 16px;
color: $text-secondary;
transition: color $transition-base $ease-out;
&:hover {
color: $text-primary;
}
}
}
}
</style>