style: Admin 视图硬编码蓝灰改国聘红(保留 echarts 色板)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
355d8af37c
commit
d92fb79878
|
|
@ -256,7 +256,7 @@ async function downloadAttachment() {
|
|||
padding: 12px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #409eff;
|
||||
border-left: 3px solid var(--brand-red);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,377 @@
|
|||
<template>
|
||||
<div v-loading="loading">
|
||||
<!-- 职位概览卡片 -->
|
||||
<el-card class="stat-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">我的职位</span>
|
||||
<el-button type="primary" link @click="$router.push('/admin/jobs')">
|
||||
<el-icon style="margin-right:2px"><Edit /></el-icon>发布职位
|
||||
</el-button>
|
||||
</div>
|
||||
<el-row :gutter="24" style="margin-top:20px">
|
||||
<el-col :span="6">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">在线职位</div>
|
||||
<div class="stat-value primary">{{ stats.job_stats?.published ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">已下线职位</div>
|
||||
<div class="stat-value">{{ stats.job_stats?.closed ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">草稿</div>
|
||||
<div class="stat-value">{{ stats.job_stats?.draft ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">全部职位</div>
|
||||
<div class="stat-value">{{ stats.job_stats?.total ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 投递概览卡片 -->
|
||||
<el-card class="stat-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">投递概览</span>
|
||||
<el-button type="primary" link @click="$router.push('/admin/applications')">
|
||||
查看详情
|
||||
</el-button>
|
||||
</div>
|
||||
<el-row :gutter="24" style="margin-top:20px">
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总投递</div>
|
||||
<div class="stat-value primary">{{ stats.app_stats?.total ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">待查看</div>
|
||||
<div class="stat-value warning">{{ stats.app_stats?.pending ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">已查看</div>
|
||||
<div class="stat-value">{{ stats.app_stats?.viewed ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">面试中</div>
|
||||
<div class="stat-value info">{{ stats.app_stats?.interviewing ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">已录用</div>
|
||||
<div class="stat-value success">{{ stats.app_stats?.hired ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">已拒绝</div>
|
||||
<div class="stat-value danger">{{ stats.app_stats?.rejected ?? 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 饼图区域 -->
|
||||
<el-card class="stat-card">
|
||||
<div class="card-title" style="margin-bottom:16px">投递人数分布概览</div>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<div ref="schoolChartRef" class="chart-box"></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div ref="degreeChartRef" class="chart-box"></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 职位类别分布 + 投递状态分布 -->
|
||||
<el-card class="stat-card">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<div class="card-title" style="margin-bottom:16px">职位类别分布</div>
|
||||
<div ref="categoryChartRef" class="chart-box"></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="card-title" style="margin-bottom:16px">投递状态分布</div>
|
||||
<div ref="appStatusChartRef" class="chart-box"></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { Edit } from '@element-plus/icons-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getDashboard } from '@/api/jobs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const stats = ref({})
|
||||
const schoolChartRef = ref(null)
|
||||
const degreeChartRef = ref(null)
|
||||
const categoryChartRef = ref(null)
|
||||
const appStatusChartRef = ref(null)
|
||||
|
||||
let schoolChart = null
|
||||
let degreeChart = null
|
||||
let categoryChart = null
|
||||
let appStatusChart = null
|
||||
|
||||
const COLORS = {
|
||||
school: ['#ff9800', '#4caf50', '#f44336', '#2196f3', '#9c27b0'],
|
||||
degree: ['#2196f3', '#4caf50', '#ff9800', '#f44336', '#9c27b0', '#00bcd4'],
|
||||
category: ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#b37feb', '#5cdbd3', '#ff85c0', '#ffc53d', '#73d13d'],
|
||||
}
|
||||
|
||||
function createDonutChart(el, data, title, colors) {
|
||||
const total = data.reduce((s, d) => s + d.value, 0)
|
||||
const chart = echarts.init(el)
|
||||
chart.setOption({
|
||||
color: colors,
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
bottom: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
textStyle: { fontSize: 12, color: '#666' },
|
||||
},
|
||||
graphic: {
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '38%',
|
||||
style: {
|
||||
text: `${total}\n${title}`,
|
||||
textAlign: 'center',
|
||||
fill: '#333',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 24,
|
||||
},
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'],
|
||||
center: ['50%', '45%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: { show: false },
|
||||
data: data,
|
||||
}],
|
||||
})
|
||||
return chart
|
||||
}
|
||||
|
||||
function createRadarChart(el, data, title, color) {
|
||||
const isGradient = typeof color === 'object'
|
||||
const lineColor = isGradient ? color.line : color
|
||||
const areaColor = isGradient ? color.area : undefined
|
||||
const maxVal = Math.max(...data.map(d => d.value), 1)
|
||||
const chart = echarts.init(el)
|
||||
chart.setOption({
|
||||
color: [lineColor],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: [title],
|
||||
textStyle: { fontSize: 12, color: '#666' },
|
||||
},
|
||||
radar: {
|
||||
center: ['50%', '45%'],
|
||||
radius: '60%',
|
||||
indicator: data.map(d => ({ name: d.name, max: maxVal })),
|
||||
axisName: { color: '#666', fontSize: 12 },
|
||||
splitArea: { areaStyle: { color: ['#fff', '#f5f7fa'] } },
|
||||
splitLine: { lineStyle: { color: '#e0e6ed' } },
|
||||
},
|
||||
series: [{
|
||||
type: 'radar',
|
||||
name: title,
|
||||
data: [{
|
||||
value: data.map(d => d.value),
|
||||
name: title,
|
||||
areaStyle: areaColor ? { color: areaColor } : { opacity: 0.25 },
|
||||
lineStyle: { width: 2, color: lineColor },
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
itemStyle: { color: lineColor },
|
||||
}],
|
||||
}],
|
||||
})
|
||||
return chart
|
||||
}
|
||||
|
||||
function createFunnelChart(el, data) {
|
||||
const chart = echarts.init(el)
|
||||
chart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}',
|
||||
},
|
||||
legend: {
|
||||
bottom: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
textStyle: { fontSize: 12, color: '#666' },
|
||||
},
|
||||
series: [{
|
||||
type: 'funnel',
|
||||
left: '15%',
|
||||
right: '15%',
|
||||
top: 10,
|
||||
bottom: 40,
|
||||
minSize: '20%',
|
||||
maxSize: '100%',
|
||||
sort: 'none',
|
||||
gap: 4,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
formatter: '{b}\n{c}',
|
||||
fontSize: 13,
|
||||
color: '#fff',
|
||||
lineHeight: 18,
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
data: data,
|
||||
}],
|
||||
})
|
||||
return chart
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
schoolChart?.resize()
|
||||
degreeChart?.resize()
|
||||
categoryChart?.resize()
|
||||
appStatusChart?.resize()
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await getDashboard()
|
||||
stats.value = data
|
||||
|
||||
await nextTick()
|
||||
|
||||
// 学校分布饼图
|
||||
const schoolData = data.school_type_dist?.length
|
||||
? data.school_type_dist
|
||||
: [{ name: '暂无数据', value: 0 }]
|
||||
schoolChart = createDonutChart(schoolChartRef.value, schoolData, '学校分布', COLORS.school)
|
||||
|
||||
// 学历分布饼图
|
||||
const degreeData = data.degree_dist?.length
|
||||
? data.degree_dist
|
||||
: [{ name: '暂无数据', value: 0 }]
|
||||
degreeChart = createDonutChart(degreeChartRef.value, degreeData, '学历分布', COLORS.degree)
|
||||
|
||||
// 职位类别雷达图
|
||||
const categoryData = data.category_dist?.length
|
||||
? data.category_dist
|
||||
: [{ name: '暂无数据', value: 0 }]
|
||||
categoryChart = createRadarChart(categoryChartRef.value, categoryData, '职位类别', {
|
||||
line: '#e53935',
|
||||
area: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(255,138,128,0.6)' },
|
||||
{ offset: 1, color: 'rgba(183,28,28,0.6)' },
|
||||
]),
|
||||
})
|
||||
|
||||
// 投递状态漏斗图
|
||||
const appData = data.app_stats
|
||||
const appStatusData = [
|
||||
{ name: '待查看', value: appData?.pending ?? 0, itemStyle: { color: '#e6a23c' } },
|
||||
{ name: '已查看', value: appData?.viewed ?? 0, itemStyle: { color: '#409eff' } },
|
||||
{ name: '面试中', value: appData?.interviewing ?? 0, itemStyle: { color: '#1a73e8' } },
|
||||
{ name: '已录用', value: appData?.hired ?? 0, itemStyle: { color: '#67c23a' } },
|
||||
{ name: '已拒绝', value: appData?.rejected ?? 0, itemStyle: { color: '#f56c6c' } },
|
||||
]
|
||||
appStatusChart = createFunnelChart(appStatusChartRef.value, appStatusData)
|
||||
} catch {
|
||||
ElMessage.error('加载统计数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
schoolChart?.dispose()
|
||||
degreeChart?.dispose()
|
||||
categoryChart?.dispose()
|
||||
appStatusChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stat-value.primary { color: #1a73e8; }
|
||||
.stat-value.success { color: #67c23a; }
|
||||
.stat-value.warning { color: #e6a23c; }
|
||||
.stat-value.danger { color: #f56c6c; }
|
||||
.stat-value.info { color: var(--brand-red); }
|
||||
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue