Files
tiantian-system/app/pages/oa/tasks.vue
赵忠林 f9e1286ab1 refactor(developer-config): 移除开发者配置页面相关代码和文档
- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue
- 移除开发者文档页面及其导航与样式实现
- 清理开发者侧功能完善工作日志文件
- 删除全局.gitignore配置文件,清理无用忽略规则
- 优化应用配置页面的参数读取和路由结构,解决刷新404问题
- 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入
- 移除对后端配置加密字段的 secret 标记,修正加密异常问题
2026-04-09 07:35:34 +08:00

1031 lines
27 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
<div class="tasks-page">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<h1 class="page-title">任务看板</h1>
<p class="page-description">可视化任务管理团队工作一目了然</p>
</div>
<div class="header-actions">
<a-space>
<a-dropdown>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu-item key="personal">个人任务</a-menu-item>
<a-menu-item key="team">团队任务</a-menu-item>
<a-menu-item key="all">全部任务</a-menu-item>
</a-menu>
</template>
<a-button>
视图: {{ viewModeText }}
<DownOutlined />
</a-button>
</a-dropdown>
<a-button type="primary" @click="showCreateModal = true">
<template #icon>
<PlusOutlined />
</template>
新建任务
</a-button>
</a-space>
</div>
</div>
<!-- 看板区域 -->
<div class="kanban-board">
<!-- To Do -->
<div class="kanban-column" :style="{ borderColor: '#ff4d4f' }">
<div class="column-header">
<div class="column-title">
<span class="column-icon">📋</span>
<span>待处理</span>
<span class="task-count">{{ todoTasks.length }}</span>
</div>
<a-tag color="red" class="priority-badge">高优先</a-tag>
</div>
<draggable
v-model="todoTasks"
group="tasks"
item-key="id"
class="task-list"
@change="handleTaskMove"
>
<template #item="{ element: task }">
<div class="task-card" @click="openTaskDetail(task.id)">
<div class="task-card-header">
<div class="task-priority">
<a-tag :color="getPriorityColor(task.priority)" size="small">
{{ getPriorityText(task.priority) }}
</a-tag>
</div>
<div class="task-actions">
<a-dropdown :trigger="['click']">
<a-button type="text" size="small" class="more-button">
<MoreOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="edit" @click="editTask(task)">
<EditOutlined /> 编辑
</a-menu-item>
<a-menu-item key="assign" @click="assignTask(task)">
<UserAddOutlined /> 分配
</a-menu-item>
<a-menu-item key="delete" danger @click="deleteTask(task)">
<DeleteOutlined /> 删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
<div class="task-title">{{ task.title }}</div>
<div class="task-description" v-if="task.description">
{{ task.description }}
</div>
<div class="task-meta">
<div class="meta-item">
<UserOutlined />
<span>{{ task.assignee }}</span>
</div>
<div class="meta-item">
<CalendarOutlined />
<span :class="{ 'deadline-warning': isDeadlineWarning(task.dueDate) }">
{{ task.dueDate }}
</span>
</div>
</div>
<div class="task-footer">
<div class="task-project">
<span class="project-tag">#{{ task.project }}</span>
</div>
<div class="task-avatars">
<a-avatar-group :max-count="2" size="small">
<a-avatar v-for="avatar in task.collaborators" :key="avatar" :src="avatar" />
</a-avatar-group>
<span v-if="task.collaborators.length > 2" class="more-count">
+{{ task.collaborators.length - 2 }}
</span>
</div>
</div>
</div>
</template>
</draggable>
</div>
<!-- In Progress -->
<div class="kanban-column" :style="{ borderColor: '#faad14' }">
<div class="column-header">
<div class="column-title">
<span class="column-icon"></span>
<span>进行中</span>
<span class="task-count">{{ inProgressTasks.length }}</span>
</div>
<a-tag color="orange" class="priority-badge">进行中</a-tag>
</div>
<draggable
v-model="inProgressTasks"
group="tasks"
item-key="id"
class="task-list"
@change="handleTaskMove"
>
<template #item="{ element: task }">
<div class="task-card" @click="openTaskDetail(task.id)">
<div class="task-card-header">
<div class="task-priority">
<a-tag :color="getPriorityColor(task.priority)" size="small">
{{ getPriorityText(task.priority) }}
</a-tag>
</div>
<div class="task-actions">
<a-dropdown :trigger="['click']">
<a-button type="text" size="small" class="more-button">
<MoreOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="edit" @click="editTask(task)">
<EditOutlined /> 编辑
</a-menu-item>
<a-menu-item key="complete" @click="completeTask(task)">
<CheckCircleOutlined /> 完成
</a-menu-item>
<a-menu-item key="delete" danger @click="deleteTask(task)">
<DeleteOutlined /> 删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
<div class="task-title">{{ task.title }}</div>
<div class="task-description" v-if="task.description">
{{ task.description }}
</div>
<div class="task-meta">
<div class="meta-item">
<UserOutlined />
<span>{{ task.assignee }}</span>
</div>
<div class="meta-item">
<FieldTimeOutlined />
<span>已耗时 {{ task.elapsedTime || '--' }}</span>
</div>
</div>
<div class="task-progress">
<a-progress :percent="task.progress || 0" size="small" />
</div>
<div class="task-footer">
<div class="task-project">
<span class="project-tag">#{{ task.project }}</span>
</div>
<div class="task-avatars">
<a-avatar-group :max-count="2" size="small">
<a-avatar v-for="avatar in task.collaborators" :key="avatar" :src="avatar" />
</a-avatar-group>
</div>
</div>
</div>
</template>
</draggable>
</div>
<!-- Review -->
<div class="kanban-column" :style="{ borderColor: '#1890ff' }">
<div class="column-header">
<div class="column-title">
<span class="column-icon">👁</span>
<span>待审核</span>
<span class="task-count">{{ reviewTasks.length }}</span>
</div>
<a-tag color="blue" class="priority-badge">待审核</a-tag>
</div>
<draggable
v-model="reviewTasks"
group="tasks"
item-key="id"
class="task-list"
@change="handleTaskMove"
>
<template #item="{ element: task }">
<div class="task-card" @click="openTaskDetail(task.id)">
<div class="task-card-header">
<div class="task-priority">
<a-tag :color="getPriorityColor(task.priority)" size="small">
{{ getPriorityText(task.priority) }}
</a-tag>
</div>
<div class="task-reviewer">
<a-tag color="cyan" size="small">审核: {{ task.reviewer }}</a-tag>
</div>
</div>
<div class="task-title">{{ task.title }}</div>
<div class="task-description" v-if="task.description">
{{ task.description }}
</div>
<div class="task-meta">
<div class="meta-item">
<UserOutlined />
<span>{{ task.assignee }}</span>
</div>
<div class="meta-item">
<CalendarOutlined />
<span>提交: {{ task.submittedDate }}</span>
</div>
</div>
<div class="task-actions-row">
<a-space>
<a-button size="small" type="primary" @click.stop="approveTask(task)">
<CheckCircleOutlined /> 通过
</a-button>
<a-button size="small" danger @click.stop="rejectTask(task)">
<CloseCircleOutlined /> 驳回
</a-button>
</a-space>
</div>
<div class="task-footer">
<div class="task-project">
<span class="project-tag">#{{ task.project }}</span>
</div>
</div>
</div>
</template>
</draggable>
</div>
<!-- Done -->
<div class="kanban-column" :style="{ borderColor: '#52c41a' }">
<div class="column-header">
<div class="column-title">
<span class="column-icon"></span>
<span>已完成</span>
<span class="task-count">{{ doneTasks.length }}</span>
</div>
<a-tag color="green" class="priority-badge">已完成</a-tag>
</div>
<draggable
v-model="doneTasks"
group="tasks"
item-key="id"
class="task-list"
@change="handleTaskMove"
>
<template #item="{ element: task }">
<div class="task-card completed" @click="openTaskDetail(task.id)">
<div class="task-card-header">
<div class="task-priority">
<a-tag :color="'green'" size="small">
已完成
</a-tag>
</div>
<div class="task-completed-time">
<a-tag color="green" size="small">
{{ task.completedTime }}
</a-tag>
</div>
</div>
<div class="task-title">
<span class="completed-text">{{ task.title }}</span>
</div>
<div class="task-description" v-if="task.description">
{{ task.description }}
</div>
<div class="task-meta">
<div class="meta-item">
<UserOutlined />
<span>{{ task.assignee }}</span>
</div>
<div class="meta-item">
<CheckCircleOutlined />
<span>{{ task.completedBy }}</span>
</div>
</div>
<div class="task-comment" v-if="task.comment">
<div class="comment-label">评价:</div>
<div class="comment-content">{{ task.comment }}</div>
</div>
<div class="task-footer">
<div class="task-project">
<span class="project-tag">#{{ task.project }}</span>
</div>
<div class="task-rating" v-if="task.rating">
<a-rate :value="task.rating" disabled />
</div>
</div>
</div>
</template>
</draggable>
</div>
</div>
<!-- 统计数据 -->
<a-card class="stats-card" :bordered="false">
<a-row :gutter="[24, 24]">
<a-col :span="24" :md="12" :lg="6">
<div class="stat-item">
<div class="stat-icon total">
📊
</div>
<div class="stat-content">
<div class="stat-number">{{ totalTasks }}</div>
<div class="stat-label">总任务数</div>
</div>
</div>
</a-col>
<a-col :span="24" :md="12" :lg="6">
<div class="stat-item">
<div class="stat-icon completed">
</div>
<div class="stat-content">
<div class="stat-number">{{ doneTasks.length }}</div>
<div class="stat-label">已完成</div>
</div>
</div>
</a-col>
<a-col :span="24" :md="12" :lg="6">
<div class="stat-item">
<div class="stat-icon overdue">
</div>
<div class="stat-content">
<div class="stat-number">{{ overdueTasksCount }}</div>
<div class="stat-label">已逾期</div>
</div>
</div>
</a-col>
<a-col :span="24" :md="12" :lg="6">
<div class="stat-item">
<div class="stat-icon completion">
📈
</div>
<div class="stat-content">
<div class="stat-number">{{ completionRate }}%</div>
<div class="stat-label">完成率</div>
</div>
</div>
</a-col>
</a-row>
</a-card>
<!-- 新建任务模态框 -->
<a-modal
v-model:open="showCreateModal"
title="新建任务"
:footer="null"
width="600px"
@cancel="resetForm"
>
<task-form
ref="taskFormRef"
:mode="formMode"
:initial-data="editingTask"
@submit="handleTaskSubmit"
@cancel="resetForm"
/>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import draggable from 'vuedraggable'
import {
PlusOutlined, DownOutlined, MoreOutlined, EditOutlined,
DeleteOutlined, UserOutlined, CalendarOutlined, FieldTimeOutlined,
CheckCircleOutlined, CloseCircleOutlined, UserAddOutlined
} from '@ant-design/icons-vue'
interface Task {
id: number
title: string
description: string
priority: 'low' | 'medium' | 'high'
assignee: string
reviewer?: string
project: string
status: 'todo' | 'in_progress' | 'review' | 'done'
dueDate: string
submittedDate?: string
completedTime?: string
completedBy?: string
comment?: string
rating?: number
collaborators: string[]
progress?: number
elapsedTime?: string
}
// 视图模式
const viewMode = ref<'personal' | 'team' | 'all'>('all')
const viewModeText = computed(() => {
const map = { personal: '个人', team: '团队', all: '全部' }
return map[viewMode.value]
})
// 任务数据
const todoTasks = ref<Task[]>([
{
id: 1,
title: '完成产品需求文档',
description: '撰写完整的产品需求文档,包含功能规格和验收标准',
priority: 'high',
assignee: '张三',
project: '智慧园区',
status: 'todo',
dueDate: '2024-10-22',
collaborators: [
'https://randomuser.me/api/portraits/men/32.jpg',
'https://randomuser.me/api/portraits/women/44.jpg'
]
},
{
id: 2,
title: '用户界面设计评审',
description: '审查最新版本的UI设计稿提供修改建议',
priority: 'medium',
assignee: '李四',
project: '移动端App',
status: 'todo',
dueDate: '2024-10-23',
collaborators: [
'https://randomuser.me/api/portraits/men/67.jpg'
]
}
])
const inProgressTasks = ref<Task[]>([
{
id: 3,
title: 'API接口开发',
description: '实现用户管理模块的API接口',
priority: 'high',
assignee: '王五',
project: '数据中心',
status: 'in_progress',
dueDate: '2024-10-25',
collaborators: [
'https://randomuser.me/api/portraits/men/32.jpg',
'https://randomuser.me/api/portraits/men/67.jpg',
'https://randomuser.me/api/portraits/women/44.jpg'
],
progress: 85,
elapsedTime: '3天'
},
{
id: 4,
title: '前端页面开发',
description: '开发用户管理页面',
priority: 'medium',
assignee: '赵六',
project: '智慧园区',
status: 'in_progress',
dueDate: '2024-10-24',
collaborators: [
'https://randomuser.me/api/portraits/women/23.jpg'
],
progress: 60,
elapsedTime: '2天'
}
])
const reviewTasks = ref<Task[]>([
{
id: 5,
title: '数据库设计方案',
description: '数据库表结构设计和优化方案',
priority: 'high',
assignee: '钱七',
reviewer: '张三',
project: '数据中心',
status: 'review',
dueDate: '2024-10-28',
submittedDate: '2024-10-20'
}
])
const doneTasks = ref<Task[]>([
{
id: 6,
title: '项目启动会纪要',
description: '整理项目启动会议纪要并分发',
priority: 'low',
assignee: '孙八',
project: '内部分享平台',
status: 'done',
dueDate: '2024-10-19',
completedTime: '昨天 16:30',
completedBy: '孙八',
comment: '纪要内容完整,分发及时',
rating: 5
}
])
// 统计数据
const totalTasks = computed(() =>
todoTasks.value.length + inProgressTasks.value.length +
reviewTasks.value.length + doneTasks.value.length
)
const overdueTasksCount = computed(() => {
const today = new Date().toISOString().split('T')[0]
return todoTasks.value.filter(task => task.dueDate < today).length +
inProgressTasks.value.filter(task => task.dueDate < today).length
})
const completionRate = computed(() => {
if (totalTasks.value === 0) return 0
return Math.round((doneTasks.value.length / totalTasks.value) * 100)
})
// 新建/编辑任务相关
const showCreateModal = ref(false)
const formMode = ref<'create' | 'edit'>('create')
const editingTask = ref<Task | null>(null)
const taskFormRef = ref()
// 方法
function handleMenuClick({ key }: { key: string }) {
viewMode.value = key as 'personal' | 'team' | 'all'
message.info(`切换至${viewModeText.value}视图`)
}
function getPriorityColor(priority: string) {
const colors = { high: 'red', medium: 'orange', low: 'green' }
return colors[priority as keyof typeof colors]
}
function getPriorityText(priority: string) {
const texts = { high: '高', medium: '中', low: '低' }
return texts[priority as keyof typeof texts] || priority
}
function isDeadlineWarning(dueDate: string) {
const deadline = new Date(dueDate)
const today = new Date()
const diffDays = Math.ceil((deadline.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
return diffDays <= 3 && diffDays > 0
}
function handleTaskMove(event: any) {
if (event.added) {
const task = event.added.element
const newStatus = getColumnStatus(event.added.newIndex)
updateTaskStatus(task.id, newStatus)
message.success(`任务已移动到${getStatusText(newStatus)}`)
}
}
function getColumnStatus(index: number) {
const statusMap: Record<number, Task['status']> = {
0: 'todo',
1: 'in_progress',
2: 'review',
3: 'done'
}
return statusMap[index]
}
function getStatusText(status: Task['status']) {
const texts = {
todo: '待处理',
in_progress: '进行中',
review: '待审核',
done: '已完成'
}
return texts[status]
}
function updateTaskStatus(taskId: number, status: Task['status']) {
// 在实际应用中这里应该调用API更新任务状态
console.log(`更新任务${taskId}状态为${status}`)
}
function openTaskDetail(taskId: number) {
navigateTo(`/oa/tasks/${taskId}`)
}
function editTask(task: Task) {
editingTask.value = { ...task }
formMode.value = 'edit'
showCreateModal.value = true
}
function assignTask(task: Task) {
message.info(`分配任务: ${task.title}`)
}
function deleteTask(task: Task) {
const { id, title } = task
message.warning(`删除任务: ${title} (ID: ${id})`)
}
function completeTask(task: Task) {
const { id, title } = task
message.success(`完成任务: ${title} (ID: ${id})`)
// 这里应该移动任务到已完成列
}
function approveTask(task: Task) {
const { id, title } = task
message.success(`通过审核: ${title} (ID: ${id})`)
// 这里应该移动任务到已完成列
}
function rejectTask(task: Task) {
const { id, title } = task
message.warning(`驳回审核: ${title} (ID: ${id})`)
// 这里应该移动任务到待处理列
}
function handleTaskSubmit(taskData: any) {
if (formMode.value === 'create') {
const newTask: Task = {
id: Date.now(),
title: taskData.title,
description: taskData.description,
priority: taskData.priority,
assignee: taskData.assignee,
project: taskData.project,
status: 'todo',
dueDate: taskData.dueDate?.format('YYYY-MM-DD') || taskData.dueDate,
collaborators: []
}
todoTasks.value.unshift(newTask)
message.success('任务创建成功')
} else {
message.success('任务更新成功')
}
resetForm()
}
function resetForm() {
showCreateModal.value = false
editingTask.value = null
formMode.value = 'create'
if (taskFormRef.value) {
taskFormRef.value.reset()
}
}
onMounted(() => {
// 页面初始化逻辑
})
</script>
<style scoped>
.tasks-page {
min-height: calc(100vh - 140px);
max-width: 100%;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.header-content {
flex: 1;
min-width: 0;
}
.page-title {
font-size: 24px;
font-weight: 600;
margin: 0;
color: rgba(0, 0, 0, 0.85);
}
.page-description {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin: 4px 0 0;
}
.header-actions {
flex-shrink: 0;
}
/* 看板样式 */
.kanban-board {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
min-height: 600px;
}
.kanban-column {
display: flex;
flex-direction: column;
background: #fafafa;
border-radius: 8px;
border: 2px solid #f0f0f0;
padding: 16px;
min-height: 500px;
}
.column-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.column-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
}
.column-icon {
font-size: 20px;
}
.task-count {
background: rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 2px 8px;
font-size: 12px;
font-weight: 500;
}
.priority-badge {
font-size: 11px;
padding: 1px 6px;
border-radius: 4px;
}
.task-list {
flex: 1;
min-height: 200px;
overflow-y: auto;
}
.task-card {
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
cursor: move;
border: 1px solid #f0f0f0;
transition: all 0.2s ease;
}
.task-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
border-color: #d9d9d9;
}
.task-card.completed {
opacity: 0.8;
background: #fafafa;
}
.task-card.completed .task-title,
.task-card.completed .task-description {
color: rgba(0, 0, 0, 0.45);
}
.task-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.task-priority {
flex: 1;
}
.task-actions {
margin-top: -8px;
margin-right: -8px;
}
.more-button {
opacity: 0.6;
}
.more-button:hover {
opacity: 1;
}
.task-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: rgba(0, 0, 0, 0.85);
}
.completed-text {
text-decoration: line-through;
color: rgba(0, 0, 0, 0.45);
}
.task-description {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
margin-bottom: 12px;
line-height: 1.5;
}
.task-meta {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
}
.deadline-warning {
color: #fa541c;
font-weight: 600;
}
.task-progress {
margin-bottom: 12px;
}
.task-actions-row {
margin-bottom: 12px;
}
.task-comment {
padding: 8px;
background: #f6ffed;
border-radius: 4px;
margin-bottom: 12px;
border: 1px solid #b7eb8f;
}
.comment-label {
font-size: 12px;
color: #52c41a;
font-weight: 500;
margin-bottom: 4px;
}
.comment-content {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
line-height: 1.4;
}
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
}
.project-tag {
font-size: 11px;
color: #1890ff;
background: #e6f7ff;
padding: 2px 8px;
border-radius: 4px;
}
.task-avatars {
display: flex;
align-items: center;
gap: 4px;
}
.more-count {
font-size: 11px;
color: rgba(0, 0, 0, 0.45);
}
.task-rating {
font-size: 12px;
}
/* 统计数据卡片 */
.stats-card {
margin-top: 24px;
}
.stat-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #f0f0f0;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.stat-icon.total {
background: #e6f7ff;
color: #1890ff;
}
.stat-icon.completed {
background: #f6ffed;
color: #52c41a;
}
.stat-icon.overdue {
background: #fff7e6;
color: #fa8c16;
}
.stat-icon.completion {
background: #f9f0ff;
color: #722ed1;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 24px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 2px;
}
.stat-label {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
}
/* 响应式调整 */
@media (max-width: 1200px) {
.kanban-board {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.kanban-board {
grid-template-columns: 1fr;
}
.page-header {
flex-direction: column;
align-items: stretch;
}
.header-actions {
width: 100%;
}
.stat-item {
flex-direction: column;
text-align: center;
}
}
</style>