style: Admin 视图硬编码蓝灰改国聘红(保留 echarts 色板)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude 2026-06-25 17:29:31 +08:00
parent 355d8af37c
commit d92fb79878
2 changed files with 378 additions and 1 deletions

View File

@ -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 {

View File

@ -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>