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

792 lines
20 KiB
Vue
Raw Permalink 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>
<div class="projects-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-button type="primary" @click="showCreateModal = true">
<template #icon>
<PlusOutlined />
</template>
新建项目
</a-button>
<a-button @click="exportProjects">导出</a-button>
</a-space>
</div>
</div>
<!-- 项目筛选 -->
<a-card class="filter-card">
<a-row :gutter="[16, 16]">
<a-col :span="24" :md="12" :lg="8">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索项目名称、负责人..."
allow-clear
@search="handleSearch"
/>
</a-col>
<a-col :span="24" :md="12" :lg="8">
<a-select
v-model:value="filterStatus"
placeholder="项目状态"
style="width: 100%"
allow-clear
>
<a-select-option value="all">全部状态</a-select-option>
<a-select-option value="planning">规划中</a-select-option>
<a-select-option value="in_progress">进行中</a-select-option>
<a-select-option value="delayed">已延期</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="archived">已归档</a-select-option>
</a-select>
</a-col>
<a-col :span="24" :md="12" :lg="8">
<a-select
v-model:value="filterOwner"
placeholder="负责人"
style="width: 100%"
allow-clear
>
<a-select-option v-for="owner in owners" :key="owner" :value="owner">
{{ owner }}
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-card>
<!-- 项目网格 -->
<div class="projects-grid">
<a-card
v-for="project in filteredProjects"
:key="project.id"
class="project-card"
:class="{ 'highlight-card': project.priority === 'high' }"
>
<template #title>
<div class="project-title">
<span class="project-icon">{{ project.icon }}</span>
<span>{{ project.name }}</span>
<a-badge v-if="project.isNew" color="blue" text="NEW" class="new-badge" />
</div>
</template>
<div class="project-meta">
<div class="meta-item">
<span class="meta-label">负责人</span>
<span class="meta-value">{{ project.owner }}</span>
</div>
<div class="meta-item">
<span class="meta-label">截止日期</span>
<span class="meta-value" :class="{ 'deadline-warning': isDeadlineWarning(project.deadline) }">
{{ project.deadline }}
</span>
</div>
</div>
<div class="project-progress">
<div class="progress-header">
<span>项目进度</span>
<span>{{ project.progress }}%</span>
</div>
<a-progress
:percent="project.progress"
:stroke-color="getProgressColor(project.progress)"
:show-info="false"
size="small"
/>
</div>
<div class="project-stats">
<div class="stat-item">
<div class="stat-number">{{ project.taskCount }}</div>
<div class="stat-label">任务数</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ project.completedCount }}</div>
<div class="stat-label">已完成</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ project.memberCount }}</div>
<div class="stat-label">成员数</div>
</div>
</div>
<div class="project-tags">
<a-tag v-for="tag in project.tags" :key="tag" color="blue">
{{ tag }}
</a-tag>
</div>
<template #actions>
<a-space class="project-actions">
<a-button type="link" size="small" @click="viewProject(project.id)">
<EyeOutlined />
查看
</a-button>
<a-button type="link" size="small" @click="editProject(project.id)">
<EditOutlined />
编辑
</a-button>
<a-button type="link" size="small" danger @click="archiveProject(project.id)">
<DeleteOutlined />
归档
</a-button>
</a-space>
</template>
</a-card>
</div>
<!-- 空状态 -->
<div v-if="filteredProjects.length === 0" class="empty-state">
<div class="empty-content">
<span class="empty-icon">📋</span>
<h3>暂无项目</h3>
<p>点击"新建项目"按钮开始您的第一个项目</p>
<a-button type="primary" @click="showCreateModal = true">创建项目</a-button>
</div>
</div>
<!-- 新建项目模态框 -->
<a-modal
v-model:open="showCreateModal"
title="新建项目"
:footer="null"
width="600px"
@cancel="resetForm"
>
<a-form
ref="formRef"
:model="newProject"
:rules="formRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="项目名称" name="name">
<a-input v-model:value="newProject.name" placeholder="请输入项目名称" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="项目描述" name="description">
<a-textarea
v-model:value="newProject.description"
placeholder="请输入项目描述"
:rows="3"
show-count
:maxlength="500"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="负责人" name="owner">
<a-select
v-model:value="newProject.owner"
placeholder="选择负责人"
style="width: 100%"
>
<a-select-option v-for="owner in owners" :key="owner" :value="owner">
{{ owner }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="优先级" name="priority">
<a-select
v-model:value="newProject.priority"
placeholder="选择优先级"
style="width: 100%"
>
<a-select-option value="low"></a-select-option>
<a-select-option value="medium"></a-select-option>
<a-select-option value="high"></a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="开始日期" name="startDate">
<a-date-picker
v-model:value="newProject.startDate"
placeholder="选择开始日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="截止日期" name="deadline">
<a-date-picker
v-model:value="newProject.deadline"
placeholder="选择截止日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="项目标签" name="tags">
<a-select
v-model:value="newProject.tags"
mode="tags"
placeholder="输入标签,按回车添加"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item>
<a-space>
<a-button type="primary" :loading="creating" @click="createProject">
创建项目
</a-button>
<a-button @click="resetForm">重置</a-button>
</a-space>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import type { FormInstance } from 'ant-design-vue'
import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
interface Project {
id: number
name: string
description: string
icon: string
owner: string
progress: number
taskCount: number
completedCount: number
memberCount: number
deadline: string
priority: 'low' | 'medium' | 'high'
status: 'planning' | 'in_progress' | 'delayed' | 'completed' | 'archived'
tags: string[]
isNew: boolean
startDate: string
}
// 数据
const projects = ref<Project[]>([
{
id: 1,
name: '智慧园区管理系统',
description: '现代化的智慧园区综合管理平台,包含安防、能源、物业等功能模块',
icon: '🏢',
owner: '张三',
progress: 85,
taskCount: 42,
completedCount: 36,
memberCount: 8,
deadline: '2024-10-30',
priority: 'high',
status: 'in_progress',
tags: ['智慧园区', '物联网', '大数据'],
isNew: true,
startDate: '2024-08-01'
},
{
id: 2,
name: '客户关系管理升级',
description: 'CRM系统升级优化客户管理流程提升销售效率',
icon: '📊',
owner: '李四',
progress: 45,
taskCount: 28,
completedCount: 13,
memberCount: 6,
deadline: '2024-11-15',
priority: 'medium',
status: 'in_progress',
tags: ['CRM', '销售', '客户管理'],
isNew: false,
startDate: '2024-09-01'
},
{
id: 3,
name: '移动端App开发',
description: '新一代移动应用开发包含iOS和Android双平台',
icon: '📱',
owner: '王五',
progress: 92,
taskCount: 35,
completedCount: 32,
memberCount: 10,
deadline: '2024-10-25',
priority: 'high',
status: 'in_progress',
tags: ['移动端', 'React Native', 'App'],
isNew: false,
startDate: '2024-07-15'
},
{
id: 4,
name: '数据中心建设',
description: '企业级数据中心建设与迁移项目',
icon: '🖥️',
owner: '赵六',
progress: 68,
taskCount: 56,
completedCount: 38,
memberCount: 12,
deadline: '2024-11-05',
priority: 'high',
status: 'delayed',
tags: ['数据中心', '云计算', '服务器'],
isNew: false,
startDate: '2024-06-01'
},
{
id: 5,
name: 'Q4市场活动策划',
description: '第四季度市场推广活动整体策划',
icon: '🎯',
owner: '钱七',
progress: 20,
taskCount: 18,
completedCount: 4,
memberCount: 5,
deadline: '2024-12-10',
priority: 'medium',
status: 'planning',
tags: ['市场活动', '推广', '策划'],
isNew: true,
startDate: '2024-10-01'
},
{
id: 6,
name: '内部分享平台',
description: '企业内部知识分享与学习平台',
icon: '📚',
owner: '孙八',
progress: 100,
taskCount: 24,
completedCount: 24,
memberCount: 4,
deadline: '2024-09-30',
priority: 'low',
status: 'completed',
tags: ['知识管理', '学习平台', '内部工具'],
isNew: false,
startDate: '2024-07-01'
}
])
// 负责人列表
const owners = computed(() => {
const allOwners = projects.value.map(p => p.owner)
return [...new Set(allOwners)]
})
// 筛选条件
const searchKeyword = ref('')
const filterStatus = ref('all')
const filterOwner = ref('')
// 筛选后的项目
const filteredProjects = computed(() => {
return projects.value.filter(project => {
// 搜索关键词
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
const matches = project.name.toLowerCase().includes(keyword) ||
project.description.toLowerCase().includes(keyword) ||
project.owner.toLowerCase().includes(keyword)
if (!matches) return false
}
// 状态筛选
if (filterStatus.value && filterStatus.value !== 'all') {
if (project.status !== filterStatus.value) return false
}
// 负责人筛选
if (filterOwner.value) {
if (project.owner !== filterOwner.value) return false
}
return true
})
})
// 新建项目相关
const showCreateModal = ref(false)
const creating = ref(false)
const formRef = ref<FormInstance>()
const newProject = ref({
name: '',
description: '',
owner: '',
priority: 'medium' as const,
startDate: null,
deadline: null,
tags: []
})
// 表单验证规则
const formRules = {
name: [
{ required: true, message: '请输入项目名称', trigger: 'blur' },
{ min: 2, max: 50, message: '项目名称长度为2-50个字符', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入项目描述', trigger: 'blur' }
],
owner: [
{ required: true, message: '请选择负责人', trigger: 'change' }
]
}
// 方法
function handleSearch() {
// 搜索逻辑已在computed中实现
}
function getProgressColor(progress: number) {
if (progress < 30) return '#ff4d4f'
if (progress < 70) return '#faad14'
return '#52c41a'
}
function isDeadlineWarning(deadline: string) {
const deadlineDate = new Date(deadline)
const today = new Date()
const diffDays = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
return diffDays <= 7 && diffDays > 0
}
function viewProject(id: number) {
navigateTo(`/oa/projects/${id}`)
}
function editProject(id: number) {
const project = projects.value.find(p => p.id === id)
if (project) {
Object.assign(newProject.value, {
name: project.name,
description: project.description,
owner: project.owner,
priority: project.priority,
tags: [...project.tags]
})
showCreateModal.value = true
message.info('编辑功能开发中...')
}
}
function archiveProject(id: number) {
const projectIndex = projects.value.findIndex(p => p.id === id)
if (projectIndex !== -1) {
projects.value[projectIndex].status = 'archived'
message.success('项目已归档')
}
}
function exportProjects() {
const data = JSON.stringify(filteredProjects.value, null, 2)
const blob = new Blob([data], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `projects-${new Date().getTime()}.json`
link.click()
URL.revokeObjectURL(url)
message.success('导出成功')
}
async function createProject() {
try {
await formRef.value?.validate()
creating.value = true
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1000))
const newId = projects.value.length + 1
const project: Project = {
id: newId,
name: newProject.value.name,
description: newProject.value.description,
icon: getRandomIcon(),
owner: newProject.value.owner,
progress: 0,
taskCount: 0,
completedCount: 0,
memberCount: 1,
deadline: newProject.value.deadline?.format('YYYY-MM-DD') || '',
priority: newProject.value.priority,
status: 'planning',
tags: newProject.value.tags,
isNew: true,
startDate: newProject.value.startDate?.format('YYYY-MM-DD') || ''
}
projects.value.unshift(project)
message.success('项目创建成功')
showCreateModal.value = false
resetForm()
} catch (error) {
console.error('创建失败:', error)
} finally {
creating.value = false
}
}
function resetForm() {
newProject.value = {
name: '',
description: '',
owner: '',
priority: 'medium',
startDate: null,
deadline: null,
tags: []
}
formRef.value?.resetFields()
}
function getRandomIcon() {
const icons = ['🏢', '📊', '📱', '🖥️', '🎯', '📚', '🚀', '💼', '🏭', '🏥', '🏫', '🛒']
return icons[Math.floor(Math.random() * icons.length)]
}
onMounted(() => {
// 页面初始化逻辑
})
</script>
<style scoped>
.projects-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;
}
.filter-card {
margin-bottom: 24px;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
margin-top: 24px;
}
.project-card {
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
height: 100%;
display: flex;
flex-direction: column;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.highlight-card {
border-left: 4px solid #1890ff;
}
.project-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
}
.project-icon {
font-size: 20px;
flex-shrink: 0;
}
.new-badge {
font-size: 11px;
padding: 1px 6px;
border-radius: 4px;
}
.project-meta {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
.meta-item {
display: flex;
flex-direction: column;
}
.meta-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 2px;
}
.meta-value {
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
}
.deadline-warning {
color: #fa541c;
font-weight: 600;
}
.project-progress {
margin-bottom: 16px;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
}
.project-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.stat-item {
text-align: center;
padding: 8px;
background: #fafafa;
border-radius: 6px;
transition: all 0.2s ease;
}
.stat-item:hover {
background: #f0f0f0;
}
.stat-number {
font-size: 18px;
font-weight: 600;
color: #1890ff;
margin-bottom: 2px;
}
.stat-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.project-tags {
margin-top: auto;
margin-bottom: 16px;
}
.project-tags :deep(.ant-tag) {
margin-bottom: 4px;
}
.project-actions {
width: 100%;
display: flex;
justify-content: center;
border-top: 1px solid #f0f0f0;
padding-top: 16px;
margin-top: auto;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
text-align: center;
background: #fafafa;
border-radius: 8px;
margin-top: 24px;
}
.empty-content {
padding: 48px;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
display: block;
}
.empty-state h3 {
font-size: 20px;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 8px;
}
.empty-state p {
color: rgba(0, 0, 0, 0.45);
margin-bottom: 24px;
}
@media (max-width: 768px) {
.projects-grid {
grid-template-columns: 1fr;
}
.page-header {
flex-direction: column;
align-items: stretch;
}
.header-actions {
width: 100%;
}
}
</style>