1951 lines
65 KiB
Vue
Raw 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>
<DefaultLayout>
<div class="analytics-page">
<div class="container">
<!-- 顶部筛选栏 -->
<div class="header-actions">
<div class="filter-group">
<a-date-picker
v-model:value="selectedMonth"
picker="month"
:placeholder="'选择月份'"
format="YYYY年MM月"
valueFormat="YYYY-MM"
@change="handleMonthChange"
class="month-picker"
:allowClear="true"
/>
<a-select v-model:value="dateRange" class="date-range-select" @change="loadAllData" v-if="!selectedMonth">
<a-select-option value="week">近一周</a-select-option>
<a-select-option value="month">近一月</a-select-option>
<a-select-option value="quarter">近三月</a-select-option>
<a-select-option value="year">近一年</a-select-option>
<a-select-option value="all">全部</a-select-option>
</a-select>
</div>
<div class="action-group">
<a-button @click="loadAllData">
<template #icon><Icon name="refresh" size="sm" /></template>
刷新
</a-button>
<a-button type="primary" @click="exportData">
<template #icon><Icon name="download" size="sm" /></template>
导出报表
</a-button>
</div>
</div>
<a-spin :spinning="loading">
<!-- 核心指标卡片 -->
<div class="overview-section">
<h2 class="section-title">核心指标</h2>
<a-row :gutter="[16, 16]">
<a-col :xs="12" :sm="8" :md="4">
<div class="stat-card">
<div class="stat-icon">
<Icon name="document" size="lg" color="#31f08b" />
</div>
<div class="stat-content">
<div class="stat-label">总任务数</div>
<div class="stat-value">{{ overview.totalDemands }}</div>
</div>
</div>
</a-col>
<a-col :xs="12" :sm="8" :md="4">
<div class="stat-card warning">
<div class="stat-icon">
<Icon name="clock" size="lg" color="#FFA500" />
</div>
<div class="stat-content">
<div class="stat-label">待处理</div>
<div class="stat-value">{{ overview.pendingDemands }}</div>
</div>
</div>
</a-col>
<a-col :xs="12" :sm="8" :md="4">
<div class="stat-card info">
<div class="stat-icon">
<Icon name="loading" size="lg" color="#3B82F6" />
</div>
<div class="stat-content">
<div class="stat-label">进行中</div>
<div class="stat-value">{{ inProgressCount }}</div>
</div>
</div>
</a-col>
<a-col :xs="12" :sm="8" :md="4">
<div class="stat-card success">
<div class="stat-icon">
<Icon name="checkCircle" size="lg" color="#31f08b" />
</div>
<div class="stat-content">
<div class="stat-label">已完成</div>
<div class="stat-value">{{ overview.completedDemands }}</div>
</div>
</div>
</a-col>
<a-col :xs="12" :sm="8" :md="4">
<div class="stat-card">
<div class="stat-icon">
<Icon name="chart" size="lg" color="#31f08b" />
</div>
<div class="stat-content">
<div class="stat-label">完成率</div>
<div class="stat-value">{{ completionRate }}%</div>
</div>
</div>
</a-col>
<a-col :xs="12" :sm="8" :md="4">
<div class="stat-card">
<div class="stat-icon">
<Icon name="timer" size="lg" color="#31f08b" />
</div>
<div class="stat-content">
<div class="stat-label">准时率</div>
<div class="stat-value">{{ onTimeRate }}%</div>
</div>
</div>
</a-col>
</a-row>
</div>
<!-- Tab 切换 -->
<div class="tabs-section">
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<a-tab-pane key="demand" tab="需求分析">
<!-- 需求分析内容 -->
<div class="tab-content">
<a-row :gutter="16">
<a-col :xs="24" :lg="12">
<div class="chart-card">
<h3 class="chart-title">需求趋势</h3>
<div ref="trendChartRef" style="width: 100%; height: 300px;"></div>
</div>
</a-col>
<a-col :xs="24" :lg="12">
<div class="chart-card">
<h3 class="chart-title">类型分布</h3>
<div ref="typeChartRef" style="width: 100%; height: 300px;"></div>
</div>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px;">
<a-col :xs="24" :lg="12">
<div class="chart-card">
<h3 class="chart-title">部门需求量</h3>
<div ref="deptChartRef" style="width: 100%; height: 300px;"></div>
</div>
</a-col>
<a-col :xs="24" :lg="12">
<div class="chart-card">
<h3 class="chart-title">积分消耗趋势</h3>
<div ref="scoreChartRef" style="width: 100%; height: 300px;"></div>
</div>
</a-col>
</a-row>
<!-- 用户排行榜 -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">用户需求排行</h3>
</div>
<a-table
:columns="userColumns"
:data-source="statistics.userStats || []"
:pagination="{ pageSize: 10 }"
row-key="userId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'rank'">
<span class="rank-badge" :class="getRankClass(index + 1)">{{ index + 1 }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<a-progress
:percent="Math.round(record.completionRate)"
size="small"
:stroke-color="getProgressColor(record.completionRate)"
:format="(percent) => `${percent}%`"
class="completion-rate-progress"
/>
</template>
</template>
</a-table>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="designer" tab="设计师绩效">
<!-- 设计师绩效内容 -->
<div class="tab-content">
<!-- 汇总卡片 -->
<a-row :gutter="[16, 16]" style="margin-bottom: 16px;">
<a-col :xs="12" :sm="8" :md="6">
<div class="mini-stat-card">
<div class="mini-stat-label">设计师总数</div>
<div class="mini-stat-value">{{ designerPerformance.summary?.totalDesigners || 0 }}</div>
</div>
</a-col>
<a-col :xs="12" :sm="8" :md="6">
<div class="mini-stat-card">
<div class="mini-stat-label">总接单量</div>
<div class="mini-stat-value">{{ designerPerformance.summary?.totalReceived || 0 }}</div>
</div>
</a-col>
<a-col :xs="12" :sm="8" :md="6">
<div class="mini-stat-card">
<div class="mini-stat-label">平均准时率</div>
<div class="mini-stat-value">{{ designerPerformance.summary?.avgOnTimeRate || 0 }}%</div>
</div>
</a-col>
<a-col :xs="12" :sm="8" :md="6">
<div class="mini-stat-card" :class="{ 'warning': (designerPerformance.summary?.overloadCount || 0) > 0 }">
<div class="mini-stat-label">过载设计师</div>
<div class="mini-stat-value">{{ designerPerformance.summary?.overloadCount || 0 }}</div>
</div>
</a-col>
</a-row>
<!-- 设计师绩效排行榜 - 全宽显示 -->
<div class="table-card full-width">
<div class="table-header">
<h3 class="chart-title">设计师绩效排行榜</h3>
<div class="filter-bar">
<a-select
v-model:value="selectedDeptId"
placeholder="全部部门"
style="width: 180px"
allow-clear
@change="handleDeptChange"
:dropdown-style="{ background: '#1f1f1f', borderColor: '#303030' }"
>
<a-select-option v-for="dept in departmentList" :key="dept.id" :value="dept.id">
{{ dept.deptName }}
</a-select-option>
</a-select>
</div>
</div>
<a-table
:columns="designerColumns"
:data-source="designerPerformance.designerStats || []"
:pagination="{ pageSize: 10 }"
row-key="userId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'rank'">
<span class="rank-badge" :class="getRankClass(index + 1)">
{{ index + 1 }}
</span>
</template>
<template v-else-if="column.key === 'onTimeRate'">
<span :class="record.onTimeRate >= 80 ? 'text-success' : record.onTimeRate >= 60 ? 'text-warning' : 'text-danger'">
{{ record.onTimeRate }}%
</span>
</template>
<template v-else-if="column.key === 'avgRating'">
<a-rate :value="record.avgRating" disabled allow-half :count="5" class="white-rate" />
</template>
<template v-else-if="column.key === 'totalScore'">
<span class="score-value">{{ record.totalScore || 0 }}</span>
</template>
<template v-else-if="column.key === 'deductionScore'">
<a-button
v-if="record.deductionScore && record.deductionScore > 0"
type="link"
class="deduction-link"
@click.stop="showDeductionDetails(record)"
:style="{ padding: 0, height: 'auto', color: '#ef4444' }"
>
{{ record.deductionScore || 0 }}
</a-button>
<span v-else class="deduction-value">
{{ record.deductionScore || 0 }}
</span>
</template>
<template v-else-if="column.key === 'netScore'">
<span class="net-score-value">{{ record.netScore || 0 }}</span>
</template>
<template v-else-if="column.key === 'targetScore'">
<span>{{ record.targetScore || '-' }}</span>
</template>
<template v-else-if="column.key === 'targetCompletionRate'">
<span :class="getTargetRateClass(record.targetCompletionRate)">
{{ record.targetScore > 0 ? (record.targetCompletionRate || 0) + '%' : '-' }}
</span>
</template>
</template>
</a-table>
</div>
<!-- 工作负载分布 - 全宽显示 -->
<div class="chart-card full-width" style="margin-top: 16px;">
<h3 class="chart-title">工作负载分布</h3>
<div ref="workloadChartRef" style="width: 100%; height: 350px;"></div>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="efficiency" tab="流程效率">
<!-- 流程效率内容 -->
<div class="tab-content">
<!-- 流程耗时展示 -->
<div class="process-flow-card">
<h3 class="chart-title">任务流程平均耗时</h3>
<div class="process-flow">
<div class="process-step">
<div class="step-name">发起</div>
<div class="step-arrow">→</div>
</div>
<div class="process-step">
<div class="step-name">需求审核</div>
<div class="step-time">{{ processEfficiency.stageTimeStats?.demandApprovalTime || 0 }}h</div>
<div class="step-arrow">→</div>
</div>
<div class="process-step">
<div class="step-name">接单审核</div>
<div class="step-time">{{ processEfficiency.stageTimeStats?.receivingApprovalTime || 0 }}h</div>
<div class="step-arrow">→</div>
</div>
<div class="process-step">
<div class="step-name">抢单等待</div>
<div class="step-time">{{ processEfficiency.stageTimeStats?.grabWaitTime || 0 }}h</div>
<div class="step-arrow">→</div>
</div>
<div class="process-step bottleneck">
<div class="step-name">任务处理</div>
<div class="step-time">{{ processEfficiency.stageTimeStats?.processTime?.toFixed(1) || 0 }}h</div>
<div class="step-arrow">→</div>
</div>
<div class="process-step">
<div class="step-name">完结</div>
</div>
</div>
<div class="process-summary">
<span>总平均耗时: <strong>{{ processEfficiency.stageTimeStats?.totalCycleTime?.toFixed(1) || 0 }}</strong> 小时</span>
<span class="bottleneck-info">瓶颈环节: <strong>{{ processEfficiency.stageTimeStats?.bottleneckStage || '-' }}</strong> ({{ processEfficiency.stageTimeStats?.bottleneckPercentage?.toFixed(1) || 0 }}%)</span>
</div>
</div>
<a-row :gutter="16" style="margin-top: 16px;">
<a-col :xs="24" :lg="12">
<div class="chart-card">
<h3 class="chart-title">各环节耗时占比</h3>
<div ref="stageChartRef" style="width: 100%; height: 300px;"></div>
</div>
</a-col>
<a-col :xs="24" :lg="12">
<div class="table-card">
<h3 class="chart-title">效率指标详情</h3>
<a-table
:columns="efficiencyColumns"
:data-source="efficiencyTableData"
:pagination="false"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'change'">
<span :class="record.changeValue < 0 ? 'text-success' : 'text-danger'">
{{ record.changeValue < 0 ? '↓' : '↑' }}{{ Math.abs(record.changeValue).toFixed(1) }}%
</span>
</template>
</template>
</a-table>
</div>
</a-col>
</a-row>
</div>
</a-tab-pane>
<a-tab-pane key="warning" tab="预警监控">
<!-- 预警监控内容 -->
<div class="tab-content">
<!-- 预警统计卡片 -->
<a-row :gutter="[16, 16]" style="margin-bottom: 16px;">
<a-col :xs="12" :sm="6">
<div class="warning-stat-card urgent" @click="warningFilter = 'urgent'">
<div class="warning-icon">🔴</div>
<div class="warning-label">紧急</div>
<div class="warning-count">{{ urgentCount }}</div>
</div>
</a-col>
<a-col :xs="12" :sm="6">
<div class="warning-stat-card warning" @click="warningFilter = 'warning'">
<div class="warning-icon">🟡</div>
<div class="warning-label">警告</div>
<div class="warning-count">{{ warningCount }}</div>
</div>
</a-col>
<a-col :xs="12" :sm="6">
<div class="warning-stat-card normal" @click="warningFilter = 'normal'">
<div class="warning-icon">🟢</div>
<div class="warning-label">正常</div>
<div class="warning-count">{{ normalCount }}</div>
</div>
</a-col>
<a-col :xs="12" :sm="6">
<div class="warning-stat-card all" @click="warningFilter = 'all'">
<div class="warning-icon">📊</div>
<div class="warning-label">全部</div>
<div class="warning-count">{{ warningTasks.length }}</div>
</div>
</a-col>
</a-row>
<!-- 预警任务列表 -->
<div class="table-card">
<h3 class="chart-title">
{{ warningFilter === 'urgent' ? '紧急预警' : warningFilter === 'warning' ? '警告预警' : '预警任务列表' }}
<span class="count-badge">({{ filteredWarningTasks.length }})</span>
</h3>
<a-table
:columns="warningColumns"
:data-source="filteredWarningTasks"
:pagination="{ pageSize: 10 }"
row-key="taskId"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'level'">
<span class="level-badge" :class="record.warningLevel">
{{ record.warningLevel === 'urgent' ? '🔴紧急' : '🟡警告' }}
</span>
</template>
<template v-else-if="column.key === 'action'">
<a-button type="link" size="small" @click="viewTask(record)">查看</a-button>
<a-button type="link" size="small" @click="handleWarningAction(record)">
{{ getWarningActionText(record.warningType) }}
</a-button>
</template>
</template>
</a-table>
</div>
</div>
</a-tab-pane>
</a-tabs>
</div>
</a-spin>
</div>
</div>
<!-- 扣分明细弹窗 -->
<a-modal
v-model:open="deductionDetailVisible"
title="扣分明细"
width="800px"
:footer="null"
class="deduction-detail-modal"
>
<a-spin :spinning="deductionDetailLoading">
<div v-if="currentDesigner" class="deduction-detail-content">
<div class="designer-info">
<h4>{{ currentDesigner.realName || currentDesigner.userName }}</h4>
<p class="designer-dept">部门:{{ currentDesigner.deptName }}</p>
<p class="total-deduction">总扣分:<span class="deduction-number">{{ deductionDetail.totalDeduction || 0 }}</span> 分</p>
</div>
<!-- 延误扣分明细 -->
<div v-if="deductionDetail.delayDeductions && deductionDetail.delayDeductions.length > 0" class="deduction-section">
<h5>延误扣分明细</h5>
<a-table
:columns="delayDeductionColumns"
:data-source="deductionDetail.delayDeductions"
:pagination="false"
size="small"
row-key="taskId"
class="deduction-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'deductionScore'">
<span class="text-danger">{{ record.deductionScore }} 分</span>
</template>
<template v-else-if="column.key === 'overdueHours'">
<span>{{ record.overdueHours }} 小时</span>
</template>
<template v-else-if="column.key === 'deadline' || column.key === 'actualEndTime'">
{{ formatDateTime(record[column.key]) }}
</template>
</template>
</a-table>
</div>
<!-- 进度未更新扣分明细 -->
<div v-if="deductionDetail.progressDeductions && deductionDetail.progressDeductions.length > 0" class="deduction-section">
<h5>进度未更新扣分明细</h5>
<a-table
:columns="progressDeductionColumns"
:data-source="deductionDetail.progressDeductions"
:pagination="false"
size="small"
row-key="taskId"
class="deduction-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'deductionScore'">
<span class="text-danger">{{ record.deductionScore }} 分</span>
</template>
<template v-else-if="column.key === 'deductionDate'">
{{ formatDateOnly(record.deductionDate) }}
</template>
</template>
</a-table>
</div>
<!-- 无扣分记录 -->
<div v-if="(!deductionDetail.delayDeductions || deductionDetail.delayDeductions.length === 0) &&
(!deductionDetail.progressDeductions || deductionDetail.progressDeductions.length === 0)"
class="no-data">
<p>暂无扣分记录</p>
</div>
</div>
</a-spin>
</a-modal>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue'
import { useRouter } from 'vue-router'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import {
getDemandStatistics,
getDesignerPerformance,
getProcessEfficiency,
getWarningTasks,
getDesignerDeductionDetails,
type DemandStatistics,
type DesignerPerformance,
type ProcessEfficiency,
type WarningTask,
type DeductionDetail,
type DelayDeductionItem,
type ProgressDeductionItem
} from '@/api/analytics'
import { message } from 'ant-design-vue'
import * as echarts from 'echarts'
import Icon from '@/components/Icon.vue'
import dayjs, { Dayjs } from 'dayjs'
import { fetchDepartmentOverview } from '@/api/department'
const router = useRouter()
const loading = ref(false)
const selectedMonth = ref<string | null>(null)
const dateRange = ref('month')
const activeTab = ref('demand')
// 数据状态
const statistics = reactive<DemandStatistics>({
overview: {
totalDemands: 0,
todayDemands: 0,
weekDemands: 0,
monthDemands: 0,
completedDemands: 0,
pendingDemands: 0,
canceledDemands: 0,
totalScoreUsed: 0,
todayScoreUsed: 0,
weekScoreUsed: 0,
monthScoreUsed: 0,
avgCompletionTime: 0,
avgSatisfaction: 0
},
trendData: [],
typeDistribution: [],
userStats: [],
deptStats: []
})
const designerPerformance = reactive<DesignerPerformance>({
designerStats: [],
workloadStats: [],
summary: {
totalDesigners: 0,
totalReceived: 0,
totalCompleted: 0,
avgCompletionRate: 0,
avgOnTimeRate: 0,
avgProcessTime: 0,
avgSatisfaction: 0,
overloadCount: 0
}
})
const processEfficiency = reactive<ProcessEfficiency>({
stageTimeStats: {
demandApprovalTime: 0,
receivingApprovalTime: 0,
grabWaitTime: 0,
processTime: 0,
totalCycleTime: 0,
bottleneckStage: '',
bottleneckPercentage: 0
},
efficiencyMetrics: {
weekDemandApprovalTime: 0,
weekReceivingApprovalTime: 0,
weekGrabWaitTime: 0,
weekProcessTime: 0,
weekFirstPassRate: 0,
lastWeekDemandApprovalTime: 0,
lastWeekReceivingApprovalTime: 0,
lastWeekGrabWaitTime: 0,
lastWeekProcessTime: 0,
lastWeekFirstPassRate: 0,
monthDemandApprovalTime: 0,
monthReceivingApprovalTime: 0,
monthGrabWaitTime: 0,
monthProcessTime: 0,
monthFirstPassRate: 0,
totalRejected: 0,
rejectionRate: 0
},
approvalTrend: [],
stageDistribution: []
})
const warningTasks = ref<WarningTask[]>([])
const warningFilter = ref('all')
// 部门筛选
const selectedDeptId = ref<number | undefined>(undefined)
const departmentList = ref<any[]>([])
// 扣分明细弹窗
const deductionDetailVisible = ref(false)
const deductionDetailLoading = ref(false)
const currentDesigner = ref<any>(null)
const deductionDetail = ref<DeductionDetail>({
totalDeduction: 0,
delayDeductions: [],
progressDeductions: []
})
// 图表引用
const trendChartRef = ref<HTMLElement>()
const typeChartRef = ref<HTMLElement>()
const deptChartRef = ref<HTMLElement>()
const scoreChartRef = ref<HTMLElement>()
const workloadChartRef = ref<HTMLElement>()
const stageChartRef = ref<HTMLElement>()
// 计算属性
const overview = computed(() => statistics.overview)
const inProgressCount = computed(() => {
return overview.value.totalDemands - overview.value.completedDemands - overview.value.pendingDemands - overview.value.canceledDemands
})
const completionRate = computed(() => {
const total = overview.value.totalDemands || 0
const completed = overview.value.completedDemands || 0
if (total === 0) return '0.0'
return ((completed / total) * 100).toFixed(1)
})
const onTimeRate = computed(() => {
return designerPerformance.summary?.avgOnTimeRate?.toFixed(1) || '0.0'
})
const urgentCount = computed(() => warningTasks.value.filter(t => t.warningLevel === 'urgent').length)
const warningCount = computed(() => warningTasks.value.filter(t => t.warningLevel === 'warning').length)
const normalCount = computed(() => 0) // 正常任务不在预警列表中
const filteredWarningTasks = computed(() => {
if (warningFilter.value === 'all') return warningTasks.value
if (warningFilter.value === 'normal') return []
return warningTasks.value.filter(t => t.warningLevel === warningFilter.value)
})
// 效率表格数据
const efficiencyTableData = computed(() => [
{
key: '1',
metric: '需求审批时长',
week: processEfficiency.efficiencyMetrics?.weekDemandApprovalTime?.toFixed(1) + 'h',
lastWeek: processEfficiency.efficiencyMetrics?.lastWeekDemandApprovalTime?.toFixed(1) + 'h',
changeValue: ((processEfficiency.efficiencyMetrics?.weekDemandApprovalTime || 0) - (processEfficiency.efficiencyMetrics?.lastWeekDemandApprovalTime || 0)) / (processEfficiency.efficiencyMetrics?.lastWeekDemandApprovalTime || 1) * 100,
month: processEfficiency.efficiencyMetrics?.monthDemandApprovalTime?.toFixed(1) + 'h',
target: '< 4h'
},
{
key: '2',
metric: '接单审批时长',
week: processEfficiency.efficiencyMetrics?.weekReceivingApprovalTime?.toFixed(1) + 'h',
lastWeek: processEfficiency.efficiencyMetrics?.lastWeekReceivingApprovalTime?.toFixed(1) + 'h',
changeValue: ((processEfficiency.efficiencyMetrics?.weekReceivingApprovalTime || 0) - (processEfficiency.efficiencyMetrics?.lastWeekReceivingApprovalTime || 0)) / (processEfficiency.efficiencyMetrics?.lastWeekReceivingApprovalTime || 1) * 100,
month: processEfficiency.efficiencyMetrics?.monthReceivingApprovalTime?.toFixed(1) + 'h',
target: '< 2h'
},
{
key: '3',
metric: '平均抢单时长',
week: processEfficiency.efficiencyMetrics?.weekGrabWaitTime?.toFixed(1) + 'h',
lastWeek: processEfficiency.efficiencyMetrics?.lastWeekGrabWaitTime?.toFixed(1) + 'h',
changeValue: ((processEfficiency.efficiencyMetrics?.weekGrabWaitTime || 0) - (processEfficiency.efficiencyMetrics?.lastWeekGrabWaitTime || 0)) / (processEfficiency.efficiencyMetrics?.lastWeekGrabWaitTime || 1) * 100,
month: processEfficiency.efficiencyMetrics?.monthGrabWaitTime?.toFixed(1) + 'h',
target: '< 1h'
},
{
key: '4',
metric: '平均处理时长',
week: processEfficiency.efficiencyMetrics?.weekProcessTime?.toFixed(1) + 'h',
lastWeek: processEfficiency.efficiencyMetrics?.lastWeekProcessTime?.toFixed(1) + 'h',
changeValue: ((processEfficiency.efficiencyMetrics?.weekProcessTime || 0) - (processEfficiency.efficiencyMetrics?.lastWeekProcessTime || 0)) / (processEfficiency.efficiencyMetrics?.lastWeekProcessTime || 1) * 100,
month: processEfficiency.efficiencyMetrics?.monthProcessTime?.toFixed(1) + 'h',
target: '< 8h'
},
{
key: '5',
metric: '一次通过率',
week: processEfficiency.efficiencyMetrics?.weekFirstPassRate?.toFixed(1) + '%',
lastWeek: processEfficiency.efficiencyMetrics?.lastWeekFirstPassRate?.toFixed(1) + '%',
changeValue: (processEfficiency.efficiencyMetrics?.weekFirstPassRate || 0) - (processEfficiency.efficiencyMetrics?.lastWeekFirstPassRate || 0),
month: processEfficiency.efficiencyMetrics?.monthFirstPassRate?.toFixed(1) + '%',
target: '> 80%'
}
])
// 表格列定义
const userColumns = [
{ title: '排名', key: 'rank', width: 80, align: 'center' },
{ title: '用户名', dataIndex: 'realName', key: 'realName', width: 120 },
{ title: '部门', dataIndex: 'deptName', key: 'deptName' },
{ title: '发起数', dataIndex: 'totalDemands', key: 'totalDemands', sorter: (a: any, b: any) => a.totalDemands - b.totalDemands, align: 'center' },
{ title: '完成数', dataIndex: 'completedDemands', key: 'completedDemands', align: 'center' },
{ title: '总积分', dataIndex: 'totalScore', key: 'totalScore', align: 'center' },
{ title: '完成率', key: 'completionRate', width: 150, align: 'center' }
]
const designerColumns = [
{ title: '排名', key: 'rank', width: 60, align: 'center' },
{ title: '设计师', dataIndex: 'realName', key: 'realName', width: 80 },
{ title: '部门', dataIndex: 'deptName', key: 'deptName', width: 100 },
{ title: '接单', dataIndex: 'receivedCount', key: 'receivedCount', sorter: (a: any, b: any) => a.receivedCount - b.receivedCount, align: 'center', width: 60 },
{ title: '完成', dataIndex: 'completedCount', key: 'completedCount', align: 'center', width: 60 },
{ title: '准时率', key: 'onTimeRate', align: 'center', width: 70 },
{ title: '获得积分', key: 'totalScore', align: 'center', width: 80 },
{ title: '延误扣分', key: 'deductionScore', align: 'center', width: 80 },
{ title: '净积分', key: 'netScore', sorter: (a: any, b: any) => (a.netScore || 0) - (b.netScore || 0), align: 'center', width: 70 },
{ title: '目标积分', key: 'targetScore', align: 'center', width: 80 },
{ title: '目标完成率', key: 'targetCompletionRate', align: 'center', width: 90 },
{ title: '评分', key: 'avgRating', width: 120, align: 'center' }
]
const efficiencyColumns = [
{ title: '指标', dataIndex: 'metric', key: 'metric' },
{ title: '本周', dataIndex: 'week', key: 'week', align: 'right' },
{ title: '上周', dataIndex: 'lastWeek', key: 'lastWeek', align: 'right' },
{ title: '环比', key: 'change', align: 'right' },
{ title: '本月', dataIndex: 'month', key: 'month', align: 'right' },
{ title: '目标', dataIndex: 'target', key: 'target', align: 'right' }
]
const warningColumns = [
{ title: '级别', key: 'level', width: 100, align: 'center' },
{ title: '任务名称', dataIndex: 'taskTitle', key: 'taskTitle', ellipsis: true },
{ title: '处理人', dataIndex: 'receiverName', key: 'receiverName', align: 'center' },
{ title: '预警原因', dataIndex: 'warningReason', key: 'warningReason' },
{ title: '操作', key: 'action', width: 140, align: 'center' }
]
// 扣分明细表格列定义
const delayDeductionColumns = [
{ title: '任务编号', dataIndex: 'taskNo', key: 'taskNo', width: 120 },
{ title: '任务名称', dataIndex: 'taskTitle', key: 'taskTitle', ellipsis: true },
{ title: '任务积分', dataIndex: 'taskScore', key: 'taskScore', width: 100, align: 'center' },
{ title: '截止时间', key: 'deadline', width: 150 },
{ title: '实际完成时间', key: 'actualEndTime', width: 150 },
{ title: '延误时长', key: 'overdueHours', width: 100, align: 'center' },
{ title: '扣分', key: 'deductionScore', width: 100, align: 'center' },
{ title: '扣分原因', dataIndex: 'deductionReason', key: 'deductionReason', width: 150 }
]
const progressDeductionColumns = [
{ title: '任务编号', dataIndex: 'taskNo', key: 'taskNo', width: 120 },
{ title: '任务名称', dataIndex: 'taskTitle', key: 'taskTitle', ellipsis: true },
{ title: '扣分日期', key: 'deductionDate', width: 120 },
{ title: '扣分', key: 'deductionScore', width: 100, align: 'center' },
{ title: '扣分原因', dataIndex: 'reason', key: 'reason' }
]
// 方法
const handleMonthChange = (value: string | null) => {
selectedMonth.value = value
loadAllData()
}
const handleTabChange = async (key: string) => {
activeTab.value = key
await nextTick()
if (key === 'demand') {
renderDemandCharts()
} else if (key === 'designer') {
renderWorkloadChart()
} else if (key === 'efficiency') {
renderStageChart()
}
}
const loadAllData = async () => {
loading.value = true
try {
const month = selectedMonth.value || undefined
// 并行加载所有数据
const [demandData, designerData, efficiencyData, warningsData] = await Promise.all([
getDemandStatistics(dateRange.value, undefined, undefined, month),
getDesignerPerformance(month, selectedDeptId.value),
getProcessEfficiency(month),
getWarningTasks()
])
// 更新需求统计
Object.assign(statistics, demandData)
// 更新设计师绩效
Object.assign(designerPerformance, designerData)
// 更新流程效率
Object.assign(processEfficiency, efficiencyData)
// 更新预警任务
warningTasks.value = warningsData
await nextTick()
// 渲染当前Tab的图表
if (activeTab.value === 'demand') {
renderDemandCharts()
} else if (activeTab.value === 'designer') {
renderWorkloadChart()
} else if (activeTab.value === 'efficiency') {
renderStageChart()
}
} catch (error) {
message.error('加载数据失败')
console.error('加载数据分析数据失败:', error)
} finally {
loading.value = false
}
}
const renderDemandCharts = () => {
// 趋势图
if (trendChartRef.value && statistics.trendData?.length) {
const chart = echarts.init(trendChartRef.value)
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', backgroundColor: '#2A2A2A', borderColor: '#3A3A3A', textStyle: { color: '#ECECEC' } },
legend: { data: ['需求数量', '完成数量'], textStyle: { color: '#ECECEC' } },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: statistics.trendData.map(d => d.date), axisLine: { lineStyle: { color: '#3A3A3A' } }, axisLabel: { color: '#AFAFAF' } },
yAxis: { type: 'value', axisLine: { lineStyle: { color: '#3A3A3A' } }, axisLabel: { color: '#AFAFAF' }, splitLine: { lineStyle: { color: '#262626' } } },
series: [
{ name: '需求数量', type: 'line', data: statistics.trendData.map(d => d.demandCount || 0), smooth: true, itemStyle: { color: '#31f08b' } },
{ name: '完成数量', type: 'line', data: statistics.trendData.map(d => d.completedCount || 0), smooth: true, itemStyle: { color: '#3B82F6' } }
]
})
}
// 类型分布
if (typeChartRef.value && statistics.typeDistribution?.length) {
const chart = echarts.init(typeChartRef.value)
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'item', backgroundColor: '#2A2A2A', borderColor: '#3A3A3A', textStyle: { color: '#ECECEC' } },
legend: { orient: 'vertical', left: 'left', textStyle: { color: '#ECECEC' } },
series: [{
type: 'pie',
radius: '60%',
data: statistics.typeDistribution.map(d => ({ name: d.typeName || '未知', value: d.count || 0 })),
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}]
})
}
// 部门需求量
if (deptChartRef.value && statistics.deptStats?.length) {
const chart = echarts.init(deptChartRef.value)
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', backgroundColor: '#2A2A2A', borderColor: '#3A3A3A', textStyle: { color: '#ECECEC' } },
xAxis: { type: 'category', data: statistics.deptStats.map(d => d.deptName || '未知'), axisLabel: { rotate: 30, color: '#AFAFAF' }, axisLine: { lineStyle: { color: '#3A3A3A' } } },
yAxis: { type: 'value', axisLine: { lineStyle: { color: '#3A3A3A' } }, axisLabel: { color: '#AFAFAF' }, splitLine: { lineStyle: { color: '#262626' } } },
series: [{ type: 'bar', data: statistics.deptStats.map(d => d.totalDemands || 0), itemStyle: { color: '#31f08b' } }]
})
}
// 积分消耗趋势
if (scoreChartRef.value && statistics.trendData?.length) {
const chart = echarts.init(scoreChartRef.value)
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', backgroundColor: '#2A2A2A', borderColor: '#3A3A3A', textStyle: { color: '#ECECEC' } },
xAxis: { type: 'category', data: statistics.trendData.map(d => d.date), axisLine: { lineStyle: { color: '#3A3A3A' } }, axisLabel: { color: '#AFAFAF' } },
yAxis: { type: 'value', axisLine: { lineStyle: { color: '#3A3A3A' } }, axisLabel: { color: '#AFAFAF' }, splitLine: { lineStyle: { color: '#262626' } } },
series: [{ type: 'line', data: statistics.trendData.map(d => d.scoreUsed || 0), areaStyle: { color: 'rgba(49, 240, 139, 0.3)' }, itemStyle: { color: '#31f08b' } }]
})
}
}
const renderWorkloadChart = () => {
if (workloadChartRef.value && designerPerformance.workloadStats?.length) {
const chart = echarts.init(workloadChartRef.value)
const data = designerPerformance.workloadStats.slice(0, 10).reverse()
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', backgroundColor: '#2A2A2A', borderColor: '#3A3A3A', textStyle: { color: '#ECECEC' } },
grid: { left: '15%', right: '10%', bottom: '5%', top: '5%' },
xAxis: { type: 'value', axisLine: { lineStyle: { color: '#3A3A3A' } }, axisLabel: { color: '#AFAFAF' }, splitLine: { lineStyle: { color: '#262626' } } },
yAxis: { type: 'category', data: data.map(d => d.realName || d.userName), axisLine: { lineStyle: { color: '#3A3A3A' } }, axisLabel: { color: '#AFAFAF' } },
series: [{
type: 'bar',
data: data.map(d => ({
value: d.currentLoad,
itemStyle: { color: d.loadLevel === 'overload' ? '#EF4444' : d.loadLevel === 'high' ? '#FFA500' : '#31f08b' }
})),
label: { show: true, position: 'right', color: '#AFAFAF', formatter: '{c} 任务' }
}]
})
}
}
const renderStageChart = () => {
if (stageChartRef.value && processEfficiency.stageDistribution?.length) {
const chart = echarts.init(stageChartRef.value)
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'item', backgroundColor: '#2A2A2A', borderColor: '#3A3A3A', textStyle: { color: '#ECECEC' }, formatter: '{b}: {c}h ({d}%)' },
legend: { orient: 'vertical', left: 'left', textStyle: { color: '#ECECEC' } },
series: [{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: { show: false, position: 'center' },
emphasis: { label: { show: true, fontSize: 16, fontWeight: 'bold', color: '#ECECEC' } },
labelLine: { show: false },
data: processEfficiency.stageDistribution.map(d => ({ name: d.stageName, value: d.avgTime }))
}]
})
}
}
const getRankClass = (rank: number) => {
if (rank === 1) return 'rank-1'
if (rank === 2) return 'rank-2'
if (rank === 3) return 'rank-3'
return ''
}
// 目标完成率样式
const getTargetRateClass = (rate: number | null | undefined) => {
if (rate === null || rate === undefined) return ''
if (rate >= 100) return 'text-success' // 完成目标 - 绿色
if (rate >= 80) return 'text-primary' // 接近目标 - 蓝色
if (rate >= 50) return 'text-warning' // 进度一般 - 黄色
return 'text-danger' // 进度落后 - 红色
}
const getProgressColor = (rate: number) => {
if (rate >= 80) return '#31f08b'
if (rate >= 60) return '#3B82F6'
if (rate >= 40) return '#FFA500'
return '#EF4444'
}
const viewTask = (task: WarningTask) => {
router.push({ path: '/tasks', query: { taskId: task.taskId.toString() } })
}
const handleWarningAction = (task: WarningTask) => {
message.info('功能开发中...')
}
const getWarningActionText = (type: string) => {
switch (type) {
case 'overdue':
case 'near_deadline':
return '催办'
case 'long_pending':
return '指派'
case 'approval_delay':
return '催审'
default:
return '处理'
}
}
// 导出数据为 CSV
const exportData = () => {
const monthStr = selectedMonth.value || dayjs().format('YYYY-MM')
let csvContent = '\uFEFF' // BOM for UTF-8 encoding
// 1. 概览数据
csvContent += '【核心指标概览】\n'
csvContent += '指标,数值\n'
csvContent += `总任务数,${overview.value.totalDemands}\n`
csvContent += `待处理,${overview.value.pendingDemands}\n`
csvContent += `进行中,${inProgressCount.value}\n`
csvContent += `已完成,${overview.value.completedDemands}\n`
csvContent += `完成率,${completionRate.value}%\n`
csvContent += `准时率,${onTimeRate.value}%\n`
csvContent += '\n'
// 2. 用户需求统计
if (statistics.userStats && statistics.userStats.length > 0) {
csvContent += '【用户需求统计】\n'
csvContent += '排名,用户名,部门,发起数,完成数,总积分,完成率\n'
statistics.userStats.forEach((user: any, index: number) => {
const rate = user.totalDemands > 0 ? ((user.completedDemands / user.totalDemands) * 100).toFixed(1) : '0.0'
csvContent += `${index + 1},${user.userName || '-'},${user.deptName || '-'},${user.totalDemands},${user.completedDemands},${user.totalScore || 0},${rate}%\n`
})
csvContent += '\n'
}
// 3. 部门需求统计
if (statistics.deptStats && statistics.deptStats.length > 0) {
csvContent += '【部门需求统计】\n'
csvContent += '排名,部门名称,发起数,完成数,完成率\n'
statistics.deptStats.forEach((dept: any, index: number) => {
const rate = dept.totalDemands > 0 ? ((dept.completedDemands / dept.totalDemands) * 100).toFixed(1) : '0.0'
csvContent += `${index + 1},${dept.deptName || '-'},${dept.totalDemands},${dept.completedDemands},${rate}%\n`
})
csvContent += '\n'
}
// 4. 设计师绩效
if (designerPerformance.designerStats && designerPerformance.designerStats.length > 0) {
csvContent += '【设计师绩效】\n'
csvContent += '排名,设计师,部门,接单量,完成量,完成率,准时率,评分\n'
designerPerformance.designerStats.forEach((designer: any, index: number) => {
csvContent += `${index + 1},${designer.realName || '-'},${designer.deptName || '-'},${designer.receivedCount || 0},${designer.completedCount || 0},${designer.completionRate || 0}%,${designer.onTimeRate || 0}%,${designer.avgRating?.toFixed(1) || '-'}\n`
})
csvContent += '\n'
}
// 5. 流程效率指标
csvContent += '【流程效率指标】\n'
csvContent += '指标,本周,上周,本月,目标\n'
efficiencyTableData.value.forEach((row: any) => {
csvContent += `${row.metric},${row.week},${row.lastWeek},${row.month},${row.target}\n`
})
csvContent += '\n'
// 6. 预警任务
if (warningTasks.value && warningTasks.value.length > 0) {
csvContent += '【预警任务】\n'
csvContent += '级别,任务名称,处理人,预警原因\n'
warningTasks.value.forEach((task: any) => {
const level = task.warningLevel === 'urgent' ? '紧急' : task.warningLevel === 'warning' ? '警告' : '正常'
csvContent += `${level},${task.taskTitle || '-'},${task.receiverName || '-'},${task.warningReason || '-'}\n`
})
csvContent += '\n'
}
// 创建并下载文件
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `数据分析报表_${monthStr}.csv`
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(link.href)
message.success('报表导出成功')
}
// 加载接单部门列表(只显示到二级部门)
const loadDepartments = async () => {
try {
const res = await fetchDepartmentOverview()
const allDepts = res.data || []
// 获取所有接单部门
const receivingDepts = allDepts.filter((d: any) => d.deptType === 'receiving')
// 找出一级部门ID(parentId 为 0 或 null)
const level1DeptIds = new Set(
receivingDepts
.filter((d: any) => !d.parentId || d.parentId === 0)
.map((d: any) => d.id)
)
// 只保留一级和二级部门(parentId 为 0/null 或 parentId 在一级部门中)
departmentList.value = receivingDepts.filter((d: any) =>
!d.parentId || d.parentId === 0 || level1DeptIds.has(d.parentId)
)
} catch (error) {
console.error('加载部门列表失败:', error)
}
}
// 部门筛选变化
const handleDeptChange = (deptId: number | undefined) => {
selectedDeptId.value = deptId
loadDesignerData()
}
// 显示扣分明细
const showDeductionDetails = async (record: any) => {
console.log('点击扣分数字:', record)
const deduction = record.deductionScore || 0
console.log('扣分值:', deduction)
if (!deduction || deduction <= 0) {
console.log('扣分为0,不显示弹窗')
return // 没有扣分,不显示弹窗
}
console.log('打开扣分明细弹窗,userId:', record.userId)
currentDesigner.value = record
deductionDetailVisible.value = true
deductionDetailLoading.value = true
try {
const month = selectedMonth.value || undefined
console.log('请求扣分明细,userId:', record.userId, 'month:', month)
const detail = await getDesignerDeductionDetails(record.userId, month)
console.log('扣分明细数据:', detail)
deductionDetail.value = detail
} catch (error) {
message.error('加载扣分明细失败')
console.error('加载扣分明细失败:', error)
} finally {
deductionDetailLoading.value = false
}
}
// 格式化时间
const formatDateTime = (time: string | undefined) => {
if (!time) return '--'
return dayjs(time).format('YYYY-MM-DD HH:mm')
}
// 格式化日期
const formatDateOnly = (date: string | undefined) => {
if (!date) return '--'
return dayjs(date).format('YYYY-MM-DD')
}
// 单独加载设计师绩效数据(用于部门筛选)
const loadDesignerData = async () => {
loading.value = true
try {
const month = selectedMonth.value || undefined
const designerData = await getDesignerPerformance(month, selectedDeptId.value)
Object.assign(designerPerformance, designerData)
await nextTick()
if (activeTab.value === 'designer') {
renderWorkloadChart()
}
} catch (error) {
message.error('加载设计师绩效数据失败')
console.error('加载设计师绩效数据失败:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
loadDepartments()
loadAllData()
})
</script>
<style lang="scss" scoped>
@import "@/styles/variables.scss";
.analytics-page {
min-height: 100vh;
padding: $space-xl 0;
background: $bg-primary;
}
.container {
max-width: $container-max-width;
margin: 0 auto;
padding: 0 $space-xl;
}
.header-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $space-xl;
flex-wrap: wrap;
gap: $space-md;
.filter-group {
display: flex;
gap: $space-md;
}
.action-group {
display: flex;
gap: $space-sm;
}
.month-picker {
width: 150px;
}
.date-range-select {
width: 120px;
}
}
.section-title {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $text-primary;
margin-bottom: $space-lg;
}
.overview-section {
margin-bottom: $space-xl;
}
.stat-card {
background: $bg-card;
border-radius: $radius-lg;
padding: $space-lg;
display: flex;
gap: $space-md;
transition: all $transition-base $ease-out;
border: 1px solid rgba(255, 255, 255, 0.03);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
&:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
border-color: rgba($brand-primary, 0.3);
}
.stat-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: rgba($brand-primary, 0.1);
border-radius: 12px;
}
.stat-content {
flex: 1;
min-width: 0;
.stat-label {
font-size: $font-size-sm;
color: $text-secondary;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: $text-primary;
line-height: 1;
}
}
}
.tabs-section {
:deep(.ant-tabs-nav) {
margin-bottom: $space-xl;
.ant-tabs-tab {
color: $text-secondary;
padding: 12px 0;
transition: all 0.3s;
&:hover {
color: $text-primary;
}
&.ant-tabs-tab-active .ant-tabs-tab-btn {
color: $brand-primary;
font-weight: 600;
}
}
.ant-tabs-ink-bar {
background: $brand-primary;
height: 3px;
border-radius: 3px;
}
}
}
.tab-content {
min-height: 400px;
}
.chart-card, .table-card {
background: $bg-card;
border-radius: $radius-lg;
padding: $space-xl;
border: 1px solid rgba(255, 255, 255, 0.03);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $space-lg;
.chart-title {
margin-bottom: 0;
}
.filter-bar {
display: flex;
align-items: center;
gap: 12px;
}
}
.chart-title {
font-size: $font-size-lg;
font-weight: 600;
color: $text-primary;
margin-bottom: $space-lg;
display: flex;
align-items: center;
gap: 8px;
.count-badge {
font-size: $font-size-sm;
font-weight: normal;
color: $text-secondary;
background: rgba(255, 255, 255, 0.05);
padding: 2px 8px;
border-radius: 10px;
}
}
}
.mini-stat-card {
background: $bg-card;
border-radius: $radius-md;
padding: $space-lg;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.03);
&.warning {
border: 1px solid rgba($color-warning, 0.3);
background: rgba($color-warning, 0.05);
}
.mini-stat-label {
font-size: $font-size-sm;
color: $text-secondary;
margin-bottom: 8px;
}
.mini-stat-value {
font-size: 24px;
font-weight: 700;
color: $text-primary;
}
}
.rank-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
font-weight: 700;
font-size: 12px;
background: rgba(255, 255, 255, 0.05);
color: $text-secondary;
border: 1px solid rgba(255, 255, 255, 0.1);
&.rank-1 {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #fff;
border: none;
box-shadow: 0 2px 4px rgba(255, 215, 0, 0.3);
}
&.rank-2 {
background: linear-gradient(135deg, #E0E0E0 0%, #A0A0A0 100%);
color: #fff;
border: none;
box-shadow: 0 2px 4px rgba(192, 192, 192, 0.3);
}
&.rank-3 {
background: linear-gradient(135deg, #CD7F32 0%, #8B4513 100%);
color: #fff;
border: none;
box-shadow: 0 2px 4px rgba(205, 127, 50, 0.3);
}
}
.text-success { color: $color-success; }
.text-warning { color: $color-warning; }
.text-danger { color: $color-error; }
// 流程图样式
.process-flow-card {
background: $bg-card;
border-radius: $radius-lg;
padding: $space-xl;
border: 1px solid rgba(255, 255, 255, 0.03);
}
.process-flow {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: $space-lg;
margin: 40px 0;
}
.process-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
position: relative;
.step-name {
font-size: $font-size-sm;
color: $text-primary;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.step-time {
font-size: 13px;
color: $brand-primary;
font-weight: 600;
}
.step-arrow {
color: rgba(255, 255, 255, 0.2);
font-size: 20px;
position: absolute;
right: -24px;
top: 50%;
transform: translateY(-50%);
}
&:last-child .step-arrow {
display: none;
}
&.bottleneck {
.step-name {
background: rgba($color-warning, 0.1);
border-color: rgba($color-warning, 0.3);
color: $color-warning;
}
}
}
.process-summary {
text-align: center;
color: $text-secondary;
margin-top: $space-lg;
padding-top: $space-lg;
border-top: 1px solid rgba(255, 255, 255, 0.05);
strong {
color: $text-primary;
font-size: 16px;
}
.bottleneck-info {
margin-left: $space-xl;
color: $color-warning;
}
}
// 预警卡片样式
.warning-stat-card {
background: $bg-card;
border-radius: $radius-lg;
padding: $space-lg;
text-align: center;
cursor: pointer;
transition: all $transition-base;
border: 1px solid rgba(255, 255, 255, 0.03);
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.2);
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
}
&.urgent::before { background: $color-error; }
&.warning::before { background: $color-warning; }
&.normal::before { background: $color-success; }
&.all::before { background: $brand-primary; }
.warning-icon {
font-size: 28px;
margin-bottom: 12px;
opacity: 0.8;
}
.warning-label {
font-size: $font-size-sm;
color: $text-secondary;
margin-bottom: 4px;
}
.warning-count {
font-size: 32px;
font-weight: 700;
color: $text-primary;
}
}
.level-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.urgent {
background: rgba($color-error, 0.15);
color: lighten($color-error, 10%);
border: 1px solid rgba($color-error, 0.2);
}
&.warning {
background: rgba($color-warning, 0.15);
color: lighten($color-warning, 10%);
border: 1px solid rgba($color-warning, 0.2);
}
}
// Ant Design 深色主题覆盖
:deep(.ant-table) {
background: transparent !important;
color: $text-primary;
// 移除边框
.ant-table-container {
border: none !important;
}
.ant-table-thead > tr > th {
background: transparent !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.02) !important;
color: $text-secondary !important;
font-weight: 500;
padding: 16px 24px;
&::before {
display: none !important; // 移除表头分割线
}
}
.ant-table-tbody > tr > td {
border-bottom: none !important; // 彻底移除分割线
padding: 16px 24px;
color: $text-primary;
transition: background 0.3s ease;
}
// 极柔和的悬停效果,避免闪白
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.02) !important;
}
// 偶数行增加极淡的背景,形成斑马纹,替代分割线
.ant-table-tbody > tr:nth-child(even) > td {
background: rgba(255, 255, 255, 0.005);
}
// 移除所有垂直边框
.ant-table-cell {
border-right: none !important;
}
}
:deep(.ant-tabs) {
color: $text-primary;
}
:deep(.ant-select) {
.ant-select-selector {
background: $bg-tertiary !important;
border-color: rgba(255, 255, 255, 0.1) !important;
color: $text-primary !important;
border-radius: 6px !important;
}
.ant-select-arrow {
color: $text-secondary !important;
}
}
:deep(.ant-picker) {
background: $bg-tertiary !important;
border-color: rgba(255, 255, 255, 0.1) !important;
border-radius: 6px !important;
.ant-picker-input > input {
color: $text-primary !important;
}
.ant-picker-suffix, .ant-picker-clear {
color: $text-secondary !important;
}
}
:deep(.ant-btn) {
border-radius: 6px;
}
:deep(.ant-rate) {
font-size: 14px;
color: rgba(255, 255, 255, 0.15);
.ant-rate-star-full .ant-rate-star-second {
color: #FFD700 !important;
}
}
// 白色星星评分样式
.white-rate {
:deep(.ant-rate-star) {
color: rgba(255, 255, 255, 0.2) !important;
&.ant-rate-star-full,
&.ant-rate-star-half {
.ant-rate-star-second {
color: #ffffff !important;
}
}
}
}
// 积分显示样式
.score-value {
font-weight: 600;
color: $brand-primary;
font-size: 14px;
}
// 延误扣分样式
.deduction-value {
font-weight: 500;
color: #ef4444;
font-size: 14px;
}
// 扣分链接按钮样式
:deep(.deduction-link) {
padding: 0 !important;
height: auto !important;
font-weight: 500;
color: #ef4444 !important;
font-size: 14px;
text-decoration: underline;
text-decoration-style: dashed;
text-underline-offset: 2px;
&:hover {
color: #ff6b6b !important;
text-decoration-color: #ff6b6b;
}
}
// 净积分样式
.net-score-value {
font-weight: 600;
color: #60a5fa;
font-size: 14px;
}
// 文字颜色
.text-primary {
color: #60a5fa !important;
}
// 全宽卡片
.full-width {
width: 100%;
}
// 深色表格样式覆盖 - 防止闪白
.dark-table {
:deep(.ant-table) {
background: transparent !important;
// 移除所有表格边框
.ant-table-container,
.ant-table-content,
.ant-table-thead,
.ant-table-tbody {
border: none !important;
}
table {
border-collapse: collapse !important;
border-spacing: 0 !important;
}
.ant-table-thead > tr > th {
background: rgba(0, 0, 0, 0.4) !important;
border: none !important;
color: rgba(255, 255, 255, 0.6) !important;
font-weight: 500;
font-size: 13px;
padding: 12px 8px;
white-space: nowrap;
&::before {
display: none !important; // 移除表头分隔线
}
}
.ant-table-tbody > tr {
background: transparent !important;
> td {
background: transparent !important;
border: none !important;
padding: 10px 8px;
color: rgba(255, 255, 255, 0.85);
transition: none !important;
font-size: 13px;
white-space: nowrap; // 不允许换行
overflow: hidden;
text-overflow: ellipsis;
max-width: 120px; // 限制最大宽度
}
// 斑马纹效果替代边框线
&:nth-child(even) > td {
background: rgba(255, 255, 255, 0.015) !important;
}
&:hover > td {
background: rgba(49, 240, 139, 0.06) !important;
}
}
// 移除所有可能的边框线
.ant-table-cell {
border: none !important;
&::before, &::after {
display: none !important;
}
}
// 星星评分白色 - 强制覆盖
.ant-rate {
font-size: 12px !important;
.ant-rate-star {
color: rgba(255, 255, 255, 0.2) !important;
svg {
fill: rgba(255, 255, 255, 0.2) !important;
}
}
.ant-rate-star-full,
.ant-rate-star-half {
.ant-rate-star-first,
.ant-rate-star-second {
color: #ffffff !important;
svg {
fill: #ffffff !important;
}
}
}
.ant-rate-star-full .ant-rate-star-second {
color: #ffffff !important;
svg {
fill: #ffffff !important;
}
}
}
// 分页器深色样式
.ant-pagination {
.ant-pagination-item {
background: transparent;
border-color: rgba(255, 255, 255, 0.1);
a {
color: rgba(255, 255, 255, 0.65);
}
&:hover, &-active {
border-color: $brand-primary;
a {
color: $brand-primary;
}
}
}
.ant-pagination-prev, .ant-pagination-next {
.ant-pagination-item-link {
background: transparent;
border-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.65);
}
&:hover .ant-pagination-item-link {
border-color: $brand-primary;
color: $brand-primary;
}
}
.ant-pagination-options {
.ant-select-selector {
background: rgba(0, 0, 0, 0.2) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
}
}
}
// 排序图标
.ant-table-column-sorter {
color: rgba(255, 255, 255, 0.45);
&-up.active, &-down.active {
color: $brand-primary;
}
}
// 进度条百分比文字颜色
.completion-rate-progress {
.ant-progress-text {
color: rgba(255, 255, 255, 0.85) !important;
}
}
}
}
// 扣分明细弹窗样式
.deduction-detail-modal {
:deep(.ant-modal-content) {
background: #1f1f1f;
border: 1px solid #303030;
}
:deep(.ant-modal-header) {
background: #1f1f1f;
border-bottom: 1px solid #303030;
.ant-modal-title {
color: #ffffff;
}
}
:deep(.ant-modal-body) {
background: #1f1f1f;
padding: 24px;
}
}
.deduction-detail-content {
.designer-info {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #303030;
h4 {
color: #ffffff;
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
}
.designer-dept {
color: rgba(255, 255, 255, 0.65);
font-size: 14px;
margin: 0 0 8px 0;
}
.total-deduction {
color: rgba(255, 255, 255, 0.85);
font-size: 16px;
margin: 0;
.deduction-number {
color: #ef4444;
font-weight: 600;
font-size: 18px;
}
}
}
.deduction-section {
margin-bottom: 24px;
h5 {
color: #ffffff;
font-size: 16px;
font-weight: 600;
margin: 0 0 12px 0;
}
}
.deduction-table {
:deep(.ant-table) {
background: #141414;
.ant-table-thead > tr > th {
background: rgba(0, 0, 0, 0.4);
border-bottom: 1px solid #303030;
color: rgba(255, 255, 255, 0.85);
}
.ant-table-tbody > tr > td {
background: #141414;
border-bottom: 1px solid #262626;
color: rgba(255, 255, 255, 0.85);
}
.ant-table-tbody > tr:hover > td {
background: #1a1a1a;
}
}
}
.no-data {
text-align: center;
padding: 40px 0;
color: rgba(255, 255, 255, 0.45);
}
}
</style>