356 lines
10 KiB
Vue
356 lines
10 KiB
Vue
<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>
|