3372 lines
124 KiB
Vue
3372 lines
124 KiB
Vue
<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>
|