瀏覽代碼

feat(招聘): 更新职位卡片和招聘页面功能

- 在职位集合中新增sn字段以支持产品序列号
- 优化JobCard组件,移除不必要的注释代码,提升代码整洁性
- 新增职位详情页面,展示职位信息和申请方式
- 更新招聘页面样式,增强用户体验和响应式设计
master
lizhuang 1 天之前
父節點
當前提交
ff512bf254

+ 28
- 153
components/JobCard.vue 查看文件

@@ -1,7 +1,6 @@
<template>
<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">
@@ -20,12 +19,6 @@
</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="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">
<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>
@@ -83,72 +76,20 @@
<div class="text-xs text-zinc-500">
{{ formatDate(job.updateTime) }} {{ t('careers.PositionCard.update') }}
</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"
@click.stop="handleApply"
:to="`${homepagePath}/careers/${job.id}`"
>
<span class="mr-2">{{ t('careers.PositionCard.JobDetails') }}</span>
<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>
</svg>
</button>
</nuxt-link>
</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>
</template>

@@ -157,7 +98,11 @@ import type { Job } from '~/data/jobs'
import { locationMap, workTypeMap } from '~/data/jobs'
import { useI18n } from 'vue-i18n'

const { t } = useI18n()
const { t, locale } = useI18n()

const homepagePath = computed(() => {
return locale.value === "zh" ? "" : `/${locale.value}`;
});

/**
* 组件属性
@@ -168,13 +113,6 @@ interface Props {

const props = defineProps<Props>()

/**
* 组件事件
*/
const emit = defineEmits<{
click: [job: Job]
}>()

/**
* 获取地点名称
* @param location 地点编码
@@ -184,16 +122,6 @@ const getLocationName = (location: number): string => {
return locationMap[location] || 'Other'
}

/**
* 获取工作类型
* @param webSite 工作类型编码
* @returns 工作类型
*/
const getWorkType = (webSite: number): string => {
// 假设 1 代表全职,其他值可以扩展
return workTypeMap[webSite] || t('careers.workType.fullTime')
}

/**
* 格式化日期
* @param dateString 日期字符串
@@ -214,6 +142,16 @@ const formatDate = (dateString: string | undefined): string => {
}
}

/**
* 移除字符串中的HTML标签
* @param htmlString 包含HTML的字符串
* @returns 纯文本字符串
*/
const stripHtml = (htmlString: string): string => {
if (!htmlString) return ''
return htmlString.replace(/<\/?[^>]+(>|$)/g, "")
}

