519 lines
13 KiB
Vue
519 lines
13 KiB
Vue
<template>
|
||
<div class="job-detail-page" v-loading="loading">
|
||
<!-- 顶部 Banner -->
|
||
<div class="job-banner" v-if="job">
|
||
<div class="banner-inner">
|
||
<div class="banner-left">
|
||
<h1 class="job-title">{{ job.title }}</h1>
|
||
<div class="job-salary">{{ job.salary }}</div>
|
||
<div class="job-meta-row">
|
||
<span class="meta-item">
|
||
<el-icon><Location /></el-icon>
|
||
{{ job.location }}
|
||
</span>
|
||
<span class="meta-divider">|</span>
|
||
<span class="meta-item">
|
||
<el-icon><OfficeBuilding /></el-icon>
|
||
{{ job.organization?.name }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="banner-right">
|
||
<div class="banner-date">发布于 {{ formatDate(job.created_at) }}</div>
|
||
<div class="banner-actions">
|
||
<el-button class="btn-collect" plain @click="toggleCollect">
|
||
<el-icon><Star /></el-icon>
|
||
{{ collected ? '已收藏' : '收藏' }}
|
||
</el-button>
|
||
<el-button
|
||
class="btn-apply"
|
||
@click="handleApply"
|
||
:loading="applying"
|
||
:disabled="applied"
|
||
>
|
||
{{ applied ? '已投递' : '立即投递' }}
|
||
</el-button>
|
||
</div>
|
||
<p class="apply-hint" v-if="!auth.isLoggedIn">登录后才能投递</p>
|
||
<p class="apply-hint success" v-if="applied">已投递,可在「我的投递」查看进度</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主体内容 -->
|
||
<div class="job-body" v-if="job">
|
||
<div class="body-inner">
|
||
<!-- 左栏 -->
|
||
<div class="body-left">
|
||
<!-- 职位标签 -->
|
||
<div class="info-tags">
|
||
<div class="tag-item">
|
||
<span class="tag-label">职位类别</span>
|
||
<span class="tag-value">{{ job.category || '未填写' }}</span>
|
||
</div>
|
||
<div class="tag-item">
|
||
<span class="tag-label">工作地点</span>
|
||
<span class="tag-value">{{ job.location }}</span>
|
||
</div>
|
||
<div class="tag-item">
|
||
<span class="tag-label">学历要求</span>
|
||
<span class="tag-value">{{ job.education || '不限' }}</span>
|
||
</div>
|
||
<div class="tag-item">
|
||
<span class="tag-label">薪资范围</span>
|
||
<span class="tag-value salary-val">{{ job.salary }}</span>
|
||
</div>
|
||
<div class="tag-item">
|
||
<span class="tag-label">发布时间</span>
|
||
<span class="tag-value">{{ formatDate(job.created_at) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<el-divider />
|
||
|
||
<!-- 职位介绍 -->
|
||
<div class="section">
|
||
<div class="section-title">
|
||
<span class="title-bar"></span>职位介绍
|
||
</div>
|
||
<div class="category-tag" v-if="job.category">{{ job.category }}</div>
|
||
<div class="description-content" v-html="job.description"></div>
|
||
</div>
|
||
|
||
<el-divider />
|
||
|
||
<!-- 工作地点 -->
|
||
<div class="section">
|
||
<div class="section-title">
|
||
<span class="title-bar"></span>工作地点
|
||
</div>
|
||
<div class="location-text">
|
||
<el-icon><Location /></el-icon>
|
||
{{ job.location }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右栏 -->
|
||
<div class="body-right">
|
||
<!-- 公司信息 -->
|
||
<div class="company-card">
|
||
<div class="company-card-header">单位信息</div>
|
||
<div class="company-logo-row company-link" @click="router.push({ name: 'CompanyDetail', params: { id: job.organization?.id } })">
|
||
<div class="company-logo">
|
||
<img v-if="job.organization?.logo" :src="job.organization.logo" alt="logo" />
|
||
<div v-else class="logo-placeholder">{{ job.organization?.name?.charAt(0) }}</div>
|
||
</div>
|
||
<div class="company-info">
|
||
<div class="company-name">{{ job.organization?.name }}</div>
|
||
<div class="company-desc" v-if="job.organization?.description">
|
||
{{ job.organization.description.slice(0, 40) }}{{ job.organization.description.length > 40 ? '...' : '' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-divider style="margin: 12px 0" />
|
||
<div class="company-contact" v-if="job.organization?.email">
|
||
<el-icon><Message /></el-icon>
|
||
<span>{{ job.organization.email }}</span>
|
||
</div>
|
||
<el-button
|
||
class="btn-apply-sm"
|
||
@click="handleApply"
|
||
:loading="applying"
|
||
:disabled="applied"
|
||
style="margin-top:12px;width:100%"
|
||
>
|
||
{{ applied ? '已投递' : '立即投递' }}
|
||
</el-button>
|
||
<p class="apply-hint-sm" v-if="applied">已投递成功</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { getJob, toggleFavorite, getMyFavorites } from '@/api/jobs'
|
||
import { applyJob, getMyApplications } from '@/api/applications'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { ElMessage } from 'element-plus'
|
||
import { Location, OfficeBuilding, Star, Message } from '@element-plus/icons-vue'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const auth = useAuthStore()
|
||
const job = ref(null)
|
||
const loading = ref(false)
|
||
const applying = ref(false)
|
||
const applied = ref(false)
|
||
const collected = ref(false)
|
||
|
||
onMounted(async () => {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await getJob(route.params.id)
|
||
job.value = data
|
||
|
||
// 如果用户已登录,检查是否已投递、已收藏
|
||
if (auth.isLoggedIn && auth.isSeeker) {
|
||
try {
|
||
const [{ data: applications }, { data: favorites }] = await Promise.all([
|
||
getMyApplications(),
|
||
getMyFavorites(),
|
||
])
|
||
applied.value = applications.results?.some(app => app.job === parseInt(route.params.id)) || false
|
||
collected.value = favorites.results?.some(f => f.job.id === parseInt(route.params.id)) || false
|
||
} catch (e) {
|
||
console.error('Failed to fetch user state:', e)
|
||
}
|
||
}
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
})
|
||
|
||
function formatDate(dt) {
|
||
if (!dt) return ''
|
||
return new Date(dt).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
|
||
}
|
||
|
||
async function toggleCollect() {
|
||
if (!auth.isLoggedIn) return router.push({ name: 'Login', query: { redirect: route.fullPath } })
|
||
if (!auth.isSeeker) return ElMessage.warning('管理员账号无法收藏职位')
|
||
const { data } = await toggleFavorite(job.value.id)
|
||
collected.value = data.collected
|
||
ElMessage.success(collected.value ? '已收藏,可在「关注职位」中查看' : '已取消收藏')
|
||
}
|
||
|
||
async function handleApply() {
|
||
if (!auth.isLoggedIn) return router.push({ name: 'Login', query: { redirect: route.fullPath } })
|
||
if (!auth.isSeeker) return ElMessage.warning('管理员账号无法投递职位')
|
||
if (applied.value) return ElMessage.warning('您已投递过该职位')
|
||
|
||
applying.value = true
|
||
try {
|
||
await applyJob(job.value.id)
|
||
applied.value = true
|
||
ElMessage.success('投递成功!')
|
||
} catch (e) {
|
||
if (e.response?.status === 400) {
|
||
applied.value = true
|
||
ElMessage.warning(e.response.data?.detail || '您已投递过该职位')
|
||
} else {
|
||
ElMessage.error('投递失败,请先完善简历')
|
||
}
|
||
} finally {
|
||
applying.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.job-detail-page {
|
||
background: #f5f6fa;
|
||
min-height: calc(100vh - 120px);
|
||
}
|
||
|
||
/* Banner */
|
||
.job-banner {
|
||
background: linear-gradient(135deg, #e8f4fd 0%, #d0e9f8 60%, #b8d9f0 100%);
|
||
padding: 36px 0 32px;
|
||
border-bottom: 1px solid #dce8f3;
|
||
}
|
||
.banner-inner {
|
||
max-width: 1100px;
|
||
margin: 0 auto;
|
||
padding: 0 24px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 24px;
|
||
}
|
||
.job-title {
|
||
font-size: 26px;
|
||
font-weight: 700;
|
||
color: #1a1a2e;
|
||
margin: 0 0 10px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
.job-salary {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #e63329;
|
||
margin-bottom: 12px;
|
||
}
|
||
.job-meta-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #555;
|
||
font-size: 14px;
|
||
}
|
||
.meta-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.meta-divider {
|
||
color: #ccc;
|
||
}
|
||
.banner-right {
|
||
text-align: right;
|
||
flex-shrink: 0;
|
||
}
|
||
.banner-date {
|
||
font-size: 13px;
|
||
color: #888;
|
||
margin-bottom: 14px;
|
||
}
|
||
.banner-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: flex-end;
|
||
}
|
||
.btn-collect {
|
||
border-color: #e63329 !important;
|
||
color: #e63329 !important;
|
||
background: transparent !important;
|
||
}
|
||
.btn-collect:hover {
|
||
background: #fff0f0 !important;
|
||
}
|
||
.btn-apply {
|
||
background: #e63329 !important;
|
||
border-color: #e63329 !important;
|
||
color: #fff !important;
|
||
font-weight: 600;
|
||
padding: 10px 28px;
|
||
font-size: 15px;
|
||
}
|
||
.btn-apply:hover:not(:disabled) {
|
||
background: #c42820 !important;
|
||
border-color: #c42820 !important;
|
||
}
|
||
.btn-apply:disabled {
|
||
background: #ccc !important;
|
||
border-color: #ccc !important;
|
||
color: #fff !important;
|
||
cursor: not-allowed !important;
|
||
}
|
||
.apply-hint {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 8px;
|
||
}
|
||
.apply-hint.success {
|
||
color: #52c41a;
|
||
}
|
||
|
||
/* Body */
|
||
.job-body {
|
||
padding: 28px 0 40px;
|
||
}
|
||
.body-inner {
|
||
max-width: 1100px;
|
||
margin: 0 auto;
|
||
padding: 0 24px;
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: flex-start;
|
||
}
|
||
.body-left {
|
||
flex: 1;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 24px 28px;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,.06);
|
||
}
|
||
.body-right {
|
||
width: 280px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Info tags */
|
||
.info-tags {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px 24px;
|
||
padding-bottom: 4px;
|
||
}
|
||
.tag-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
}
|
||
.tag-label {
|
||
color: #888;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.tag-label::before {
|
||
content: '';
|
||
display: inline-block;
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: #e63329;
|
||
flex-shrink: 0;
|
||
}
|
||
.tag-value {
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
.tag-value.salary-val {
|
||
color: #e63329;
|
||
}
|
||
|
||
/* Section */
|
||
.section {
|
||
padding: 4px 0;
|
||
}
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1a1a2e;
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.title-bar {
|
||
display: inline-block;
|
||
width: 4px;
|
||
height: 16px;
|
||
background: #e63329;
|
||
border-radius: 2px;
|
||
}
|
||
.category-tag {
|
||
display: inline-block;
|
||
background: #fff0f0;
|
||
color: #e63329;
|
||
border: 1px solid #fad2d0;
|
||
border-radius: 4px;
|
||
padding: 3px 10px;
|
||
font-size: 13px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.description-content {
|
||
font-size: 14px;
|
||
line-height: 1.9;
|
||
color: #444;
|
||
white-space: pre-wrap;
|
||
}
|
||
.location-text {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 14px;
|
||
color: #555;
|
||
}
|
||
|
||
/* Company card */
|
||
.company-card {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,.06);
|
||
}
|
||
.company-card-header {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #1a1a2e;
|
||
margin-bottom: 16px;
|
||
}
|
||
.company-logo-row {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
}
|
||
.company-link {
|
||
cursor: pointer;
|
||
border-radius: 6px;
|
||
padding: 6px;
|
||
margin: -6px;
|
||
transition: background 0.2s;
|
||
}
|
||
.company-link:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
.company-link:hover .company-name {
|
||
color: #e63329;
|
||
}
|
||
.company-logo {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
border: 1px solid #eee;
|
||
}
|
||
.company-logo img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
.logo-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
color: #fff;
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.company-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.company-name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #222;
|
||
margin-bottom: 4px;
|
||
line-height: 1.4;
|
||
}
|
||
.company-desc {
|
||
font-size: 12px;
|
||
color: #888;
|
||
line-height: 1.5;
|
||
}
|
||
.company-contact {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
color: #666;
|
||
word-break: break-all;
|
||
}
|
||
.btn-apply-sm {
|
||
background: #e63329 !important;
|
||
border-color: #e63329 !important;
|
||
color: #fff !important;
|
||
font-weight: 600;
|
||
}
|
||
.btn-apply-sm:hover:not(:disabled) {
|
||
background: #c42820 !important;
|
||
border-color: #c42820 !important;
|
||
}
|
||
.btn-apply-sm:disabled {
|
||
background: #ccc !important;
|
||
border-color: #ccc !important;
|
||
color: #fff !important;
|
||
cursor: not-allowed !important;
|
||
}
|
||
.apply-hint-sm {
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: #52c41a;
|
||
margin-top: 8px;
|
||
}
|
||
</style>
|