| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="process-tracking"> |
| | | <div class="header"> |
| | | <h2>è¿ç¨è¿½è¸ª</h2> |
| | | <el-button type="primary" @click="refreshData">å·æ°æ°æ®</el-button> |
| | | </div> |
| | | |
| | | <!-- 项ç®ç¶æç»è®¡ --> |
| | | <div class="status-cards"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6" v-for="(item, index) in statusStats" :key="index"> |
| | | <el-card class="status-card" :class="item.type"> |
| | | <div class="card-content"> |
| | | <div class="card-icon"> |
| | | <el-icon :size="24"> |
| | | <component :is="item.icon" /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="card-info"> |
| | | <div class="card-title">{{ item.label }}</div> |
| | | <div class="card-count">{{ item.count }}</div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <!-- 项ç®å表 --> |
| | | <el-card class="project-list"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>项ç®å表</span> |
| | | <el-button type="text" @click="toggleView"> |
| | | {{ viewMode === 'table' ? '忢å°çç¹å¾' : '忢å°å表' }} |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- è¡¨æ ¼è§å¾ --> |
| | | <div v-if="viewMode === 'table'"> |
| | | <el-table :data="projectList" style="width: 100%"> |
| | | <el-table-column prop="name" label="项ç®åç§°" /> |
| | | <el-table-column prop="manager" label="è´è´£äºº"/> |
| | | <el-table-column label="ç¶æ" > |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getStatusType(row.status)"> |
| | | {{ getStatusText(row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="è¿åº¦" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-progress :percentage="row.progress" :status="getProgressStatus(row.status)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="startDate" label="å¼å§æ¶é´" width="120" /> |
| | | <el-table-column prop="endDate" label="ç»ææ¶é´" width="120" /> |
| | | <el-table-column label="æä½" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-button type="text" @click="updateStatus(row)">æ´æ°ç¶æ</el-button> |
| | | <el-button type="text" @click="viewDetails(row)">详æ
</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <!-- çç¹å¾è§å¾ --> |
| | | <div v-else class="gantt-container"> |
| | | <div ref="ganttChart" style="width: 100%; height: 400px;"></div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- ç¶ææ´æ°å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="statusDialogVisible" title="æ´æ°é¡¹ç®ç¶æ" width="400px"> |
| | | <el-form :model="statusForm" label-width="80px"> |
| | | <el-form-item label="项ç®åç§°"> |
| | | <el-input v-model="statusForm.name" disabled /> |
| | | </el-form-item> |
| | | <el-form-item label="å½åç¶æ"> |
| | | <el-select v-model="statusForm.status" placeholder="è¯·éæ©ç¶æ"> |
| | | <el-option label="æªå¼å§" value="not_started" /> |
| | | <el-option label="è¿è¡ä¸" value="in_progress" /> |
| | | <el-option label="已宿" value="completed" /> |
| | | <el-option label="å»¶æ" value="delayed" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="è¿åº¦"> |
| | | <el-slider v-model="statusForm.progress" :max="100" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="statusDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="confirmStatusUpdate">确认</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, nextTick } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { Clock, Loading, Check, Warning } from '@element-plus/icons-vue' |
| | | import * as echarts from 'echarts' |
| | | |
| | | // ååºå¼æ°æ® |
| | | const viewMode = ref('table') |
| | | const statusDialogVisible = ref(false) |
| | | const ganttChart = ref(null) |
| | | let chartInstance = null |
| | | |
| | | // ç¶æç»è®¡æ°æ® |
| | | const statusStats = reactive([ |
| | | { label: 'æªå¼å§', count: 3, type: 'not-started', icon: Clock }, |
| | | { label: 'è¿è¡ä¸', count: 5, type: 'in-progress', icon: Loading }, |
| | | { label: '已宿', count: 8, type: 'completed', icon: Check }, |
| | | { label: 'å»¶æ', count: 2, type: 'delayed', icon: Warning } |
| | | ]) |
| | | |
| | | // 项ç®åè¡¨æ°æ® |
| | | const projectList = reactive([ |
| | | { |
| | | id: 1, |
| | | name: '产åéæ±åæ', |
| | | manager: 'å¼ ä¸', |
| | | status: 'completed', |
| | | progress: 100, |
| | | startDate: '2024-01-01', |
| | | endDate: '2024-01-15' |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: 'ç³»ç»æ¶æè®¾è®¡', |
| | | manager: 'æå', |
| | | status: 'in_progress', |
| | | progress: 75, |
| | | startDate: '2024-01-10', |
| | | endDate: '2024-01-25' |
| | | }, |
| | | { |
| | | id: 3, |
| | | name: 'å端å¼å', |
| | | manager: 'çäº', |
| | | status: 'in_progress', |
| | | progress: 60, |
| | | startDate: '2024-01-20', |
| | | endDate: '2024-02-10' |
| | | }, |
| | | { |
| | | id: 4, |
| | | name: 'å端å¼å', |
| | | manager: 'èµµå
', |
| | | status: 'in_progress', |
| | | progress: 45, |
| | | startDate: '2024-01-25', |
| | | endDate: '2024-02-15' |
| | | }, |
| | | { |
| | | id: 5, |
| | | name: 'æ°æ®åºè®¾è®¡', |
| | | manager: 'åä¸', |
| | | status: 'delayed', |
| | | progress: 30, |
| | | startDate: '2024-01-15', |
| | | endDate: '2024-01-30' |
| | | }, |
| | | { |
| | | id: 6, |
| | | name: 'æµè¯ç¨ä¾ç¼å', |
| | | manager: 'å¨å
«', |
| | | status: 'not_started', |
| | | progress: 0, |
| | | startDate: '2024-02-01', |
| | | endDate: '2024-02-20' |
| | | } |
| | | ]) |
| | | |
| | | // ç¶ææ´æ°è¡¨å |
| | | const statusForm = reactive({ |
| | | id: null, |
| | | name: '', |
| | | status: '', |
| | | progress: 0 |
| | | }) |
| | | |
| | | // è·åç¶æç±»å |
| | | const getStatusType = (status) => { |
| | | const typeMap = { |
| | | not_started: 'info', |
| | | in_progress: 'warning', |
| | | completed: 'success', |
| | | delayed: 'danger' |
| | | } |
| | | return typeMap[status] || 'info' |
| | | } |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const textMap = { |
| | | not_started: 'æªå¼å§', |
| | | in_progress: 'è¿è¡ä¸', |
| | | completed: '已宿', |
| | | delayed: 'å»¶æ' |
| | | } |
| | | return textMap[status] || 'æªç¥' |
| | | } |
| | | |
| | | // è·åè¿åº¦ç¶æ |
| | | const getProgressStatus = (status) => { |
| | | if (status === 'completed') return 'success' |
| | | if (status === 'delayed') return 'exception' |
| | | return null |
| | | } |
| | | |
| | | // 忢è§å¾æ¨¡å¼ |
| | | const toggleView = () => { |
| | | viewMode.value = viewMode.value === 'table' ? 'gantt' : 'table' |
| | | if (viewMode.value === 'gantt') { |
| | | nextTick(() => { |
| | | initGanttChart() |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // åå§åçç¹å¾ |
| | | const initGanttChart = () => { |
| | | if (!ganttChart.value) return |
| | | |
| | | if (chartInstance) { |
| | | chartInstance.dispose() |
| | | } |
| | | |
| | | chartInstance = echarts.init(ganttChart.value) |
| | | |
| | | // åå¤çç¹å¾æ°æ® |
| | | const data = projectList.map((project, index) => ({ |
| | | name: project.name, |
| | | value: [ |
| | | index, |
| | | new Date(project.startDate).getTime(), |
| | | new Date(project.endDate).getTime(), |
| | | project.progress |
| | | ], |
| | | itemStyle: { |
| | | color: getGanttColor(project.status) |
| | | } |
| | | })) |
| | | |
| | | const option = { |
| | | title: { |
| | | text: '项ç®çç¹å¾', |
| | | left: 'center' |
| | | }, |
| | | tooltip: { |
| | | formatter: (params) => { |
| | | const project = projectList[params.value[0]] |
| | | return ` |
| | | <div> |
| | | <strong>${project.name}</strong><br/> |
| | | è´è´£äºº: ${project.manager}<br/> |
| | | ç¶æ: ${getStatusText(project.status)}<br/> |
| | | è¿åº¦: ${project.progress}%<br/> |
| | | å¼å§æ¶é´: ${project.startDate}<br/> |
| | | ç»ææ¶é´: ${project.endDate} |
| | | </div> |
| | | ` |
| | | } |
| | | }, |
| | | grid: { |
| | | left: '15%', |
| | | right: '10%', |
| | | top: '15%', |
| | | bottom: '15%' |
| | | }, |
| | | xAxis: { |
| | | type: 'time', |
| | | axisLabel: { |
| | | formatter: (value) => { |
| | | return echarts.format.formatTime('MM-dd', value) |
| | | } |
| | | } |
| | | }, |
| | | yAxis: { |
| | | type: 'category', |
| | | data: projectList.map(p => p.name), |
| | | inverse: true |
| | | }, |
| | | series: [{ |
| | | type: 'custom', |
| | | renderItem: (params, api) => { |
| | | const categoryIndex = api.value(0) |
| | | const start = api.coord([api.value(1), categoryIndex]) |
| | | const end = api.coord([api.value(2), categoryIndex]) |
| | | const height = api.size([0, 1])[1] * 0.6 |
| | | |
| | | return { |
| | | type: 'rect', |
| | | shape: { |
| | | x: start[0], |
| | | y: start[1] - height / 2, |
| | | width: end[0] - start[0], |
| | | height: height |
| | | }, |
| | | style: api.style() |
| | | } |
| | | }, |
| | | data: data |
| | | }] |
| | | } |
| | | |
| | | chartInstance.setOption(option) |
| | | } |
| | | |
| | | // è·åçç¹å¾é¢è² |
| | | const getGanttColor = (status) => { |
| | | const colorMap = { |
| | | not_started: '#909399', |
| | | in_progress: '#E6A23C', |
| | | completed: '#67C23A', |
| | | delayed: '#F56C6C' |
| | | } |
| | | return colorMap[status] || '#909399' |
| | | } |
| | | |
| | | // æ´æ°ç¶æ |
| | | const updateStatus = (project) => { |
| | | statusForm.id = project.id |
| | | statusForm.name = project.name |
| | | statusForm.status = project.status |
| | | statusForm.progress = project.progress |
| | | statusDialogVisible.value = true |
| | | } |
| | | |
| | | // ç¡®è®¤ç¶ææ´æ° |
| | | const confirmStatusUpdate = () => { |
| | | const project = projectList.find(p => p.id === statusForm.id) |
| | | if (project) { |
| | | project.status = statusForm.status |
| | | project.progress = statusForm.progress |
| | | |
| | | // æ´æ°ç»è®¡æ°æ® |
| | | updateStatusStats() |
| | | |
| | | // 妿æ¯çç¹å¾è§å¾ï¼éæ°æ¸²æ |
| | | if (viewMode.value === 'gantt') { |
| | | nextTick(() => { |
| | | initGanttChart() |
| | | }) |
| | | } |
| | | |
| | | ElMessage.success('ç¶ææ´æ°æå') |
| | | } |
| | | statusDialogVisible.value = false |
| | | } |
| | | |
| | | // æ´æ°ç¶æç»è®¡ |
| | | const updateStatusStats = () => { |
| | | const stats = { |
| | | not_started: 0, |
| | | in_progress: 0, |
| | | completed: 0, |
| | | delayed: 0 |
| | | } |
| | | |
| | | projectList.forEach(project => { |
| | | stats[project.status]++ |
| | | }) |
| | | |
| | | statusStats[0].count = stats.not_started |
| | | statusStats[1].count = stats.in_progress |
| | | statusStats[2].count = stats.completed |
| | | statusStats[3].count = stats.delayed |
| | | } |
| | | |
| | | // æ¥ç详æ
|
| | | const viewDetails = (project) => { |
| | | ElMessage.info(`æ¥ç项ç®è¯¦æ
: ${project.name}`) |
| | | } |
| | | |
| | | // å·æ°æ°æ® |
| | | const refreshData = () => { |
| | | // 模æå®æ¶æ´æ° |
| | | const randomProject = projectList[Math.floor(Math.random() * projectList.length)] |
| | | if (randomProject.status === 'in_progress') { |
| | | randomProject.progress = Math.min(100, randomProject.progress + Math.floor(Math.random() * 10)) |
| | | if (randomProject.progress === 100) { |
| | | randomProject.status = 'completed' |
| | | } |
| | | } |
| | | |
| | | updateStatusStats() |
| | | |
| | | if (viewMode.value === 'gantt') { |
| | | nextTick(() => { |
| | | initGanttChart() |
| | | }) |
| | | } |
| | | |
| | | ElMessage.success('æ°æ®å·²å·æ°') |
| | | } |
| | | |
| | | onMounted(() => { |
| | | updateStatusStats() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .process-tracking { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .header h2 { |
| | | margin: 0; |
| | | color: #303133; |
| | | } |
| | | |
| | | .status-cards { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .status-card { |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .status-card:hover { |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .card-content { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .card-icon { |
| | | margin-right: 15px; |
| | | padding: 10px; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .status-card.not-started .card-icon { |
| | | background-color: #f4f4f5; |
| | | color: #909399; |
| | | } |
| | | |
| | | .status-card.in-progress .card-icon { |
| | | background-color: #fdf6ec; |
| | | color: #E6A23C; |
| | | } |
| | | |
| | | .status-card.completed .card-icon { |
| | | background-color: #f0f9ff; |
| | | color: #67C23A; |
| | | } |
| | | |
| | | .status-card.delayed .card-icon { |
| | | background-color: #fef0f0; |
| | | color: #F56C6C; |
| | | } |
| | | |
| | | .card-info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-title { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .card-count { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | |
| | | .project-list { |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .gantt-container { |
| | | min-height: 400px; |
| | | } |
| | | </style> |