|
|
@@ -1,4 +1,5 @@
|
|
|
import { getApiBaseUrl, getServerConfig, request } from '@/utils/auth'
|
|
|
+import { positionApplications } from '@/data/mock'
|
|
|
|
|
|
// ============ 模式判断 ============
|
|
|
function isServerMode() {
|
|
|
@@ -34,6 +35,20 @@ export const QUALIFICATION_STATUS_LABEL = {
|
|
|
UNQUALIFIED: '无人达标',
|
|
|
}
|
|
|
|
|
|
+// 岗位申请状态(与后端一致):草稿 / 审批中 / 已通过 / 已驳回
|
|
|
+export const POSITION_APPLICATION_STATUS = Object.freeze({
|
|
|
+ DRAFT: 'DRAFT',
|
|
|
+ PENDING: 'PENDING',
|
|
|
+ APPROVED: 'APPROVED',
|
|
|
+ REJECTED: 'REJECTED',
|
|
|
+})
|
|
|
+export const POSITION_APPLICATION_STATUS_LABEL = {
|
|
|
+ DRAFT: '草稿',
|
|
|
+ PENDING: '审批中',
|
|
|
+ APPROVED: '已通过',
|
|
|
+ REJECTED: '已驳回',
|
|
|
+}
|
|
|
+
|
|
|
// 排序方式(与后端 orderBy 枚举一致)
|
|
|
export const ORDER_BY = {
|
|
|
ASC: 'ascending',
|
|
|
@@ -56,9 +71,9 @@ const DEMO_POSITIONS = [
|
|
|
positionLevels: ['M1', 'M2', 'M3'],
|
|
|
positionType: 'MGMT',
|
|
|
positionTypeName: '管理',
|
|
|
- deptId: 101,
|
|
|
- deptName: '研发中心',
|
|
|
- organizationId: 1,
|
|
|
+ deptId: 104,
|
|
|
+ deptName: '产品研发部',
|
|
|
+ organizationId: 10,
|
|
|
organizationName: '中盈智能科技股份有限公司',
|
|
|
status: 1,
|
|
|
contractTemplateId: 1,
|
|
|
@@ -88,9 +103,9 @@ const DEMO_POSITIONS = [
|
|
|
positionLevels: ['P1', 'P2', 'P3'],
|
|
|
positionType: 'PROF',
|
|
|
positionTypeName: '专业',
|
|
|
- deptId: 101,
|
|
|
- deptName: '研发中心',
|
|
|
- organizationId: 1,
|
|
|
+ deptId: 201,
|
|
|
+ deptName: '前端组',
|
|
|
+ organizationId: 10,
|
|
|
organizationName: '中盈智能科技股份有限公司',
|
|
|
status: 1,
|
|
|
contractTemplateId: 1,
|
|
|
@@ -120,9 +135,9 @@ const DEMO_POSITIONS = [
|
|
|
positionLevels: ['T1', 'T2'],
|
|
|
positionType: 'TECH',
|
|
|
positionTypeName: '技术',
|
|
|
- deptId: 102,
|
|
|
- deptName: '生产运营部',
|
|
|
- organizationId: 1,
|
|
|
+ deptId: 206,
|
|
|
+ deptName: '设备管理部',
|
|
|
+ organizationId: 10,
|
|
|
organizationName: '中盈智能科技股份有限公司',
|
|
|
status: 1,
|
|
|
contractTemplateId: 1,
|
|
|
@@ -152,9 +167,9 @@ const DEMO_POSITIONS = [
|
|
|
positionLevels: ['O1', 'O2'],
|
|
|
positionType: 'OPS',
|
|
|
positionTypeName: '操作',
|
|
|
- deptId: 103,
|
|
|
- deptName: '生产车间',
|
|
|
- organizationId: 1,
|
|
|
+ deptId: 204,
|
|
|
+ deptName: '生产一部',
|
|
|
+ organizationId: 10,
|
|
|
organizationName: '中盈智能科技股份有限公司',
|
|
|
status: 0,
|
|
|
contractTemplateId: null,
|
|
|
@@ -184,9 +199,9 @@ const DEMO_POSITIONS = [
|
|
|
positionLevels: ['P1', 'P2'],
|
|
|
positionType: 'PROF',
|
|
|
positionTypeName: '专业',
|
|
|
- deptId: 104,
|
|
|
+ deptId: 102,
|
|
|
deptName: '人力资源部',
|
|
|
- organizationId: 1,
|
|
|
+ organizationId: 10,
|
|
|
organizationName: '中盈智能科技股份有限公司',
|
|
|
status: 2,
|
|
|
contractTemplateId: 1,
|
|
|
@@ -207,6 +222,38 @@ const DEMO_POSITIONS = [
|
|
|
createUserId: 1,
|
|
|
lastChangeTime: '2026-09-01T14:00:00',
|
|
|
},
|
|
|
+ {
|
|
|
+ id: 6,
|
|
|
+ positionCode: 'P-PRO-103',
|
|
|
+ positionName: '人力资源专员',
|
|
|
+ positionSequence: '专业',
|
|
|
+ positionLevel: 'P1',
|
|
|
+ positionLevels: ['P1', 'P2'],
|
|
|
+ positionType: 'PROF',
|
|
|
+ positionTypeName: '专业',
|
|
|
+ deptId: 102,
|
|
|
+ deptName: '人力资源部',
|
|
|
+ organizationId: 10,
|
|
|
+ organizationName: '中盈智能科技股份有限公司',
|
|
|
+ status: 1,
|
|
|
+ contractTemplateId: 1,
|
|
|
+ establishmentCount: 3,
|
|
|
+ occupancyCount: 1,
|
|
|
+ plannedRecruitmentCount: 2,
|
|
|
+ occupancyRate: 33.33,
|
|
|
+ salaryMin: 7000,
|
|
|
+ salaryMax: 11000,
|
|
|
+ requiredCertificateCount: 0,
|
|
|
+ qualifiedEmployeeCount: 1,
|
|
|
+ evaluatedEmployeeCount: 1,
|
|
|
+ qualificationRate: 100,
|
|
|
+ qualificationStatus: 'UNCONFIGURED',
|
|
|
+ sourceApplicationId: 9003,
|
|
|
+ revokeDate: null,
|
|
|
+ createTime: '2026-06-02T09:10:00',
|
|
|
+ createUserId: 1,
|
|
|
+ lastChangeTime: '2026-09-06T10:20:00',
|
|
|
+ },
|
|
|
]
|
|
|
|
|
|
function matchDemoPosition(query) {
|
|
|
@@ -292,10 +339,10 @@ export async function pagePosition(query = {}) {
|
|
|
orderBy: query.orderBy || ORDER_BY.ASC,
|
|
|
}
|
|
|
if (query.deptId !== undefined && query.deptId !== null && query.deptId !== '') {
|
|
|
- body.deptId = Number(query.deptId) || 0
|
|
|
+ body.deptId = String(query.deptId || 0)
|
|
|
}
|
|
|
if (query.organizationId !== undefined && query.organizationId !== null && query.organizationId !== '') {
|
|
|
- body.organizationId = Number(query.organizationId) || 0
|
|
|
+ body.organizationId = String(query.organizationId) || 0
|
|
|
}
|
|
|
if (query.keyword !== undefined && query.keyword !== null) {
|
|
|
body.keyword = String(query.keyword)
|
|
|
@@ -341,6 +388,172 @@ export async function pagePosition(query = {}) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/** 只保留正整数主键,避免把空值/非法值当条件发给后端 */
|
|
|
+function positiveId(value) {
|
|
|
+ const num = Number(value)
|
|
|
+ return Number.isFinite(num) && num > 0 ? num : undefined
|
|
|
+}
|
|
|
+
|
|
|
+/** 岗位申请列表项 → 页面形状(只保留选岗需要的字段) */
|
|
|
+export function adaptPositionApplication(raw = {}) {
|
|
|
+ return {
|
|
|
+ id: raw.id ?? raw.applicationId ?? '',
|
|
|
+ positionName: raw.positionName || '',
|
|
|
+ deptId: raw.deptId ?? '',
|
|
|
+ deptName: raw.deptName || '',
|
|
|
+ organizationId: raw.organizationId ?? '',
|
|
|
+ status: String(raw.status || '').toUpperCase(),
|
|
|
+ effectivePositionId: raw.effectivePositionId ?? '',
|
|
|
+ positionSequence: raw.positionSequence || '',
|
|
|
+ positionLevel: raw.positionLevel || '',
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 岗位申请分页查询
|
|
|
+ * POST /hr/position/apply/page
|
|
|
+ *
|
|
|
+ * 请求参数(与后端约定,仅传非空项):
|
|
|
+ * - deptId 所属部门主键
|
|
|
+ * - organizationId 所属机构主键
|
|
|
+ * - keyword 岗位编码 / 岗位名称 / 申请人姓名
|
|
|
+ * - status 申请状态:DRAFT | PENDING | APPROVED | REJECTED
|
|
|
+ */
|
|
|
+export async function pagePositionApplication(query = {}) {
|
|
|
+ const body = {
|
|
|
+ pageNum: Number(query.pageNum) || 1,
|
|
|
+ size: Number(query.size) || 20,
|
|
|
+ sortName: query.sortName || DEFAULT_SORT_NAME,
|
|
|
+ orderBy: query.orderBy || ORDER_BY.ASC,
|
|
|
+ }
|
|
|
+ const deptId = positiveId(query.deptId)
|
|
|
+ if (deptId) body.deptId = String(deptId)
|
|
|
+ const organizationId = positiveId(query.organizationId)
|
|
|
+ if (organizationId) body.organizationId = organizationId
|
|
|
+ if (query.status) body.status = String(query.status).toUpperCase()
|
|
|
+ if (query.keyword !== undefined && query.keyword !== null && query.keyword !== '') {
|
|
|
+ body.keyword = String(query.keyword)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isServerMode()) {
|
|
|
+ await new Promise((r) => setTimeout(r, 160))
|
|
|
+ const rows = positionApplications
|
|
|
+ .filter((item) => !body.deptId || Number(item.deptId) === Number(body.deptId))
|
|
|
+ .filter((item) => !body.organizationId || Number(item.organizationId) === Number(body.organizationId))
|
|
|
+ .filter((item) => !body.status || String(item.status).toUpperCase() === body.status)
|
|
|
+ .map(adaptPositionApplication)
|
|
|
+ return {
|
|
|
+ count: rows.length,
|
|
|
+ list: paginate(rows, body.pageNum, body.size),
|
|
|
+ pageNum: body.pageNum,
|
|
|
+ size: body.size,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await request({
|
|
|
+ url: `${serverPath()}/hr/position/apply/page`,
|
|
|
+ method: 'POST',
|
|
|
+ data: body,
|
|
|
+ })
|
|
|
+ return res.data || { count: 0, list: [], pageNum: body.pageNum, size: body.size }
|
|
|
+}
|
|
|
+
|
|
|
+/** 岗位基本信息 → 选岗选项(页面只依赖这几个字段) */
|
|
|
+function positionOption(raw = {}) {
|
|
|
+ return {
|
|
|
+ id: String(raw.id ?? raw.positionId ?? ''),
|
|
|
+ name: raw.positionName || raw.name || '',
|
|
|
+ code: raw.positionCode || '',
|
|
|
+ deptId: raw.deptId ?? '',
|
|
|
+ deptName: raw.deptName || '',
|
|
|
+ level: raw.positionLevel || '',
|
|
|
+ levels: Array.isArray(raw.positionLevels) ? raw.positionLevels : [],
|
|
|
+ sequence: raw.positionSequence || '',
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 演示模式下,该部门「已通过岗位申请」沉淀出的正式岗位主键集合 */
|
|
|
+function demoApprovedPositionIds(deptId) {
|
|
|
+ return new Set(
|
|
|
+ positionApplications
|
|
|
+ .filter((item) => String(item.status).toUpperCase() === POSITION_APPLICATION_STATUS.APPROVED)
|
|
|
+ .filter((item) => Number(item.deptId) === Number(deptId))
|
|
|
+ .map((item) => String(item.effectivePositionId ?? ''))
|
|
|
+ .filter(Boolean),
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 入职可选岗位:口径与 PC 端「员工档案」选岗一致。
|
|
|
+ * 1) 未选部门时返回空列表(不支持全局选岗);
|
|
|
+ * 2) 只取该部门下启用状态(status=1)的岗位;
|
|
|
+ * 3) 优先只保留「该部门已通过岗位申请」沉淀出的正式岗位;
|
|
|
+ * 该部门没有已通过申请时,回落到部门全部启用岗位,并把 restricted 置为 false;
|
|
|
+ * 4) 岗位分页拿不到数据但存在已通过申请时,用申请里的岗位信息兜底。
|
|
|
+ *
|
|
|
+ * @returns {Promise<{list: Array, restricted: boolean}>} restricted=true 表示当前是严格口径
|
|
|
+ */
|
|
|
+export async function loadSelectablePositions({ deptId, keyword = '' } = {}) {
|
|
|
+ const deptKey = positiveId(deptId)
|
|
|
+ const text = String(keyword || '').trim().toLowerCase()
|
|
|
+ if (!deptKey) return { list: [], restricted: false }
|
|
|
+ const sameDept = (item) =>
|
|
|
+ item.deptId == null || item.deptId === '' || Number(item.deptId) === deptKey
|
|
|
+ const byKeyword = (rows) =>
|
|
|
+ text ? rows.filter((row) => `${row.code || ''}${row.name || ''}`.toLowerCase().includes(text)) : rows
|
|
|
+
|
|
|
+ if (!isServerMode()) {
|
|
|
+ await new Promise((r) => setTimeout(r, 200))
|
|
|
+ const deptPositions = matchDemoPosition({
|
|
|
+ deptId: deptKey,
|
|
|
+ status: POSITION_STATUS.ENABLED,
|
|
|
+ }).map(positionOption)
|
|
|
+ const approvedIds = demoApprovedPositionIds(deptKey)
|
|
|
+ const approved = deptPositions.filter((item) => approvedIds.has(item.id))
|
|
|
+ const list = approved.length ? approved : deptPositions
|
|
|
+ return { list: byKeyword(list), restricted: approved.length > 0 }
|
|
|
+ }
|
|
|
+
|
|
|
+ const [page, applications] = await Promise.all([
|
|
|
+ pagePosition({ pageNum: 1, size: 200, status: POSITION_STATUS.ENABLED, deptId: deptKey }).catch(
|
|
|
+ () => ({ list: [] }),
|
|
|
+ ),
|
|
|
+ pagePositionApplication({
|
|
|
+ pageNum: 1,
|
|
|
+ size: 200,
|
|
|
+ status: POSITION_APPLICATION_STATUS.APPROVED,
|
|
|
+ deptId: deptKey,
|
|
|
+ }).catch(() => ({ list: [] })),
|
|
|
+ ])
|
|
|
+ const positions = (page?.list || []).filter(sameDept).map(positionOption)
|
|
|
+ const apps = (applications?.list || []).filter(sameDept)
|
|
|
+ const approvedIds = new Set(
|
|
|
+ apps.map((item) => String(item.effectivePositionId || '')).filter(Boolean),
|
|
|
+ )
|
|
|
+ const approvedNames = new Set(
|
|
|
+ apps.map((item) => String(item.positionName || '').trim()).filter(Boolean),
|
|
|
+ )
|
|
|
+ const approved = positions.filter(
|
|
|
+ (item) => approvedIds.has(String(item.id)) || (item.name && approvedNames.has(item.name)),
|
|
|
+ )
|
|
|
+ let list = approved.length ? approved : positions
|
|
|
+ if (!list.length && apps.length) {
|
|
|
+ list = apps
|
|
|
+ .filter((item) => item.positionName)
|
|
|
+ .map((item) =>
|
|
|
+ positionOption({
|
|
|
+ id: item.effectivePositionId || item.id,
|
|
|
+ positionName: item.positionName,
|
|
|
+ deptId: item.deptId,
|
|
|
+ deptName: item.deptName,
|
|
|
+ positionSequence: item.positionSequence,
|
|
|
+ positionLevel: item.positionLevel,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ return { list: byKeyword(list), restricted: approved.length > 0 }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 把后端 status(0|1|2)翻译成中文标签
|
|
|
*/
|