/**
* 格式化工作描述
* @param description 工作描述
@@ -222,12 +160,13 @@ const formatDate = (dateString: string | undefined): string => {
const formatJobDescription = (description: string): string => {
if (!description) return ''
const plainText = stripHtml(description)
// 将数字开头的项目转换为更易读的格式
return description
return plainText
.replace(/(\d+\.)/g, '\n$1')
.replace(/;/g, ';\n')
.trim()
.substring(0, 200) + (description.length > 200 ? '...' : '')
.substring(0, 200) + (plainText.length > 200 ? '...' : '')
}

/**
@@ -238,10 +177,11 @@ const formatJobDescription = (description: string): string => {
const formatBenefits = (benefits: string): string => {
if (!benefits) return ''
return benefits
const plainText = stripHtml(benefits)
return plainText
.replace(/(\d+\.)/g, '\n$1')
.trim()
.substring(0, 100) + (benefits.length > 100 ? '...' : '')
.substring(0, 100) + (plainText.length > 100 ? '...' : '')
}

/**
@@ -252,80 +192,15 @@ const formatBenefits = (benefits: string): string => {
const formatWorkTime = (workTime: string): string => {
if (!workTime) return ''
return workTime
const plainText = stripHtml(workTime)
return plainText
.replace(/\\n/g, '\n')
.replace(/(\d+\.)/g, '\n$1')
.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>

<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 {
opacity: 0;

+ 1
- 0
content.config.ts 查看文件

@@ -39,6 +39,7 @@ const productCollection = defineCollection({
sort: z.number(),
recommend: z.boolean(),
recommendIndex: z.number(),
sn: z.string(),
body: z.object({
value: z.string(),
}),

+ 1
- 0
content/products/zh/103.SD-XC128GBV6.md 查看文件

@@ -13,6 +13,7 @@ gallery: [
tag: "SD"
summary: 适用于数码相机、摄像机,读取速度高达280MB/秒
capacities: ["128GB"]
sn: SD-XC0000V6
sort: 99
---


+ 1
- 0
content/products/zh/104.SD-XC256GBV6.md 查看文件

@@ -13,6 +13,7 @@ gallery: [
tag: "SD"
summary: 适用于数码相机、摄像机,读取速度高达280MB/秒
capacities: ["256GB"]
sn: SD-XC0000V6
sort: 99
---


+ 1
- 0
content/products/zh/105.SD-XC512GBV6.md 查看文件

@@ -13,6 +13,7 @@ gallery: [
tag: "SD"
summary: 适用于数码相机、摄像机,读取速度高达280MB/秒
capacities: ["512GB"]
sn: SD-XC0000V6
sort: 99
---


+ 1
- 0
content/products/zh/106.SD-XC64GBV3.md 查看文件

@@ -13,6 +13,7 @@ gallery: [
tag: "SD"
summary: 适用于数码相机、摄像机,读取速度高达100MB/秒
capacities: ["64GB"]
sn: SD-XC0000V3
sort: 99
---
# 产品特征

+ 1
- 0
content/products/zh/107.SD-XC128GBV3.md 查看文件

@@ -13,6 +13,7 @@ gallery: [
tag: "SD"
summary: 适用于数码相机、摄像机,读取速度高达100MB/秒
capacities: ["128GB"]
sn: SD-XC0000V3
sort: 99
---


+ 1
- 0
content/products/zh/108.SD-XC256GBV3.md 查看文件

@@ -13,6 +13,7 @@ gallery: [
tag: "SD"
summary: 适用于数码相机、摄像机,读取速度高达100MB/秒
capacities: ["256GB"]
sn: SD-XC0000V3
sort: 99
---


+ 10
- 1
data/jobs.ts 查看文件

@@ -23,7 +23,7 @@ export interface Job {
}

/**
* API 响应接口
* API 列表响应接口
*/
export interface JobsApiResponse {
code: number
@@ -32,6 +32,15 @@ export interface JobsApiResponse {
rows: Job[]
}

/**
* API 单个职位响应接口
*/
export interface JobApiResponse {
code: number
msg: string
data: Job
}

/**
* 地点映射表
*/

+ 3
- 1
i18n/locales/en.ts 查看文件

@@ -451,6 +451,7 @@ export default {
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.",
slogan:"Join Us, Create the Future Together",
CompanyHistory: "Years of Corporate History",
TeamMembers: "Team Members",
ProductModel: "Product Model",
@@ -497,7 +498,8 @@ export default {
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",
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: {
title: "Didn't find a suitable position?",

+ 3
- 1
i18n/locales/ja.ts 查看文件

@@ -442,6 +442,7 @@ export default {
careers: {
title: "採用情報",
subtitle: "私たちと共に、ストレージ技術の未来を創造しませんか。技術革新を推進するため、情熱ある人材を募集しています。",
slogan:"優秀な人材と共に、未来を創造する",
CompanyHistory: "年の企業沿革",
TeamMembers: "チームメンバー",
ProductModel: "製品モデル",
@@ -488,7 +489,8 @@ export default {
description: "私たちは優秀な人材をチームに迎えることを楽しみにしています。以下の職種をご覧になり、あなたに合った機会を見つけてください。",
button: "応募する",
noPositions: "現在募集中の職種はありません",
checkLater: "しばらくしてから再度ご確認いただくか、最新の求人情報についてお問い合わせください。"
checkLater: "しばらくしてから再度ご確認いただくか、最新の求人情報についてお問い合わせください。",
sendTip: "あなたの履歴書と求職書を上記のメールアドレスに送信してください。お問い合わせください。"
},
cta: {
title: "希望の職種が見つからない場合",

+ 3
- 3
i18n/locales/zh.ts 查看文件

@@ -439,6 +439,7 @@ export default {
careers: {
title: "招聘信息",
subtitle: "加入我们,共同创造存储技术的未来。我们寻找有激情的人才,与我们一起推动技术创新。",
slogan:"与优秀同行,共创未来",
CompanyHistory:"年企业历程",
TeamMembers:"团队成员",
ProductModel:"产品型号",
@@ -447,13 +448,11 @@ export default {
sectionTitle: "人才招聘",
CurrentlyOpen:"当前开放",
Aposition:"个职位",
// --- 新增 ---
workType: {
fullTime: "全职",
partTime: "兼职",
internship: "实习",
},
// --- 新增结束 ---
culture: {
sectionTitle: "企业文化",
title: "我们的价值观",
@@ -487,7 +486,8 @@ export default {
description: "我们正在寻找优秀的人才加入我们的团队。浏览下方职位,找到适合您的机会。",
button: "申请职位",
noPositions: "暂无招聘职位",
checkLater: "请稍后查看或联系我们了解最新职位信息"
checkLater: "请稍后查看或联系我们了解最新职位信息",
sendTip: "请将您的简历和求职信发送至上述邮箱地址,我们会尽快与您联系。"
},
cta: {
title: "没有找到合适的职位?",

+ 367
- 0
pages/careers/[id].vue 查看文件

@@ -0,0 +1,367 @@
<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>

pages/careers.vue → pages/careers/index.vue 查看文件

@@ -4,32 +4,33 @@
<!-- 顶部间距 -->
<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">
<!-- 页面标题区域 -->
<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">
<img
src="/assets/images/careersbg.webp"
alt="Background"
class="w-full h-full object-cover opacity-10"
class="w-full h-full object-cover opacity-20"
/>
</div>

@@ -38,63 +39,21 @@
>
<!-- 标题 -->
<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
class="text-white text-3xl sm:text-4xl md:text-6xl font-bold tracking-wider leading-tight"
>
<span
class="bg-gradient-to-r from-white to-zinc-300 bg-clip-text text-transparent"
>
{{ t("careers.title") }}
{{ t("careers.slogan") }}
</span>
</h1>

<div class="w-20 h-px bg-[#35F1FF] mx-auto"></div>

<p
class="text-zinc-300/80 text-base md:text-xl max-w-4xl mx-auto leading-relaxed"
>
{{ t("careers.subtitle") }}
</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>
@@ -102,22 +61,11 @@

<!-- 企业文化区域 -->
<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
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="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
class="text-white text-2xl md:text-4xl font-bold mb-6 tracking-wider"
>
@@ -168,7 +116,7 @@
<!-- 团队协作 -->
<div class="culture-card group">
<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
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"
@@ -206,7 +154,7 @@
<!-- 品质第一 -->
<div class="culture-card group">
<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
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"
@@ -244,7 +192,7 @@
<!-- 共同成长 -->
<div class="culture-card group">
<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
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"
@@ -285,9 +233,7 @@

<!-- 职位列表区域 -->
<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 -->
<div class="absolute inset-0 opacity-5">
<div
@@ -299,41 +245,12 @@
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="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
class="text-white text-2xl md:text-4xl font-bold mb-6 tracking-wider"
>
{{ t("careers.jobs.title") }}
</h3>
<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>

<!-- 加载状态 -->
@@ -347,12 +264,7 @@
<div v-else-if="jobsList.length > 0">
<!-- 职位卡片网格 -->
<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>

@@ -390,12 +302,10 @@

<!-- 底部CTA区域 -->
<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"
class="absolute inset-0 w-full h-full object-cover opacity-40 z-0"
/>
@@ -408,7 +318,9 @@
>
{{ t("careers.cta.title") }}
</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") }}
</p>
<div class="pt-4">
@@ -419,7 +331,9 @@
<span class="text-sm md:text-base font-normal text-white">{{
t("careers.cta.button")
}}</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>
</div>
</div>
@@ -466,7 +380,7 @@ const API_BASE_URL = "https://digital.sohomall.jp/prod-api/system/workInfo";
*/
const fetchJobsFromApi = async (
pageNum: number = 1,
pageSize: number = 10
pageSize: number = 20
): Promise<JobsApiResponse> => {
const response = await $fetch<JobsApiResponse>(
`${API_BASE_URL}/noVerifyList`,
@@ -499,9 +413,9 @@ const initializeJobs = async () => {

console.log(apiResponse);

// 过滤掉已禁用的职位
// 过滤掉已禁用的职位 (isDisabled为"1"表示启用,"0"表示禁用)
const availableJobs = apiResponse.rows.filter(
(job: Job) => job.isDisabled !== "0"
(job: Job) => job.isDisabled === "1"
);

jobsList.value = availableJobs;
@@ -512,24 +426,21 @@ const initializeJobs = async () => {
console.warn("API获取失败,使用本地数据:", error);

// 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数据
console.log(`使用本地数据加载 ${availableLocalJobs.length} 个职位信息`);
} finally {
jobsLoading.value = false;
}
};

/**
* 处理职位点击事件
* @param job 职位信息
*/
const handleJobClick = (job: Job) => {
// 这里可以添加职位详情页面跳转或弹窗逻辑
console.log("Job clicked:", job);
};

/**
* 处理联系按钮点击
*/
@@ -648,7 +559,7 @@ onMounted(async () => {
.section-block {
position: relative;
transition: all 0.5s ease;
overflow: hidden;
overflow: hidden;
}

.section-block::before {
@@ -667,30 +578,29 @@ onMounted(async () => {

/* 为每个模块单独定义蒙层颜色 */
.culture-section .section-block::before {
background: linear-gradient(
background: linear-gradient(
45deg,
transparent,
rgba(99, 102, 241, 0.1), /* Indigo */
transparent
rgba(99, 102, 241, 0.1),
/* Indigo */ transparent
);
}

.jobs-section .section-block::before {
background: linear-gradient(
background: linear-gradient(
45deg,
transparent,
rgba(52, 211, 153, 0.1), /* Emerald */
transparent
rgba(52, 211, 153, 0.1),
/* Emerald */ transparent
);
}

.cta-section .section-block::before {
background: linear-gradient(
background: linear-gradient(
45deg,
transparent,
rgba(241, 252, 255, 0.021), /* Rose */
transparent
rgba(241, 252, 255, 0.021),
/* Rose */ transparent
);
}

</style>
</style>

+ 11
- 1
pages/products/[id].vue 查看文件

@@ -590,6 +590,7 @@ const product = computed<Product | any>(() => {
content: productContent.value,
tag: meta.tag || "",
series: Array.isArray(meta.series) ? meta.series : [],
sn: meta.sn || "",
meta: {
series: Array.isArray(meta.series) ? meta.series : [],
name: String(meta.name || ""),
@@ -597,6 +598,7 @@ const product = computed<Product | any>(() => {
image: String(meta.image || ""),
summary: String(meta.summary || ""),
audiences: categoryContent.value?.meta?.audiences || 0,
sn: meta.sn || "",
},
};
});
@@ -615,7 +617,15 @@ const { data: relatedProductsContent } = await useAsyncData(
const content = await queryCollection("content")
.where("path", "LIKE", `/products/${locale.value}/%`)
.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) {
console.error("Error fetching related products:", err);
return [];

Loading…
取消
儲存