1786 lines
48 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="my-tasks-page">
<div class="container">
<div class="tasks-panel chart-card">
<div class="panel-header">
<div class="view-tabs">
<button
:class="['tab-button', { active: activeTab === 'receiver' }]"
@click="switchTab('receiver')"
>
我接收的任务 ({{ stats.receiverCount }})
</button>
<button
:class="['tab-button', { active: activeTab === 'publisher' }]"
@click="switchTab('publisher')"
>
我发起的任务 ({{ stats.publisherCount }})
</button>
</div>
<div class="view-switch">
<a-radio-group v-model:value="viewMode" size="small">
<a-radio-button value="list">
<template #icon><Icon name="list" size="sm" /></template>
列表视图
</a-radio-button>
<a-radio-button value="calendar">
<template #icon><Icon name="calendar" size="sm" /></template>
日历视图
</a-radio-button>
</a-radio-group>
</div>
</div>
<div class="panel-toolbar" v-if="viewMode === 'list'">
<a-radio-group v-model:value="statusFilter" @change="handleFilterChange" size="small">
<a-radio-button value="">全部</a-radio-button>
<a-radio-button value="pending">待接单</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-button value="rejected">已驳回</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>
<template v-else>
<div v-if="viewMode === 'calendar'" class="calendar-view">
<a-calendar
:value="calendarDate"
@panelChange="handleCalendarPanelChange"
@select="handleCalendarSelect"
>
<template #dateCellRender="{ current }">
<div class="calendar-date-cell">
<div
v-for="task in getTasksByDate(current)"
:key="task.id"
:class="['calendar-task-item', `task-status-${task.status}`]"
@click="handleTaskClick(task)"
>
<div class="task-dot"></div>
<span class="task-title">{{ task.title }}</span>
<span :class="['task-status', `status-${task.status}`]">
{{ getStatusText(task.status) }}
</span>
</div>
</div>
</template>
</a-calendar>
<div class="calendar-legend">
<div class="legend-item">
<span class="legend-dot status-pending"></span>
<span>待接单</span>
</div>
<div class="legend-item">
<span class="legend-dot status-in_progress"></span>
<span>进行中</span>
</div>
<div class="legend-item">
<span class="legend-dot status-review"></span>
<span>待审核</span>
</div>
<div class="legend-item">
<span class="legend-dot status-completed"></span>
<span>已完成</span>
</div>
<div class="legend-item">
<span class="legend-dot status-rejected"></span>
<span>已驳回</span>
</div>
</div>
</div>
<div v-else class="list-view">
<div v-if="taskList.length === 0" class="empty-state">
<div class="empty-icon">
<Icon name="inbox" size="xl" color="#6B6B6B" />
</div>
<h3>暂无任务</h3>
<p v-if="activeTab === 'receiver'">
您还没有接收任何任务,快去任务大厅抢单吧!
</p>
<p v-else>您还没有发起任何任务</p>
<router-link
v-if="activeTab === 'publisher'"
to="/tasks/create"
class="btn btn-primary"
style="margin-top: 16px"
>
+ 发起新任务
</router-link>
</div>
<div v-else>
<div 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="task-type">{{ getTaskTypeName(task.taskTypeId) }}</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.isUrgent" class="badge-urgent">
<Icon name="fire" size="sm" color="#F59E0B" style="margin-right: 4px; vertical-align: middle;" />
紧急
</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 class="info-col">
<span class="label">预计工时</span>
<span class="value">{{ task.estimatedHours || '--' }} 小时</span>
</div>
<div v-if="task.progress !== undefined && task.progress !== null" class="info-col">
<span class="label">进度</span>
<span class="value">{{ task.progress }}%</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 v-if="activeTab === 'publisher' && task.auditTime" class="info-col">
<span class="label">审核时间</span>
<span class="value">{{ formatTime(task.auditTime) }}</span>
</div>
<div v-if="activeTab === 'publisher' && task.auditorName" class="info-col">
<span class="label">审核人</span>
<span class="value">{{ task.auditorName }}</span>
</div>
</div>
<!-- 审核意见(如果有) -->
<div v-if="task.auditComment && (task.auditStatus === 'audit_reject' || task.auditStatus === 'audit_pass')" class="audit-comment">
<span class="label">
<Icon name="message" size="sm" style="margin-right: 4px; vertical-align: middle;" />
审核意见:
</span>
<span class="comment-text">{{ task.auditComment }}</span>
</div>
<!-- 交付路径(如果有) -->
<div v-if="task.deliveryPath" class="delivery-info">
<span class="label">📁 交付路径:</span>
<span class="path">{{ task.deliveryPath }}</span>
</div>
</div>
<!-- 任务操作 -->
<div class="task-item-footer">
<!-- 我接收的任务 - 操作按钮 -->
<template v-if="activeTab === 'receiver'">
<button
v-if="task.status === 'in_progress'"
class="btn btn-primary"
@click="handleSubmit(task)"
>
提交交付
</button>
<button
v-if="task.status === 'rejected'"
class="btn btn-primary"
@click="handleSubmit(task)"
>
重新提交
</button>
</template>
<!-- 我发起的任务 - 操作按钮 -->
<template v-else>
<!-- 草稿状态:提交审核 -->
<button
v-if="task.auditStatus === 'draft'"
class="btn btn-primary"
@click="handleSubmitAudit(task)"
>
<Icon name="send" size="sm" style="margin-right: 4px; vertical-align: middle;" />
提交审核
</button>
<!-- 待审核状态:显示审核按钮(仅负责人可见) -->
<button
v-if="task.auditStatus === 'pending_audit' && isLeader"
class="btn btn-success"
@click="handleAuditTask(task)"
>
<Icon name="check-circle" size="sm" style="margin-right: 4px; vertical-align: middle;" />
审核任务
</button>
<!-- 审核驳回:重新编辑 -->
<button
v-if="task.auditStatus === 'audit_reject'"
class="btn btn-warning"
@click="handleEditTask(task)"
>
<Icon name="edit" size="sm" style="margin-right: 4px; vertical-align: middle;" />
重新编辑
</button>
<!-- 审核通过且待接单:取消任务 -->
<button
v-if="task.status === 'pending' && task.auditStatus === 'audit_pass'"
class="btn btn-secondary"
@click="handleCancel(task)"
>
取消任务
</button>
<!-- 任务交付审核按钮 -->
<button
v-if="task.status === 'review'"
class="btn btn-primary"
@click="handleReview(task)"
>
审核任务
</button>
<!-- 已完成任务:评分按钮 -->
<button
v-if="task.status === 'completed' && !task.hasRated"
class="btn btn-warning"
@click="handleRateTask(task)"
>
<Icon name="star" 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>
</template>
</div>
</div>
</div>
</div>
<!-- 激励弹窗 -->
<IncentiveModal
ref="incentiveModalRef"
:type="incentiveData.type"
:title="incentiveData.title"
:description="incentiveData.description"
:details="incentiveData.details"
:reward="incentiveData.reward"
:show-share="incentiveData.showShare"
@confirm="handleIncentiveConfirm"
@share="handleIncentiveShare"
/>
<!-- 提交任务弹窗 -->
<a-modal
v-model:open="submitModalVisible"
title="提交任务交付"
width="600px"
:footer="null"
class="custom-modal"
>
<div class="modal-content">
<div class="task-info-summary">
<h4>{{ currentSubmitTask?.title }}</h4>
<p class="task-no">任务编号:{{ currentSubmitTask?.taskNo }}</p>
</div>
<a-form layout="vertical" class="custom-form">
<a-form-item label="交付文件路径" required>
<a-textarea
v-model:value="submitForm.deliveryPath"
placeholder="请输入交付文件的路径或链接"
:rows="4"
class="custom-input"
/>
</a-form-item>
<a-form-item label="交付说明">
<a-textarea
v-model:value="submitForm.description"
placeholder="请输入交付说明(选填)"
:rows="3"
class="custom-input"
/>
</a-form-item>
</a-form>
<div class="modal-footer">
<button class="btn btn-secondary" @click="submitModalVisible = false">
取消
</button>
<button class="btn btn-primary" @click="confirmSubmitTask">
<Icon name="send" size="sm" style="margin-right: 4px; vertical-align: middle;" />
提交
</button>
</div>
</div>
</a-modal>
<!-- 任务审核弹窗 -->
<a-modal
v-model:open="auditModalVisible"
title="任务审核"
width="600px"
:footer="null"
class="custom-modal"
>
<div class="modal-content">
<div class="task-info-summary">
<h4>{{ currentAuditTask?.title }}</h4>
<p class="task-no">任务编号:{{ currentAuditTask?.taskNo }}</p>
</div>
<a-form layout="vertical" class="custom-form">
<a-form-item label="审核结果" required>
<a-radio-group v-model:value="auditForm.result" class="custom-radio-group">
<a-radio value="pass" class="custom-radio">
<Icon name="check-circle" size="sm" color="#31f08b" style="margin-right: 4px; vertical-align: middle;" />
通过
</a-radio>
<a-radio value="reject" class="custom-radio">
<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"
class="custom-input"
/>
</a-form-item>
</a-form>
<div class="modal-footer">
<button class="btn btn-secondary" @click="auditModalVisible = false">
取消
</button>
<button class="btn btn-primary" @click="submitAudit">
<Icon name="check" size="sm" style="margin-right: 4px; vertical-align: middle;" />
确认审核
</button>
</div>
</div>
</a-modal>
<!-- 评分弹窗 -->
<a-modal
v-model:open="ratingModalVisible"
title="任务评分"
width="600px"
:footer="null"
class="custom-modal"
>
<div class="modal-content">
<div class="task-info-summary">
<h4>{{ currentRatingTask?.title }}</h4>
<p class="task-no">任务编号:{{ currentRatingTask?.taskNo }}</p>
<p class="task-receiver">接单人:{{ currentRatingTask?.receiverName }}</p>
</div>
<a-form layout="vertical" class="custom-form">
<a-form-item label="满意度评分" required>
<div class="star-rating">
<Icon
v-for="star in 5"
:key="star"
name="star"
size="xl"
:color="star <= ratingForm.rating ? '#FFD700' : '#4A5568'"
:class="['star-icon', { 'star-active': star <= ratingForm.rating }]"
@click="ratingForm.rating = star"
/>
</div>
<p class="rating-text">
{{ ratingForm.rating === 0 ? '请选择评分' : `${ratingForm.rating} 星` }}
<span v-if="ratingForm.rating > 0" class="rating-desc">
{{ getRatingDesc(ratingForm.rating) }}
</span>
</p>
</a-form-item>
<a-form-item label="评价内容">
<a-textarea
v-model:value="ratingForm.comment"
placeholder="请输入评价内容(选填)"
:rows="4"
class="custom-input"
/>
</a-form-item>
</a-form>
<div class="modal-footer">
<button class="btn btn-secondary" @click="ratingModalVisible = false">
取消
</button>
<button class="btn btn-primary" @click="submitRating" :disabled="ratingForm.rating === 0">
<Icon name="star" size="sm" style="margin-right: 4px; vertical-align: middle;" />
提交评分
</button>
</div>
</div>
</a-modal>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, computed, watch } 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 IncentiveModal, { type IncentiveDetail } from '@/components/IncentiveModal.vue'
import * as taskApi from '@/api/task'
import type { Task } from '@/api/task'
import { useConfigStore } from '@/stores/config'
import dayjs, { Dayjs } from 'dayjs'
const router = useRouter()
// 全局配置store
const configStore = useConfigStore()
// 状态
const loading = ref(false)
const taskList = ref<Task[]>([])
const allTasks = ref<Task[]>([]) // 用于日历视图的所有任务
const total = ref(0)
const activeTab = ref<'receiver' | 'publisher'>('receiver')
const statusFilter = ref('')
const viewMode = ref<'list' | 'calendar'>('list')
const calendarDate = ref<Dayjs>(dayjs())
const incentiveModalRef = ref<InstanceType<typeof IncentiveModal> | null>(null)
const incentiveData = reactive<{
type: 'task_complete' | 'achievement' | 'ranking' | 'milestone' | 'score_sprint'
title: string
description: string
details?: IncentiveDetail[]
reward?: number
showShare: boolean
}>({
type: 'task_complete',
title: '',
description: '',
details: undefined,
reward: undefined,
showShare: false
})
// 统计
const stats = reactive({
receiverCount: 0,
publisherCount: 0
})
// 分页
const pagination = reactive({
page: 1,
pageSize: 10
})
// 获取任务类型名称(从配置store获取)
const getTaskTypeName = (typeId: number) => {
const type = configStore.demandTypes.find(t => t.id === typeId)
return type?.name || '未知类型'
}
// 获取状态文本
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 isLeader = ref(false)
// 格式化时间
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 receiverRes = await taskApi.getMyTasks({
role: 'receiver',
page: 1,
pageSize: 1
})
if (receiverRes.code === 200) {
stats.receiverCount = receiverRes.data.total || 0
}
// 获取我发起的任务数量
const publisherRes = await taskApi.getMyTasks({
role: 'publisher',
page: 1,
pageSize: 1
})
if (publisherRes.code === 200) {
stats.publisherCount = publisherRes.data.total || 0
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 加载任务列表
const loadTasks = async () => {
loading.value = true
try {
const response = await taskApi.getMyTasks({
role: activeTab.value,
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
// 更新当前tab的统计数量(仅在没有筛选条件时更新总数)
if (!statusFilter.value) {
if (activeTab.value === 'receiver') {
stats.receiverCount = response.data.total || 0
} else {
stats.publisherCount = response.data.total || 0
}
}
// 如果是日历视图,加载所有任务(不分页)
if (viewMode.value === 'calendar') {
await loadAllTasksForCalendar()
}
}
} catch (error) {
console.error('加载任务失败:', error)
message.error('加载任务失败')
} finally {
loading.value = false
}
}
// 加载所有任务用于日历视图
const loadAllTasksForCalendar = async () => {
try {
const response = await taskApi.getMyTasks({
role: activeTab.value,
status: undefined, // 加载所有状态
page: 1,
pageSize: 1000 // 加载足够多的任务
})
if (response.code === 200) {
allTasks.value = response.data.records || []
}
} catch (error) {
console.error('加载日历任务失败:', error)
}
}
// 根据日期获取任务
const getTasksByDate = (date: Dayjs) => {
const dateStr = date.format('YYYY-MM-DD')
return allTasks.value.filter(task => {
// 检查任务的截止时间、开始时间或完成时间是否在该日期
if (task.deadline && dayjs(task.deadline).format('YYYY-MM-DD') === dateStr) {
return true
}
if (task.startTime && dayjs(task.startTime).format('YYYY-MM-DD') === dateStr) {
return true
}
if (task.completeTime && dayjs(task.completeTime).format('YYYY-MM-DD') === dateStr) {
return true
}
// 如果任务在该日期范围内
if (task.startTime && task.deadline) {
const start = dayjs(task.startTime)
const end = dayjs(task.deadline)
return date.isSameOrAfter(start, 'day') && date.isSameOrBefore(end, 'day')
}
return false
})
}
// 日历面板变化
const handleCalendarPanelChange = (date: Dayjs) => {
calendarDate.value = date
loadAllTasksForCalendar()
}
// 日历日期选择
const handleCalendarSelect = (date: Dayjs) => {
calendarDate.value = date
const tasks = getTasksByDate(date)
if (tasks.length > 0) {
// 可以显示任务详情或切换到列表视图
console.log('选中日期的任务:', tasks)
}
}
// 任务点击
const handleTaskClick = (task: Task) => {
handleViewDetail(task)
}
// 切换视角
const switchTab = (tab: 'receiver' | 'publisher') => {
activeTab.value = tab
statusFilter.value = ''
pagination.page = 1
loadTasks()
}
// 监听视图模式变化
watch(viewMode, (newMode) => {
if (newMode === 'calendar') {
loadAllTasksForCalendar()
} else {
loadTasks()
}
})
// 监听 activeTab 变化,重新加载日历数据
watch(activeTab, () => {
if (viewMode.value === 'calendar') {
loadAllTasksForCalendar()
}
})
// 筛选变更
const handleFilterChange = () => {
pagination.page = 1
loadTasks()
}
// 页码变更
const handlePageChange = () => {
loadTasks()
}
// 提交任务弹窗
const submitModalVisible = ref(false)
const currentSubmitTask = ref<Task | null>(null)
const submitForm = reactive({
deliveryPath: '',
description: ''
})
const handleSubmit = (task: Task) => {
currentSubmitTask.value = task
submitForm.deliveryPath = task.deliveryPath || ''
submitForm.description = ''
submitModalVisible.value = true
}
const confirmSubmitTask = async () => {
if (!currentSubmitTask.value?.id) return
if (!submitForm.deliveryPath.trim()) {
message.warning('请输入交付文件路径')
return
}
try {
const response = await taskApi.submitTask(currentSubmitTask.value.id, submitForm.deliveryPath)
if (response.code === 200) {
message.success('提交成功,等待审核')
submitModalVisible.value = false
await loadTasks()
await loadStats()
} else {
message.error(response.message || '提交失败')
}
} catch (error: any) {
message.error(error.message || '提交失败')
}
}
// 审核任务
const handleReview = (task: Task) => {
// TODO: 打开审核弹窗
const approved = confirm('是否通过审核?')
const rating = approved ? Number(prompt('请评分(1-5星):', '5')) : undefined
const comment = prompt(approved ? '审核评语:' : '驳回原因:', '')
if (comment !== null) {
taskApi.reviewTask(task.id!, approved, comment || undefined, rating)
.then((response) => {
if (response.code === 200) {
message.success(approved ? '审核通过' : '已驳回')
loadTasks()
}
})
.catch((error) => {
message.error(error.message || '操作失败')
})
}
}
// 显示任务完成激励弹窗
const showTaskCompleteIncentive = (task: Task, rating: number) => {
const isEarly = task.deadline && task.completeTime && dayjs(task.completeTime).isBefore(dayjs(task.deadline))
const delayHours = task.deadline && task.completeTime
? Math.max(0, dayjs(task.completeTime).diff(dayjs(task.deadline), 'hour'))
: 0
incentiveData.type = 'task_complete'
incentiveData.title = '恭喜完成任务!'
incentiveData.description = isEarly
? `您提前完成了任务,太棒了!获得了${rating}星好评!`
: delayHours > 0
? `您完成了任务,获得了${rating}星好评。注意:任务延误了${delayHours}小时。`
: `您按时完成了任务,获得了${rating}星好评!`
incentiveData.details = [
{ label: '任务', value: task.title },
{ label: '完成时间', value: formatTime(task.completeTime) },
{ label: '评分', value: `${rating} 星` }
]
if (task.score) {
incentiveData.reward = task.score
}
incentiveData.showShare = true
incentiveModalRef.value?.show()
}
// 激励弹窗确认
const handleIncentiveConfirm = () => {
// 可以在这里添加后续操作,比如刷新数据
}
// 激励弹窗分享
const handleIncentiveShare = () => {
message.info('分享功能开发中...')
}
// 取消任务
const handleCancel = (task: Task) => {
const reason = prompt('请输入取消原因:')
if (reason) {
taskApi.cancelTask(task.id!, reason)
.then((response) => {
if (response.code === 200) {
message.success('已取消任务')
loadTasks()
}
})
.catch((error) => {
message.error(error.message || '取消失败')
})
}
}
// 提交审核
const handleSubmitAudit = async (task: Task) => {
if (!task.id) return
try {
const response = await taskApi.submitTaskAudit(task.id)
if (response.code === 200) {
message.success('已提交审核,请等待部门负责人审核')
await loadTasks()
await loadStats()
} else {
message.error(response.message || '提交失败')
}
} catch (error: any) {
message.error(error.message || '提交失败')
}
}
// 审核任务(部门负责人)
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 ratingModalVisible = ref(false)
const currentRatingTask = ref<Task | null>(null)
const ratingForm = reactive({
rating: 0,
comment: ''
})
const handleRateTask = (task: Task) => {
currentRatingTask.value = task
ratingForm.rating = 0
ratingForm.comment = ''
ratingModalVisible.value = true
}
const getRatingDesc = (rating: number) => {
const descs = ['', '非常不满意', '不满意', '一般', '满意', '非常满意']
return descs[rating] || ''
}
const submitRating = async () => {
if (!currentRatingTask.value?.id) return
if (ratingForm.rating === 0) {
message.warning('请选择评分')
return
}
try {
// TODO: 调用评分API
// const response = await taskApi.rateTask({
// taskId: currentRatingTask.value.id,
// rating: ratingForm.rating,
// comment: ratingForm.comment
// })
message.success('评分成功')
ratingModalVisible.value = false
await loadTasks()
await loadStats()
} catch (error: any) {
message.error(error.message || '评分失败')
}
}
// 重新编辑(审核驳回后)
const handleEditTask = (task: Task) => {
// 跳转到创建任务页面,并传递任务ID用于编辑
router.push({
path: '/create-task',
query: { editId: task.id }
})
}
// 查看详情
const handleViewDetail = (task: Task) => {
// TODO: 打开任务详情弹窗
console.log('查看任务详情:', task)
}
// 监听配置更新事件
const handleConfigUpdate = async (event: CustomEvent) => {
console.log('配置已更新,刷新配置数据:', event.detail)
// 配置更新后,任务类型名称会自动更新(因为使用了computed)
}
// 组件挂载
onMounted(() => {
// 加载配置数据(如果还没有加载)
configStore.loadAllConfigs()
loadTasks()
// 加载统计数据
loadStats()
// 监听配置更新事件
window.addEventListener('configUpdated', handleConfigUpdate as EventListener)
})
// 组件卸载
onUnmounted(() => {
window.removeEventListener('configUpdated', handleConfigUpdate as EventListener)
})
</script>
<style lang="scss" scoped>
.my-tasks-page {
min-height: 100vh;
padding: $space-xl 0;
background: $bg-primary;
}
.container {
max-width: $container-max-width;
margin: 0 auto;
padding: 0 $space-xl;
@media (max-width: $breakpoint-tablet) {
padding: 0 $space-lg;
}
@media (max-width: $breakpoint-mobile) {
padding: 0 $space-md;
}
}
// 图表卡片 - Cursor 风格
.chart-card {
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: $space-xl;
transition: all $transition-base $ease-out;
&:hover {
border-color: $border-color;
background: $bg-card-hover;
}
}
// 视角切换
.tasks-panel {
display: flex;
flex-direction: column;
gap: $space-lg;
margin-bottom: $space-xl;
}
.panel-header {
display: flex;
justify-content: space-between;
gap: $space-lg;
flex-wrap: wrap;
align-items: center;
}
.panel-toolbar {
display: flex;
justify-content: space-between;
gap: $space-lg;
flex-wrap: wrap;
align-items: center;
.stats {
flex-shrink: 0;
}
}
.panel-content {
min-height: 200px;
}
.panel-loading {
min-height: 200px;
}
.view-tabs {
display: flex;
gap: $space-md;
flex-wrap: wrap;
}
.view-switch {
flex-shrink: 0;
}
.tab-button {
padding: 12px 24px;
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-small;
color: $text-secondary;
font-size: $font-size-base;
font-weight: $font-weight-medium;
cursor: pointer;
transition: all $transition-base $ease-out;
&:hover {
border-color: $brand-primary;
color: $text-primary;
}
&.active {
background: rgba($brand-primary, 0.1);
border-color: $brand-primary;
color: $brand-primary;
}
}
// 筛选栏
.filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $space-xl;
padding: $space-lg;
@media (max-width: $breakpoint-mobile) {
flex-direction: column;
gap: $space-md;
align-items: stretch;
}
}
// 加载状态
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
// 空状态
.empty-state {
text-align: center;
padding: $space-xxxl;
.empty-icon {
font-size: 64px;
margin-bottom: $space-lg;
}
h3 {
font-size: $font-size-2xl;
color: $text-primary;
margin-bottom: $space-sm;
}
p {
color: $text-secondary;
}
}
.list-view {
display: flex;
flex-direction: column;
gap: $space-xl;
}
// 任务列表
.task-list {
display: flex;
flex-direction: column;
gap: $space-lg;
}
.task-item {
padding: $space-xl;
border-radius: $radius-lg;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
transition: all 0.2s ease-out;
&:hover {
transform: translateY(-2px);
border-color: rgba($brand-primary, 0.4);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25);
}
}
.task-item-header {
margin-bottom: $space-md;
}
.task-basic-info {
.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;
}
.task-type {
padding: 4px 8px;
background: rgba($brand-primary, 0.1);
border-radius: $radius-small;
font-size: $font-size-xs;
color: $brand-primary;
}
.status-badge {
padding: 4px 8px;
border-radius: $radius-small;
font-size: $font-size-xs;
font-weight: $font-weight-medium;
&.status-pending {
background: rgba($color-info, 0.1);
color: $color-info;
}
&.status-in_progress {
background: rgba($color-warning, 0.1);
color: $color-warning;
}
&.status-review {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
}
&.status-completed {
background: rgba($color-success, 0.1);
color: $color-success;
}
&.status-rejected {
background: rgba($color-error, 0.1);
color: $color-error;
}
}
.audit-badge {
padding: 4px 8px;
border-radius: $radius-small;
font-size: $font-size-xs;
font-weight: $font-weight-medium;
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);
}
}
.badge-urgent {
padding: 4px 8px;
background: rgba($color-error, 0.1);
border: 1px solid rgba($color-error, 0.3);
border-radius: $radius-small;
color: $color-error;
font-size: $font-size-xs;
}
}
// 进度信息
.progress-info {
display: flex;
align-items: center;
gap: $space-md;
margin-top: $space-md;
.progress-bar {
flex: 1;
height: 8px;
background: $border-primary;
border-radius: $radius-small;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, $brand-primary 0%, lighten($brand-primary, 10%) 100%);
transition: width $transition-base $ease-out;
}
}
.progress-text {
font-size: $font-size-sm;
font-weight: $font-weight-medium;
color: $brand-primary;
min-width: 40px;
text-align: right;
}
}
// 任务详情
.task-item-body {
margin-bottom: $space-md;
}
.task-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: $space-md;
margin-bottom: $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;
}
}
.delivery-info {
padding: $space-md;
background: darken($bg-card, 3%);
border-radius: $radius-small;
font-size: $font-size-sm;
.label {
color: $text-secondary;
margin-right: $space-sm;
}
.path {
color: $brand-primary;
font-family: monospace;
}
}
// 任务操作
.task-item-footer {
display: flex;
gap: $space-sm;
padding-top: $space-md;
border-top: 1px solid $border-primary;
}
// 分页
.pagination {
display: flex;
justify-content: center;
margin-top: $space-xl;
&.inside-panel {
justify-content: flex-end;
margin-top: $space-lg;
}
}
// 日历视图
.calendar-view {
background: $bg-tertiary;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: $space-lg;
:deep(.ant-picker-calendar) {
background: transparent;
color: $text-primary;
}
:deep(.ant-picker-calendar-date-content) {
min-height: 60px;
}
:deep(.ant-picker-panel) {
background: transparent;
}
:deep(.ant-picker-calendar-date) {
color: $text-primary;
border-color: $border-primary;
}
:deep(.ant-picker-cell-in-view.ant-picker-calendar-date-today) {
border-color: rgba($brand-primary, 0.6);
}
:deep(.ant-picker-cell-inner) {
color: $text-primary;
}
}
.calendar-date-cell {
display: flex;
flex-direction: column;
gap: 4px;
min-height: 60px;
}
.calendar-task-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: $radius-small;
font-size: $font-size-xs;
cursor: pointer;
transition: all $transition-base;
margin-bottom: 2px;
&:hover {
background: rgba($brand-primary, 0.1);
transform: translateX(2px);
}
.task-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.task-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: $text-primary;
}
.task-status {
font-size: $font-size-xs;
padding: 2px 6px;
border-radius: $radius-small;
flex-shrink: 0;
}
// 不同状态的颜色
&.task-status-pending {
background: rgba(87, 169, 255, 0.1);
border-left: 3px solid #57a9ff;
.task-dot {
background: #57a9ff;
}
}
&.task-status-in_progress {
background: rgba(255, 193, 7, 0.1);
border-left: 3px solid #ffc107;
.task-dot {
background: #ffc107;
}
}
&.task-status-review {
background: rgba(156, 39, 176, 0.1);
border-left: 3px solid #9c27b0;
.task-dot {
background: #9c27b0;
}
}
&.task-status-completed {
background: rgba(76, 175, 80, 0.1);
border-left: 3px solid #4caf50;
.task-dot {
background: #4caf50;
}
}
&.task-status-rejected {
background: rgba(244, 67, 54, 0.1);
border-left: 3px solid #f44336;
.task-dot {
background: #f44336;
}
}
}
// 日历图例
.calendar-legend {
display: flex;
gap: $space-lg;
margin-top: $space-lg;
padding-top: $space-lg;
border-top: 1px solid $border-primary;
flex-wrap: wrap;
.legend-item {
display: flex;
align-items: center;
gap: $space-sm;
font-size: $font-size-sm;
color: $text-secondary;
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
&.status-pending {
background: #57a9ff;
}
&.status-in_progress {
background: #ffc107;
}
&.status-review {
background: #9c27b0;
}
&.status-completed {
background: #4caf50;
}
&.status-rejected {
background: #f44336;
}
}
}
}
// 审核意见显示
.audit-comment {
margin-top: $space-md;
padding: $space-md;
background: rgba($brand-primary, 0.05);
border-left: 3px solid $brand-primary;
border-radius: $radius-md;
.label {
font-size: $font-size-sm;
color: $text-secondary;
font-weight: $font-weight-medium;
display: flex;
align-items: center;
margin-bottom: 4px;
}
.comment-text {
font-size: $font-size-base;
color: $text-primary;
line-height: 1.5;
}
}
// 统一弹窗样式
:deep(.custom-modal) {
.ant-modal-content {
background: $bg-secondary;
border: 1px solid $border-primary;
border-radius: $radius-lg;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.ant-modal-header {
background: $bg-secondary;
border-bottom: 1px solid $border-primary;
padding: $space-lg $space-xl;
.ant-modal-title {
color: $text-primary;
font-size: $font-size-xl;
font-weight: $font-weight-semibold;
}
}
.ant-modal-body {
padding: $space-xl;
background: $bg-secondary;
}
.ant-modal-close {
color: $text-tertiary;
&:hover {
color: $text-primary;
}
}
}
// 弹窗内容
.modal-content {
.task-info-summary {
padding: $space-lg;
background: rgba($brand-primary, 0.08);
border: 1px solid rgba($brand-primary, 0.2);
border-radius: $radius-md;
margin-bottom: $space-xl;
h4 {
font-size: $font-size-lg;
color: $text-primary;
margin: 0 0 $space-sm 0;
font-weight: $font-weight-semibold;
}
.task-no,
.task-receiver {
font-size: $font-size-sm;
color: $text-tertiary;
font-family: monospace;
margin: 4px 0 0 0;
}
.task-receiver {
color: $text-secondary;
font-family: $font-family-base;
}
}
.custom-form {
:deep(.ant-form-item-label > label) {
color: $text-primary;
font-weight: $font-weight-medium;
}
:deep(.ant-form-item-required)::before {
color: $brand-primary !important;
}
.custom-input {
background: $bg-tertiary;
border: 1px solid $border-primary;
color: $text-primary;
border-radius: $radius-md;
transition: all 0.3s ease;
&:hover {
border-color: rgba($brand-primary, 0.5);
}
&:focus {
border-color: $brand-primary;
box-shadow: 0 0 0 2px rgba($brand-primary, 0.1);
}
:deep(.ant-input) {
background: transparent;
color: $text-primary;
}
}
.custom-radio-group {
display: flex;
gap: $space-lg;
.custom-radio {
:deep(.ant-radio) {
.ant-radio-inner {
background: $bg-tertiary;
border-color: $border-primary;
}
&.ant-radio-checked .ant-radio-inner {
background: $brand-primary;
border-color: $brand-primary;
&::after {
background-color: #1a1d23;
}
}
}
:deep(.ant-radio + *) {
color: $text-primary;
}
}
}
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: $space-md;
margin-top: $space-xl;
padding-top: $space-lg;
border-top: 1px solid $border-primary;
}
}
// 星星评分样式
.star-rating {
display: flex;
gap: $space-sm;
margin-bottom: $space-md;
.star-icon {
cursor: pointer;
transition: all 0.2s ease;
filter: drop-shadow(0 0 2px rgba(49, 240, 139, 0.3));
&:hover {
transform: scale(1.2);
filter: drop-shadow(0 0 6px rgba(49, 240, 139, 0.6));
}
&.star-active {
animation: starPulse 0.3s ease;
}
}
}
@keyframes starPulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.3);
}
}
.rating-text {
font-size: 14px;
color: #9ca3af;
margin: 8px 0 0 0;
.rating-desc {
margin-left: 16px;
color: #31f08b;
font-weight: 500;
}
}
// 保留原有审核表单样式(向后兼容)
.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>