This commit is contained in:
qwq
2026-06-04 11:01:29 +08:00
commit cccb578669
14509 changed files with 2636947 additions and 0 deletions
+736
View File
@@ -0,0 +1,736 @@
<template>
<div class="dashboard-page">
<div class="page-header" style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<h1>全局统计看板</h1>
<p>多维度查看工时数据销售视角 / 研发视角 / 项目视角</p>
</div>
<div style="display: flex; gap: 12px; align-items: center;">
<el-date-picker v-model="allExportDateRange" type="daterange" range-separator="至"
start-placeholder="全部时间起" end-placeholder="全部时间止" value-format="YYYY-MM-DD" size="large" />
<el-button type="success" @click="exportData('all')" :loading="exportingAll" size="large">
📊 总导出 (All in One)
</el-button>
</div>
</div>
<!-- Tabs -->
<el-tabs v-model="activeTab" type="border-card" class="dashboard-tabs">
<!-- ==================== Sales Perspective ==================== -->
<el-tab-pane label="📈 销售视角" name="sales">
<div class="filter-bar">
<el-date-picker v-model="salesFilter.dateRange" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" size="default" />
<el-button type="primary" @click="loadSalesData">查询</el-button>
<el-button @click="exportData('sales')">📥 导出该页</el-button>
</div>
<div class="two-col-layout">
<!-- Sales Table -->
<div class="table-col">
<div class="section-title">📋 销售人员汇总 (点击展开查看研发明细)</div>
<el-table :data="salesSummary" stripe v-loading="salesLoading" style="width: 100%" row-key="sales_name">
<el-table-column type="expand">
<template #default="props">
<div style="padding: 10px 40px; background: rgba(0,0,0,0.2);">
<div style="margin-bottom: 8px; font-weight: bold; color: var(--text-secondary);">
[ {{ props.row.sales_name }} ] 的研发工时消耗明细
</div>
<el-table :data="props.row.rdList" border style="width: 100%" size="small">
<el-table-column prop="rd_name" label="研发人员" width="150" />
<el-table-column prop="total_hours" label="消耗工时(h)" width="120" />
<el-table-column prop="work_days" label="工作天数" width="120" />
</el-table>
</div>
</template>
</el-table-column>
<el-table-column prop="sales_name" label="销售人员" min-width="140" />
<el-table-column prop="project_count" label="项目数" min-width="100" />
<el-table-column prop="rd_count" label="涉及研发" min-width="120" />
<el-table-column prop="total_hours" label="总消耗工时(h)" min-width="130" sortable />
</el-table>
</div>
<!-- Sales Charts -->
<div class="chart-col">
<div class="chart-card">
<div class="chart-title">销售占用工时占比饼图</div>
<div ref="salesChartRef" class="big-chart"></div>
</div>
<div class="chart-card" style="margin-top: 16px">
<div class="chart-title">各销售工时对比柱状图</div>
<div ref="salesBarRef" class="big-chart"></div>
</div>
</div>
</div>
</el-tab-pane>
<!-- ==================== R&D Perspective ==================== -->
<el-tab-pane label="👨‍💻 研发视角" name="rd">
<div class="filter-bar">
<el-date-picker v-model="rdFilter.dateRange" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" size="default" />
<el-select v-model="rdFilter.user_id" placeholder="全部研发" clearable size="default" style="width: 150px">
<el-option v-for="u in rdUsers" :key="u.id" :label="u.realname" :value="u.id" />
</el-select>
<el-button type="primary" @click="loadRdData">查询</el-button>
<el-button @click="exportData('rd')">📥 导出该页</el-button>
</div>
<div class="two-col-layout">
<!-- R&D Table -->
<div class="table-col">
<div class="section-title">📋 研发人员填报明细</div>
<el-table :data="rdData" stripe v-loading="rdLoading" style="width: 100%">
<el-table-column prop="user_name" label="研发人员" min-width="120" />
<el-table-column prop="client_name" label="客户/项目" min-width="180" />
<el-table-column prop="work_date" label="日期" min-width="120" />
<el-table-column prop="hours" label="工时(h)" min-width="100" />
<el-table-column prop="remark" label="备注" min-width="180" />
</el-table>
</div>
<!-- R&D Charts -->
<div class="chart-col">
<div class="chart-card">
<div class="chart-title">研发人员工时产出占比饼图</div>
<div ref="rdChartRef" class="big-chart"></div>
</div>
<div class="chart-card" style="margin-top: 16px">
<div class="chart-title">各研发人员工时对比柱状图</div>
<div ref="rdBarRef" class="big-chart"></div>
</div>
</div>
</div>
</el-tab-pane>
<!-- ==================== Project Perspective ==================== -->
<el-tab-pane label="🏢 项目视角" name="project">
<div class="filter-bar">
<el-date-picker v-model="projFilter.dateRange" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" size="default" style="margin-right: 12px;" />
<el-select v-model="projFilter.sales_id" clearable placeholder="全部销售" style="width: 140px; margin-right: 12px;">
<el-option v-for="u in salesUsers" :key="u.id" :label="u.realname" :value="u.id" />
</el-select>
<el-button type="primary" @click="loadProjectData">查询</el-button>
<el-button @click="exportData('project')">📥 导出该页</el-button>
</div>
<div class="two-col-layout">
<!-- Project Table -->
<div class="table-col">
<div class="section-title">📋 项目总览 (点击展开查看研发明细)</div>
<el-table :data="projectData" stripe v-loading="projLoading" style="width: 100%" row-key="client_id">
<el-table-column type="expand">
<template #default="props">
<div style="padding: 10px 40px; background: rgba(0,0,0,0.2);">
<div style="margin-bottom: 8px; font-weight: bold; color: var(--text-secondary);">
[ {{ props.row.client_name }} ] 的参与研发明细
</div>
<el-table :data="props.row.rdList" border style="width: 100%" size="small">
<el-table-column prop="rd_name" label="研发人员" width="150" />
<el-table-column prop="total_hours" label="消耗工时(h)" width="120" sortable />
<el-table-column prop="work_days" label="工作天数" width="120" />
<el-table-column prop="first_date" label="首次介入" width="120" />
<el-table-column prop="last_date" label="最近介入" width="120" />
</el-table>
</div>
</template>
</el-table-column>
<el-table-column prop="client_name" label="客户/项目" min-width="200" />
<el-table-column prop="sales_name" label="所属销售" min-width="120" />
<el-table-column prop="client_status" label="状态" min-width="90">
<template #default="{ row }">
<el-tag :type="row.client_status === 1 ? 'success' : 'info'" effect="dark" round>
{{ row.client_status === 1 ? '进行中' : '已结算' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="total_hours" label="总工时(h)" min-width="110" sortable />
<el-table-column prop="rd_count" label="研发人数" min-width="100" />
</el-table>
</div>
<!-- Project Charts -->
<div class="chart-col">
<div class="chart-card">
<div class="chart-title">项目工时分布饼图</div>
<div ref="projectChartRef" class="big-chart"></div>
</div>
<div class="chart-card" style="margin-top: 16px">
<div class="chart-title">各项目工时对比柱状图</div>
<div ref="projectBarRef" class="big-chart"></div>
</div>
</div>
</div>
</el-tab-pane>
<!-- ==================== Matrix Summary ==================== -->
<el-tab-pane label="📊 研发工时汇总" name="matrix">
<div class="filter-bar">
<el-date-picker v-model="matrixFilter.dateRange" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" size="default" />
<el-button type="primary" @click="loadMatrixData">查询</el-button>
<el-button @click="exportMatrix">📥 导出该页</el-button>
</div>
<div class="content-card" style="margin-top: 16px; overflow-x: auto;" v-loading="matrixLoading">
<table class="matrix-table" v-if="matrixRows.length">
<thead>
<tr>
<th class="row-header">研发人员</th>
<th v-for="col in matrixCols" :key="col">{{ col }}</th>
<th class="total-col">合计</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, ri) in matrixRows" :key="ri" :class="{ 'total-row': ri === matrixRows.length - 1 }">
<td class="row-header">{{ row.label }}</td>
<td v-for="col in matrixCols" :key="col">{{ row.cells[col] || '-' }}</td>
<td class="total-col">{{ row.total }}</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无工时数据" />
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, nextTick, watch, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { adminApi } from '../../api'
import * as echarts from 'echarts'
const activeTab = ref('sales')
const salesUsers = ref([])
const rdUsers = ref([])
const salesChartRef = ref(null)
const rdChartRef = ref(null)
const projectChartRef = ref(null)
const salesBarRef = ref(null)
const rdBarRef = ref(null)
const projectBarRef = ref(null)
let salesChartInstance = null
let rdChartInstance = null
let projectChartInstance = null
let salesBarInstance = null
let rdBarInstance = null
let projectBarInstance = null
const exportingAll = ref(false)
const getRecentMonthRange = () => {
const end = new Date()
const start = new Date()
start.setMonth(start.getMonth() - 1)
const pad = (n) => String(n).padStart(2, '0')
const format = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
return [format(start), format(end)]
}
const defaultRange = getRecentMonthRange()
const allExportDateRange = ref(defaultRange)
// ====== Sales ======
const salesFilter = reactive({ dateRange: defaultRange })
const salesRawData = ref([])
const salesDetailData = ref([])
const salesLoading = ref(false)
const salesSummary = computed(() => {
const map = {}
// Aggregate detail row by rd_id into the sales object
salesDetailData.value.forEach(row => {
if (!map[row.sales_id]) {
map[row.sales_id] = {
sales_id: row.sales_id,
sales_name: row.sales_name,
projects: new Set(),
rdMap: {},
total_hours: 0
}
}
const sales = map[row.sales_id]
if (!sales.rdMap[row.rd_id]) {
sales.rdMap[row.rd_id] = { rd_name: row.rd_name, total_hours: 0, work_days: 0 }
}
sales.rdMap[row.rd_id].total_hours += row.total_hours
sales.rdMap[row.rd_id].work_days += row.work_days
})
// Add project counts & total hours from raw overviews to base object
salesRawData.value.forEach(row => {
if (!map[row.sales_id]) {
map[row.sales_id] = {
sales_id: row.sales_id,
sales_name: row.sales_name,
projects: new Set(),
rdMap: {},
total_hours: 0
}
}
map[row.sales_id].projects.add(row.client_id)
map[row.sales_id].total_hours += row.total_hours
})
return Object.values(map).map(m => {
return {
sales_name: m.sales_name,
project_count: m.projects.size,
rd_count: Object.keys(m.rdMap).length,
total_hours: Math.round(m.total_hours * 10) / 10,
rdList: Object.values(m.rdMap).sort((a,b)=> b.total_hours - a.total_hours)
}
})
})
async function loadSalesData() {
salesLoading.value = true
try {
const params = {}
if (salesFilter.dateRange) {
params.start_date = salesFilter.dateRange[0]
params.end_date = salesFilter.dateRange[1]
}
const [overview, detail] = await Promise.all([
adminApi.getSalesStats(params),
adminApi.getSalesDetail(params)
])
salesRawData.value = overview.data
salesDetailData.value = detail.data
await nextTick()
const pieData = salesSummary.value.filter(s => s.total_hours > 0).map(s => ({name: s.sales_name, value: s.total_hours}))
renderPieChart(salesChartRef, salesChartInstance, 'sales', pieData)
const salesBarData = salesSummary.value.filter(s => s.total_hours > 0)
renderBarChart(salesBarRef, salesBarInstance, 'sales',
salesBarData.map(s => s.sales_name),
salesBarData.map(s => s.total_hours)
)
} finally { salesLoading.value = false }
}
// ====== R&D ======
const rdFilter = reactive({ dateRange: defaultRange, user_id: '' })
const rdData = ref([])
const rdLoading = ref(false)
const rdSummary = computed(() => {
const map = {}
rdData.value.forEach(row => {
if (!map[row.user_id]) {
map[row.user_id] = { name: row.user_name, value: 0 }
}
map[row.user_id].value += row.hours
})
return Object.values(map).filter(r => r.value > 0)
})
async function loadRdData() {
rdLoading.value = true
try {
const params = {}
if (rdFilter.dateRange) {
params.start_date = rdFilter.dateRange[0]
params.end_date = rdFilter.dateRange[1]
}
if (rdFilter.user_id) params.user_id = rdFilter.user_id
const res = await adminApi.getRdStats(params)
rdData.value = res.data
await nextTick()
renderPieChart(rdChartRef, rdChartInstance, 'rd', rdSummary.value)
renderBarChart(rdBarRef, rdBarInstance, 'rd',
rdSummary.value.map(r => r.name),
rdSummary.value.map(r => r.value)
)
} finally { rdLoading.value = false }
}
// ====== Project ======
const projFilter = reactive({ dateRange: defaultRange, sales_id: '' })
const projectData = ref([])
const projLoading = ref(false)
const projectDetailData = ref([])
async function loadProjectData() {
projLoading.value = true
try {
const params = {}
if (projFilter.dateRange) {
params.start_date = projFilter.dateRange[0]
params.end_date = projFilter.dateRange[1]
}
if (projFilter.sales_id) params.sales_id = projFilter.sales_id
const [overview, detail] = await Promise.all([
adminApi.getProjectStats(params),
adminApi.getProjectDetail(params)
])
// Group details by project
const detailMap = {}
detail.data.forEach(row => {
if (!detailMap[row.client_id]) detailMap[row.client_id] = []
detailMap[row.client_id].push(row)
})
// Attach rdList to each project
projectData.value = overview.data.map(p => {
p.rdList = detailMap[p.client_id] || []
return p
})
await nextTick()
const projPieData = projectData.value.filter(p => p.total_hours > 0).map(p => ({name: p.client_name, value: p.total_hours}))
renderPieChart(projectChartRef, projectChartInstance, 'project', projPieData)
const projBarData = projectData.value.filter(p => p.total_hours > 0)
renderBarChart(projectBarRef, projectBarInstance, 'project',
projBarData.map(p => p.client_name),
projBarData.map(p => p.total_hours)
)
} finally { projLoading.value = false }
}
// ====== Charts ======
function renderPieChart(domRef, instanceRef, type, dataArray) {
if (!domRef.value || domRef.value.clientWidth === 0) return
if (instanceRef) instanceRef.dispose()
let inst = echarts.init(domRef.value, 'dark')
if (type === 'sales') salesChartInstance = inst
if (type === 'rd') rdChartInstance = inst
if (type === 'project') projectChartInstance = inst
inst.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'item', formatter: '{b}: {c}h ({d}%)', confine: true },
legend: { type: 'scroll', bottom: 10, textStyle: { color: '#94a3b8' } },
series: [{
type: 'pie', radius: ['35%', '65%'], center: ['50%', '45%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 8, borderColor: '#1e293b', borderWidth: 2 },
label: { color: '#f1f5f9', fontSize: 13 },
emphasis: {
label: { show: true, fontSize: 16, fontWeight: 'bold' },
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.5)' }
},
data: dataArray
}]
})
}
function renderBarChart(domRef, instanceRef, type, categories, values) {
if (!domRef.value || domRef.value.clientWidth === 0) return
if (instanceRef) instanceRef.dispose()
let inst = echarts.init(domRef.value, 'dark')
if (type === 'sales') salesBarInstance = inst
if (type === 'rd') rdBarInstance = inst
if (type === 'project') projectBarInstance = inst
inst.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', formatter: (p) => `${p[0].name}: ${p[0].value}h`, confine: true },
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
xAxis: {
type: 'category', data: categories,
axisLabel: { color: '#94a3b8', interval: 0, rotate: categories.length > 5 ? 30 : 0, fontSize: 12 }
},
yAxis: { type: 'value', name: '工时(h)', axisLabel: { color: '#94a3b8' } },
series: [{
type: 'bar', barMaxWidth: 50, data: values,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#818cf8' }, { offset: 1, color: '#4f46e5' }
]),
borderRadius: [4, 4, 0, 0]
},
label: { show: true, position: 'top', color: '#f1f5f9', fontSize: 12, formatter: '{c}h' },
markLine: type === 'rd' && values.length > 0 ? {
data: [{ type: 'average', name: '平均值' }],
lineStyle: { type: 'dashed', color: '#f59e0b', width: 2 },
label: { formatter: '平均: {c}h', position: 'end', color: '#f59e0b', fontWeight: 'bold' }
} : undefined
}]
})
}
function handleResize() {
salesChartInstance?.resize()
rdChartInstance?.resize()
projectChartInstance?.resize()
salesBarInstance?.resize()
rdBarInstance?.resize()
projectBarInstance?.resize()
}
// ====== Export ======
async function exportData(type) {
try {
if (type === 'all') {
exportingAll.value = true
let params = {}
let start = '开始', end = '至今'
if (allExportDateRange.value && allExportDateRange.value.length === 2) {
params.start_date = allExportDateRange.value[0]
params.end_date = allExportDateRange.value[1]
start = params.start_date
end = params.end_date
}
const response = await adminApi.exportAllExcel(params)
downloadBlob(response.data, `总报表_${start}${end}.xlsx`)
ElMessage.success('全导成功')
exportingAll.value = false
return
}
let params = {}
let start = '开始', end = '至今', prefix = ''
if (type === 'sales') {
prefix = '销售视角'
if (salesFilter.dateRange && salesFilter.dateRange.length === 2) {
params.start_date = salesFilter.dateRange[0]; params.end_date = salesFilter.dateRange[1];
start = params.start_date; end = params.end_date;
}
} else if (type === 'rd') {
prefix = '研发视角'
if (rdFilter.dateRange && rdFilter.dateRange.length === 2) {
params.start_date = rdFilter.dateRange[0]; params.end_date = rdFilter.dateRange[1];
start = params.start_date; end = params.end_date;
}
} else if (type === 'project') {
prefix = '项目视角'
if (projFilter.dateRange && projFilter.dateRange.length === 2) {
params.start_date = projFilter.dateRange[0]; params.end_date = projFilter.dateRange[1];
start = params.start_date; end = params.end_date;
}
if (projFilter.sales_id) params.sales_id = projFilter.sales_id;
}
const response = await adminApi.exportExcel(type, params)
downloadBlob(response.data, `${prefix}_${start}${end}.xlsx`)
ElMessage.success('导出成功')
} catch (e) {
exportingAll.value = false
ElMessage.error('导出失败')
}
}
function downloadBlob(data, filename) {
const url = URL.createObjectURL(new Blob([data]))
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
watch(activeTab, (tab) => {
nextTick(() => {
if (tab === 'sales') {
const filtered = salesSummary.value.filter(s => s.total_hours > 0)
renderPieChart(salesChartRef, salesChartInstance, 'sales', filtered.map(s => ({name: s.sales_name, value: s.total_hours})))
renderBarChart(salesBarRef, salesBarInstance, 'sales', filtered.map(s => s.sales_name), filtered.map(s => s.total_hours))
} else if (tab === 'rd') {
renderPieChart(rdChartRef, rdChartInstance, 'rd', rdSummary.value)
renderBarChart(rdBarRef, rdBarInstance, 'rd', rdSummary.value.map(r => r.name), rdSummary.value.map(r => r.value))
} else if (tab === 'project') {
const filtered = projectData.value.filter(p => p.total_hours > 0)
renderPieChart(projectChartRef, projectChartInstance, 'project', filtered.map(p => ({name: p.client_name, value: p.total_hours})))
renderBarChart(projectBarRef, projectBarInstance, 'project', filtered.map(p => p.client_name), filtered.map(p => p.total_hours))
} else if (tab === 'matrix') {
loadMatrixData()
}
})
})
onMounted(async () => {
window.addEventListener('resize', handleResize)
try {
const [s, r] = await Promise.all([adminApi.getSalesUsers(), adminApi.getRdUsers()])
salesUsers.value = s.data
rdUsers.value = r.data
} catch (e) {}
loadSalesData()
loadRdData()
loadProjectData()
loadMatrixData()
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
salesChartInstance?.dispose()
rdChartInstance?.dispose()
projectChartInstance?.dispose()
salesBarInstance?.dispose()
rdBarInstance?.dispose()
projectBarInstance?.dispose()
})
// ====== Matrix (研发工时汇总) ======
const matrixFilter = reactive({ dateRange: defaultRange })
const matrixLoading = ref(false)
const matrixCols = ref([]) // 项目-子项目 labels
const matrixRows = ref([]) // { label, cells:{colName:hours}, total }
async function loadMatrixData() {
matrixLoading.value = true
try {
const params = {}
if (matrixFilter.dateRange && matrixFilter.dateRange.length === 2) {
params.start_date = matrixFilter.dateRange[0]
params.end_date = matrixFilter.dateRange[1]
}
const res = await adminApi.getSummaryMatrix(params)
const raw = res.data
// Build cols = unique 项目-子项目 labels
const colSet = new Set(raw.map(r => r.sub_project ? `${r.client_name}-${r.sub_project}` : r.client_name))
matrixCols.value = [...colSet].sort()
// Build row keys = 研发人员
const rowKeyMap = {}
raw.forEach(r => {
const ck = r.sub_project ? `${r.client_name}-${r.sub_project}` : r.client_name
if (!rowKeyMap[r.user_name]) rowKeyMap[r.user_name] = {}
rowKeyMap[r.user_name][ck] = (rowKeyMap[r.user_name][ck] || 0) + r.hours
})
const dataRows = Object.entries(rowKeyMap).map(([label, cells]) => {
const total = Object.values(cells).reduce((s, v) => s + v, 0)
return { label, cells, total: Math.round(total * 10) / 10 }
})
// Totals row
const totalCells = {}
matrixCols.value.forEach(c => {
totalCells[c] = Math.round(dataRows.reduce((s, r) => s + (r.cells[c] || 0), 0) * 10) / 10
})
const grandTotal = Math.round(dataRows.reduce((s, r) => s + r.total, 0) * 10) / 10
dataRows.push({ label: '合计', cells: totalCells, total: grandTotal })
matrixRows.value = dataRows
} finally { matrixLoading.value = false }
}
async function exportMatrix() {
const params = {}
let start = '开始', end = '至今'
if (matrixFilter.dateRange && matrixFilter.dateRange.length === 2) {
params.start_date = matrixFilter.dateRange[0]
params.end_date = matrixFilter.dateRange[1]
start = params.start_date; end = params.end_date
}
const response = await adminApi.exportMatrixExcel(params)
downloadBlob(response.data, `研发工时汇总_${start}${end}.xlsx`)
ElMessage.success('导出成功')
}
</script>
<style scoped>
/* Tabs Overrides */
.dashboard-page :deep(.el-tabs--border-card) {
background: var(--bg-card) !important;
border-color: var(--border-color) !important;
border-radius: var(--radius) !important;
overflow: visible !important;
}
.dashboard-page :deep(.el-tabs__content) {
overflow: visible !important;
padding: 20px !important;
}
.dashboard-page :deep(.el-tab-pane) {
overflow: visible !important;
}
.dashboard-page :deep(.el-tabs__header) {
background: rgba(51, 65, 85, 0.3) !important;
border-bottom-color: var(--border-color) !important;
}
.dashboard-page :deep(.el-tabs__item) { color: var(--text-secondary) !important; }
.dashboard-page :deep(.el-tabs__item.is-active) {
color: var(--primary-light) !important;
background: var(--bg-card) !important;
}
.dashboard-page :deep(.el-tabs__item:hover) { color: var(--text-primary) !important; }
/* Table Overrides for Expandable Rows */
.dashboard-page :deep(.el-table__expanded-cell) {
background-color: rgba(0,0,0,0.1) !important;
padding: 0 !important;
}
.dashboard-page :deep(.el-table__expanded-cell:hover) {
background-color: rgba(0,0,0,0.1) !important;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 14px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.two-col-layout {
display: flex;
gap: 24px;
align-items: flex-start;
margin-top: 16px;
}
.table-col {
flex: 2;
min-width: 0;
}
.chart-col {
flex: 1;
min-width: 350px;
}
.chart-card {
background: rgba(30, 41, 59, 0.5);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 20px;
}
.chart-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
text-align: center;
}
.big-chart {
width: 100%;
height: 400px;
}
/* Matrix Table */
.matrix-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.matrix-table th, .matrix-table td {
border: 1px solid var(--border-color);
padding: 8px 12px;
text-align: center;
white-space: nowrap;
}
.matrix-table thead th {
background: rgba(99, 102, 241, 0.15);
color: var(--primary-light);
font-weight: 600;
}
.matrix-table .row-header {
text-align: left;
font-weight: 500;
background: rgba(255,255,255,0.03);
min-width: 160px;
}
.matrix-table .total-col {
font-weight: 700;
color: #818cf8;
background: rgba(99,102,241,0.08);
}
.matrix-table .total-row td {
font-weight: 700;
background: rgba(99,102,241,0.12);
color: #c7d2fe;
}
.matrix-table tbody tr:hover td {
background: rgba(255,255,255,0.04);
}
</style>