Recruitment_site/offer_frontend/src/views/portal/HomeView.vue

356 lines
10 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>
<div class="two-panel">
<!-- 左栏公司列表 -->
<aside class="panel-left">
<div class="left-header">
<span class="left-title">全部企业</span>
<span class="left-count">{{ totalOrgs }}</span>
</div>
<div class="left-body">
<template v-if="orgsLoading">
<div v-for="i in 4" :key="i" class="skeleton-row" />
</template>
<template v-else-if="orgsError">
<div class="state-tip">加载失败,请刷新重试</div>
</template>
<template v-else-if="orgs.length === 0">
<div class="state-tip">暂无公司数据</div>
</template>
<template v-else>
<template v-for="org in orgs" :key="org.id">
<!-- 集团 -->
<div
class="org-row"
:class="{ active: selectedOrg?.id === org.id }"
@click="selectOrg(org)"
>
<div class="org-avatar parent-avatar">
<img v-if="org.logo" :src="org.logo" :alt="org.name" />
<span v-else>{{ org.name[0] }}</span>
</div>
<div class="org-meta">
<span class="org-name">{{ org.name }}</span>
<span class="org-stat">在招 <em>{{ org.job_count }}</em> 个岗位</span>
</div>
</div>
<!-- 子公司 -->
<div
v-for="child in org.children"
:key="child.id"
class="org-row org-child"
:class="{ active: selectedOrg?.id === child.id }"
@click="selectOrg(child)"
>
<div class="child-indent">
<span class="child-line"></span>
</div>
<div class="org-avatar child-avatar">
<img v-if="child.logo" :src="child.logo" :alt="child.name" />
<span v-else>{{ child.name[0] }}</span>
</div>
<div class="org-meta">
<span class="org-name child-name">{{ child.name }}</span>
<span class="org-stat">在招 <em>{{ child.job_count }}</em> 个岗位</span>
</div>
</div>
</template>
</template>
</div>
</aside>
<!-- ── 右栏:岗位列表 ── -->
<section class="panel-right">
<div class="right-header">
<span class="right-title">职位列表</span>
<span v-if="jobs.length" class="right-count">{{ jobs.length }} 个</span>
</div>
<div class="right-body">
<template v-if="jobsLoading">
<div v-for="i in 4" :key="i" class="skeleton-row" />
</template>
<template v-else-if="jobsError">
<div class="state-tip">加载失败,请刷新重试</div>
</template>
<template v-else-if="jobs.length === 0">
<div class="state-tip">请选择企业查看职位</div>
</template>
<template v-else>
<div
v-for="job in jobs"
:key="job.id"
class="job-row"
@click="goToJobDetail(job)"
>
<div class="job-row-title">{{ job.title }}</div>
<div class="job-row-meta">
<span class="job-org">{{ job.organization?.name }}</span>
</div>
<div class="job-row-tags">
<span class="tag tag-loc">{{ job.location }}</span>
<span class="tag tag-sal">{{ job.salary }}</span>
<span class="tag tag-cat">{{ job.category }}</span>
</div>
</div>
</template>
</div>
</section>
<!-- 右栏岗位详情 -->
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
import { getOrganizations } from '@/api/organizations'
import { getJobs } from '@/api/jobs'
const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
const orgs = ref([])
const orgsLoading = ref(false)
const orgsError = ref(false)
const selectedOrg = ref(null)
const jobs = ref([])
const jobsLoading = ref(false)
const jobsError = ref(false)
const totalOrgs = computed(() => {
return orgs.value.reduce((n, o) => n + 1 + (o.children?.length || 0), 0)
})
function formatDate(dt) {
if (!dt) return ''
return new Date(dt).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
}
async function selectOrg(org) {
selectedOrg.value = org
jobs.value = []
jobsError.value = false
jobsLoading.value = true
try {
const { data } = await getJobs({ organization: org.id })
jobs.value = data.results
} catch (err) {
console.error('Failed to fetch jobs:', err)
jobsError.value = true
} finally {
jobsLoading.value = false
}
}
function goToJobDetail(job) {
router.push({ name: 'JobDetail', params: { id: job.id } })
}
onMounted(async () => {
orgsLoading.value = true
try {
const { data } = await getOrganizations()
orgs.value = data.results
const targetOrgId = route.query.org ? Number(route.query.org) : null
let targetOrg = null
if (targetOrgId) {
for (const org of orgs.value) {
if (org.id === targetOrgId) { targetOrg = org; break }
const child = org.children?.find(c => c.id === targetOrgId)
if (child) { targetOrg = child; break }
}
}
if (targetOrg) selectOrg(targetOrg)
else if (orgs.value.length > 0) selectOrg(orgs.value[0])
} catch { orgsError.value = true }
finally { orgsLoading.value = false }
})
</script>
<style scoped>
/* ── 变量 ── */
.two-panel {
--red: #B8860B;
--dark: #1A1A1A;
--gold: #B8860B;
--gold-lt: #D4AF37;
--cream: #FAFAFA;
--border: #D3D3D3;
--text: #2A1A1A;
--muted: #3A3A3A;
display: flex;
height: calc(100vh - 97px);
min-height: 520px;
overflow: hidden;
border-radius: 0;
border: none;
box-shadow: none;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
/* ── 左栏 ── */
.panel-left {
width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--dark);
border-right: 1px solid rgba(255,255,255,0.07);
}
.left-header {
padding: 16px 18px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(255,255,255,0.08);
flex-shrink: 0;
}
.left-title {
font-size: 13px;
font-weight: 700;
color: rgba(255,255,255,0.9);
letter-spacing: 0.1em;
}
.left-count {
background: var(--red);
color: #fff;
font-size: 11px;
font-weight: 700;
padding: 2px 8px;
border-radius: 10px;
min-width: 24px;
text-align: center;
}
.left-body { flex: 1; overflow-y: auto; }
.left-body::-webkit-scrollbar { width: 4px; }
.left-body::-webkit-scrollbar-track { background: transparent; }
.left-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
/* 公司行 */
.org-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-left: 3px solid transparent;
border-bottom: 1px solid rgba(255,255,255,0.05);
cursor: pointer;
transition: background 0.15s;
}
.org-row:hover { background: rgba(255,255,255,0.06); }
.org-row.active {
border-left-color: var(--gold);
background: rgba(184,134,11,0.1);
}
.org-avatar {
width: 34px; height: 34px;
border-radius: 6px;
background: linear-gradient(135deg, #B8860B, #8B6407);
display: flex; align-items: center; justify-content: center;
font-size: 15px; font-weight: 900; color: #fff;
flex-shrink: 0; overflow: hidden;
}
.org-avatar img { width: 100%; height: 100%; object-fit: cover; }
.child-avatar {
width: 28px; height: 28px; font-size: 12px;
background: linear-gradient(135deg, #A67C07, #7D5A05);
}
.org-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.org-name {
font-size: 13px; font-weight: 600;
color: rgba(255,255,255,0.88);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.child-name { font-size: 12px; color: rgba(255,255,255,0.7); }
.org-row.active .org-name { color: var(--gold-lt); }
.org-stat { font-size: 11px; color: rgba(255,255,255,0.38); }
.org-stat em { font-style: normal; color: var(--gold); font-weight: 700; }
/* 子公司缩进 */
.org-child { padding-left: 12px; background: rgba(0,0,0,0.12); }
.child-indent {
display: flex; align-items: center; flex-shrink: 0;
padding-left: 6px;
}
.child-line {
display: block; width: 10px; height: 1px;
background: rgba(184,134,11,0.35);
}
/* ── 右栏 (职位列表) ── */
.panel-right {
flex: 1;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--cream);
border-left: 1px solid var(--border);
}
.right-header {
padding: 14px 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid var(--border);
flex-shrink: 0;
background: #fff;
}
.right-title { font-size: 13px; font-weight: 600; color: var(--text); }
.right-count {
font-size: 11px; color: var(--muted);
background: #F0F0F0; border-radius: 10px; padding: 1px 8px;
}
.right-body { flex: 1; overflow-y: auto; }
/* 岗位行 */
.job-row {
padding: 13px 16px;
border-left: 3px solid transparent;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.15s;
background: #fff;
}
.job-row:hover { background: #F8F8F8; }
.job-row-title {
font-size: 13px; font-weight: 600;
color: var(--text); margin-bottom: 5px;
}
.job-row-meta {
font-size: 11px; color: var(--muted); margin-bottom: 6px;
}
.job-org { color: var(--red); }
.job-row-tags { display: flex; gap: 5px; flex-wrap: wrap; }
.tag {
font-size: 11px; padding: 2px 7px;
border-radius: 3px; font-weight: 500;
}
.tag-loc { background: #E8EEF8; color: #2C5282; }
.tag-sal { background: #FFF3E0; color: #B7610A; }
.tag-cat { background: #E8F5E9; color: #2E7D32; }
/* 空状态 / 骨架 */
.state-tip {
display: flex; align-items: center; justify-content: center;
height: 100%; min-height: 100px;
color: var(--muted); font-size: 13px;
}
.full-tip { flex-direction: column; gap: 12px; }
.empty-icon { opacity: 0.5; }
.detail-loading { padding: 24px 28px; }
.skeleton-row {
height: 14px; background: linear-gradient(90deg, #E8E8E8 25%, #F0F0F0 50%, #E8E8E8 75%);
background-size: 200% 100%;
border-radius: 3px; margin-bottom: 10px;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
</style>