|
|
@@ -0,0 +1,293 @@
|
|
|
+<template>
|
|
|
+ <div class="schedule-container">
|
|
|
+ <!-- 顶部导航栏 -->
|
|
|
+ <div class="top-bar">
|
|
|
+ <div class="top-bar-left">
|
|
|
+ <span class="today-badge">Schedule</span>
|
|
|
+ <div class="week-info">
|
|
|
+ <div class="week-range">{{ weekRangeText }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="top-bar-right">
|
|
|
+ <el-button-group>
|
|
|
+ <el-button icon="el-icon-arrow-left" size="mini" @click="prevWeek" />
|
|
|
+ <el-button :disabled="isCurrentWeek" icon="el-icon-arrow-right" size="mini" @click="nextWeek" />
|
|
|
+ </el-button-group>
|
|
|
+ <el-button size="mini" style="margin-left: 12px" type="primary" @click="goCurrentWeek">本周</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 统计表格 -->
|
|
|
+ <div v-loading="loading" class="table-section">
|
|
|
+ <el-empty v-if="!loading && tableData.length === 0" description="暂无排期数据" />
|
|
|
+ <el-table
|
|
|
+ v-else
|
|
|
+ border
|
|
|
+ :data="tableData"
|
|
|
+ :header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: 'bold', textAlign: 'center' }"
|
|
|
+ size="medium"
|
|
|
+ stripe>
|
|
|
+ <el-table-column align="center" fixed="left" label="人员" prop="opsUserName" width="100" />
|
|
|
+
|
|
|
+ <el-table-column
|
|
|
+ v-for="(day, idx) in weekDays"
|
|
|
+ :key="idx"
|
|
|
+ align="center"
|
|
|
+ :label="day.headerLabel"
|
|
|
+ min-width="120">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <div v-if="row.dayStats && row.dayStats[idx]">
|
|
|
+ <div class="stat-row">
|
|
|
+ <span class="stat-label">任务数</span>
|
|
|
+ <span class="stat-value">{{ row.dayStats[idx].taskCount }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-row">
|
|
|
+ <span class="stat-label">预估工时</span>
|
|
|
+ <span class="stat-value stat-hour">{{ formatStatHours(row.dayStats[idx].estimateWorkHour) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <span v-else class="no-data">-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column align="center" fixed="right" label="本周合计" min-width="140">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <div v-if="row.weekTotal">
|
|
|
+ <div class="stat-row total-row">
|
|
|
+ <span class="stat-label">任务数</span>
|
|
|
+ <span class="stat-value total-value">{{ row.weekTotal.taskCount }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-row total-row">
|
|
|
+ <span class="stat-label">预估工时</span>
|
|
|
+ <span class="stat-value total-value stat-hour">
|
|
|
+ {{ formatStatHours(row.weekTotal.estimateWorkHour) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <span v-else class="no-data">-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div v-if="tableData.length > 0" class="table-footer">
|
|
|
+ <span class="footer-label">共 {{ tableData.length }} 人</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import opsEventTaskApi from '@/api/devops/opsEventTask'
|
|
|
+
|
|
|
+ const WEEK_LABELS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
|
|
+
|
|
|
+ export default {
|
|
|
+ name: 'ScheduleStats',
|
|
|
+ data() {
|
|
|
+ const now = new Date()
|
|
|
+ const dayOfWeek = now.getDay()
|
|
|
+ const monday = new Date(now)
|
|
|
+ monday.setDate(now.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1))
|
|
|
+ monday.setHours(0, 0, 0, 0)
|
|
|
+
|
|
|
+ return {
|
|
|
+ currentWeekStart: monday,
|
|
|
+ tableData: [],
|
|
|
+ loading: false,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ weekDays() {
|
|
|
+ const days = []
|
|
|
+ for (let i = 0; i < 7; i++) {
|
|
|
+ const date = new Date(this.currentWeekStart)
|
|
|
+ date.setDate(date.getDate() + i)
|
|
|
+ days.push({
|
|
|
+ dateKey: this.formatDateKey(date),
|
|
|
+ headerLabel: `${WEEK_LABELS[i]} ${date.getMonth() + 1}/${date.getDate()}`,
|
|
|
+ isToday: this.isDateToday(date),
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return days
|
|
|
+ },
|
|
|
+ weekRangeText() {
|
|
|
+ const start = this.currentWeekStart
|
|
|
+ const end = new Date(start)
|
|
|
+ end.setDate(end.getDate() + 6)
|
|
|
+ return `${start.getMonth() + 1}/${start.getDate()} - ${end.getMonth() + 1}/${end.getDate()}`
|
|
|
+ },
|
|
|
+ weekEnd() {
|
|
|
+ if (!this.currentWeekStart) return ''
|
|
|
+ const end = new Date(this.currentWeekStart)
|
|
|
+ end.setDate(end.getDate() + 6)
|
|
|
+ return this.formatDateKey(end)
|
|
|
+ },
|
|
|
+ isCurrentWeek() {
|
|
|
+ const now = new Date()
|
|
|
+ const dayOfWeek = now.getDay()
|
|
|
+ const monday = new Date(now)
|
|
|
+ monday.setDate(now.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1))
|
|
|
+ monday.setHours(0, 0, 0, 0)
|
|
|
+ return this.formatDateKey(monday) === this.formatDateKey(this.currentWeekStart)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ formatDateKey(date) {
|
|
|
+ if (!date || !(date instanceof Date) || isNaN(date.getTime())) return ''
|
|
|
+ const y = date.getFullYear()
|
|
|
+ const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
+ const d = String(date.getDate()).padStart(2, '0')
|
|
|
+ return `${y}-${m}-${d}`
|
|
|
+ },
|
|
|
+ isDateToday(date) {
|
|
|
+ return this.formatDateKey(date) === this.formatDateKey(new Date())
|
|
|
+ },
|
|
|
+ formatStatHours(hours) {
|
|
|
+ const h = Number(hours)
|
|
|
+ if (!h || h <= 0) return '0h'
|
|
|
+ return h % 1 === 0 ? `${h}h` : `${h.toFixed(1)}h`
|
|
|
+ },
|
|
|
+ goCurrentWeek() {
|
|
|
+ const now = new Date()
|
|
|
+ const dayOfWeek = now.getDay()
|
|
|
+ const monday = new Date(now)
|
|
|
+ monday.setDate(now.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1))
|
|
|
+ monday.setHours(0, 0, 0, 0)
|
|
|
+ this.currentWeekStart = monday
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
+ prevWeek() {
|
|
|
+ const newStart = new Date(this.currentWeekStart)
|
|
|
+ newStart.setDate(newStart.getDate() - 7)
|
|
|
+ this.currentWeekStart = newStart
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
+ nextWeek() {
|
|
|
+ if (this.isCurrentWeek) return
|
|
|
+ const newStart = new Date(this.currentWeekStart)
|
|
|
+ newStart.setDate(newStart.getDate() + 7)
|
|
|
+ this.currentWeekStart = newStart
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
+ async fetchData() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const startDate = this.formatDateKey(this.currentWeekStart)
|
|
|
+ const endDate = this.weekEnd
|
|
|
+ const res = await opsEventTaskApi.getScheduleStats({ weekStart: startDate, weekEnd: endDate })
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ this.tableData = res.data.list || res.data || []
|
|
|
+ } else {
|
|
|
+ this.tableData = []
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取排期统计失败', error)
|
|
|
+ this.$message.error('获取排期统计数据失败')
|
|
|
+ this.tableData = []
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+ .schedule-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: calc(100vh - 130px);
|
|
|
+ padding: 16px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .top-bar {
|
|
|
+ display: flex;
|
|
|
+ flex-shrink: 0;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding: 12px 16px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
|
|
+ }
|
|
|
+
|
|
|
+ .top-bar-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .today-badge {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 2px 10px;
|
|
|
+ background: #409eff;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-right: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .week-range {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-section {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ background: #fff;
|
|
|
+ padding: 16px;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 2px 0;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-row.total-row {
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ color: #303133;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .total-value {
|
|
|
+ color: #409eff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-hour {
|
|
|
+ min-width: 40px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ .no-data {
|
|
|
+ color: #c0c4cc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-footer {
|
|
|
+ margin-top: 12px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ .footer-label {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+</style>
|