Your Name 651608ebf0 fix: 完善数据分析导出并避免乱码
为导出CSV增加转义处理,补齐多维度与一级需求部门数据,避免数据缺失。
2026-02-04 19:21:30 +08:00

3372 lines
124 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"
:locale="locale"
:popup-class-name="'dark-date-picker'"
@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"
@change="handleUserTableChange"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'rank'">
<span class="rank-badge" :class="getRankClass(getUserGlobalRank(index))">
{{ getUserGlobalRank(index) }}
</span>
</template>
<template v-else-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</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"
@change="handleDesignerTableChange"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'rank'">
<span class="rank-badge" :class="getRankClass(getGlobalRank(index))">
{{ getGlobalRank(index) }}
</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'">
<a-button
v-if="record.totalScore && record.totalScore > 0"
type="link"
class="score-link"
@click.stop="showScoreDetails(record)"
:style="{ padding: 0, height: 'auto', color: '#31F08B' }"
>
{{ formatScore(record.totalScore) }}
</a-button>
<span v-else class="score-value">{{ formatScore(record.totalScore) }}</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' }"
>
{{ formatScore(record.deductionScore) }}
</a-button>
<span v-else class="deduction-value">
{{ formatScore(record.deductionScore) }}
</span>
</template>
<template v-else-if="column.key === 'netScore'">
<span class="net-score-value">{{ formatScore(record.netScore) }}</span>
</template>
<template v-else-if="column.key === 'targetScore'">
<span>{{ record.targetScore ? formatScore(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-tab-pane key="dimension" tab="多维度分析">
<!-- 多维度分析内容 -->
<div class="tab-content">
<!-- 类目分析 - 全宽显示 -->
<div class="table-card full-width">
<div class="table-header">
<h3 class="chart-title">类目分析</h3>
</div>
<a-table
:columns="categoryColumns"
:data-source="dimensionData.categoryAnalysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 800 }"
row-key="categoryId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
</template>
</a-table>
</div>
<!-- 品牌分析 - 全宽显示 -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">品牌分析</h3>
</div>
<a-table
:columns="brandColumns"
:data-source="dimensionData.brandAnalysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 800 }"
row-key="brandId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
</template>
</a-table>
</div>
<!-- 接单部门分析 - 全宽显示 -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">接单部门分析</h3>
</div>
<a-table
:columns="receivingDeptColumns"
:data-source="dimensionData.receivingDeptAnalysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 1000 }"
row-key="deptId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'avgTasksPerMember'">
<span class="score-value">{{ formatScore(record.avgTasksPerMember) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
<template v-else-if="column.key === 'avgCompletionHours'">
<span>{{ record.avgCompletionHours ? formatScore(record.avgCompletionHours) + 'h' : '-' }}</span>
</template>
</template>
</a-table>
</div>
<!-- 需求部门分析 - 全宽显示 -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">需求部门分析</h3>
</div>
<a-table
:columns="demandDeptColumns"
:data-source="dimensionData.demandDeptAnalysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 900 }"
row-key="deptId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'avgTasksPerMember'">
<span class="score-value">{{ formatScore(record.avgTasksPerMember) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
</template>
</a-table>
</div>
<!-- 平台分析 - 全宽显示 -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">平台分析</h3>
</div>
<a-table
:columns="platformColumns"
:data-source="dimensionData.platformAnalysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 800 }"
row-key="platformId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
</template>
</a-table>
</div>
<!-- 任务类型分析(一级分类) -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">任务类型分析(一级分类)</h3>
</div>
<a-table
:columns="taskTypeColumns"
:data-source="dimensionData.taskTypeAnalysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 1200 }"
row-key="typeId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'avgScore'">
<span class="score-value">{{ formatScore(record.avgScore) }}</span>
</template>
<template v-else-if="column.key === 'unitScore'">
<span class="score-value">{{ formatScore(record.unitScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
<template v-else-if="column.key === 'onTimeRate'">
<span :class="record.onTimeRate >= 90 ? 'text-success' : record.onTimeRate >= 70 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.onTimeRate) }}%
</span>
</template>
<template v-else-if="column.key === 'avgCompletionHours'">
<span>{{ record.avgCompletionHours ? formatScore(record.avgCompletionHours) + 'h' : '-' }}</span>
</template>
<template v-else-if="column.key === 'estimatedHours'">
<span>{{ record.estimatedHours ? formatScore(record.estimatedHours) + 'h' : '-' }}</span>
</template>
</template>
</a-table>
</div>
<!-- 任务类型分析(二级分类) -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">任务类型分析(二级分类)</h3>
</div>
<a-table
:columns="taskTypeLevel2Columns"
:data-source="dimensionData.taskTypeLevel2Analysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 1300 }"
row-key="typeId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'avgScore'">
<span class="score-value">{{ formatScore(record.avgScore) }}</span>
</template>
<template v-else-if="column.key === 'unitScore'">
<span class="score-value">{{ formatScore(record.unitScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
<template v-else-if="column.key === 'onTimeRate'">
<span :class="record.onTimeRate >= 90 ? 'text-success' : record.onTimeRate >= 70 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.onTimeRate) }}%
</span>
</template>
<template v-else-if="column.key === 'avgCompletionHours'">
<span>{{ record.avgCompletionHours ? formatScore(record.avgCompletionHours) + 'h' : '-' }}</span>
</template>
<template v-else-if="column.key === 'estimatedHours'">
<span>{{ record.estimatedHours ? formatScore(record.estimatedHours) + 'h' : '-' }}</span>
</template>
</template>
</a-table>
</div>
<!-- 任务类型分析(三级分类) -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">任务类型分析(三级分类)</h3>
</div>
<a-table
:columns="taskTypeLevel3Columns"
:data-source="dimensionData.taskTypeLevel3Analysis"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 1400 }"
row-key="typeId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'avgScore'">
<span class="score-value">{{ formatScore(record.avgScore) }}</span>
</template>
<template v-else-if="column.key === 'unitScore'">
<span class="score-value">{{ formatScore(record.unitScore) }}</span>
</template>
<template v-else-if="column.key === 'completionRate'">
<span :class="record.completionRate >= 80 ? 'text-success' : record.completionRate >= 50 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.completionRate) }}%
</span>
</template>
<template v-else-if="column.key === 'onTimeRate'">
<span :class="record.onTimeRate >= 90 ? 'text-success' : record.onTimeRate >= 70 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.onTimeRate) }}%
</span>
</template>
<template v-else-if="column.key === 'avgCompletionHours'">
<span>{{ record.avgCompletionHours ? formatScore(record.avgCompletionHours) + 'h' : '-' }}</span>
</template>
<template v-else-if="column.key === 'estimatedHours'">
<span>{{ record.estimatedHours ? formatScore(record.estimatedHours) + 'h' : '-' }}</span>
</template>
</template>
</a-table>
</div>
</div>
</a-tab-pane>
<!-- 一级需求部门统计 -->
<a-tab-pane key="primary" tab="一级需求部门统计">
<div class="tab-content">
<!-- 统计卡片 -->
<a-row :gutter="[16, 16]" style="margin-bottom: 16px;">
<a-col :xs="12" :sm="6" :md="6">
<div class="mini-stat-card">
<div class="mini-stat-label">发起任务数</div>
<div class="mini-stat-value">{{ primaryDeptStats.totalTasks || 0 }}</div>
</div>
</a-col>
<a-col :xs="12" :sm="6" :md="6">
<div class="mini-stat-card success">
<div class="mini-stat-label">审核通过数</div>
<div class="mini-stat-value">{{ primaryDeptStats.approvedTasks || 0 }}</div>
</div>
</a-col>
<a-col :xs="12" :sm="6" :md="6">
<div class="mini-stat-card warning">
<div class="mini-stat-label">审核驳回数</div>
<div class="mini-stat-value">{{ primaryDeptStats.rejectedTasks || 0 }}</div>
</div>
</a-col>
<a-col :xs="12" :sm="6" :md="6">
<div class="mini-stat-card">
<div class="mini-stat-label">消耗积分</div>
<div class="mini-stat-value">{{ formatScore(primaryDeptStats.consumedScore) }}</div>
</div>
</a-col>
</a-row>
<!-- 一级需求部门排名 -->
<div class="table-card full-width">
<div class="table-header">
<h3 class="chart-title">一级需求部门排名</h3>
</div>
<a-table
:columns="primaryDeptColumns"
:data-source="primaryDeptRanking"
:pagination="{ pageSize: 10 }"
row-key="deptId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'rank'">
<span :class="['rank-badge', record.rank <= 3 ? 'top-' + record.rank : '']">
{{ record.rank }}
</span>
</template>
<template v-else-if="column.key === 'approvalRate'">
<span :class="record.approvalRate >= 90 ? 'text-success' : record.approvalRate >= 70 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.approvalRate) }}%
</span>
</template>
<template v-else-if="column.key === 'consumedScore'">
<span class="score-value">{{ formatScore(record.consumedScore) }}</span>
</template>
<template v-else-if="column.key === 'avgResponseHours'">
{{ record.avgResponseHours }} 小时
</template>
</template>
</a-table>
</div>
<!-- 品牌任务分布 -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">品牌任务分布(一级需求)</h3>
</div>
<a-table
:columns="brandDistributionColumns"
:data-source="brandDistribution"
:pagination="{ pageSize: 10 }"
row-key="brandId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'percentage'">
<a-progress :percent="Number(record.percentage)" :stroke-color="'#31f08b'" size="small" />
</template>
<template v-else-if="column.key === 'consumedScore'">
<span class="score-value">{{ formatScore(record.consumedScore) }}</span>
</template>
</template>
</a-table>
</div>
<!-- 用户维度统计 -->
<div class="table-card full-width" style="margin-top: 16px;">
<div class="table-header">
<h3 class="chart-title">用户维度统计(一级需求)</h3>
</div>
<a-table
:columns="primaryDeptUserColumns"
:data-source="primaryDeptUserRanking"
:pagination="{ pageSize: 10 }"
:scroll="{ x: 1400 }"
row-key="userId"
size="middle"
class="dark-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'rank'">
<span :class="['rank-badge', record.rank <= 3 ? 'top-' + record.rank : '']">
{{ record.rank }}
</span>
</template>
<template v-else-if="column.key === 'approvalRate'">
<span :class="record.approvalRate >= 90 ? 'text-success' : record.approvalRate >= 70 ? 'text-warning' : 'text-danger'">
{{ formatScore(record.approvalRate) }}%
</span>
</template>
<template v-else-if="column.key === 'consumedScore'">
<span class="score-value">{{ formatScore(record.consumedScore) }}</span>
</template>
</template>
</a-table>
</div>
</div>
</a-tab-pane>
</a-tabs>
</div>
</a-spin>
</div>
</div>
<!-- 扣分明细弹窗 -->
<a-modal
v-model:open="deductionDetailVisible"
title="扣分明细"
width="1200px"
:footer="null"
wrap-class-name="deduction-detail-modal-wrap"
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">{{ formatScore(deductionDetail.totalDeduction) }}</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"
:scroll="{ y: 300 }"
row-key="taskId"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'deductionScore'">
<span class="text-danger">{{ formatScore(record.deductionScore) }} 分</span>
</template>
<template v-else-if="column.key === 'taskScore'">
<span>{{ formatScore(record.taskScore) }}</span>
</template>
<template v-else-if="column.key === 'overdueHours'">
<span>{{ record.overdueHours }} 小时</span>
</template>
<template v-else-if="column.key === 'claimTime' || 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"
:scroll="{ y: 300 }"
row-key="taskId"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'deductionScore'">
<span class="text-danger">{{ formatScore(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>
<!-- 积分明细弹窗 -->
<a-modal
v-model:open="scoreDetailVisible"
title="积分明细"
width="1200px"
:footer="null"
wrap-class-name="score-detail-modal-wrap"
class="score-detail-modal"
>
<a-spin :spinning="scoreDetailLoading">
<div v-if="currentDesigner" class="score-detail-content">
<div class="designer-info">
<h4>{{ currentDesigner.realName || currentDesigner.userName }}</h4>
<p class="designer-dept">部门:{{ currentDesigner.deptName }}</p>
<p class="total-score">总积分:<span class="score-number">{{ formatScore(scoreDetail.totalScore) }}</span> 分</p>
</div>
<!-- 子任务积分明细 -->
<div v-if="scoreDetail.subTasks && scoreDetail.subTasks.length > 0" class="score-section">
<h5>子任务积分明细 ({{ scoreDetail.subTasks.length }} 条)</h5>
<a-table
:columns="scoreDetailColumns"
:data-source="scoreDetail.subTasks"
:pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (total: number) => `共 ${total} 条` }"
:scroll="{ y: 300 }"
row-key="id"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'score'">
<span class="text-success">+{{ formatScore(record.score) }} 分</span>
</template>
<template v-else-if="column.key === 'completeTime'">
{{ formatDateTime(record.completeTime) }}
</template>
</template>
</a-table>
</div>
<!-- 主任务积分明细 -->
<div v-if="scoreDetail.tasks && scoreDetail.tasks.length > 0" class="score-section">
<h5>主任务积分明细 ({{ scoreDetail.tasks.length }} 条)</h5>
<a-table
:columns="scoreDetailColumns"
:data-source="scoreDetail.tasks"
:pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (total: number) => `共 ${total} 条` }"
:scroll="{ y: 300 }"
row-key="id"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'score'">
<span class="text-success">+{{ formatScore(record.score) }} 分</span>
</template>
<template v-else-if="column.key === 'completeTime'">
{{ formatDateTime(record.completeTime) }}
</template>
</template>
</a-table>
</div>
<!-- 无积分记录 -->
<div v-if="(!scoreDetail.tasks || scoreDetail.tasks.length === 0) &&
(!scoreDetail.subTasks || scoreDetail.subTasks.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,
getDesignerScoreDetails,
getDimensionAnalysis,
getPrimaryDeptStatistics,
getPrimaryDeptRanking,
getPrimaryTaskBrandDistribution,
getPrimaryDeptUserRanking,
type DemandStatistics,
type DesignerPerformance,
type ProcessEfficiency,
type WarningTask,
type DeductionDetail,
type DelayDeductionItem,
type ProgressDeductionItem,
type DimensionAnalysis,
type TaskTypeAnalysis,
type PrimaryDeptStatistics,
type PrimaryDeptRanking,
type BrandTaskDistribution,
type PrimaryDeptUserRanking
} 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 'dayjs/locale/zh-cn'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { fetchDepartmentOverview } from '@/api/department'
// 设置 dayjs 中文
dayjs.locale('zh-cn')
// Ant Design 中文配置
const locale = zhCN
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,
inProgressDemands: 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 dimensionData = reactive<DimensionAnalysis>({
categoryAnalysis: [],
brandAnalysis: [],
receivingDeptAnalysis: [],
demandDeptAnalysis: [],
platformAnalysis: [],
taskTypeAnalysis: [],
taskTypeLevel2Analysis: [],
taskTypeLevel3Analysis: []
})
// 一级需求部门统计数据
const primaryDeptStats = reactive<PrimaryDeptStatistics>({
totalTasks: 0,
approvedTasks: 0,
rejectedTasks: 0,
consumedScore: 0,
taskGrowthRate: 0,
approvalGrowthRate: 0,
rejectGrowthRate: 0,
scoreGrowthRate: 0,
pendingTasks: 0,
inProgressTasks: 0,
completedTasks: 0
})
const primaryDeptRanking = ref<PrimaryDeptRanking[]>([])
const brandDistribution = ref<BrandTaskDistribution[]>([])
const primaryDeptUserRanking = ref<PrimaryDeptUserRanking[]>([])
// 部门筛选
const selectedDeptId = ref<number | undefined>(undefined)
const departmentList = ref<any[]>([])
// 设计师绩效表格分页
const designerTablePage = ref(1)
// 用户需求表格分页
const userTablePage = ref(1)
// 扣分明细弹窗
const deductionDetailVisible = ref(false)
const deductionDetailLoading = ref(false)
const currentDesigner = ref<any>(null)
const deductionDetail = ref<DeductionDetail>({
totalDeduction: 0,
delayDeductions: [],
progressDeductions: []
})
// 积分明细弹窗
const scoreDetailVisible = ref(false)
const scoreDetailLoading = ref(false)
const scoreDetail = ref<{
totalScore: number;
tasks: any[];
subTasks: any[];
designerName: string;
deptName: string;
}>({
totalScore: 0,
tasks: [],
subTasks: [],
designerName: '',
deptName: ''
})
// 图表引用
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)
// 进行中任务数:直接使用后端返回的 inProgressDemands
const inProgressCount = computed(() => {
return overview.value.inProgressDemands || 0
})
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: '总积分', 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: 'claimTime', width: 150 },
{ 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 scoreDetailColumns = [
{ title: '任务编号', dataIndex: 'itemNo', key: 'itemNo', width: 200, customRender: ({ record }: any) => record.itemNo || record.taskNo },
{ title: '主任务编号', dataIndex: 'taskNo', key: 'taskNo', width: 200 },
{ title: '任务名称', dataIndex: 'title', key: 'title', ellipsis: true },
{ title: '数量', dataIndex: 'quantity', key: 'quantity', width: 80, align: 'center' },
{ title: '完成时间', key: 'completeTime', width: 170 },
{ title: '获得积分', key: 'score', width: 100, align: 'center' }
]
// 多维度分析表格列定义
const categoryColumns = [
{ title: '类目名称', dataIndex: 'categoryName', key: 'categoryName', ellipsis: true },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 80 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 80 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 80 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 80 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 80 }
]
const brandColumns = [
{ title: '品牌名称', dataIndex: 'brandName', key: 'brandName', ellipsis: true },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 80 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 80 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 80 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 80 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 80 }
]
const receivingDeptColumns = [
{ title: '部门名称', dataIndex: 'deptName', key: 'deptName', ellipsis: true },
{ title: '部门人数', dataIndex: 'memberCount', key: 'memberCount', align: 'center', width: 80 },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 70 },
{ title: '人均任务', key: 'avgTasksPerMember', align: 'center', width: 80 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 70 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 70 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 70 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 70 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 70 },
{ title: '平均耗时', key: 'avgCompletionHours', align: 'center', width: 90 }
]
const demandDeptColumns = [
{ title: '部门名称', dataIndex: 'deptName', key: 'deptName', ellipsis: true },
{ title: '部门人数', dataIndex: 'memberCount', key: 'memberCount', align: 'center', width: 80 },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 80 },
{ title: '人均任务', key: 'avgTasksPerMember', align: 'center', width: 90 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 80 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 80 },
{ title: '待处理', dataIndex: 'pendingTasks', key: 'pendingTasks', align: 'center', width: 80 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 80 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 80 }
]
const platformColumns = [
{ title: '平台名称', dataIndex: 'platformName', key: 'platformName', ellipsis: true },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 100 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 100 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 100 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 100 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 100 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 100 }
]
// 任务类型分析列(一级分类)
const taskTypeColumns = [
{ title: '类型名称', dataIndex: 'typeName', key: 'typeName', ellipsis: true, width: 150 },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 80 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 80 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 80 },
{ title: '待接单', dataIndex: 'pendingTasks', key: 'pendingTasks', align: 'center', width: 80 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 90 },
{ title: '准时率', key: 'onTimeRate', align: 'center', width: 90 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 100 },
{ title: '平均积分', key: 'avgScore', align: 'center', width: 100 },
{ title: '单价分值', key: 'unitScore', align: 'center', width: 100 },
{ title: '预估工时', key: 'estimatedHours', align: 'center', width: 100 },
{ title: '平均完成时长', key: 'avgCompletionHours', align: 'center', width: 120 }
]
// 任务类型分析列(二级分类,带父级名称)
const taskTypeLevel2Columns = [
{ title: '父级分类', dataIndex: 'parentName', key: 'parentName', ellipsis: true, width: 120 },
{ title: '类型名称', dataIndex: 'typeName', key: 'typeName', ellipsis: true, width: 150 },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 80 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 80 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 80 },
{ title: '待接单', dataIndex: 'pendingTasks', key: 'pendingTasks', align: 'center', width: 80 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 90 },
{ title: '准时率', key: 'onTimeRate', align: 'center', width: 90 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 100 },
{ title: '平均积分', key: 'avgScore', align: 'center', width: 100 },
{ title: '单价分值', key: 'unitScore', align: 'center', width: 100 },
{ title: '预估工时', key: 'estimatedHours', align: 'center', width: 100 },
{ title: '平均完成时长', key: 'avgCompletionHours', align: 'center', width: 120 }
]
// 任务类型分析列(三级分类,带父级名称)
const taskTypeLevel3Columns = [
{ title: '父级分类', dataIndex: 'parentName', key: 'parentName', ellipsis: true, width: 120 },
{ title: '类型名称', dataIndex: 'typeName', key: 'typeName', ellipsis: true, width: 150 },
{ title: '任务数', dataIndex: 'totalTasks', key: 'totalTasks', align: 'center', width: 80 },
{ title: '总数量', dataIndex: 'totalQuantity', key: 'totalQuantity', align: 'center', width: 80 },
{ title: '已完成', dataIndex: 'completedTasks', key: 'completedTasks', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressTasks', key: 'inProgressTasks', align: 'center', width: 80 },
{ title: '待接单', dataIndex: 'pendingTasks', key: 'pendingTasks', align: 'center', width: 80 },
{ title: '完成率', key: 'completionRate', align: 'center', width: 90 },
{ title: '准时率', key: 'onTimeRate', align: 'center', width: 90 },
{ title: '总积分', key: 'totalScore', align: 'center', width: 100 },
{ title: '平均积分', key: 'avgScore', align: 'center', width: 100 },
{ title: '单价分值', key: 'unitScore', align: 'center', width: 100 },
{ title: '预估工时', key: 'estimatedHours', align: 'center', width: 100 },
{ title: '平均完成时长', key: 'avgCompletionHours', align: 'center', width: 120 }
]
// 一级需求部门排名列
const primaryDeptColumns = [
{ title: '排名', key: 'rank', dataIndex: 'rank', align: 'center', width: 60 },
{ title: '部门名称', dataIndex: 'deptName', key: 'deptName', ellipsis: true },
{ title: '一级部门', dataIndex: 'firstLevelDeptName', key: 'firstLevelDeptName', ellipsis: true, width: 120 },
{ title: '二级部门', dataIndex: 'secondLevelDeptName', key: 'secondLevelDeptName', ellipsis: true, width: 120 },
{ title: '发起任务数', dataIndex: 'taskCount', key: 'taskCount', align: 'center', width: 100 },
{ title: '审核通过率', key: 'approvalRate', align: 'center', width: 100 },
{ title: '消耗积分', key: 'consumedScore', align: 'center', width: 100 },
{ title: '平均响应时间', key: 'avgResponseHours', align: 'center', width: 120 },
{ title: '已完成', dataIndex: 'completedCount', key: 'completedCount', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressCount', key: 'inProgressCount', align: 'center', width: 80 }
]
// 品牌任务分布列
const brandDistributionColumns = [
{ title: '品牌名称', dataIndex: 'brandName', key: 'brandName', ellipsis: true },
{ title: '任务数', dataIndex: 'taskCount', key: 'taskCount', align: 'center', width: 80 },
{ title: '占比', key: 'percentage', align: 'center', width: 150 },
{ title: '负责部门', dataIndex: 'responsibleDeptName', key: 'responsibleDeptName', ellipsis: true },
{ title: '消耗积分', key: 'consumedScore', align: 'center', width: 100 },
{ title: '已完成', dataIndex: 'completedCount', key: 'completedCount', align: 'center', width: 80 }
]
// 一级需求部门用户排名列(用户维度统计)
const primaryDeptUserColumns = [
{ title: '排名', key: 'rank', dataIndex: 'rank', align: 'center', width: 60 },
{ title: '用户姓名', dataIndex: 'userName', key: 'userName', ellipsis: true, width: 120 },
{ title: '部门名称', dataIndex: 'deptName', key: 'deptName', ellipsis: true, width: 150 },
{ title: '一级部门', dataIndex: 'firstLevelDeptName', key: 'firstLevelDeptName', ellipsis: true, width: 120 },
{ title: '二级部门', dataIndex: 'secondLevelDeptName', key: 'secondLevelDeptName', ellipsis: true, width: 120 },
{ title: '发起任务数', dataIndex: 'taskCount', key: 'taskCount', align: 'center', width: 100 },
{ title: '审核通过数', dataIndex: 'approvedCount', key: 'approvedCount', align: 'center', width: 100 },
{ title: '审核驳回数', dataIndex: 'rejectedCount', key: 'rejectedCount', align: 'center', width: 100 },
{ title: '审核通过率', key: 'approvalRate', align: 'center', width: 100 },
{ title: '消耗积分', key: 'consumedScore', align: 'center', width: 100 },
{ title: '已完成', dataIndex: 'completedCount', key: 'completedCount', align: 'center', width: 80 },
{ title: '进行中', dataIndex: 'inProgressCount', key: 'inProgressCount', align: 'center', width: 80 },
{ title: '待审核', dataIndex: 'pendingCount', key: 'pendingCount', align: 'center', width: 80 }
]
// 方法
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 now = dayjs()
let startTime: string
let endTime: string = now.format('YYYY-MM-DDTHH:mm:ss')
if (month) {
startTime = dayjs(month).startOf('month').format('YYYY-MM-DDTHH:mm:ss')
endTime = dayjs(month).endOf('month').format('YYYY-MM-DDTHH:mm:ss')
} else {
switch (dateRange.value) {
case 'week':
startTime = now.subtract(7, 'day').format('YYYY-MM-DDTHH:mm:ss')
break
case 'quarter':
startTime = now.subtract(3, 'month').format('YYYY-MM-DDTHH:mm:ss')
break
case 'year':
startTime = now.subtract(1, 'year').format('YYYY-MM-DDTHH:mm:ss')
break
case 'all':
startTime = '2020-01-01T00:00:00'
break
default: // month
startTime = now.subtract(1, 'month').format('YYYY-MM-DDTHH:mm:ss')
}
}
// 并行加载所有数据
const [demandData, designerData, efficiencyData, warningsData, dimensionAnalysisData] = await Promise.all([
getDemandStatistics(dateRange.value, undefined, undefined, month),
getDesignerPerformance(month, selectedDeptId.value),
getProcessEfficiency(month),
getWarningTasks(),
getDimensionAnalysis(dateRange.value, month)
])
// 更新需求统计
Object.assign(statistics, demandData)
// 更新设计师绩效
Object.assign(designerPerformance, designerData)
// 更新流程效率
Object.assign(processEfficiency, efficiencyData)
// 更新预警任务
warningTasks.value = warningsData
// 更新多维度分析数据
Object.assign(dimensionData, dimensionAnalysisData)
// 加载一级需求部门统计数据
try {
const [primaryStats, primaryRanking, brandDist, userRanking] = await Promise.all([
getPrimaryDeptStatistics(startTime, endTime),
getPrimaryDeptRanking(startTime, endTime),
getPrimaryTaskBrandDistribution(startTime, endTime),
getPrimaryDeptUserRanking(startTime, endTime)
])
Object.assign(primaryDeptStats, primaryStats)
primaryDeptRanking.value = primaryRanking
brandDistribution.value = brandDist
primaryDeptUserRanking.value = userRanking
} catch (e) {
console.warn('加载一级需求部门统计失败:', e)
}
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 = () => {
if (loading.value) {
message.warning('数据正在加载中,请稍后再导出')
return
}
const monthStr = selectedMonth.value || dayjs().format('YYYY-MM')
let csvContent = '\uFEFF' // BOM for UTF-8 encoding
const csvEscape = (value: any) => {
const str = value === null || value === undefined ? '' : String(value)
const escaped = str.replace(/"/g, '""')
return /[",\n\r]/.test(escaped) ? `"${escaped}"` : escaped
}
const appendRow = (fields: any[]) => {
csvContent += `${fields.map(csvEscape).join(',')}\n`
}
// 1. 概览数据
csvContent += '【核心指标概览】\n'
appendRow(['指标', '数值'])
appendRow(['总任务数', overview.value.totalDemands])
appendRow(['待处理', overview.value.pendingDemands])
appendRow(['进行中', inProgressCount.value])
appendRow(['已完成', overview.value.completedDemands])
appendRow(['完成率', `${completionRate.value}%`])
appendRow(['准时率', `${onTimeRate.value}%`])
csvContent += '\n'
// 2. 用户需求统计
if (statistics.userStats && statistics.userStats.length > 0) {
csvContent += '【用户需求统计】\n'
appendRow(['排名', '用户名', '部门', '发起数', '完成数', '总积分', '完成率'])
statistics.userStats.forEach((user: any, index: number) => {
const rate = user.totalDemands > 0 ? ((user.completedDemands / user.totalDemands) * 100).toFixed(1) : '0.0'
appendRow([
index + 1,
user.realName || user.userName || '-',
user.deptName || '-',
user.totalDemands,
user.completedDemands,
formatScore(user.totalScore),
`${rate}%`
])
})
csvContent += '\n'
}
// 3. 部门需求统计
if (statistics.deptStats && statistics.deptStats.length > 0) {
csvContent += '【部门需求统计】\n'
appendRow(['排名', '部门名称', '发起数', '完成数', '完成率'])
statistics.deptStats.forEach((dept: any, index: number) => {
const rate = dept.totalDemands > 0 ? ((dept.completedDemands / dept.totalDemands) * 100).toFixed(1) : '0.0'
appendRow([
index + 1,
dept.deptName || '-',
dept.totalDemands,
dept.completedDemands,
`${rate}%`
])
})
csvContent += '\n'
}
// 4. 设计师绩效
if (designerPerformance.designerStats && designerPerformance.designerStats.length > 0) {
csvContent += '【设计师绩效】\n'
appendRow(['排名', '设计师', '部门', '接单量', '完成量', '获得积分', '扣分', '净积分', '目标积分', '目标完成率', '准时率', '评分'])
designerPerformance.designerStats.forEach((designer: any, index: number) => {
appendRow([
index + 1,
designer.realName || '-',
designer.deptName || '-',
designer.receivedCount || 0,
designer.completedCount || 0,
formatScore(designer.totalScore),
formatScore(designer.deductionScore),
formatScore(designer.netScore),
designer.targetScore ? formatScore(designer.targetScore) : '-',
designer.targetScore > 0 ? `${designer.targetCompletionRate || 0}%` : '-',
`${designer.onTimeRate || 0}%`,
designer.avgRating?.toFixed(1) || '-'
])
})
csvContent += '\n'
}
// 5. 流程效率指标
csvContent += '【流程效率指标】\n'
appendRow(['指标', '本周', '上周', '本月', '目标'])
efficiencyTableData.value.forEach((row: any) => {
appendRow([row.metric, row.week, row.lastWeek, row.month, row.target])
})
csvContent += '\n'
// 6. 预警任务
if (warningTasks.value && warningTasks.value.length > 0) {
csvContent += '【预警任务】\n'
appendRow(['级别', '任务名称', '处理人', '预警原因'])
warningTasks.value.forEach((task: any) => {
const level = task.warningLevel === 'urgent' ? '紧急' : task.warningLevel === 'warning' ? '警告' : '正常'
appendRow([level, task.taskTitle || '-', task.receiverName || '-', task.warningReason || '-'])
})
csvContent += '\n'
}
// 7. 多维度分析
if (dimensionData.categoryAnalysis?.length) {
csvContent += '【类目分析】\n'
appendRow(['类目名称', '任务数', '总数量', '已完成', '进行中', '完成率', '总积分'])
dimensionData.categoryAnalysis.forEach((item: any) => {
appendRow([
item.categoryName || '-',
item.totalTasks || 0,
item.totalQuantity || 0,
item.completedTasks || 0,
item.inProgressTasks || 0,
`${formatScore(item.completionRate)}%`,
formatScore(item.totalScore)
])
})
csvContent += '\n'
}
if (dimensionData.brandAnalysis?.length) {
csvContent += '【品牌分析】\n'
appendRow(['品牌名称', '任务数', '总数量', '已完成', '进行中', '完成率', '总积分'])
dimensionData.brandAnalysis.forEach((item: any) => {
appendRow([
item.brandName || '-',
item.totalTasks || 0,
item.totalQuantity || 0,
item.completedTasks || 0,
item.inProgressTasks || 0,
`${formatScore(item.completionRate)}%`,
formatScore(item.totalScore)
])
})
csvContent += '\n'
}
if (dimensionData.receivingDeptAnalysis?.length) {
csvContent += '【接单部门分析】\n'
appendRow(['部门名称', '部门人数', '任务数', '人均任务', '总数量', '已完成', '进行中', '完成率', '总积分', '平均耗时'])
dimensionData.receivingDeptAnalysis.forEach((item: any) => {
appendRow([
item.deptName || '-',
item.memberCount || 0,
item.totalTasks || 0,
formatScore(item.avgTasksPerMember),
item.totalQuantity || 0,
item.completedTasks || 0,
item.inProgressTasks || 0,
`${formatScore(item.completionRate)}%`,
formatScore(item.totalScore),
item.avgCompletionHours ? `${formatScore(item.avgCompletionHours)}h` : '-'
])
})
csvContent += '\n'
}
if (dimensionData.demandDeptAnalysis?.length) {
csvContent += '【需求部门分析】\n'
appendRow(['部门名称', '部门人数', '任务数', '人均任务', '总数量', '已完成', '待处理', '完成率', '总积分'])
dimensionData.demandDeptAnalysis.forEach((item: any) => {
appendRow([
item.deptName || '-',
item.memberCount || 0,
item.totalTasks || 0,
formatScore(item.avgTasksPerMember),
item.totalQuantity || 0,
item.completedTasks || 0,
item.pendingTasks || 0,
`${formatScore(item.completionRate)}%`,
formatScore(item.totalScore)
])
})
csvContent += '\n'
}
if (dimensionData.platformAnalysis?.length) {
csvContent += '【平台分析】\n'
appendRow(['平台名称', '任务数', '总数量', '已完成', '进行中', '完成率', '总积分'])
dimensionData.platformAnalysis.forEach((item: any) => {
appendRow([
item.platformName || '-',
item.totalTasks || 0,
item.totalQuantity || 0,
item.completedTasks || 0,
item.inProgressTasks || 0,
`${formatScore(item.completionRate)}%`,
formatScore(item.totalScore)
])
})
csvContent += '\n'
}
const exportTaskTypeAnalysis = (title: string, data: any[]) => {
if (!data?.length) return
csvContent += `【${title}】\n`
appendRow(['父级分类', '类型名称', '任务数', '总数量', '已完成', '进行中', '待接单', '完成率', '准时率', '总积分', '平均积分', '单价分值', '预估工时', '平均完成时长'])
data.forEach((item: any) => {
appendRow([
item.parentName || '-',
item.typeName || '-',
item.totalTasks || 0,
item.totalQuantity || 0,
item.completedTasks || 0,
item.inProgressTasks || 0,
item.pendingTasks || 0,
`${formatScore(item.completionRate)}%`,
`${formatScore(item.onTimeRate)}%`,
formatScore(item.totalScore),
formatScore(item.avgScore),
formatScore(item.unitScore),
item.estimatedHours ? `${formatScore(item.estimatedHours)}h` : '-',
item.avgCompletionHours ? `${formatScore(item.avgCompletionHours)}h` : '-'
])
})
csvContent += '\n'
}
exportTaskTypeAnalysis('任务类型分析(一级分类)', dimensionData.taskTypeAnalysis || [])
exportTaskTypeAnalysis('任务类型分析(二级分类)', (dimensionData.taskTypeLevel2Analysis || []).map((item: any) => ({ ...item, parentName: item.parentName || '-' })))
exportTaskTypeAnalysis('任务类型分析(三级分类)', (dimensionData.taskTypeLevel3Analysis || []).map((item: any) => ({ ...item, parentName: item.parentName || '-' })))
// 8. 一级需求部门统计
if (primaryDeptStats) {
csvContent += '【一级需求部门统计】\n'
appendRow(['指标', '数值'])
appendRow(['发起任务数', primaryDeptStats.totalTasks || 0])
appendRow(['审核通过', primaryDeptStats.approvedTasks || 0])
appendRow(['审核驳回', primaryDeptStats.rejectedTasks || 0])
appendRow(['消耗积分', formatScore(primaryDeptStats.consumedScore)])
appendRow(['待处理', primaryDeptStats.pendingTasks || 0])
appendRow(['进行中', primaryDeptStats.inProgressTasks || 0])
appendRow(['已完成', primaryDeptStats.completedTasks || 0])
csvContent += '\n'
}
if (primaryDeptRanking.value?.length) {
csvContent += '【一级需求部门排名】\n'
appendRow(['排名', '部门名称', '一级部门', '二级部门', '发起任务数', '审核通过率', '消耗积分', '平均响应时间', '已完成', '进行中'])
primaryDeptRanking.value.forEach((item: any, index: number) => {
appendRow([
item.rank || index + 1,
item.deptName || '-',
item.firstLevelDeptName || '-',
item.secondLevelDeptName || '-',
item.taskCount || 0,
`${formatScore(item.approvalRate)}%`,
formatScore(item.consumedScore),
item.avgResponseHours ? `${formatScore(item.avgResponseHours)}h` : '-',
item.completedCount || 0,
item.inProgressCount || 0
])
})
csvContent += '\n'
}
if (brandDistribution.value?.length) {
csvContent += '【品牌任务分布(一级需求)】\n'
appendRow(['品牌名称', '任务数', '占比', '负责部门', '消耗积分', '已完成'])
brandDistribution.value.forEach((item: any) => {
appendRow([
item.brandName || '-',
item.taskCount || 0,
`${formatScore(item.percentage)}%`,
item.responsibleDeptName || '-',
formatScore(item.consumedScore),
item.completedCount || 0
])
})
csvContent += '\n'
}
if (primaryDeptUserRanking.value?.length) {
csvContent += '【一级需求部门用户排名】\n'
appendRow(['排名', '用户姓名', '部门名称', '一级部门', '二级部门', '发起任务数', '审核通过数', '审核驳回数', '审核通过率', '消耗积分', '已完成', '进行中', '待审核'])
primaryDeptUserRanking.value.forEach((item: any, index: number) => {
appendRow([
item.rank || index + 1,
item.userName || '-',
item.deptName || '-',
item.firstLevelDeptName || '-',
item.secondLevelDeptName || '-',
item.taskCount || 0,
item.approvedCount || 0,
item.rejectedCount || 0,
`${formatScore(item.approvalRate)}%`,
formatScore(item.consumedScore),
item.completedCount || 0,
item.inProgressCount || 0,
item.pendingCount || 0
])
})
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
designerTablePage.value = 1 // 重置到第一页
loadDesignerData()
}
// 计算全局排名(考虑分页)- 设计师绩效
const getGlobalRank = (index: number): number => {
return (designerTablePage.value - 1) * 10 + index + 1
}
// 计算全局排名(考虑分页)- 用户需求
const getUserGlobalRank = (index: number): number => {
return (userTablePage.value - 1) * 10 + index + 1
}
// 处理设计师绩效表格变化(翻页等)
const handleDesignerTableChange = (pagination: any) => {
if (pagination && pagination.current) {
designerTablePage.value = pagination.current
}
}
// 处理用户需求表格变化(翻页等)
const handleUserTableChange = (pagination: any) => {
if (pagination && pagination.current) {
userTablePage.value = pagination.current
}
}
// 显示积分明细
const showScoreDetails = async (record: any) => {
console.log('点击积分数字:', record)
const score = record.totalScore || 0
if (!score || score <= 0) {
return // 没有积分,不显示弹窗
}
currentDesigner.value = record
scoreDetailVisible.value = true
scoreDetailLoading.value = true
try {
const month = selectedMonth.value || undefined
console.log('请求积分明细,userId:', record.userId, 'month:', month)
const detail = await getDesignerScoreDetails(record.userId, month)
console.log('积分明细数据:', detail)
scoreDetail.value = detail
} catch (error) {
message.error('加载积分明细失败')
console.error('加载积分明细失败:', error)
} finally {
scoreDetailLoading.value = false
}
}
// 显示扣分明细
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 formatScore = (score: number | null | undefined): string => {
if (score === null || score === undefined) return '0.00'
// 先乘以100,四舍五入取整,再除以100,避免浮点数精度问题
return (Math.round(Number(score) * 100) / 100).toFixed(2)
}
// 单独加载设计师绩效数据(用于部门筛选)
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;
// 修复页面滚动条样式
&::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05) !important;
border-radius: 4px !important;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2) !important;
border-radius: 4px !important;
&:hover {
background: rgba(255, 255, 255, 0.3) !important;
}
}
}
.container {
max-width: $container-max-width;
margin: 0 auto;
padding: 0 $space-xl;
// 修复容器滚动条样式
&::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05) !important;
border-radius: 4px !important;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2) !important;
border-radius: 4px !important;
&:hover {
background: rgba(255, 255, 255, 0.3) !important;
}
}
}
.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;
}
}
// 积分链接按钮样式
:deep(.score-link) {
padding: 0 !important;
height: auto !important;
font-weight: 500;
color: #31F08B !important;
font-size: 14px;
text-decoration: underline;
text-decoration-style: dashed;
text-underline-offset: 2px;
&:hover {
color: #5eff9e !important;
text-decoration-color: #5eff9e;
}
}
// 净积分样式
.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;
}
// 修复表格滚动条样式
.ant-table-body,
.ant-table-container {
&::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05) !important;
border-radius: 4px !important;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2) !important;
border-radius: 4px !important;
&:hover {
background: rgba(255, 255, 255, 0.3) !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 {
color: rgba(255, 255, 255, 0.65);
.ant-select-selector {
background: rgba(0, 0, 0, 0.2) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
color: rgba(255, 255, 255, 0.65) !important;
}
.ant-select-selection-item {
color: rgba(255, 255, 255, 0.65) !important;
}
.ant-select-arrow {
color: rgba(255, 255, 255, 0.45) !important;
}
}
.ant-pagination-total-text {
color: rgba(255, 255, 255, 0.65);
}
.ant-pagination-options-quick-jumper {
color: rgba(255, 255, 255, 0.65);
input {
background: rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.85);
}
}
// 修复分页器滚动条样式
&::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05) !important;
border-radius: 4px !important;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2) !important;
border-radius: 4px !important;
&:hover {
background: rgba(255, 255, 255, 0.3) !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;
}
}
}
}
// 扣分明细弹窗样式(使用非scoped样式,因为Modal渲染到body)
</style>
<style lang="scss">
@import "@/styles/variables.scss";
// 扣分明细弹窗样式 - 非scoped,因为Modal渲染到body
.deduction-detail-modal-wrap {
.ant-modal-content {
background: $bg-card;
border: 1px solid $border-primary;
}
.ant-modal-header {
background: $bg-card;
border-bottom: 1px solid $border-color;
.ant-modal-title {
color: $text-primary;
}
}
.ant-modal-body {
background: $bg-card;
padding: 24px;
}
// 表格样式 - 与用户管理页面严格一致
.ant-table {
background: transparent !important;
.ant-table-container {
border: none !important;
}
.ant-table-thead > tr > th {
background: $bg-tertiary !important;
color: $text-secondary !important;
font-weight: $font-weight-semibold !important;
border-bottom: 1px solid $border-color !important;
padding: 12px 16px !important;
border-top: none !important;
border-left: none !important;
border-right: none !important;
&::before {
display: none !important;
}
}
.ant-table-tbody > tr {
background: transparent !important;
transition: none !important;
&:hover > td {
background: rgba($brand-primary, 0.03) !important;
transition: none !important;
}
> td {
background: transparent !important;
border-bottom: 1px solid $border-color !important;
padding: 16px !important;
color: $text-primary !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
transition: none !important;
}
}
// 移除所有其他边框
.ant-table-cell {
border-left: none !important;
border-right: none !important;
}
// 修复滚动条样式 - 确保滚动条正常显示
.ant-table-body {
&::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05) !important;
border-radius: 4px !important;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2) !important;
border-radius: 4px !important;
&:hover {
background: rgba(255, 255, 255, 0.3) !important;
}
}
}
}
.deduction-detail-content {
.designer-info {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid $border-color;
h4 {
color: $text-primary;
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
}
.designer-dept {
color: $text-secondary;
font-size: 14px;
margin: 0 0 8px 0;
}
.total-deduction {
color: $text-primary;
font-size: 16px;
margin: 0;
.deduction-number {
color: #ef4444;
font-weight: 600;
font-size: 18px;
}
}
}
.deduction-section {
margin-bottom: 24px;
h5 {
color: $text-primary;
font-size: 16px;
font-weight: 600;
margin: 0 0 12px 0;
}
}
.no-data {
text-align: center;
padding: 40px 0;
color: $text-tertiary;
}
}
// 积分明细弹窗内容样式
.score-detail-content {
.designer-info {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid $border-color;
h4 {
color: $text-primary;
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
}
.designer-dept {
color: $text-secondary;
font-size: 14px;
margin: 0 0 8px 0;
}
.total-score {
color: $text-primary;
font-size: 16px;
margin: 0;
.score-number {
color: #31F08B;
font-weight: 600;
font-size: 18px;
}
}
}
.score-section {
margin-bottom: 24px;
h5 {
color: $text-primary;
font-size: 16px;
font-weight: 600;
margin: 0 0 12px 0;
}
}
.no-data {
text-align: center;
padding: 40px 0;
color: $text-tertiary;
}
}
}
// 积分明细弹窗样式
.score-detail-modal-wrap {
.ant-modal-content {
background: $bg-card;
border: 1px solid $border-primary;
border-radius: 12px;
}
.ant-modal-header {
background: $bg-card;
border-bottom: 1px solid $border-color;
.ant-modal-title {
color: $text-primary;
}
}
.ant-modal-close {
color: $text-secondary;
&:hover {
color: $text-primary;
}
}
.ant-modal-body {
background: $bg-card;
padding: 24px;
}
.ant-table {
background: transparent;
.ant-table-thead > tr > th {
background: rgba(255, 255, 255, 0.05);
color: $text-secondary;
border-bottom: 1px solid $border-color;
}
.ant-table-tbody > tr > td {
background: transparent;
color: $text-primary;
border-bottom: 1px solid $border-color;
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.05);
}
}
.ant-pagination {
.ant-pagination-item {
background: transparent;
border-color: $border-color;
a {
color: $text-primary;
}
&:hover, &-active {
border-color: $brand-primary;
a {
color: $brand-primary;
}
}
}
.ant-pagination-prev, .ant-pagination-next {
.ant-pagination-item-link {
background: transparent;
border-color: $border-color;
color: $text-primary;
}
}
.ant-pagination-options {
.ant-select-selector {
background: transparent;
border-color: $border-color;
color: $text-primary;
}
}
}
}
</style>
<style lang="scss" scoped>
@import "@/styles/variables.scss";
</style>
<style lang="scss">
@import "@/styles/variables.scss";
// 修复数据分析页面整体滚动条样式(非scoped,应用到全局)
html, body {
&::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05) !important;
border-radius: 4px !important;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2) !important;
border-radius: 4px !important;
&:hover {
background: rgba(255, 255, 255, 0.3) !important;
}
}
}
</style>