Recruitment_site/docs/superpowers/specs/2026-03-25-company-jobs-thr...

153 lines
7.1 KiB
Markdown
Raw Permalink 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.

# 公司列表三栏布局设计文档
**日期:** 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 字,为空则不显示)
- **不显示在招岗位数**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`
### 右栏 — 岗位详情
**右栏渲染优先级(严格按顺序):**
1. `detailLoading === true` → 显示 `el-skeleton`
2. `detailError === true` → 显示「加载失败,请刷新重试」
3. `selectedJob !== null` → 显示岗位详情
4. 以上均不满足(`selectedJob === null` 且无加载/错误)→ 显示空状态
**各状态内容:**
- **空状态**:居中空状态图标 + 「← 请选择左侧职位查看详情」
- **岗位详情**
- 顶部:公司首字 Logo + 岗位名称(`job.title`+ 公司/城市副标题 + 「立即申请」按钮
- 标签行:`job.location`、`job.salary`、`job.category`(无经验字段,不显示)
- 正文:`job.description`(用 `white-space: pre-wrap` 保留换行)
- **加载状态**`el-skeleton`5 行,含头部和正文)
- **错误状态**:「加载失败,请刷新重试」居中提示
- **「立即申请」按钮行为**
- 已登录seeker`router.push({ name: 'JobDetail', params: { id: job.id } })`(跳转现有详情页完成申请)
- 未登录:`router.push({ name: 'Login', query: { redirect: '/jobs/' + job.id } })`
---
## 数据结构
`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>` | 右栏错误状态 |
---
## 交互流程
1. `onMounted` → 设置 `orgsLoading = true`,调用 `getOrganizations()`
- 成功且列表非空:`orgsLoading = false`,渲染左栏,自动调用 `selectOrg(orgs.value[0])`
- 成功但列表为空:`orgsLoading = false`,左栏显示「暂无公司数据」
- 失败:`orgsLoading = false``orgsError = true`,左栏显示「加载失败,请刷新重试」
2. `selectOrg(org)`
- 重置 `jobsError = false`,清空 `selectedJob``jobs = []`
- 设置 `selectedOrg = org``jobsLoading = true`
- 调用 `getJobs({ organization: org.id })`,取 `data.results` 更新 `jobs`
- 完成后 `jobsLoading = false`;失败则 `jobsError = true`
3. `selectJob(job)`
- 重置 `detailError = false`
- 设置 `detailLoading = true``selectedJob = null`
- 调用 `getJob(job.id)` 更新 `selectedJob`
- 完成后 `detailLoading = false`;失败则 `detailError = true`
4. 「立即申请」按钮点击 →
-`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 无此字段)