feat: add public portal pages (home, job list, job detail, companies)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
TianyangZhang 2026-03-25 08:42:37 +08:00
parent 734c83b7fe
commit 564fb06e3b
7 changed files with 223 additions and 7 deletions

View File

@ -1,3 +1,10 @@
<template> <template>
<div>CompanyCard - 开发中</div> <el-card shadow="hover" @click="$router.push(`/companies/${org.id}`)" style="cursor:pointer;margin-bottom:16px">
<el-avatar :src="org.logo" size="large" />
<div style="margin-top:8px;font-weight:600">{{ org.name }}</div>
<div style="color:#666;font-size:13px">{{ org.description?.slice(0,60) }}</div>
</el-card>
</template> </template>
<script setup>
defineProps({ org: Object })
</script>

View File

@ -1,3 +1,22 @@
<template> <template>
<div>JobCard - 开发中</div> <el-card class="job-card" shadow="hover" @click="$router.push(`/jobs/${job.id}`)">
<div class="job-title">{{ job.title }}</div>
<div class="job-meta">
<span>{{ job.company_name || job.organization_name }}</span>
<el-divider direction="vertical" />
<span>{{ job.location }}</span>
<el-divider direction="vertical" />
<span class="salary">{{ job.salary }}</span>
</div>
<el-tag size="small">{{ job.category }}</el-tag>
</el-card>
</template> </template>
<script setup>
defineProps({ job: Object })
</script>
<style scoped>
.job-card { cursor: pointer; margin-bottom: 12px; }
.job-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
.job-meta { color: #666; font-size: 13px; margin-bottom: 8px; }
.salary { color: #f56c6c; font-weight: 500; }
</style>

View File

@ -1,3 +1,31 @@
<template> <template>
<div>公司详情 - 开发中</div> <div v-if="org" style="max-width:900px;margin:0 auto">
<el-card>
<template #header><h2>{{ org.name }}</h2></template>
<p>{{ org.description }}</p>
<p>联系邮箱{{ org.email }}</p>
</el-card>
<h3 style="margin-top:24px">在招职位</h3>
<JobCard v-for="job in jobs" :key="job.id" :job="job" />
<el-empty v-if="jobs.length === 0" description="暂无在招职位" />
</div>
</template> </template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { getOrganization } from '@/api/organizations'
import { getJobs } from '@/api/jobs'
import JobCard from '@/components/JobCard.vue'
const route = useRoute()
const org = ref(null)
const jobs = ref([])
onMounted(async () => {
const [orgRes, jobRes] = await Promise.all([
getOrganization(route.params.id),
getJobs({ organization: route.params.id })
])
org.value = orgRes.data
jobs.value = jobRes.data.results
})
</script>

View File

@ -1,3 +1,20 @@
<template> <template>
<div>公司列表 - 开发中</div> <div style="max-width:1000px;margin:0 auto">
<h2>公司列表</h2>
<el-row :gutter="16">
<el-col v-for="org in orgs" :key="org.id" :span="8">
<CompanyCard :org="org" />
</el-col>
</el-row>
</div>
</template> </template>
<script setup>
import { ref, onMounted } from 'vue'
import CompanyCard from '@/components/CompanyCard.vue'
import { getOrganizations } from '@/api/organizations'
const orgs = ref([])
onMounted(async () => {
const { data } = await getOrganizations()
orgs.value = data.results
})
</script>

View File

@ -1,3 +1,37 @@
<template> <template>
<div>首页 - 开发中</div> <div class="home">
<div class="hero">
<h1>发现你的理想职位</h1>
<el-input v-model="keyword" placeholder="搜索职位..." size="large" @keyup.enter="search">
<template #append><el-button @click="search">搜索</el-button></template>
</el-input>
</div>
<div class="latest-jobs">
<h3>最新职位</h3>
<JobCard v-for="job in latestJobs" :key="job.id" :job="job" />
</div>
</div>
</template> </template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import JobCard from '@/components/JobCard.vue'
import { getJobs } from '@/api/jobs'
const router = useRouter()
const keyword = ref('')
const latestJobs = ref([])
onMounted(async () => {
const { data } = await getJobs({ page: 1 })
latestJobs.value = data.results.slice(0, 6)
})
const search = () => router.push({ name: 'JobList', query: { search: keyword.value } })
</script>
<style scoped>
.hero { text-align: center; padding: 60px 20px; background: linear-gradient(135deg, #409eff22, #fff); }
.hero h1 { font-size: 36px; margin-bottom: 24px; }
.el-input { max-width: 500px; }
.latest-jobs { max-width: 900px; margin: 40px auto; }
</style>

View File

@ -1,3 +1,65 @@
<template> <template>
<div>职位详情 - 开发中</div> <el-row :gutter="24" v-loading="loading" style="max-width:1000px;margin:0 auto">
<el-col :span="16">
<el-card v-if="job">
<template #header>
<h2>{{ job.title }}</h2>
<div class="meta">
{{ job.organization?.name }} · {{ job.location }} · {{ job.salary }}
</div>
</template>
<div v-html="job.description" class="description"></div>
</el-card>
</el-col>
<el-col :span="8">
<el-card>
<el-button type="primary" size="large" style="width:100%" @click="handleApply">
立即投递
</el-button>
<p class="hint" v-if="!auth.isLoggedIn">登录后才能投递</p>
<p class="hint success" v-if="applied">已投递可在"我的投递"查看进度</p>
</el-card>
</el-col>
</el-row>
</template> </template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getJob } from '@/api/jobs'
import { applyJob } from '@/api/applications'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const job = ref(null)
const loading = ref(false)
const applied = ref(false)
onMounted(async () => {
loading.value = true
const { data } = await getJob(route.params.id)
job.value = data
loading.value = false
})
async function handleApply() {
if (!auth.isLoggedIn) return router.push({ name: 'Login', query: { redirect: route.fullPath } })
if (!auth.isSeeker) return ElMessage.warning('管理员账号无法投递职位')
try {
await applyJob(job.value.id)
applied.value = true
ElMessage.success('投递成功!')
} catch (e) {
if (e.response?.status === 400) ElMessage.warning('您已投递过该职位')
else ElMessage.error('投递失败,请先完善简历')
}
}
</script>
<style scoped>
.meta { color: #666; margin-top: 8px; }
.description { line-height: 1.8; }
.hint { color: #999; font-size: 13px; margin-top: 12px; }
.success { color: #67c23a; }
</style>

View File

@ -1,3 +1,52 @@
<template> <template>
<div>职位列表 - 开发中</div> <div class="job-list-page">
<el-card class="search-bar">
<el-form :model="filters" inline>
<el-form-item><el-input v-model="filters.search" placeholder="职位名称/关键词" clearable @change="fetchJobs" /></el-form-item>
<el-form-item><el-input v-model="filters.location" placeholder="城市" clearable @change="fetchJobs" /></el-form-item>
<el-form-item><el-input v-model="filters.category" placeholder="职位类别" clearable @change="fetchJobs" /></el-form-item>
<el-form-item><el-button type="primary" @click="fetchJobs">搜索</el-button></el-form-item>
</el-form>
</el-card>
<div v-loading="loading" class="job-results">
<div class="result-count"> {{ total }} 个职位</div>
<JobCard v-for="job in jobs" :key="job.id" :job="job" />
<el-pagination
v-if="total > pageSize"
layout="prev, pager, next"
:total="total"
:page-size="pageSize"
v-model:current-page="page"
@current-change="fetchJobs"
/>
</div>
</div>
</template> </template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import JobCard from '@/components/JobCard.vue'
import { getJobs } from '@/api/jobs'
const jobs = ref([])
const total = ref(0)
const loading = ref(false)
const page = ref(1)
const pageSize = 20
const filters = reactive({ search: '', location: '', category: '' })
async function fetchJobs() {
loading.value = true
const { data } = await getJobs({ ...filters, page: page.value })
jobs.value = data.results
total.value = data.count
loading.value = false
}
onMounted(fetchJobs)
</script>
<style scoped>
.job-list-page { max-width: 900px; margin: 0 auto; }
.search-bar { margin-bottom: 20px; }
.result-count { color: #666; margin-bottom: 12px; }
</style>