Files
tiantian-system/app/pages/oa/tasks.vue
赵忠林 a9da04fbb8 fix(oa): 修复多处 Duplicate attribute 错误问题
- 修改 app/components/oa/TaskForm.vue 中 a-input 类型冲突为 a-input-number
- 合并 admin/supply/warehouse.vue 和 production/equipment.vue 中多个 :class 绑定,避免重复属性
- 统一改为数组方式绑定静态和动态 class,防止 Vue 编译器 Duplicate attribute 警告
- 清理缓存并验证构建通过,确保无重复属性错误
- 通过扫描确认 app/ 目录下 Vue 文件不再存在重复属性问题
- 添加 OaTaskForm 组件类型声明及懒加载声明
- 将 ERP 演示独立 HTML 页面整合至 /app/pages,统一布局与导航
- 升级制造业管理后台页面风格,采用玻璃态和渐变设计
- 修订规划文档相关内容,更新 DEMO 系统名称及功能模块描述
- 修改 ecosystem.config.cjs 中运行端口为 10591
2026-04-09 12:08:55 +08:00

1215 lines
34 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>
<div class="task-list">
<div
v-for="task in todoTasks"
:key="task.id"
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-divider />
<a-menu-item key="move" @click="moveTaskToProgress(task)">
<RightOutlined /> 移动到进行中
</a-menu-item>
<a-menu-divider />
<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>
</div>
</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>
<div class="task-list">
<div
v-for="task in inProgressTasks"
:key="task.id"
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-divider />
<a-menu-item key="move" @click="moveTaskToReview(task)">
<RightOutlined /> 移动到待审核
</a-menu-item>
<a-menu-divider />
<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>
</div>
</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>
<div class="task-list">
<div
v-for="task in reviewTasks"
:key="task.id"
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-dropdown>
<template #overlay>
<a-menu>
<a-menu-item key="move" @click="moveTaskToDone(task)">
移动到已完成
</a-menu-item>
<a-menu-item key="back" @click="moveTaskToTodo(task)">
退回给负责人
</a-menu-item>
<a-menu-divider />
<a-menu-item key="delete" danger>删除</a-menu-item>
</a-menu>
</template>
<a-button size="small">
更多
</a-button>
</a-dropdown>
</a-space>
</div>
<div class="task-footer">
<div class="task-project">
<span class="project-tag">#{{ task.project }}</span>
</div>
</div>
</div>
</div>
</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>
<div class="task-list">
<div
v-for="task in doneTasks"
:key="task.id"
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>
</div>
</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="formMode === 'create' ? '新建任务' : '编辑任务'"
:footer="null"
width="600px"
@cancel="resetForm"
>
<a-space direction="vertical" size="large" style="width: 100%">
<div v-if="formMode === 'create'">
<a-form :model="simpleTaskForm" layout="vertical">
<a-form-item label="任务标题" required>
<a-input
v-model:value="simpleTaskForm.title"
placeholder="请输入任务标题"
:maxlength="200"
/>
</a-form-item>
<a-form-item label="任务描述(选填)">
<a-textarea
v-model:value="simpleTaskForm.description"
placeholder="请输入详细任务描述"
:rows="3"
:maxlength="500"
show-count
/>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="优先级" required>
<a-select
v-model:value="simpleTaskForm.priority"
placeholder="选择优先级"
style="width: 100%"
>
<a-select-option value="high"></a-select-option>
<a-select-option value="medium"></a-select-option>
<a-select-option value="low"></a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="负责人" required>
<a-select
v-model:value="simpleTaskForm.assignee"
placeholder="选择负责人"
style="width: 100%"
>
<a-select-option v-for="member in teamMembers" :key="member.id" :value="member.name">
{{ member.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="所属项目" required>
<a-select
v-model:value="simpleTaskForm.project"
placeholder="选择项目"
style="width: 100%"
>
<a-select-option v-for="project in activeProjects" :key="project.id" :value="project.name">
{{ project.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="截止日期">
<a-date-picker
v-model:value="simpleTaskForm.dueDate"
placeholder="选择截止日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div v-else-if="editingTask">
<div class="edit-task-info">
<h4>编辑任务: {{ editingTask.title }}</h4>
<p class="form-help-text">更新任务信息并说明修改理由</p>
</div>
<a-form layout="vertical">
<a-form-item label="任务标题" required>
<a-input
v-model:value="editingTask.title"
placeholder="请输入任务标题"
:maxlength="200"
/>
</a-form-item>
<a-form-item label="任务描述">
<a-textarea
v-model:value="editingTask.description"
placeholder="请输入详细任务描述"
:rows="3"
:maxlength="500"
show-count
/>
</a-form-item>
<a-form-item label="修改备注(选填)">
<a-textarea
v-model:value="editRemark"
placeholder="请说明本次修改的理由"
:rows="2"
:maxlength="200"
show-count
/>
</a-form-item>
</a-form>
</div>
<div class="form-actions">
<a-space>
<a-button type="primary" @click="handleTaskSubmit">
{{ formMode === 'create' ? '创建任务' : '保存修改' }}
</a-button>
<a-button @click="resetForm">取消</a-button>
</a-space>
</div>
</a-space>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import {
PlusOutlined, DownOutlined, MoreOutlined, EditOutlined,
DeleteOutlined, UserOutlined, CalendarOutlined, FieldTimeOutlined,
CheckCircleOutlined, CloseCircleOutlined, UserAddOutlined,
RightOutlined
} 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 editRemark = ref('')
// 新增任务表单
const simpleTaskForm = ref({
title: '',
description: '',
priority: 'medium' as 'low' | 'medium' | 'high',
assignee: '',
project: '',
dueDate: null as any
})
// 方法
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 moveTaskToProgress(task: Task) {
const index = todoTasks.value.findIndex(t => t.id === task.id)
if (index !== -1) {
const [movedTask] = todoTasks.value.splice(index, 1)
movedTask.status = 'in_progress'
inProgressTasks.value.push(movedTask)
message.success(`任务"${task.title}"已移动到进行中`)
}
}
function moveTaskToReview(task: Task) {
const index = inProgressTasks.value.findIndex(t => t.id === task.id)
if (index !== -1) {
const [movedTask] = inProgressTasks.value.splice(index, 1)
movedTask.status = 'review'
movedTask.submittedDate = new Date().toLocaleDateString()
reviewTasks.value.push(movedTask)
message.success(`任务"${task.title}"已提交审核`)
}
}
function moveTaskToDone(task: Task) {
const index = reviewTasks.value.findIndex(t => t.id === task.id)
if (index !== -1) {
const [movedTask] = reviewTasks.value.splice(index, 1)
movedTask.status = 'done'
movedTask.completedTime = '刚刚'
movedTask.completedBy = '审核人'
doneTasks.value.push(movedTask)
message.success(`任务"${task.title}"已标记为已完成`)
}
}
function moveTaskToTodo(task: Task) {
// 从待审核列退回
const index = reviewTasks.value.findIndex(t => t.id === task.id)
if (index !== -1) {
const [movedTask] = reviewTasks.value.splice(index, 1)
movedTask.status = 'todo'
todoTasks.value.push(movedTask)
message.success(`任务"${task.title}"已退还给负责人`)
}
// 从已完成列恢复
const doneIndex = doneTasks.value.findIndex(t => t.id === task.id)
if (doneIndex !== -1) {
const [movedTask] = doneTasks.value.splice(doneIndex, 1)
movedTask.status = 'todo'
movedTask.completedTime = ''
movedTask.completedBy = ''
todoTasks.value.push(movedTask)
message.success(`任务"${task.title}"已恢复为待处理`)
}
}
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})`)
// 这里应该移动任务到待处理列
}
// 团队成员数据(从概览页面引入)
const teamMembersData = ref([
{ id: 1, name: '张三', avatar: 'https://randomuser.me/api/portraits/men/32.jpg', role: '产品经理', status: '在线', tasks: 8 },
{ id: 2, name: '李四', avatar: 'https://randomuser.me/api/portraits/women/44.jpg', role: 'UI设计师', status: '在线', tasks: 5 },
{ id: 3, name: '王五', avatar: 'https://randomuser.me/api/portraits/men/67.jpg', role: '前端工程师', status: '忙碌', tasks: 12 },
{ id: 4, name: '赵六', avatar: 'https://randomuser.me/api/portraits/women/23.jpg', role: '后端工程师', status: '在线', tasks: 9 },
{ id: 5, name: '钱七', avatar: 'https://randomuser.me/api/portraits/men/89.jpg', role: '测试工程师', status: '离开', tasks: 7 },
{ id: 6, name: '孙八', avatar: 'https://randomuser.me/api/portraits/women/56.jpg', role: '运维工程师', status: '离线', tasks: 4 }
])
function handleTaskSubmit() {
if (formMode.value === 'create') {
if (!simpleTaskForm.value.title.trim() || !simpleTaskForm.value.assignee || !simpleTaskForm.value.project) {
message.error('请填写必填项:任务标题、负责人和所属项目')
return
}
const newTask: Task = {
id: Date.now(),
title: simpleTaskForm.value.title,
description: simpleTaskForm.value.description,
priority: simpleTaskForm.value.priority,
assignee: simpleTaskForm.value.assignee,
project: simpleTaskForm.value.project,
status: 'todo',
dueDate: simpleTaskForm.value.dueDate?.format('YYYY-MM-DD') || '今天 18:00',
collaborators: [
'https://randomuser.me/api/portraits/men/32.jpg',
'https://randomuser.me/api/portraits/women/44.jpg'
]
}
todoTasks.value.unshift(newTask)
message.success('任务创建成功')
resetForm()
} else if (editingTask.value) {
// 更新任务逻辑
message.success('任务更新成功')
resetForm()
}
}
function resetForm() {
showCreateModal.value = false
editingTask.value = null
formMode.value = 'create'
editRemark.value = ''
// 重置新建任务表单
simpleTaskForm.value = {
title: '',
description: '',
priority: 'medium',
assignee: '',
project: '',
dueDate: null
}
}
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>