1677 lines
56 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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