- 在职位集合中新增sn字段以支持产品序列号 - 优化JobCard组件,移除不必要的注释代码,提升代码整洁性 - 新增职位详情页面,展示职位信息和申请方式 - 更新招聘页面样式,增强用户体验和响应式设计master
<template> | <template> | ||||
<div | <div | ||||
class="job-card group cursor-pointer" | |||||
@click="handleClick" | |||||
class="job-card group" | |||||
> | > | ||||
<div class="relative p-8 bg-zinc-800/30 backdrop-blur-sm border border-zinc-700/30 rounded-2xl transition-all duration-700 hover:border-[#35F1FF]/50 hover:bg-zinc-800/50 hover:shadow-2xl hover:shadow-[#35F1FF]/10 hover:transform hover:scale-105"> | <div class="relative p-8 bg-zinc-800/30 backdrop-blur-sm border border-zinc-700/30 rounded-2xl transition-all duration-700 hover:border-[#35F1FF]/50 hover:bg-zinc-800/50 hover:shadow-2xl hover:shadow-[#35F1FF]/10 hover:transform hover:scale-105"> | ||||
</svg> | </svg> | ||||
{{ getLocationName(job.location) }} | {{ getLocationName(job.location) }} | ||||
</span> | </span> | ||||
<span class="flex items-center"> | |||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2-2v2m8 0H8m8 0v2a2 2 0 002 2M8 6V4h8V6M8 6v2a2 2 0 002 2h4a2 2 0 002-2V6"></path> | |||||
</svg> | |||||
{{ getWorkType(job.webSite) }} | |||||
</span> | |||||
<span class="flex items-center"> | <span class="flex items-center"> | ||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> | ||||
<div class="text-xs text-zinc-500"> | <div class="text-xs text-zinc-500"> | ||||
{{ formatDate(job.updateTime) }} {{ t('careers.PositionCard.update') }} | {{ formatDate(job.updateTime) }} {{ t('careers.PositionCard.update') }} | ||||
</div> | </div> | ||||
<button | |||||
<nuxt-link | |||||
class="inline-flex items-center px-6 py-2 bg-[#35F1FF]/10 text-[#35F1FF] text-sm font-medium rounded-lg border border-[#35F1FF]/30 hover:bg-[#35F1FF]/20 hover:border-[#35F1FF]/50 transition-all duration-300 group-hover:scale-105" | class="inline-flex items-center px-6 py-2 bg-[#35F1FF]/10 text-[#35F1FF] text-sm font-medium rounded-lg border border-[#35F1FF]/30 hover:bg-[#35F1FF]/20 hover:border-[#35F1FF]/50 transition-all duration-300 group-hover:scale-105" | ||||
@click.stop="handleApply" | |||||
:to="`${homepagePath}/careers/${job.id}`" | |||||
> | > | ||||
<span class="mr-2">{{ t('careers.PositionCard.JobDetails') }}</span> | <span class="mr-2">{{ t('careers.PositionCard.JobDetails') }}</span> | ||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path> | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path> | ||||
</svg> | </svg> | ||||
</button> | |||||
</nuxt-link> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<!-- 投递邮箱弹窗 --> | |||||
<div v-if="showApplyModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" @click="closeModal"> | |||||
<div class="relative bg-zinc-900/95 backdrop-blur-md border border-zinc-700/50 rounded-2xl p-8 max-w-md w-full mx-4 transform transition-all duration-300 scale-95 opacity-0" | |||||
:class="{ 'scale-100 opacity-100': showApplyModal }" | |||||
@click.stop> | |||||
<div class="absolute inset-0 bg-gradient-to-br from-[#35F1FF]/10 to-transparent rounded-2xl"></div> | |||||
<div class="relative"> | |||||
<!-- 关闭按钮 --> | |||||
<button @click="closeModal" class="absolute -top-2 -right-2 w-8 h-8 bg-zinc-800/80 hover:bg-zinc-700/80 rounded-full flex items-center justify-center text-zinc-400 hover:text-white transition-colors"> | |||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> | |||||
</svg> | |||||
</button> | |||||
<!-- 标题 --> | |||||
<div class="text-center mb-6"> | |||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-[#35F1FF]/10 rounded-full mb-4"> | |||||
<svg class="w-8 h-8 text-[#35F1FF]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-xl font-semibold text-white mb-2">{{ t('careers.jobs.button') }}</h3> | |||||
<p class="text-zinc-400 text-sm">{{ t('contact.submit') }}</p> | |||||
</div> | |||||
<!-- 职位信息 --> | |||||
<div class="bg-zinc-800/50 rounded-xl p-4 mb-6"> | |||||
<div class="text-sm text-zinc-400 mb-2">{{ t('careers.jobs.title') }}</div> | |||||
<div class="text-white font-medium">{{ job.name }}</div> | |||||
<div class="text-xs text-zinc-500 mt-1">ID: {{ job.id }}</div> | |||||
</div> | |||||
<!-- 邮箱信息 --> | |||||
<div class="bg-zinc-800/50 rounded-xl p-4 mb-6"> | |||||
<div class="text-sm text-zinc-400 mb-2">{{ t('contact.email') }}</div> | |||||
<div class="flex items-center justify-between"> | |||||
<div class="text-[#35F1FF] font-medium break-all">{{ job.email }}</div> | |||||
<button @click="copyEmail" class="ml-3 px-3 py-1 bg-[#35F1FF]/10 text-[#35F1FF] text-xs rounded-lg hover:bg-[#35F1FF]/20 transition-colors flex-shrink-0"> | |||||
{{ copied ? t('common.copied') : t('common.copy') }} | |||||
</button> | |||||
</div> | |||||
</div> | |||||
<!-- 操作按钮 --> | |||||
<div class="flex gap-3"> | |||||
<button @click="closeModal" class="flex-1 px-4 py-2 bg-zinc-800/80 text-zinc-300 rounded-lg hover:bg-zinc-700/80 transition-colors"> | |||||
{{ t('common.close') }} | |||||
</button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | </div> | ||||
</template> | </template> | ||||
import { locationMap, workTypeMap } from '~/data/jobs' | import { locationMap, workTypeMap } from '~/data/jobs' | ||||
import { useI18n } from 'vue-i18n' | import { useI18n } from 'vue-i18n' | ||||
const { t } = useI18n() | |||||
const { t, locale } = useI18n() | |||||
const homepagePath = computed(() => { | |||||
return locale.value === "zh" ? "" : `/${locale.value}`; | |||||
}); | |||||
/** | /** | ||||
* 组件属性 | * 组件属性 | ||||
const props = defineProps<Props>() | const props = defineProps<Props>() | ||||
/** | |||||
* 组件事件 | |||||
*/ | |||||
const emit = defineEmits<{ | |||||
click: [job: Job] | |||||
}>() | |||||
/** | /** | ||||
* 获取地点名称 | * 获取地点名称 | ||||
* @param location 地点编码 | * @param location 地点编码 | ||||
return locationMap[location] || 'Other' | return locationMap[location] || 'Other' | ||||
} | } | ||||
/** | |||||
* 获取工作类型 | |||||
* @param webSite 工作类型编码 | |||||
* @returns 工作类型 | |||||
*/ | |||||
const getWorkType = (webSite: number): string => { | |||||
// 假设 1 代表全职,其他值可以扩展 | |||||
return workTypeMap[webSite] || t('careers.workType.fullTime') | |||||
} | |||||
/** | /** | ||||
* 格式化日期 | * 格式化日期 | ||||
* @param dateString 日期字符串 | * @param dateString 日期字符串 | ||||
} | } | ||||
} | } | ||||
/** | |||||
* 移除字符串中的HTML标签 | |||||
* @param htmlString 包含HTML的字符串 | |||||
* @returns 纯文本字符串 | |||||
*/ | |||||
const stripHtml = (htmlString: string): string => { | |||||
if (!htmlString) return '' | |||||
return htmlString.replace(/<\/?[^>]+(>|$)/g, "") | |||||
} | |||||
/** | /** | ||||
* 格式化工作描述 | * 格式化工作描述 | ||||
* @param description 工作描述 | * @param description 工作描述 | ||||
const formatJobDescription = (description: string): string => { | const formatJobDescription = (description: string): string => { | ||||
if (!description) return '' | if (!description) return '' | ||||
const plainText = stripHtml(description) | |||||
// 将数字开头的项目转换为更易读的格式 | // 将数字开头的项目转换为更易读的格式 | ||||
return description | |||||
return plainText | |||||
.replace(/(\d+\.)/g, '\n$1') | .replace(/(\d+\.)/g, '\n$1') | ||||
.replace(/;/g, ';\n') | .replace(/;/g, ';\n') | ||||
.trim() | .trim() | ||||
.substring(0, 200) + (description.length > 200 ? '...' : '') | |||||
.substring(0, 200) + (plainText.length > 200 ? '...' : '') | |||||
} | } | ||||
/** | /** | ||||
const formatBenefits = (benefits: string): string => { | const formatBenefits = (benefits: string): string => { | ||||
if (!benefits) return '' | if (!benefits) return '' | ||||
return benefits | |||||
const plainText = stripHtml(benefits) | |||||
return plainText | |||||
.replace(/(\d+\.)/g, '\n$1') | .replace(/(\d+\.)/g, '\n$1') | ||||
.trim() | .trim() | ||||
.substring(0, 100) + (benefits.length > 100 ? '...' : '') | |||||
.substring(0, 100) + (plainText.length > 100 ? '...' : '') | |||||
} | } | ||||
/** | /** | ||||
const formatWorkTime = (workTime: string): string => { | const formatWorkTime = (workTime: string): string => { | ||||
if (!workTime) return '' | if (!workTime) return '' | ||||
return workTime | |||||
const plainText = stripHtml(workTime) | |||||
return plainText | |||||
.replace(/\\n/g, '\n') | .replace(/\\n/g, '\n') | ||||
.replace(/(\d+\.)/g, '\n$1') | .replace(/(\d+\.)/g, '\n$1') | ||||
.trim() | .trim() | ||||
} | } | ||||
/** | |||||
* 处理点击事件 | |||||
*/ | |||||
const handleClick = () => { | |||||
emit('click', props.job) | |||||
} | |||||
/** | |||||
* 模态框状态 | |||||
*/ | |||||
const showApplyModal = ref(false) | |||||
const copied = ref(false) | |||||
/** | |||||
* 处理申请事件 | |||||
*/ | |||||
const handleApply = () => { | |||||
showApplyModal.value = true | |||||
} | |||||
/** | |||||
* 关闭模态框 | |||||
*/ | |||||
const closeModal = () => { | |||||
showApplyModal.value = false | |||||
copied.value = false | |||||
} | |||||
/** | |||||
* 复制邮箱地址 | |||||
*/ | |||||
const copyEmail = async () => { | |||||
try { | |||||
await navigator.clipboard.writeText(props.job.email) | |||||
copied.value = true | |||||
setTimeout(() => { | |||||
copied.value = false | |||||
}, 2000) | |||||
} catch { | |||||
// 降级处理:使用传统方式复制 | |||||
const textArea = document.createElement('textarea') | |||||
textArea.value = props.job.email | |||||
document.body.appendChild(textArea) | |||||
textArea.select() | |||||
document.execCommand('copy') | |||||
document.body.removeChild(textArea) | |||||
copied.value = true | |||||
setTimeout(() => { | |||||
copied.value = false | |||||
}, 2000) | |||||
} | |||||
} | |||||
</script> | </script> | ||||
<style scoped> | <style scoped> | ||||
.line-clamp-2 { | |||||
display: -webkit-box; | |||||
-webkit-line-clamp: 2; | |||||
-webkit-box-orient: vertical; | |||||
overflow: hidden; | |||||
} | |||||
.line-clamp-3 { | |||||
display: -webkit-box; | |||||
-webkit-line-clamp: 3; | |||||
-webkit-box-orient: vertical; | |||||
overflow: hidden; | |||||
} | |||||
.job-card { | .job-card { | ||||
opacity: 0; | opacity: 0; |
sort: z.number(), | sort: z.number(), | ||||
recommend: z.boolean(), | recommend: z.boolean(), | ||||
recommendIndex: z.number(), | recommendIndex: z.number(), | ||||
sn: z.string(), | |||||
body: z.object({ | body: z.object({ | ||||
value: z.string(), | value: z.string(), | ||||
}), | }), |
tag: "SD" | tag: "SD" | ||||
summary: 适用于数码相机、摄像机,读取速度高达280MB/秒 | summary: 适用于数码相机、摄像机,读取速度高达280MB/秒 | ||||
capacities: ["128GB"] | capacities: ["128GB"] | ||||
sn: SD-XC0000V6 | |||||
sort: 99 | sort: 99 | ||||
--- | --- | ||||
tag: "SD" | tag: "SD" | ||||
summary: 适用于数码相机、摄像机,读取速度高达280MB/秒 | summary: 适用于数码相机、摄像机,读取速度高达280MB/秒 | ||||
capacities: ["256GB"] | capacities: ["256GB"] | ||||
sn: SD-XC0000V6 | |||||
sort: 99 | sort: 99 | ||||
--- | --- | ||||
tag: "SD" | tag: "SD" | ||||
summary: 适用于数码相机、摄像机,读取速度高达280MB/秒 | summary: 适用于数码相机、摄像机,读取速度高达280MB/秒 | ||||
capacities: ["512GB"] | capacities: ["512GB"] | ||||
sn: SD-XC0000V6 | |||||
sort: 99 | sort: 99 | ||||
--- | --- | ||||
tag: "SD" | tag: "SD" | ||||
summary: 适用于数码相机、摄像机,读取速度高达100MB/秒 | summary: 适用于数码相机、摄像机,读取速度高达100MB/秒 | ||||
capacities: ["64GB"] | capacities: ["64GB"] | ||||
sn: SD-XC0000V3 | |||||
sort: 99 | sort: 99 | ||||
--- | --- | ||||
# 产品特征 | # 产品特征 |
tag: "SD" | tag: "SD" | ||||
summary: 适用于数码相机、摄像机,读取速度高达100MB/秒 | summary: 适用于数码相机、摄像机,读取速度高达100MB/秒 | ||||
capacities: ["128GB"] | capacities: ["128GB"] | ||||
sn: SD-XC0000V3 | |||||
sort: 99 | sort: 99 | ||||
--- | --- | ||||
tag: "SD" | tag: "SD" | ||||
summary: 适用于数码相机、摄像机,读取速度高达100MB/秒 | summary: 适用于数码相机、摄像机,读取速度高达100MB/秒 | ||||
capacities: ["256GB"] | capacities: ["256GB"] | ||||
sn: SD-XC0000V3 | |||||
sort: 99 | sort: 99 | ||||
--- | --- | ||||
} | } | ||||
/** | /** | ||||
* API 响应接口 | |||||
* API 列表响应接口 | |||||
*/ | */ | ||||
export interface JobsApiResponse { | export interface JobsApiResponse { | ||||
code: number | code: number | ||||
rows: Job[] | rows: Job[] | ||||
} | } | ||||
/** | |||||
* API 单个职位响应接口 | |||||
*/ | |||||
export interface JobApiResponse { | |||||
code: number | |||||
msg: string | |||||
data: Job | |||||
} | |||||
/** | /** | ||||
* 地点映射表 | * 地点映射表 | ||||
*/ | */ |
careers: { | careers: { | ||||
title: "Careers", | title: "Careers", | ||||
subtitle: "Join us to create the future of storage technology together. We are looking for passionate talents to drive technological innovation with us.", | subtitle: "Join us to create the future of storage technology together. We are looking for passionate talents to drive technological innovation with us.", | ||||
slogan:"Join Us, Create the Future Together", | |||||
CompanyHistory: "Years of Corporate History", | CompanyHistory: "Years of Corporate History", | ||||
TeamMembers: "Team Members", | TeamMembers: "Team Members", | ||||
ProductModel: "Product Model", | ProductModel: "Product Model", | ||||
description: "We are looking for talented individuals to join our team. Browse the positions below to find the right opportunity for you.", | description: "We are looking for talented individuals to join our team. Browse the positions below to find the right opportunity for you.", | ||||
button: "Apply Now", | button: "Apply Now", | ||||
noPositions: "No Open Positions Currently", | noPositions: "No Open Positions Currently", | ||||
checkLater: "Please check back later or contact us for the latest job information." | |||||
checkLater: "Please check back later or contact us for the latest job information.", | |||||
sendTip: "Please send your resume and cover letter to the above email address. We will contact you soon." | |||||
}, | }, | ||||
cta: { | cta: { | ||||
title: "Didn't find a suitable position?", | title: "Didn't find a suitable position?", |
careers: { | careers: { | ||||
title: "採用情報", | title: "採用情報", | ||||
subtitle: "私たちと共に、ストレージ技術の未来を創造しませんか。技術革新を推進するため、情熱ある人材を募集しています。", | subtitle: "私たちと共に、ストレージ技術の未来を創造しませんか。技術革新を推進するため、情熱ある人材を募集しています。", | ||||
slogan:"優秀な人材と共に、未来を創造する", | |||||
CompanyHistory: "年の企業沿革", | CompanyHistory: "年の企業沿革", | ||||
TeamMembers: "チームメンバー", | TeamMembers: "チームメンバー", | ||||
ProductModel: "製品モデル", | ProductModel: "製品モデル", | ||||
description: "私たちは優秀な人材をチームに迎えることを楽しみにしています。以下の職種をご覧になり、あなたに合った機会を見つけてください。", | description: "私たちは優秀な人材をチームに迎えることを楽しみにしています。以下の職種をご覧になり、あなたに合った機会を見つけてください。", | ||||
button: "応募する", | button: "応募する", | ||||
noPositions: "現在募集中の職種はありません", | noPositions: "現在募集中の職種はありません", | ||||
checkLater: "しばらくしてから再度ご確認いただくか、最新の求人情報についてお問い合わせください。" | |||||
checkLater: "しばらくしてから再度ご確認いただくか、最新の求人情報についてお問い合わせください。", | |||||
sendTip: "あなたの履歴書と求職書を上記のメールアドレスに送信してください。お問い合わせください。" | |||||
}, | }, | ||||
cta: { | cta: { | ||||
title: "希望の職種が見つからない場合", | title: "希望の職種が見つからない場合", |
careers: { | careers: { | ||||
title: "招聘信息", | title: "招聘信息", | ||||
subtitle: "加入我们,共同创造存储技术的未来。我们寻找有激情的人才,与我们一起推动技术创新。", | subtitle: "加入我们,共同创造存储技术的未来。我们寻找有激情的人才,与我们一起推动技术创新。", | ||||
slogan:"与优秀同行,共创未来", | |||||
CompanyHistory:"年企业历程", | CompanyHistory:"年企业历程", | ||||
TeamMembers:"团队成员", | TeamMembers:"团队成员", | ||||
ProductModel:"产品型号", | ProductModel:"产品型号", | ||||
sectionTitle: "人才招聘", | sectionTitle: "人才招聘", | ||||
CurrentlyOpen:"当前开放", | CurrentlyOpen:"当前开放", | ||||
Aposition:"个职位", | Aposition:"个职位", | ||||
// --- 新增 --- | |||||
workType: { | workType: { | ||||
fullTime: "全职", | fullTime: "全职", | ||||
partTime: "兼职", | partTime: "兼职", | ||||
internship: "实习", | internship: "实习", | ||||
}, | }, | ||||
// --- 新增结束 --- | |||||
culture: { | culture: { | ||||
sectionTitle: "企业文化", | sectionTitle: "企业文化", | ||||
title: "我们的价值观", | title: "我们的价值观", | ||||
description: "我们正在寻找优秀的人才加入我们的团队。浏览下方职位,找到适合您的机会。", | description: "我们正在寻找优秀的人才加入我们的团队。浏览下方职位,找到适合您的机会。", | ||||
button: "申请职位", | button: "申请职位", | ||||
noPositions: "暂无招聘职位", | noPositions: "暂无招聘职位", | ||||
checkLater: "请稍后查看或联系我们了解最新职位信息" | |||||
checkLater: "请稍后查看或联系我们了解最新职位信息", | |||||
sendTip: "请将您的简历和求职信发送至上述邮箱地址,我们会尽快与您联系。" | |||||
}, | }, | ||||
cta: { | cta: { | ||||
title: "没有找到合适的职位?", | title: "没有找到合适的职位?", |
<template> | |||||
<div class="min-h-screen bg-black text-white"> | |||||
<!-- 顶部间距 --> | |||||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||||
<!-- 面包屑导航 --> | |||||
<div class="max-w-full xl:px-2 lg:px-2 md:px-4 px-4 mt-6 mb-1 sm:mb-4"> | |||||
<div class="max-w-screen-2xl mx-auto"> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/`" | |||||
class="justify-start text-white/60 text-base font-normal" | |||||
>{{ t("common.breadcrumb.home") }}</nuxt-link | |||||
> | |||||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/careers`" | |||||
class="text-white text-base font-normal" | |||||
>{{ t("common.careers") }}</nuxt-link | |||||
> | |||||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||||
<span class="text-white text-base font-normal">{{ job?.name }}</span> | |||||
</div> | |||||
</div> | |||||
<!-- 主要内容区域 --> | |||||
<ErrorBoundary | |||||
:error="pageError" | |||||
:title="t('common.error')" | |||||
:retry="true" | |||||
:retry-text="t('common.retry')" | |||||
@retry="handleRetry" | |||||
> | |||||
<div v-if="pending" class="flex justify-center py-12"> | |||||
<!-- 加载中 --> | |||||
<div | |||||
class="animate-spin h-8 w-8 border-4 border-cyan-400 rounded-full border-t-transparent" | |||||
></div> | |||||
</div> | |||||
<!-- 职位详情内容 --> | |||||
<div v-else-if="job" class="container mx-auto pb-16"> | |||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8"> | |||||
<!-- 主要内容 --> | |||||
<div class="lg:col-span-3 space-y-8"> | |||||
<!-- 职位头部信息 --> | |||||
<div | |||||
class="bg-zinc-900/50 backdrop-blur-sm border border-zinc-700/30 rounded-2xl p-8" | |||||
> | |||||
<h1 class="text-3xl font-bold text-white mb-4">{{ job.name }}</h1> | |||||
<div | |||||
class="flex flex-wrap items-center gap-6 text-zinc-400 text-sm mb-4" | |||||
> | |||||
<span class="flex items-center"> | |||||
<svg | |||||
class="w-4 h-4 mr-2" | |||||
fill="none" | |||||
stroke="currentColor" | |||||
viewBox="0 0 24 24" | |||||
> | |||||
<path | |||||
stroke-linecap="round" | |||||
stroke-linejoin="round" | |||||
stroke-width="2" | |||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" | |||||
></path> | |||||
<path | |||||
stroke-linecap="round" | |||||
stroke-linejoin="round" | |||||
stroke-width="2" | |||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" | |||||
></path> | |||||
</svg> | |||||
{{ getLocationName(job.location) }} | |||||
</span> | |||||
<span class="flex items-center"> | |||||
<svg | |||||
class="w-4 h-4 mr-2" | |||||
fill="none" | |||||
stroke="currentColor" | |||||
viewBox="0 0 24 24" | |||||
> | |||||
<path | |||||
stroke-linecap="round" | |||||
stroke-linejoin="round" | |||||
stroke-width="2" | |||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" | |||||
></path> | |||||
</svg> | |||||
{{ formatDate(job.createTime) }} | |||||
</span> | |||||
</div> | |||||
<div class="text-xs text-zinc-500"> | |||||
ID: {{ job.id }} | {{ formatDate(job.updateTime) }} | |||||
{{ t("careers.PositionCard.update") }} | |||||
</div> | |||||
</div> | |||||
<!-- 工作职责 --> | |||||
<div | |||||
class="bg-zinc-900/50 backdrop-blur-sm border border-zinc-700/30 rounded-2xl p-8" | |||||
> | |||||
<h2 | |||||
class="text-white text-2xl font-semibold mb-6 flex items-center" | |||||
> | |||||
<svg | |||||
class="w-6 h-6 mr-3 text-[#35F1FF]" | |||||
fill="none" | |||||
stroke="currentColor" | |||||
viewBox="0 0 24 24" | |||||
stroke-width="2" | |||||
stroke-linecap="round" | |||||
stroke-linejoin="round" | |||||
> | |||||
<rect x="4" y="2" width="16" height="20" rx="2" ry="2" /> | |||||
<line x1="8" y1="9" x2="16" y2="9" /> | |||||
<line x1="8" y1="13" x2="16" y2="13" /> | |||||
</svg> | |||||
{{ t("careers.PositionCard.job") }} | |||||
</h2> | |||||
<div | |||||
class="text-zinc-300 leading-relaxed whitespace-pre-line" | |||||
v-html="job.jobResponsibilities" | |||||
></div> | |||||
</div> | |||||
<!-- 薪资福利 --> | |||||
<div | |||||
class="bg-zinc-900/50 backdrop-blur-sm border border-zinc-700/30 rounded-2xl p-8" | |||||
> | |||||
<h2 | |||||
class="text-white text-2xl font-semibold mb-6 flex items-center" | |||||
> | |||||
<svg | |||||
class="w-6 h-6 mr-3 text-[#35F1FF]" | |||||
fill="none" | |||||
stroke="currentColor" | |||||
viewBox="0 0 24 24" | |||||
stroke-width="2" | |||||
stroke-linecap="round" | |||||
stroke-linejoin="round" | |||||
> | |||||
<path | |||||
d="M12 2v20M17 5H9.5a3.5 3.5 0 100 7h5a3.5 3.5 0 110 7H7" | |||||
></path> | |||||
</svg> | |||||
{{ t("careers.PositionCard.CB") }} | |||||
</h2> | |||||
<div class="text-zinc-300 leading-relaxed whitespace-pre-line"> | |||||
{{ job.benefits }} | |||||
</div> | |||||
</div> | |||||
<!-- 工作时间 --> | |||||
<div | |||||
class="bg-zinc-900/50 backdrop-blur-sm border border-zinc-700/30 rounded-2xl p-8" | |||||
> | |||||
<h2 | |||||
class="text-white text-2xl font-semibold mb-6 flex items-center" | |||||
> | |||||
<svg | |||||
class="w-6 h-6 mr-3 text-[#35F1FF]" | |||||
fill="none" | |||||
stroke="currentColor" | |||||
viewBox="0 0 24 24" | |||||
stroke-width="2" | |||||
stroke-linecap="round" | |||||
stroke-linejoin="round" | |||||
> | |||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect> | |||||
<line x1="16" y1="2" x2="16" y2="6"></line> | |||||
<line x1="8" y1="2" x2="8" y2="6"></line> | |||||
<line x1="3" y1="10" x2="21" y2="10"></line> | |||||
</svg> | |||||
{{ t("careers.PositionCard.workingHours") }} | |||||
</h2> | |||||
<div class="text-zinc-300 leading-relaxed whitespace-pre-line"> | |||||
{{ job.workTime }} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- 侧边栏 - 申请信息 --> | |||||
<div class="lg:col-span-1"> | |||||
<div class="sticky top-8"> | |||||
<div | |||||
class="bg-zinc-900/50 backdrop-blur-sm border border-zinc-700/30 rounded-2xl p-6" | |||||
> | |||||
<div class="text-center mb-6"> | |||||
<div | |||||
class="inline-flex items-center justify-center w-16 h-16 bg-[#35F1FF]/10 rounded-full mb-4" | |||||
> | |||||
<svg | |||||
class="w-8 h-8 text-[#35F1FF]" | |||||
fill="none" | |||||
stroke="currentColor" | |||||
viewBox="0 0 24 24" | |||||
> | |||||
<path | |||||
stroke-linecap="round" | |||||
stroke-linejoin="round" | |||||
stroke-width="2" | |||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" | |||||
></path> | |||||
</svg> | |||||
</div> | |||||
<h3 class="text-xl font-semibold text-white mb-2"> | |||||
{{ t("careers.jobs.button") }} | |||||
</h3> | |||||
</div> | |||||
<!-- 邮箱信息 --> | |||||
<div class="bg-zinc-800/30 rounded-xl p-4 mb-6"> | |||||
<div class="text-sm text-zinc-400 mb-2"> | |||||
{{ t("contact.email") }} | |||||
</div> | |||||
<div class="text-[#35F1FF] font-medium break-all mb-3"> | |||||
{{ job.email }} | |||||
</div> | |||||
<button | |||||
@click="copyEmail" | |||||
class="w-full px-4 py-3 bg-[#35F1FF]/10 text-[#35F1FF] font-medium rounded-lg hover:bg-[#35F1FF]/20 transition-colors" | |||||
> | |||||
{{ copied ? t("common.copied") : t("common.copy") }} | |||||
</button> | |||||
</div> | |||||
<!-- 申请提示 --> | |||||
<div class="text-xs text-zinc-500 text-center leading-relaxed"> | |||||
{{ t("careers.jobs.sendTip") }} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<!-- 未找到职位 --> | |||||
<div v-else class="container mx-auto px-4 py-16 text-center"> | |||||
<div | |||||
class="inline-flex items-center justify-center w-16 h-16 bg-zinc-800/30 rounded-full mb-4" | |||||
> | |||||
<svg | |||||
class="w-8 h-8 text-zinc-400" | |||||
fill="none" | |||||
stroke="currentColor" | |||||
viewBox="0 0 24 24" | |||||
> | |||||
<path | |||||
stroke-linecap="round" | |||||
stroke-linejoin="round" | |||||
stroke-width="2" | |||||
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2-2v2m8 0H8m8 0v2a2 2 0 002 2M8 6V4h8V6M8 6v2a2 2 0 002 2h4a2 2 0 002-2V6" | |||||
></path> | |||||
</svg> | |||||
</div> | |||||
<p class="text-zinc-400 mb-4">{{ t("careers.notFound") }}</p> | |||||
<NuxtLink | |||||
to="/careers" | |||||
class="inline-flex items-center px-4 py-2 bg-[#35F1FF]/10 text-[#35F1FF] rounded-lg hover:bg-[#35F1FF]/20 transition-colors" | |||||
> | |||||
{{ t("careers.backToList") }} | |||||
</NuxtLink> | |||||
</div> | |||||
</ErrorBoundary> | |||||
</div> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
import type { JobApiResponse, Job } from "~/data/jobs"; | |||||
import { locationMap, workTypeMap } from "~/data/jobs"; | |||||
import { useClipboard } from "@vueuse/core"; | |||||
/** | |||||
* 获取路由参数 | |||||
*/ | |||||
const route = useRoute(); | |||||
const { t, locale } = useI18n(); | |||||
// 计算首页路径 | |||||
const homepagePath = computed(() => { | |||||
return locale.value === "zh" ? "" : `/${locale.value}`; | |||||
}); | |||||
// API配置 | |||||
const apiUrl = computed( | |||||
() => | |||||
`https://digital.sohomall.jp/prod-api/system/workInfo/noVerify/${route.params.id}` | |||||
); | |||||
const { | |||||
data: apiResponse, | |||||
pending, | |||||
error: pageError, | |||||
refresh: handleRetry, | |||||
} = await useFetch<JobApiResponse>(apiUrl, {}); | |||||
// 从响应中获取职位信息 | |||||
const job = computed<Job | null>(() => apiResponse.value?.data ?? null); | |||||
useHead(() => { | |||||
const title = job.value | |||||
? `${job.value.name} - ${t("common.careers")} - Hanye` | |||||
: t("careers.notFound"); | |||||
const description = job.value | |||||
? job.value.jobResponsibilities.substring(0, 150) | |||||
: t("careers.notFoundDescription", "Could not find the job details."); | |||||
return { | |||||
title, | |||||
meta: [{ name: "description", content: description }], | |||||
}; | |||||
}); | |||||
/** | |||||
* 获取地点名称 | |||||
* @param locationId 地点ID | |||||
* @returns 地点名称 | |||||
*/ | |||||
const getLocationName = (locationId: number): string => { | |||||
return ( | |||||
locationMap[locationId] || t("careers.unknownLocation", "Unknown Location") | |||||
); | |||||
}; | |||||
/** | |||||
* 格式化日期 | |||||
* @param dateString 日期字符串 | |||||
* @returns 格式化后的日期 | |||||
*/ | |||||
const formatDate = (dateString?: string): string => { | |||||
if (!dateString) return ""; | |||||
const date = new Date(dateString); | |||||
// 使用 toLocaleDateString 以支持国际化 | |||||
return date.toLocaleDateString(locale.value, { | |||||
year: "numeric", | |||||
month: "long", | |||||
day: "numeric", | |||||
}); | |||||
}; | |||||
// 复制邮箱功能 | |||||
const { copy, copied, isSupported } = useClipboard({ | |||||
source: computed(() => job.value?.email ?? ""), | |||||
copiedDuring: 2000, | |||||
legacy: true, | |||||
}); | |||||
/** | |||||
* 复制邮箱地址到剪贴板 | |||||
*/ | |||||
const copyEmail = async () => { | |||||
if (!job.value?.email) return; | |||||
if (!isSupported.value) { | |||||
alert( | |||||
t("common.copyNotSupported", "Copying is not supported in your browser.") | |||||
); | |||||
return; | |||||
} | |||||
try { | |||||
await copy(); | |||||
} catch (err) { | |||||
console.error("Failed to copy email: ", err); | |||||
alert(t("common.copyFailed", "Failed to copy email.")); | |||||
} | |||||
}; | |||||
</script> |
<!-- 顶部间距 --> | <!-- 顶部间距 --> | ||||
<div class="w-full h-[55px] sm:h-[72px]"></div> | <div class="w-full h-[55px] sm:h-[72px]"></div> | ||||
<!-- 面包屑导航 --> | |||||
<div class="max-w-full xl:px-2 lg:px-2 md:px-4 px-4 mt-6 mb-1 sm:mb-4"> | |||||
<div class="max-w-screen-2xl mx-auto"> | |||||
<nuxt-link | |||||
:to="homepagePath" | |||||
class="justify-start text-white/60 text-base font-normal" | |||||
>{{ t("common.breadcrumb.home") }}</nuxt-link | |||||
> | |||||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||||
<span class="text-white text-base font-normal">{{ | |||||
t("common.careers") | |||||
}}</span> | |||||
</div> | |||||
</div> | |||||
<!-- 错误边界 --> | <!-- 错误边界 --> | ||||
<ErrorBoundary :error="pageError"> | <ErrorBoundary :error="pageError"> | ||||
<!-- 页面标题区域 --> | <!-- 页面标题区域 --> | ||||
<section class="hero-section"> | |||||
<div class="w-full py-20 md:py-40 relative overflow-hidden"> | |||||
<section class="hero-section relative overflow-hidden"> | |||||
<!-- 面包屑导航 --> | |||||
<div | |||||
class="max-w-full xl:px-2 lg:px-2 md:px-4 px-4 mt-6 mb-1 sm:mb-4" | |||||
> | |||||
<div class="max-w-screen-2xl mx-auto relative z-10"> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/`" | |||||
class="justify-start text-white/60 text-base font-normal" | |||||
>{{ t("common.breadcrumb.home") }}</nuxt-link | |||||
> | |||||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||||
<span class="text-white text-base font-normal">{{ | |||||
t("common.careers") | |||||
}}</span> | |||||
</div> | |||||
</div> | |||||
<div class="w-full py-20 md:py-40"> | |||||
<!-- 图片背景 --> | <!-- 图片背景 --> | ||||
<div class="absolute inset-0 z-0"> | <div class="absolute inset-0 z-0"> | ||||
<img | <img | ||||
src="/assets/images/careersbg.webp" | src="/assets/images/careersbg.webp" | ||||
alt="Background" | alt="Background" | ||||
class="w-full h-full object-cover opacity-10" | |||||
class="w-full h-full object-cover opacity-20" | |||||
/> | /> | ||||
</div> | </div> | ||||
> | > | ||||
<!-- 标题 --> | <!-- 标题 --> | ||||
<div class="text-center space-y-12"> | <div class="text-center space-y-12"> | ||||
<div | |||||
class="inline-flex items-center px-4 py-2 bg-[#35F1FF]/10 rounded-xl border border-[#35F1FF]/20 backdrop-blur-sm" | |||||
> | |||||
<span | |||||
class="text-[#35F1FF] text-xs font-bold tracking-[0.2em] uppercase" | |||||
> | |||||
{{ t("careers.sectionTitle") }} | |||||
</span> | |||||
</div> | |||||
<h1 | <h1 | ||||
class="text-white text-3xl sm:text-4xl md:text-6xl font-bold tracking-wider leading-tight" | class="text-white text-3xl sm:text-4xl md:text-6xl font-bold tracking-wider leading-tight" | ||||
> | > | ||||
<span | <span | ||||
class="bg-gradient-to-r from-white to-zinc-300 bg-clip-text text-transparent" | class="bg-gradient-to-r from-white to-zinc-300 bg-clip-text text-transparent" | ||||
> | > | ||||
{{ t("careers.title") }} | |||||
{{ t("careers.slogan") }} | |||||
</span> | </span> | ||||
</h1> | </h1> | ||||
<div class="w-20 h-px bg-[#35F1FF] mx-auto"></div> | |||||
<p | <p | ||||
class="text-zinc-300/80 text-base md:text-xl max-w-4xl mx-auto leading-relaxed" | class="text-zinc-300/80 text-base md:text-xl max-w-4xl mx-auto leading-relaxed" | ||||
> | > | ||||
{{ t("careers.subtitle") }} | {{ t("careers.subtitle") }} | ||||
</p> | </p> | ||||
<!-- 统计数据 --> | |||||
<div | |||||
class="grid grid-cols-2 md:grid-cols-4 gap-8 pt-12 max-w-3xl mx-auto" | |||||
> | |||||
<div class="text-center space-y-2"> | |||||
<div class="text-2xl md:text-3xl font-bold text-[#35F1FF]"> | |||||
20+ | |||||
</div> | |||||
<div class="text-sm text-zinc-400"> {{ t("careers.CompanyHistory") }}</div> | |||||
</div> | |||||
<div class="text-center space-y-2"> | |||||
<div class="text-2xl md:text-3xl font-bold text-[#35F1FF]"> | |||||
50+ | |||||
</div> | |||||
<div class="text-sm text-zinc-400">{{ t("careers.TeamMembers") }}</div> | |||||
</div> | |||||
<div class="text-center space-y-2"> | |||||
<div class="text-2xl md:text-3xl font-bold text-[#35F1FF]"> | |||||
100+ | |||||
</div> | |||||
<div class="text-sm text-zinc-400">{{ t("careers.ProductModel") }}</div> | |||||
</div> | |||||
<div class="text-center space-y-2"> | |||||
<div class="text-2xl md:text-3xl font-bold text-[#35F1FF]"> | |||||
24/7 | |||||
</div> | |||||
<div class="text-sm text-zinc-400">{{ t("careers.technicalSupport") }}</div> | |||||
</div> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<!-- 企业文化区域 --> | <!-- 企业文化区域 --> | ||||
<section class="culture-section"> | <section class="culture-section"> | ||||
<div | |||||
class="section-block w-full py-20 md:py-32 relative" | |||||
> | |||||
<div class="section-block w-full py-20 md:py-32 relative"> | |||||
<div | <div | ||||
class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10" | class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10" | ||||
> | > | ||||
<div class="text-center mb-12 md:mb-20"> | <div class="text-center mb-12 md:mb-20"> | ||||
<div | |||||
class="inline-flex items-center px-4 py-2 bg-indigo-500/10 rounded-xl border border-indigo-400/20 backdrop-blur-sm mb-6" | |||||
> | |||||
<span | |||||
class="text-indigo-400 text-xs font-bold tracking-[0.2em] uppercase" | |||||
> | |||||
{{ t("careers.culture.sectionTitle") }} | |||||
</span> | |||||
</div> | |||||
<h3 | <h3 | ||||
class="text-white text-2xl md:text-4xl font-bold mb-6 tracking-wider" | class="text-white text-2xl md:text-4xl font-bold mb-6 tracking-wider" | ||||
> | > | ||||
<!-- 团队协作 --> | <!-- 团队协作 --> | ||||
<div class="culture-card group"> | <div class="culture-card group"> | ||||
<div | <div | ||||
class="relative p-8 bg-zinc-800/30 backdrop-blur-sm border border-zinc-700/30 rounded-xl transition-all duration-700 hover:border-indigo-400/50 hover:bg-zinc-800/50 hover:shadow-2xl hover:shadow-indigo-500/10 hover:transform hover:scale-105" | |||||
class="relative p-8 bg-zinc-800/30 backdrop-blur-sm border border-zinc-700/30 rounded-xl transition-all duration-700 hover:border-indigo-400/50 hover:bg-zinc-800/50 hover:shadow-2xl hover:shadow-indigo-500/10 hover:transform hover:scale-105" | |||||
> | > | ||||
<div | <div | ||||
class="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 rounded-xl" | class="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 rounded-xl" | ||||
<!-- 品质第一 --> | <!-- 品质第一 --> | ||||
<div class="culture-card group"> | <div class="culture-card group"> | ||||
<div | <div | ||||
class="relative p-8 bg-zinc-800/30 backdrop-blur-sm border border-zinc-700/30 rounded-xl transition-all duration-700 hover:border-indigo-400/50 hover:bg-zinc-800/50 hover:shadow-2xl hover:shadow-indigo-500/10 hover:transform hover:scale-105" | |||||
class="relative p-8 bg-zinc-800/30 backdrop-blur-sm border border-zinc-700/30 rounded-xl transition-all duration-700 hover:border-indigo-400/50 hover:bg-zinc-800/50 hover:shadow-2xl hover:shadow-indigo-500/10 hover:transform hover:scale-105" | |||||
> | > | ||||
<div | <div | ||||
class="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 rounded-xl" | class="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 rounded-xl" | ||||
<!-- 共同成长 --> | <!-- 共同成长 --> | ||||
<div class="culture-card group"> | <div class="culture-card group"> | ||||
<div | <div | ||||
class="relative p-8 bg-zinc-800/30 backdrop-blur-sm border border-zinc-700/30 rounded-xl transition-all duration-700 hover:border-indigo-400/50 hover:bg-zinc-800/50 hover:shadow-2xl hover:shadow-indigo-500/10 hover:transform hover:scale-105" | |||||
class="relative p-8 bg-zinc-800/30 backdrop-blur-sm border border-zinc-700/30 rounded-xl transition-all duration-700 hover:border-indigo-400/50 hover:bg-zinc-800/50 hover:shadow-2xl hover:shadow-indigo-500/10 hover:transform hover:scale-105" | |||||
> | > | ||||
<div | <div | ||||
class="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 rounded-xl" | class="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 rounded-xl" | ||||
<!-- 职位列表区域 --> | <!-- 职位列表区域 --> | ||||
<section class="jobs-section"> | <section class="jobs-section"> | ||||
<div | |||||
class="section-block w-full py-20 md:py-32 relative" | |||||
> | |||||
<div class="section-block w-full py-20 md:py-32 relative"> | |||||
<!-- 背景网格pattern --> | <!-- 背景网格pattern --> | ||||
<div class="absolute inset-0 opacity-5"> | <div class="absolute inset-0 opacity-5"> | ||||
<div | <div | ||||
class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10" | class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10" | ||||
> | > | ||||
<div class="text-center mb-12 md:mb-20"> | <div class="text-center mb-12 md:mb-20"> | ||||
<div | |||||
class="inline-flex items-center px-4 py-2 bg-emerald-400/10 rounded-xl border border-emerald-400/20 backdrop-blur-sm mb-6" | |||||
> | |||||
<span | |||||
class="text-emerald-400 text-xs font-bold tracking-[0.2em] uppercase" | |||||
> | |||||
{{ t("careers.jobs.sectionTitle") }} | |||||
</span> | |||||
</div> | |||||
<h3 | <h3 | ||||
class="text-white text-2xl md:text-4xl font-bold mb-6 tracking-wider" | class="text-white text-2xl md:text-4xl font-bold mb-6 tracking-wider" | ||||
> | > | ||||
{{ t("careers.jobs.title") }} | {{ t("careers.jobs.title") }} | ||||
</h3> | </h3> | ||||
<div class="w-20 h-px bg-emerald-400 mx-auto mb-8"></div> | <div class="w-20 h-px bg-emerald-400 mx-auto mb-8"></div> | ||||
<p | |||||
class="text-zinc-300/80 text-base md:text-lg max-w-3xl mx-auto leading-relaxed" | |||||
> | |||||
{{ t("careers.jobs.description") }} | |||||
</p> | |||||
<!-- 职位统计 --> | |||||
<div v-if="totalJobs > 0" class="mt-8"> | |||||
<div | |||||
class="inline-flex items-center px-4 py-2 bg-emerald-400/5 rounded-xl border border-emerald-400/10" | |||||
> | |||||
<svg class="w-4 h-4 mr-2 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |||||
<path d="M21.5 12.5v-6a2 2 0 00-2-2h-15a2 2 0 00-2 2v12a2 2 0 002 2h6"></path> | |||||
<path d="M16.5 14.5a2.5 2.5 0 115 0 2.5 2.5 0 01-5 0z"></path> | |||||
<path d="M21.5 12.5v1.5a1 1 0 01-1 1h-4a1 1 0 01-1-1v-1.5"></path> | |||||
</svg> | |||||
<span class="text-emerald-400 text-sm font-medium"> | |||||
{{ t('careers.CurrentlyOpen') }} {{ jobsList.length }} {{ t('careers.Aposition') }} | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</div> | </div> | ||||
<!-- 加载状态 --> | <!-- 加载状态 --> | ||||
<div v-else-if="jobsList.length > 0"> | <div v-else-if="jobsList.length > 0"> | ||||
<!-- 职位卡片网格 --> | <!-- 职位卡片网格 --> | ||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> | <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> | ||||
<JobCard | |||||
v-for="job in jobsList" | |||||
:key="job.id" | |||||
:job="job" | |||||
@click="handleJobClick" | |||||
/> | |||||
<JobCard v-for="job in jobsList" :key="job.id" :job="job" /> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<!-- 底部CTA区域 --> | <!-- 底部CTA区域 --> | ||||
<section class="cta-section"> | <section class="cta-section"> | ||||
<div | |||||
class="section-block w-full py-20 md:py-32 relative" | |||||
> | |||||
<div class="section-block w-full py-20 md:py-32 relative"> | |||||
<!-- 图片背景 --> | <!-- 图片背景 --> | ||||
<img | |||||
src="/assets/images/careers.webp" | |||||
<img | |||||
src="/assets/images/careers.webp" | |||||
alt="CTA Background" | alt="CTA Background" | ||||
class="absolute inset-0 w-full h-full object-cover opacity-40 z-0" | class="absolute inset-0 w-full h-full object-cover opacity-40 z-0" | ||||
/> | /> | ||||
> | > | ||||
{{ t("careers.cta.title") }} | {{ t("careers.cta.title") }} | ||||
</h3> | </h3> | ||||
<p class="text-zinc-300/80 text-base md:text-xl leading-relaxed"> | |||||
<p | |||||
class="text-zinc-300/80 text-base md:text-xl leading-relaxed" | |||||
> | |||||
{{ t("careers.cta.description") }} | {{ t("careers.cta.description") }} | ||||
</p> | </p> | ||||
<div class="pt-4"> | <div class="pt-4"> | ||||
<span class="text-sm md:text-base font-normal text-white">{{ | <span class="text-sm md:text-base font-normal text-white">{{ | ||||
t("careers.cta.button") | t("careers.cta.button") | ||||
}}</span> | }}</span> | ||||
<i class="icon-arrow-right text-xs md:text-sm font-normal text-white"></i> | |||||
<i | |||||
class="icon-arrow-right text-xs md:text-sm font-normal text-white" | |||||
></i> | |||||
</nuxt-link> | </nuxt-link> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
*/ | */ | ||||
const fetchJobsFromApi = async ( | const fetchJobsFromApi = async ( | ||||
pageNum: number = 1, | pageNum: number = 1, | ||||
pageSize: number = 10 | |||||
pageSize: number = 20 | |||||
): Promise<JobsApiResponse> => { | ): Promise<JobsApiResponse> => { | ||||
const response = await $fetch<JobsApiResponse>( | const response = await $fetch<JobsApiResponse>( | ||||
`${API_BASE_URL}/noVerifyList`, | `${API_BASE_URL}/noVerifyList`, | ||||
console.log(apiResponse); | console.log(apiResponse); | ||||
// 过滤掉已禁用的职位 | |||||
// 过滤掉已禁用的职位 (isDisabled为"1"表示启用,"0"表示禁用) | |||||
const availableJobs = apiResponse.rows.filter( | const availableJobs = apiResponse.rows.filter( | ||||
(job: Job) => job.isDisabled !== "0" | |||||
(job: Job) => job.isDisabled === "1" | |||||
); | ); | ||||
jobsList.value = availableJobs; | jobsList.value = availableJobs; | ||||
console.warn("API获取失败,使用本地数据:", error); | console.warn("API获取失败,使用本地数据:", error); | ||||
// API失败时使用本地数据作为fallback | // API失败时使用本地数据作为fallback | ||||
jobsList.value = localJobs; | |||||
totalJobs.value = localJobs.length; | |||||
// 过滤掉已禁用的职位 | |||||
const availableLocalJobs = localJobs.filter( | |||||
(job: Job) => job.isDisabled === "0" | |||||
); | |||||
jobsList.value = availableLocalJobs; | |||||
totalJobs.value = availableLocalJobs.length; | |||||
// 不设置pageError,因为有fallback数据 | // 不设置pageError,因为有fallback数据 | ||||
console.log(`使用本地数据加载 ${availableLocalJobs.length} 个职位信息`); | |||||
} finally { | } finally { | ||||
jobsLoading.value = false; | jobsLoading.value = false; | ||||
} | } | ||||
}; | }; | ||||
/** | |||||
* 处理职位点击事件 | |||||
* @param job 职位信息 | |||||
*/ | |||||
const handleJobClick = (job: Job) => { | |||||
// 这里可以添加职位详情页面跳转或弹窗逻辑 | |||||
console.log("Job clicked:", job); | |||||
}; | |||||
/** | /** | ||||
* 处理联系按钮点击 | * 处理联系按钮点击 | ||||
*/ | */ | ||||
.section-block { | .section-block { | ||||
position: relative; | position: relative; | ||||
transition: all 0.5s ease; | transition: all 0.5s ease; | ||||
overflow: hidden; | |||||
overflow: hidden; | |||||
} | } | ||||
.section-block::before { | .section-block::before { | ||||
/* 为每个模块单独定义蒙层颜色 */ | /* 为每个模块单独定义蒙层颜色 */ | ||||
.culture-section .section-block::before { | .culture-section .section-block::before { | ||||
background: linear-gradient( | |||||
background: linear-gradient( | |||||
45deg, | 45deg, | ||||
transparent, | transparent, | ||||
rgba(99, 102, 241, 0.1), /* Indigo */ | |||||
transparent | |||||
rgba(99, 102, 241, 0.1), | |||||
/* Indigo */ transparent | |||||
); | ); | ||||
} | } | ||||
.jobs-section .section-block::before { | .jobs-section .section-block::before { | ||||
background: linear-gradient( | |||||
background: linear-gradient( | |||||
45deg, | 45deg, | ||||
transparent, | transparent, | ||||
rgba(52, 211, 153, 0.1), /* Emerald */ | |||||
transparent | |||||
rgba(52, 211, 153, 0.1), | |||||
/* Emerald */ transparent | |||||
); | ); | ||||
} | } | ||||
.cta-section .section-block::before { | .cta-section .section-block::before { | ||||
background: linear-gradient( | |||||
background: linear-gradient( | |||||
45deg, | 45deg, | ||||
transparent, | transparent, | ||||
rgba(241, 252, 255, 0.021), /* Rose */ | |||||
transparent | |||||
rgba(241, 252, 255, 0.021), | |||||
/* Rose */ transparent | |||||
); | ); | ||||
} | } | ||||
</style> | |||||
</style> |
content: productContent.value, | content: productContent.value, | ||||
tag: meta.tag || "", | tag: meta.tag || "", | ||||
series: Array.isArray(meta.series) ? meta.series : [], | series: Array.isArray(meta.series) ? meta.series : [], | ||||
sn: meta.sn || "", | |||||
meta: { | meta: { | ||||
series: Array.isArray(meta.series) ? meta.series : [], | series: Array.isArray(meta.series) ? meta.series : [], | ||||
name: String(meta.name || ""), | name: String(meta.name || ""), | ||||
image: String(meta.image || ""), | image: String(meta.image || ""), | ||||
summary: String(meta.summary || ""), | summary: String(meta.summary || ""), | ||||
audiences: categoryContent.value?.meta?.audiences || 0, | audiences: categoryContent.value?.meta?.audiences || 0, | ||||
sn: meta.sn || "", | |||||
}, | }, | ||||
}; | }; | ||||
}); | }); | ||||
const content = await queryCollection("content") | const content = await queryCollection("content") | ||||
.where("path", "LIKE", `/products/${locale.value}/%`) | .where("path", "LIKE", `/products/${locale.value}/%`) | ||||
.all(); | .all(); | ||||
return content; | |||||
console.log(content); | |||||
const relatedProducts = content.filter((item: any) => { | |||||
const meta = item.meta || {}; | |||||
return meta.sn === product.value.meta?.sn; | |||||
}); | |||||
return relatedProducts; | |||||
} catch (err) { | } catch (err) { | ||||
console.error("Error fetching related products:", err); | console.error("Error fetching related products:", err); | ||||
return []; | return []; |