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;
|
padding: 12px;
|
||||||
background: #f9f9f9;
|
background: #f9f9f9;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-left: 3px solid #409eff;
|
border-left: 3px solid var(--brand-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-header {
|
.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