qiangdanxitong/frontend/src/views/LeaderWorkbench.vue

698 lines
19 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>
<DefaultLayout>
<div class="leader-workbench-page">
<div class="container">
<div class="workbench-panel chart-card">
<div class="panel-header">
<h2 class="panel-title">
<Icon name="users" size="lg" style="margin-right: 12px; vertical-align: middle;" />
负责人工作台
</h2>
<div class="view-tabs">
<button
:class="['tab-button', { active: activeTab === 'demand' }]"
@click="switchTab('demand')"
>
<Icon name="send" size="sm" style="margin-right: 6px; vertical-align: middle;" />
部门发起任务 ({{ stats.demandCount }})
</button>
<button
:class="['tab-button', { active: activeTab === 'receiving' }]"
@click="switchTab('receiving')"
>
<Icon name="list" size="sm" style="margin-right: 6px; vertical-align: middle;" />
部门接单任务 ({{ stats.receivingCount }})
</button>
</div>
</div>
<div class="panel-toolbar">
<!-- 需求部门负责人:按审核状态筛选 -->
<a-radio-group v-if="activeTab === 'demand'" v-model:value="statusFilter" @change="handleFilterChange" size="small">
<a-radio-button value="">全部</a-radio-button>
<a-radio-button value="draft">草稿</a-radio-button>
<a-radio-button value="pending_audit">待审核</a-radio-button>
<a-radio-button value="audit_pass">审核通过</a-radio-button>
<a-radio-button value="audit_reject">审核驳回</a-radio-button>
</a-radio-group>
<!-- 接单部门负责人:按任务状态筛选 -->
<a-radio-group v-else v-model:value="statusFilter" @change="handleFilterChange" size="small">
<a-radio-button value="">全部</a-radio-button>
<a-radio-button value="in_progress">进行中</a-radio-button>
<a-radio-button value="review">待审核</a-radio-button>
<a-radio-button value="completed">已完成</a-radio-button>
</a-radio-group>
<div class="stats">
<span class="text-secondary">共 {{ total }} 个任务</span>
</div>
</div>
<div class="panel-content">
<div v-if="loading" class="loading-container panel-loading">
<a-spin size="large" />
</div>
<div v-else-if="taskList.length === 0" class="empty-state">
<Icon name="inbox" size="xxl" color="#6B7280" />
<p>暂无任务</p>
</div>
<div v-else class="task-list">
<div v-for="task in taskList" :key="task.id" class="task-item">
<!-- 任务头部 -->
<div class="task-item-header">
<div class="task-basic-info">
<h3 class="task-item-title">{{ task.title }}</h3>
<div class="task-meta">
<span class="task-no">{{ task.taskNo }}</span>
<span :class="['status-badge', `status-${task.status}`]">
{{ getStatusText(task.status) }}
</span>
<span v-if="task.auditStatus" :class="['audit-badge', `audit-${task.auditStatus}`]">
{{ getAuditStatusText(task.auditStatus) }}
</span>
<span v-if="task.publisherName" class="user-badge">
<Icon name="user" size="sm" style="margin-right: 4px; vertical-align: middle;" />
发起人:{{ task.publisherName }}
</span>
<span v-if="activeTab === 'receiving' && task.receiverName" class="user-badge">
<Icon name="user-check" size="sm" style="margin-right: 4px; vertical-align: middle;" />
接单人:{{ task.receiverName }}
</span>
</div>
</div>
<!-- 进度条 -->
<div v-if="task.status === 'in_progress'" class="progress-info">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: task.progress + '%' }"></div>
</div>
<span class="progress-text">{{ task.progress }}%</span>
</div>
</div>
<!-- 任务详情 -->
<div class="task-item-body">
<div class="task-info-grid">
<div class="info-col">
<span class="label">发起时间</span>
<span class="value">{{ formatTime(task.createTime) }}</span>
</div>
<div v-if="task.deadline" class="info-col">
<span class="label">截止时间</span>
<span :class="['value', { 'text-error': isOverdue(task.deadline) }]">
{{ formatTime(task.deadline) }}
</span>
</div>
<div class="info-col">
<span class="label">积分</span>
<span class="value text-brand">{{ task.score }}</span>
</div>
<div v-if="task.completeTime" class="info-col">
<span class="label">完成时间</span>
<span class="value text-success">{{ formatTime(task.completeTime) }}</span>
</div>
</div>
</div>
<!-- 任务操作 -->
<div class="task-item-footer">
<!-- 需求部门负责人操作 -->
<template v-if="activeTab === 'demand'">
<button
v-if="task.auditStatus === 'pending_audit'"
class="btn btn-success"
@click="handleAuditTask(task)"
>
<Icon name="check-circle" size="sm" style="margin-right: 4px; vertical-align: middle;" />
审核任务
</button>
</template>
<button class="btn btn-text" @click="handleViewDetail(task)">
查看详情
</button>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="total > 0" class="pagination inside-panel">
<a-pagination
v-model:current="pagination.page"
v-model:page-size="pagination.pageSize"
:total="total"
:show-size-changer="false"
@change="handlePageChange"
/>
</div>
</div>
</div>
</div>
</div>
<!-- 审核弹窗(复用MyTasks的审核逻辑) -->
<a-modal
v-model:open="auditModalVisible"
title="任务审核"
width="600px"
@ok="submitAudit"
@cancel="auditModalVisible = false"
>
<div class="audit-form">
<div class="task-info-summary">
<h4>{{ currentAuditTask?.title }}</h4>
<p class="task-no">任务编号:{{ currentAuditTask?.taskNo }}</p>
</div>
<a-form layout="vertical">
<a-form-item label="审核结果" required>
<a-radio-group v-model:value="auditForm.result">
<a-radio value="pass">
<Icon name="check-circle" size="sm" color="#10B981" style="margin-right: 4px; vertical-align: middle;" />
通过
</a-radio>
<a-radio value="reject">
<Icon name="x-circle" size="sm" color="#EF4444" style="margin-right: 4px; vertical-align: middle;" />
驳回
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item :label="auditForm.result === 'pass' ? '审核意见' : '驳回原因'" :required="auditForm.result === 'reject'">
<a-textarea
v-model:value="auditForm.comment"
:placeholder="auditForm.result === 'pass' ? '请输入审核意见(选填)' : '请输入驳回原因'"
:rows="4"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import Icon from '@/components/Icon.vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import * as taskApi from '@/api/task'
import type { Task } from '@/api/task'
import dayjs from 'dayjs'
const router = useRouter()
// 状态
const loading = ref(false)
const taskList = ref<Task[]>([])
const total = ref(0)
const activeTab = ref<'demand' | 'receiving'>('demand')
const statusFilter = ref('')
// 统计
const stats = reactive({
demandCount: 0,
receivingCount: 0
})
// 分页
const pagination = reactive({
page: 1,
pageSize: 10
})
// 获取状态文本
const getStatusText = (status: string | undefined) => {
const statusMap: Record<string, string> = {
draft: '草稿',
pending: '待接单',
in_progress: '进行中',
review: '待审核',
rejected: '已驳回',
completed: '已完成',
cancelled: '已取消'
}
return statusMap[status || ''] || '未知'
}
// 获取审核状态文本
const getAuditStatusText = (auditStatus: string | undefined) => {
const statusMap: Record<string, string> = {
draft: '草稿',
pending_audit: '待审核',
audit_pass: '审核通过',
audit_reject: '审核驳回'
}
return statusMap[auditStatus || ''] || ''
}
// 格式化时间
const formatTime = (time: string | undefined) => {
if (!time) return '--'
return dayjs(time).format('YYYY-MM-DD HH:mm')
}
// 判断是否逾期
const isOverdue = (deadline: string) => {
return dayjs(deadline).isBefore(dayjs())
}
// 加载统计数据
const loadStats = async () => {
try {
// 获取需求部门任务数量
const demandRes = await taskApi.getLeaderTaskList({ page: 1, pageSize: 1 })
if (demandRes.code === 200) {
stats.demandCount = demandRes.data.total || 0
}
// 获取接单部门任务数量
const receivingRes = await taskApi.getLeaderReceivingTaskList({ page: 1, pageSize: 1 })
if (receivingRes.code === 200) {
stats.receivingCount = receivingRes.data.total || 0
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 加载任务列表
const loadTasks = async () => {
loading.value = true
try {
let response
if (activeTab.value === 'demand') {
response = await taskApi.getLeaderTaskList({
auditStatus: statusFilter.value || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
} else {
response = await taskApi.getLeaderReceivingTaskList({
status: statusFilter.value || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
}
if (response.code === 200) {
taskList.value = response.data.records || []
total.value = response.data.total || 0
}
} catch (error) {
console.error('加载任务列表失败:', error)
message.error('加载失败')
} finally {
loading.value = false
}
}
// 切换标签
const switchTab = (tab: 'demand' | 'receiving') => {
activeTab.value = tab
statusFilter.value = ''
pagination.page = 1
loadTasks()
}
// 筛选变更
const handleFilterChange = () => {
pagination.page = 1
loadTasks()
}
// 翻页
const handlePageChange = () => {
loadTasks()
}
// 审核任务
const auditModalVisible = ref(false)
const currentAuditTask = ref<Task | null>(null)
const auditForm = reactive({
result: 'pass' as 'pass' | 'reject',
comment: ''
})
const handleAuditTask = (task: Task) => {
currentAuditTask.value = task
auditForm.result = 'pass'
auditForm.comment = ''
auditModalVisible.value = true
}
const submitAudit = async () => {
if (!currentAuditTask.value?.id) return
if (!auditForm.comment && auditForm.result === 'reject') {
message.warning('请填写驳回原因')
return
}
try {
const response = await taskApi.auditTask({
taskId: currentAuditTask.value.id,
result: auditForm.result,
comment: auditForm.comment
})
if (response.code === 200) {
message.success(auditForm.result === 'pass' ? '审核通过' : '审核驳回')
auditModalVisible.value = false
await loadTasks()
await loadStats()
} else {
message.error(response.message || '审核失败')
}
} catch (error: any) {
message.error(error.message || '审核失败')
}
}
// 查看详情
const handleViewDetail = (task: Task) => {
console.log('查看任务详情:', task)
}
onMounted(async () => {
await loadStats()
await loadTasks()
})
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
.leader-workbench-page {
min-height: calc(100vh - 64px);
padding: $space-xl;
background: $bg-primary;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.workbench-panel {
background: $bg-secondary;
border-radius: $radius-lg;
padding: 0;
overflow: hidden;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: $space-lg $space-xl;
border-bottom: 1px solid $border-color;
background: rgba($brand-primary, 0.03);
.panel-title {
font-size: $font-size-2xl;
font-weight: $font-weight-bold;
color: $text-primary;
margin: 0;
display: flex;
align-items: center;
}
.view-tabs {
display: flex;
gap: $space-sm;
.tab-button {
padding: 10px 20px;
background: $bg-secondary;
border: 1px solid $border-color;
border-radius: $radius-md;
color: $text-secondary;
font-weight: $font-weight-medium;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: rgba($brand-primary, 0.05);
border-color: $brand-primary;
color: $brand-primary;
}
&.active {
background: $brand-primary;
border-color: $brand-primary;
color: #000000;
}
}
}
}
.panel-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: $space-md $space-xl;
background: $bg-tertiary;
border-bottom: 1px solid $border-color;
.stats {
font-size: $font-size-sm;
}
}
.panel-content {
padding: $space-xl;
min-height: 400px;
position: relative;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.empty-state {
text-align: center;
padding: 80px 20px;
color: $text-tertiary;
p {
margin-top: $space-md;
font-size: $font-size-lg;
}
}
.task-list {
display: flex;
flex-direction: column;
gap: $space-lg;
}
.task-item {
background: $bg-tertiary;
border: 1px solid $border-color;
border-radius: $radius-lg;
padding: $space-lg;
transition: all 0.2s ease;
&:hover {
border-color: $brand-primary;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba($brand-primary, 0.1);
}
}
.task-item-header {
margin-bottom: $space-md;
.task-basic-info {
margin-bottom: $space-sm;
.task-item-title {
font-size: $font-size-xl;
font-weight: $font-weight-semibold;
color: $text-primary;
margin: 0 0 $space-sm 0;
}
.task-meta {
display: flex;
flex-wrap: wrap;
gap: $space-sm;
align-items: center;
.task-no {
font-size: $font-size-sm;
color: $text-tertiary;
font-family: monospace;
}
.status-badge,
.audit-badge {
padding: 4px 8px;
border-radius: $radius-small;
font-size: $font-size-xs;
font-weight: $font-weight-medium;
}
.status-badge {
&.status-draft {
background: rgba($text-tertiary, 0.1);
color: $text-tertiary;
}
&.status-pending {
background: rgba($color-info, 0.1);
color: $color-info;
}
&.status-in_progress {
background: rgba($color-warning, 0.1);
color: $color-warning;
}
&.status-completed {
background: rgba($color-success, 0.1);
color: $color-success;
}
}
.audit-badge {
border: 1px solid transparent;
&.audit-draft {
background: rgba($text-tertiary, 0.1);
color: $text-tertiary;
}
&.audit-pending_audit {
background: rgba($color-warning, 0.1);
color: $color-warning;
border-color: rgba($color-warning, 0.3);
}
&.audit-audit_pass {
background: rgba($color-success, 0.1);
color: $color-success;
border-color: rgba($color-success, 0.3);
}
&.audit-audit_reject {
background: rgba($color-error, 0.1);
color: $color-error;
border-color: rgba($color-error, 0.3);
}
}
.user-badge {
padding: 4px 10px;
background: rgba($brand-primary, 0.1);
border-radius: $radius-small;
font-size: $font-size-xs;
color: $brand-primary;
}
}
}
.progress-info {
display: flex;
align-items: center;
gap: $space-md;
.progress-bar {
flex: 1;
height: 8px;
background: $bg-secondary;
border-radius: 4px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, $brand-primary, lighten($brand-primary, 10%));
transition: width 0.3s ease;
}
}
.progress-text {
font-size: $font-size-sm;
font-weight: $font-weight-medium;
color: $brand-primary;
min-width: 40px;
text-align: right;
}
}
}
.task-item-body {
.task-info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: $space-md;
.info-col {
display: flex;
flex-direction: column;
gap: 4px;
.label {
font-size: $font-size-xs;
color: $text-tertiary;
}
.value {
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $text-primary;
}
}
}
}
.task-item-footer {
display: flex;
gap: $space-md;
margin-top: $space-lg;
padding-top: $space-md;
border-top: 1px solid $border-color;
}
.pagination {
display: flex;
justify-content: center;
margin-top: $space-xl;
padding-top: $space-xl;
border-top: 1px solid $border-color;
}
// 审核表单样式
.audit-form {
.task-info-summary {
padding: $space-md;
background: rgba($brand-primary, 0.05);
border-radius: $radius-md;
margin-bottom: $space-lg;
h4 {
font-size: $font-size-lg;
color: $text-primary;
margin: 0 0 8px 0;
font-weight: $font-weight-semibold;
}
.task-no {
font-size: $font-size-sm;
color: $text-tertiary;
font-family: monospace;
margin: 0;
}
}
}
</style>