7.1 KiB
公司列表三栏布局设计文档
日期: 2026-03-25
状态: 已批准
影响页面: /companies → CompanyListView.vue
目标
将现有 /companies 页面从网格卡片布局改造为三栏联动布局,用户可在同一页面浏览公司、查看岗位列表、阅读岗位详情,无需跳转。
布局结构
┌─────────────────────────────────────────────────────┐
│ 左栏 220px │ 中栏 260px │ 右栏 flex:1 │
│ 公司列表 │ 岗位列表 │ 岗位详情 │
└─────────────────────────────────────────────────────┘
高度设置: PortalLayout 包含 el-header(60px)+ el-main(默认 padding 20px)+ el-footer(60px)。三栏容器根元素设置 height: calc(100vh - 200px)(60 header + 60 footer + 40 el-main padding + 40 buffer),overflow: hidden,各栏内部 overflow-y: auto 独立滚动。此高度值在主流桌面分辨率下提供良好体验,无需精确到像素。
各栏设计
左栏 — 公司列表
- 顶部标题「全部企业」+ 公司总数 badge
- 每个公司卡片包含:
- Logo:如有
org.logo,显示<img>;否则显示公司名首字 + 蓝色背景 div - 公司名称(
org.name) - 简介摘要(
org.description截取前 20 字,为空则不显示)
- Logo:如有
- 不显示在招岗位数(Organization API 无此字段)
- 选中状态:左边框
3px solid #409EFF,背景白色 - 未选中:左边框透明,背景
#f5f7fa - 加载状态:
orgsLoading为 true 时显示el-skeleton(3 行) - 错误状态:
orgsError为 true 时显示「加载失败,请刷新重试」居中提示 - 空状态:
orgs为空数组时显示「暂无公司数据」居中提示 - 数据来源:
getOrganizations()→GET /organizations/public/,取data.results
中栏 — 岗位列表
- 标题:「{org.name} · 职位列表」
- 每条岗位显示:
- 岗位名称(
job.title) - 标签行:
job.location(城市)、job.salary(薪资)、job.category(类别)
- 岗位名称(
- 选中状态:左边框
3px solid #409EFF,背景#ecf5ff - 空状态:该公司无已发布岗位时,显示「暂无在招职位」居中提示
- 加载状态:
jobsLoading为 true 时显示el-skeleton(3 行) - 错误状态:API 失败时显示「加载失败,请刷新重试」
- 数据来源:
getJobs({ organization: org.id })→ 取data.results
右栏 — 岗位详情
右栏渲染优先级(严格按顺序):
detailLoading === true→ 显示el-skeletondetailError === true→ 显示「加载失败,请刷新重试」selectedJob !== null→ 显示岗位详情- 以上均不满足(
selectedJob === null且无加载/错误)→ 显示空状态
各状态内容:
- 空状态:居中空状态图标 + 「← 请选择左侧职位查看详情」
- 岗位详情:
- 顶部:公司首字 Logo + 岗位名称(
job.title)+ 公司/城市副标题 + 「立即申请」按钮 - 标签行:
job.location、job.salary、job.category(无经验字段,不显示) - 正文:
job.description(用white-space: pre-wrap保留换行)
- 顶部:公司首字 Logo + 岗位名称(
- 加载状态:
el-skeleton(5 行,含头部和正文) - 错误状态:「加载失败,请刷新重试」居中提示
- 「立即申请」按钮行为:
- 已登录(seeker):
router.push({ name: 'JobDetail', params: { id: job.id } })(跳转现有详情页完成申请) - 未登录:
router.push({ name: 'Login', query: { redirect: '/jobs/' + job.id } })
- 已登录(seeker):
数据结构
getOrganizations() 返回的 Organization 对象字段:id, name, logo (string|null), description, email, is_active
getJobs() 使用 JobListSerializer,返回字段:id, title, category, location, salary, organization (id only), organization_name, status, created_at。不含 description。
getJob(id) 使用 JobDetailSerializer,返回字段:id, title, category, location, salary, description, organization: {id, name, logo, ...}, status, created_at。含完整 description,因此点击岗位时必须调用此接口获取详情。
/jobs/public/ 后端已在 queryset 层过滤 status='published',无需前端额外传 status 参数。
Logo URL 处理:org.logo 为相对路径(如 /media/org_logos/foo.png),需拼接后端地址。通过 Vite proxy,开发环境直接使用原始路径即可(proxy 会转发到 http://127.0.0.1:8000)。渲染时用 org.logo ? org.logo : null 判断是否显示图片。
状态管理
组件内使用以下 ref:
| 变量 | 类型 | 说明 |
|---|---|---|
orgs |
Ref<Organization[]> |
所有公司列表 |
orgsLoading |
Ref<boolean> |
左栏加载状态 |
orgsError |
Ref<boolean> |
左栏错误状态 |
selectedOrg |
Ref<Organization | null> |
当前选中公司 |
jobs |
Ref<Job[]> |
当前公司的岗位列表 |
selectedJob |
Ref<Job | null> |
当前选中岗位(getJob 返回的完整 Job 对象) |
jobsLoading |
Ref<boolean> |
中栏加载状态 |
detailLoading |
Ref<boolean> |
右栏加载状态 |
jobsError |
Ref<boolean> |
中栏错误状态 |
detailError |
Ref<boolean> |
右栏错误状态 |
交互流程
-
onMounted→ 设置orgsLoading = true,调用getOrganizations()- 成功且列表非空:
orgsLoading = false,渲染左栏,自动调用selectOrg(orgs.value[0]) - 成功但列表为空:
orgsLoading = false,左栏显示「暂无公司数据」 - 失败:
orgsLoading = false,orgsError = true,左栏显示「加载失败,请刷新重试」
- 成功且列表非空:
-
selectOrg(org)→- 重置
jobsError = false,清空selectedJob,jobs = [] - 设置
selectedOrg = org,jobsLoading = true - 调用
getJobs({ organization: org.id }),取data.results更新jobs - 完成后
jobsLoading = false;失败则jobsError = true
- 重置
-
selectJob(job)→- 重置
detailError = false - 设置
detailLoading = true,selectedJob = null - 调用
getJob(job.id)更新selectedJob - 完成后
detailLoading = false;失败则detailError = true
- 重置
-
「立即申请」按钮点击 →
- 若
authStore.isSeeker:router.push({ name: 'JobDetail', params: { id: selectedJob.id } }) - 否则(未登录或非 seeker 角色):
router.push({ name: 'Login', query: { redirect: '/jobs/' + selectedJob.id } })
- 若
文件变更范围
| 文件 | 变更类型 |
|---|---|
offer_frontend/src/views/portal/CompanyListView.vue |
完全重写 |
无需改动后端、无需新增路由、无需新增组件。
不在范围内
- 搜索/筛选公司或岗位
- 分页(公司数量有限,一次性加载)
- CompanyDetailView(
/companies/:id)不变 - 在招岗位数统计(Organization API 无此字段)