feat(frontend): 首页三栏布局完全重设计,新增企业选择器和岗位联动
- 重构首页为三栏联动布局(企业 + 岗位 + 详情) - 左栏:深蓝背景企业列表,支持集团和子公司分层显示 - 选中企业时显示金色左边框 + 背景色变化 - 每个企业/子公司显示实时在招岗位数 - 子公司采用缩进 + 树形连线视觉 - 中栏:选中企业的岗位列表 - 岗位卡片显示位置/薪资/类别标签 - 朱红左边框高亮选中岗位 - 右栏:岗位详情内容(完整复制 JobDetailView) - Banner:深蓝红色渐变背景,显示岗位名/薪资/企业 - 操作:收藏 + 投递按钮,权限校验 - 详情:信息网格 + 职位介绍 + 工作地点 + 企业卡片 - 完整的投递流程和状态反馈 - 配色系统: - 深蓝 #0E1E3D(左栏背景) - 朱红 #B5272C(选中、强调) - 金色 #C8973A(accent) - 米色 #FAF7F3(右侧背景) - 完整的加载/错误/空状态处理 - 骨架屏动画和交互反馈 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
88c0bb223c
commit
7e86ec5ea0
|
|
@ -6,3 +6,5 @@ export const manageJobs = (params) => client.get('/jobs/manage/', { params })
|
|||
export const createJob = (data) => client.post('/jobs/manage/', data)
|
||||
export const updateJob = (id, data) => client.patch(`/jobs/manage/${id}/`, data)
|
||||
export const deleteJob = (id) => client.delete(`/jobs/manage/${id}/`)
|
||||
export const toggleFavorite = (id) => client.post(`/jobs/public/${id}/favorite/`)
|
||||
export const getMyFavorites = () => client.get('/jobs/favorites/')
|
||||
|
|
|
|||
|
|
@ -2,3 +2,6 @@ import client from './client'
|
|||
|
||||
export const getMyResume = () => client.get('/resumes/me/')
|
||||
export const updateMyResume = (data) => client.patch('/resumes/me/', data)
|
||||
export const uploadResumeAttachment = (formData) => client.patch('/resumes/me/', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,37 +1,700 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<div class="hero">
|
||||
<h1>发现你的理想职位</h1>
|
||||
<el-input v-model="keyword" placeholder="搜索职位..." size="large" @keyup.enter="search">
|
||||
<template #append><el-button @click="search">搜索</el-button></template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="latest-jobs">
|
||||
<h3>最新职位</h3>
|
||||
<JobCard v-for="job in latestJobs" :key="job.id" :job="job" />
|
||||
</div>
|
||||
<div class="three-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-mid">
|
||||
<div class="mid-header">
|
||||
<span v-if="selectedOrg" class="mid-title">
|
||||
<span class="mid-title-org">{{ selectedOrg.name }}</span>
|
||||
<span class="mid-title-sep"> · </span>职位列表
|
||||
</span>
|
||||
<span v-else class="mid-title muted">← 请选择企业</span>
|
||||
<span v-if="jobs.length" class="mid-count">{{ jobs.length }} 个</span>
|
||||
</div>
|
||||
<div class="mid-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 && selectedOrg">
|
||||
<div class="state-tip">暂无在招职位</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="job in jobs"
|
||||
:key="job.id"
|
||||
class="job-row"
|
||||
:class="{ active: selectedJob?.id === job.id }"
|
||||
@click="selectJob(job)"
|
||||
>
|
||||
<div class="job-row-title">{{ job.title }}</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>
|
||||
|
||||
<!-- ── 右栏:岗位详情 ── -->
|
||||
<section class="panel-right">
|
||||
<!-- 加载 -->
|
||||
<template v-if="detailLoading">
|
||||
<div class="detail-loading">
|
||||
<div v-for="i in 6" :key="i" class="skeleton-row" style="margin-bottom:12px" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 错误 -->
|
||||
<template v-else-if="detailError">
|
||||
<div class="state-tip full-tip">加载失败,请刷新重试</div>
|
||||
</template>
|
||||
<!-- 详情 -->
|
||||
<template v-else-if="selectedJob">
|
||||
<!-- Banner -->
|
||||
<div class="detail-banner">
|
||||
<div class="banner-deco"></div>
|
||||
<div class="banner-content">
|
||||
<div class="banner-left">
|
||||
<div class="banner-category">{{ selectedJob.category }}</div>
|
||||
<h2 class="banner-title">{{ selectedJob.title }}</h2>
|
||||
<div class="banner-salary">{{ selectedJob.salary }}</div>
|
||||
<div class="banner-meta">
|
||||
<span class="bmeta-item">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||
{{ selectedJob.location }}
|
||||
</span>
|
||||
<span class="bmeta-sep">|</span>
|
||||
<span class="bmeta-item">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
{{ selectedJob.organization?.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="banner-right">
|
||||
<div class="banner-date">发布于 {{ formatDate(selectedJob.created_at) }}</div>
|
||||
<div class="banner-btns">
|
||||
<button class="btn-collect" @click="handleCollect">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||
{{ collected ? '已收藏' : '收藏' }}
|
||||
</button>
|
||||
<button class="btn-apply" :class="{ loading: applying }" @click="handleApply">
|
||||
{{ applying ? '提交中…' : '立即投递' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="banner-hint" v-if="!auth.isLoggedIn">登录后才能投递</p>
|
||||
<p class="banner-hint success" v-if="applied">✓ 投递成功,可在「我的投递」查看</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体 -->
|
||||
<div class="detail-main">
|
||||
<div class="detail-left">
|
||||
<!-- 信息格 -->
|
||||
<div class="info-grid">
|
||||
<div class="info-cell">
|
||||
<span class="info-label">职位类别</span>
|
||||
<span class="info-value">{{ selectedJob.category || '未填写' }}</span>
|
||||
</div>
|
||||
<div class="info-cell">
|
||||
<span class="info-label">工作地点</span>
|
||||
<span class="info-value">{{ selectedJob.location }}</span>
|
||||
</div>
|
||||
<div class="info-cell">
|
||||
<span class="info-label">薪资范围</span>
|
||||
<span class="info-value red-val">{{ selectedJob.salary }}</span>
|
||||
</div>
|
||||
<div class="info-cell">
|
||||
<span class="info-label">发布时间</span>
|
||||
<span class="info-value">{{ formatDate(selectedJob.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<!-- 职位介绍 -->
|
||||
<div class="content-section">
|
||||
<div class="section-heading"><span class="heading-bar"></span>职位介绍</div>
|
||||
<div class="desc-text" v-html="selectedJob.description"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<!-- 工作地点 -->
|
||||
<div class="content-section">
|
||||
<div class="section-heading"><span class="heading-bar"></span>工作地点</div>
|
||||
<div class="location-row">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#B5272C" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||
{{ selectedJob.location }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公司卡片 -->
|
||||
<div class="detail-right">
|
||||
<div class="company-card">
|
||||
<div class="company-card-label">招聘单位</div>
|
||||
<div class="company-card-top">
|
||||
<div class="cc-logo">
|
||||
<img v-if="selectedJob.organization?.logo" :src="selectedJob.organization.logo" alt="" />
|
||||
<span v-else>{{ selectedJob.organization?.name?.[0] }}</span>
|
||||
</div>
|
||||
<div class="cc-info">
|
||||
<div class="cc-name">{{ selectedJob.organization?.name }}</div>
|
||||
<div class="cc-desc" v-if="selectedJob.organization?.description">
|
||||
{{ selectedJob.organization.description.slice(0, 50) }}{{ selectedJob.organization.description.length > 50 ? '…' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cc-divider"></div>
|
||||
<div class="cc-contact" v-if="selectedJob.organization?.email">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#B5272C" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
||||
{{ selectedJob.organization.email }}
|
||||
</div>
|
||||
<button class="btn-apply-card" :class="{ loading: applying }" @click="handleApply">
|
||||
{{ applying ? '提交中…' : '立即投递' }}
|
||||
</button>
|
||||
<p class="apply-success" v-if="applied">✓ 投递成功</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 空状态 -->
|
||||
<template v-else>
|
||||
<div class="state-tip full-tip">
|
||||
<div class="empty-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#C8973A" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
</div>
|
||||
<p>请从左侧选择企业及职位</p>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import JobCard from '@/components/JobCard.vue'
|
||||
import { getJobs } from '@/api/jobs'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getOrganizations } from '@/api/organizations'
|
||||
import { getJobs, getJob, toggleFavorite } from '@/api/jobs'
|
||||
import { applyJob } from '@/api/applications'
|
||||
|
||||
const router = useRouter()
|
||||
const keyword = ref('')
|
||||
const latestJobs = ref([])
|
||||
const auth = useAuthStore()
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await getJobs({ page: 1 })
|
||||
latestJobs.value = data.results.slice(0, 6)
|
||||
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 selectedJob = ref(null)
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref(false)
|
||||
const applying = ref(false)
|
||||
const applied = ref(false)
|
||||
const collected = ref(false)
|
||||
|
||||
const totalOrgs = computed(() => {
|
||||
return orgs.value.reduce((n, o) => n + 1 + (o.children?.length || 0), 0)
|
||||
})
|
||||
|
||||
const search = () => router.push({ name: 'JobList', query: { search: keyword.value } })
|
||||
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
|
||||
selectedJob.value = null
|
||||
jobs.value = []
|
||||
jobsError.value = false
|
||||
jobsLoading.value = true
|
||||
try {
|
||||
const { data } = await getJobs({ organization: org.id })
|
||||
jobs.value = data.results
|
||||
} catch { jobsError.value = true }
|
||||
finally { jobsLoading.value = false }
|
||||
}
|
||||
|
||||
async function selectJob(job) {
|
||||
detailError.value = false
|
||||
detailLoading.value = true
|
||||
selectedJob.value = null
|
||||
applied.value = false
|
||||
collected.value = false
|
||||
try {
|
||||
const { data } = await getJob(job.id)
|
||||
selectedJob.value = data
|
||||
} catch { detailError.value = true }
|
||||
finally { detailLoading.value = false }
|
||||
}
|
||||
|
||||
async function handleCollect() {
|
||||
if (!auth.isLoggedIn) return router.push({ name: 'Login' })
|
||||
if (!auth.isSeeker) return ElMessage.warning('管理员账号无法收藏职位')
|
||||
const { data } = await toggleFavorite(selectedJob.value.id)
|
||||
collected.value = data.collected
|
||||
ElMessage.success(collected.value ? '已收藏,可在「关注职位」中查看' : '已取消收藏')
|
||||
}
|
||||
|
||||
async function handleApply() {
|
||||
if (!auth.isLoggedIn) return router.push({ name: 'Login' })
|
||||
if (!auth.isSeeker) return ElMessage.warning('管理员账号无法投递职位')
|
||||
applying.value = true
|
||||
try {
|
||||
await applyJob(selectedJob.value.id)
|
||||
applied.value = true
|
||||
ElMessage.success('投递成功!')
|
||||
} catch (e) {
|
||||
if (e.response?.status === 400) ElMessage.warning(e.response.data?.detail || '您已投递过该职位')
|
||||
else ElMessage.error('投递失败,请先完善简历')
|
||||
} finally { applying.value = false }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
orgsLoading.value = true
|
||||
try {
|
||||
const { data } = await getOrganizations()
|
||||
orgs.value = data.results
|
||||
if (orgs.value.length > 0) selectOrg(orgs.value[0])
|
||||
} catch { orgsError.value = true }
|
||||
finally { orgsLoading.value = false }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hero { text-align: center; padding: 60px 20px; background: linear-gradient(135deg, #409eff22, #fff); }
|
||||
.hero h1 { font-size: 36px; margin-bottom: 24px; }
|
||||
.el-input { max-width: 500px; }
|
||||
.latest-jobs { max-width: 900px; margin: 40px auto; }
|
||||
/* ── 变量 ── */
|
||||
.three-panel {
|
||||
--red: #B5272C;
|
||||
--dark: #0E1E3D;
|
||||
--gold: #C8973A;
|
||||
--gold-lt: #F0D080;
|
||||
--cream: #FAF7F3;
|
||||
--border: #E5DDD5;
|
||||
--text: #1A1A2E;
|
||||
--muted: #7A8094;
|
||||
|
||||
display: flex;
|
||||
height: calc(100vh - 220px);
|
||||
min-height: 520px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 4px 24px rgba(14,30,61,0.12);
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
/* ── 左栏 ── */
|
||||
.panel-left {
|
||||
width: 228px;
|
||||
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(200,151,58,0.1);
|
||||
}
|
||||
.org-avatar {
|
||||
width: 34px; height: 34px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(135deg, #C8973A, #9A6E28);
|
||||
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, #3D5A8A, #1E3460);
|
||||
}
|
||||
.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(200,151,58,0.35);
|
||||
}
|
||||
|
||||
/* ── 中栏 ── */
|
||||
.panel-mid {
|
||||
width: 268px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--cream);
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.mid-header {
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 2px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.mid-title { font-size: 13px; font-weight: 600; color: var(--text); }
|
||||
.mid-title-org { color: var(--red); }
|
||||
.mid-title.muted { color: var(--muted); font-weight: 400; }
|
||||
.mid-count {
|
||||
font-size: 11px; color: var(--muted);
|
||||
background: #EDE8E1; border-radius: 10px; padding: 1px 8px;
|
||||
}
|
||||
.mid-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: #F5EFE8; }
|
||||
.job-row.active {
|
||||
border-left-color: var(--red);
|
||||
background: #FFF5F5;
|
||||
}
|
||||
.job-row-title {
|
||||
font-size: 13px; font-weight: 600;
|
||||
color: var(--text); margin-bottom: 7px;
|
||||
}
|
||||
.job-row.active .job-row-title { 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; }
|
||||
|
||||
/* ── 右栏 ── */
|
||||
.panel-right {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #F8F4EF;
|
||||
}
|
||||
|
||||
/* Banner */
|
||||
.detail-banner {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #0E1E3D 0%, #1A2E56 50%, #1C1C3A 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.banner-deco {
|
||||
position: absolute; inset: 0;
|
||||
background:
|
||||
repeating-linear-gradient(45deg, transparent, transparent 20px, rgba(200,151,58,0.03) 20px, rgba(200,151,58,0.03) 21px),
|
||||
repeating-linear-gradient(-45deg, transparent, transparent 20px, rgba(200,151,58,0.03) 20px, rgba(200,151,58,0.03) 21px);
|
||||
}
|
||||
.banner-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
padding: 28px 32px;
|
||||
}
|
||||
.banner-left { flex: 1; }
|
||||
.banner-category {
|
||||
display: inline-block;
|
||||
padding: 3px 12px;
|
||||
background: rgba(200,151,58,0.15);
|
||||
border: 1px solid rgba(200,151,58,0.35);
|
||||
color: var(--gold-lt);
|
||||
font-size: 11px; letter-spacing: 0.1em;
|
||||
border-radius: 2px; margin-bottom: 10px;
|
||||
}
|
||||
.banner-title {
|
||||
font-size: 22px; font-weight: 800;
|
||||
color: #fff; letter-spacing: 0.05em;
|
||||
margin: 0 0 8px;
|
||||
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
.banner-salary {
|
||||
font-size: 18px; font-weight: 700;
|
||||
color: #FF6B6B; margin-bottom: 12px;
|
||||
}
|
||||
.banner-meta { display: flex; align-items: center; gap: 8px; }
|
||||
.bmeta-item {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
font-size: 13px; color: rgba(255,255,255,0.6);
|
||||
}
|
||||
.bmeta-sep { color: rgba(255,255,255,0.2); }
|
||||
|
||||
.banner-right { text-align: right; flex-shrink: 0; }
|
||||
.banner-date { font-size: 12px; color: rgba(255,255,255,0.38); margin-bottom: 14px; }
|
||||
.banner-btns { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
.btn-collect {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 9px 18px;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(200,151,58,0.5);
|
||||
color: var(--gold-lt);
|
||||
border-radius: 3px; font-size: 13px;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-collect:hover { background: rgba(200,151,58,0.1); border-color: var(--gold); }
|
||||
.btn-apply {
|
||||
padding: 9px 24px;
|
||||
background: var(--red);
|
||||
border: none; color: #fff;
|
||||
border-radius: 3px; font-size: 13px; font-weight: 700;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
letter-spacing: 0.05em;
|
||||
font-family: inherit;
|
||||
box-shadow: 0 4px 16px rgba(181,39,44,0.4);
|
||||
}
|
||||
.btn-apply:hover { background: #9A1F23; box-shadow: 0 6px 20px rgba(181,39,44,0.5); }
|
||||
.btn-apply.loading { opacity: 0.7; cursor: not-allowed; }
|
||||
.banner-hint { font-size: 12px; color: rgba(255,255,255,0.4); margin-top: 8px; }
|
||||
.banner-hint.success { color: #6FCF97; }
|
||||
|
||||
/* 主体 */
|
||||
.detail-main {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 24px 28px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.detail-left {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 24px 28px;
|
||||
box-shadow: 0 2px 12px rgba(14,30,61,0.07);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.detail-right { width: 224px; flex-shrink: 0; }
|
||||
|
||||
/* 信息格 */
|
||||
.info-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 16px 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.info-cell { display: flex; flex-direction: column; gap: 4px; }
|
||||
.info-label {
|
||||
font-size: 11px; color: var(--muted); letter-spacing: 0.05em;
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
}
|
||||
.info-label::before {
|
||||
content: ''; width: 5px; height: 5px;
|
||||
background: var(--red); border-radius: 50%;
|
||||
}
|
||||
.info-value { font-size: 14px; font-weight: 600; color: var(--text); }
|
||||
.red-val { color: var(--red); }
|
||||
|
||||
.section-divider {
|
||||
height: 1px; background: var(--border);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* 内容区块 */
|
||||
.content-section { }
|
||||
.section-heading {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-size: 15px; font-weight: 700; color: var(--text);
|
||||
margin-bottom: 14px; letter-spacing: 0.05em;
|
||||
}
|
||||
.heading-bar {
|
||||
display: block; width: 4px; height: 16px;
|
||||
background: var(--red); border-radius: 2px;
|
||||
}
|
||||
.desc-text {
|
||||
font-size: 13px; line-height: 2;
|
||||
color: #3A3A4A; white-space: pre-wrap;
|
||||
}
|
||||
.location-row {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; color: #3A3A4A;
|
||||
}
|
||||
|
||||
/* 公司卡片 */
|
||||
.company-card {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 2px 12px rgba(14,30,61,0.07);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.company-card-label {
|
||||
font-size: 11px; font-weight: 700;
|
||||
color: var(--muted); letter-spacing: 0.15em;
|
||||
margin-bottom: 14px; text-transform: uppercase;
|
||||
}
|
||||
.company-card-top { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 14px; }
|
||||
.cc-logo {
|
||||
width: 48px; height: 48px;
|
||||
border-radius: 8px; overflow: hidden; flex-shrink: 0;
|
||||
background: linear-gradient(135deg, #C8973A, #9A6E28);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 20px; font-weight: 900; color: #fff;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.cc-logo img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.cc-name { font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
|
||||
.cc-desc { font-size: 11px; color: var(--muted); line-height: 1.6; }
|
||||
.cc-divider { height: 1px; background: var(--border); margin-bottom: 12px; }
|
||||
.cc-contact {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-size: 12px; color: var(--muted); word-break: break-all;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.btn-apply-card {
|
||||
width: 100%; padding: 10px;
|
||||
background: var(--red); border: none;
|
||||
color: #fff; font-size: 13px; font-weight: 700;
|
||||
border-radius: 3px; cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
font-family: inherit; letter-spacing: 0.05em;
|
||||
}
|
||||
.btn-apply-card:hover { background: #9A1F23; }
|
||||
.btn-apply-card.loading { opacity: 0.7; cursor: not-allowed; }
|
||||
.apply-success { text-align: center; font-size: 12px; color: #27AE60; margin-top: 8px; }
|
||||
|
||||
/* 空状态 / 骨架 */
|
||||
.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, #E8E0D8 25%, #F0E8E0 50%, #E8E0D8 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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue