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

519 lines
13 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="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>